Add Sodium::Cipher::SecretStream
parent
65ad5987d4
commit
96b215cf54
|
@ -24,6 +24,7 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/)
|
||||||
- Secret Box
|
- Secret Box
|
||||||
- [x] [Combined mode](https://libsodium.gitbook.io/doc/secret-key_cryptography/authenticated_encryption)
|
- [x] [Combined mode](https://libsodium.gitbook.io/doc/secret-key_cryptography/authenticated_encryption)
|
||||||
- [ ] [Detached mode](https://libsodium.gitbook.io/doc/secret-key_cryptography/authenticated_encryption)
|
- [ ] [Detached mode](https://libsodium.gitbook.io/doc/secret-key_cryptography/authenticated_encryption)
|
||||||
|
- [x] [Secret Stream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
|
||||||
- [AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead)
|
- [AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead)
|
||||||
- [ ] AES256-GCM (Requires hardware acceleration)
|
- [ ] AES256-GCM (Requires hardware acceleration)
|
||||||
- [ ] XChaCha20-Poly1305-IETF
|
- [ ] XChaCha20-Poly1305-IETF
|
||||||
|
@ -34,7 +35,7 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/)
|
||||||
- [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes.
|
- [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes.
|
||||||
- [ ] [SipHash](https://libsodium.gitbook.io/doc/hashing/short-input_hashing)
|
- [ ] [SipHash](https://libsodium.gitbook.io/doc/hashing/short-input_hashing)
|
||||||
- [Password Hashing](https://libsodium.gitbook.io/doc/password_hashing)
|
- [Password Hashing](https://libsodium.gitbook.io/doc/password_hashing)
|
||||||
- [x] [x] [Argon2](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) (Use for new applications)
|
- [x] ☑ [Argon2](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) (Use for new applications)
|
||||||
- [ ] [Scrypt](https://libsodium.gitbook.io/doc/advanced/scrypt) (For compatibility with older applications)
|
- [ ] [Scrypt](https://libsodium.gitbook.io/doc/advanced/scrypt) (For compatibility with older applications)
|
||||||
- Other
|
- Other
|
||||||
- [x] [Key Derivation](https://libsodium.gitbook.io/doc/key_derivation)
|
- [x] [Key Derivation](https://libsodium.gitbook.io/doc/key_derivation)
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
require "../../spec_helper"
|
||||||
|
require "../../../src/sodium/cipher/secret_stream"
|
||||||
|
|
||||||
|
private def new_ciphers
|
||||||
|
cipher1 = Sodium::Cipher::SecretStream::XChaCha20Poly1305.new
|
||||||
|
cipher2 = Sodium::Cipher::SecretStream::XChaCha20Poly1305.new
|
||||||
|
|
||||||
|
cipher1.encrypt
|
||||||
|
cipher2.decrypt
|
||||||
|
|
||||||
|
{cipher1, cipher2}
|
||||||
|
end
|
||||||
|
|
||||||
|
private def new_ciphers_with_data
|
||||||
|
data = Bytes.new(100)
|
||||||
|
data.bytesize.times do |i|
|
||||||
|
data[i] = (i % 256).to_u8
|
||||||
|
end
|
||||||
|
|
||||||
|
cipher1, cipher2 = new_ciphers
|
||||||
|
|
||||||
|
key = cipher1.random_key
|
||||||
|
cipher2.key = key
|
||||||
|
|
||||||
|
header = cipher1.header
|
||||||
|
cipher2.header = header
|
||||||
|
|
||||||
|
{cipher1, cipher2, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: verify against test vectors.
|
||||||
|
describe Sodium::Cipher::SecretStream do
|
||||||
|
it "encrypts/decrypts" do
|
||||||
|
cipher1, cipher2, data = new_ciphers_with_data
|
||||||
|
|
||||||
|
3.times do
|
||||||
|
output = cipher1.update data
|
||||||
|
output.should_not eq data
|
||||||
|
|
||||||
|
cipher2.update(output).should eq data
|
||||||
|
end
|
||||||
|
|
||||||
|
cipher1.final.should eq Bytes.new(0)
|
||||||
|
cipher2.final.should eq Bytes.new(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "encrypts/decrypts with additional" do
|
||||||
|
cipher1, cipher2, data = new_ciphers_with_data
|
||||||
|
|
||||||
|
["foo", "bar", nil, "baz"].each do |additional|
|
||||||
|
additional = additional.try &.to_slice
|
||||||
|
cipher1.additional = additional
|
||||||
|
output = cipher1.update data
|
||||||
|
cipher1.additional.should eq nil # Additional reset after encrypt.
|
||||||
|
output.should_not eq data
|
||||||
|
|
||||||
|
cipher2.additional = additional
|
||||||
|
cipher2.update(output).should eq data
|
||||||
|
cipher2.additional.should eq nil # Additional reset after encrypt.
|
||||||
|
end
|
||||||
|
|
||||||
|
cipher1.final.should eq Bytes.new(0)
|
||||||
|
cipher2.final.should eq Bytes.new(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "encrypts/decrypts with tags" do
|
||||||
|
cipher1, cipher2, data = new_ciphers_with_data
|
||||||
|
|
||||||
|
[cipher1.tag_push, cipher1.tag_rekey, cipher1.tag_final].each do |tag|
|
||||||
|
cipher1.tag = tag
|
||||||
|
output = cipher1.update data
|
||||||
|
cipher1.tag.should eq 0_u8 # Tag reset after encrypt.
|
||||||
|
output.should_not eq data
|
||||||
|
|
||||||
|
cipher2.update(output).should eq data
|
||||||
|
cipher2.tag.should eq tag # Tag set on decrypt.
|
||||||
|
end
|
||||||
|
|
||||||
|
cipher1.final.should eq Bytes.new(0)
|
||||||
|
cipher2.final.should eq Bytes.new(0)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,176 @@
|
||||||
|
require "../lib_sodium"
|
||||||
|
require "../secure_buffer"
|
||||||
|
|
||||||
|
module Sodium::Cipher
|
||||||
|
abstract class SecretStream
|
||||||
|
@state : SecureBuffer
|
||||||
|
@encrypt_decrypt = 0
|
||||||
|
@initialized = false
|
||||||
|
|
||||||
|
# * Set tag before encrypting
|
||||||
|
# * Tag is set after decrypting
|
||||||
|
property tag = 0_u8
|
||||||
|
|
||||||
|
# Used to authentication but not encrypt additional data.
|
||||||
|
#
|
||||||
|
# * Set this before encrypting **and** decrypting.
|
||||||
|
# * This property is set to nil after calling .update.
|
||||||
|
property additional : Bytes? = nil
|
||||||
|
|
||||||
|
@key : Bytes | SecureBuffer | Nil = nil
|
||||||
|
|
||||||
|
def initialize
|
||||||
|
@state = SecureBuffer.new state_size
|
||||||
|
end
|
||||||
|
|
||||||
|
def encrypt
|
||||||
|
@encrypt_decrypt = 1
|
||||||
|
end
|
||||||
|
|
||||||
|
def decrypt
|
||||||
|
@encrypt_decrypt = -1
|
||||||
|
end
|
||||||
|
|
||||||
|
def key=(key : Bytes | SecureBuffer)
|
||||||
|
raise ArgumentError.new("key must be #{key_size} bytes, got #{key.bytesize}") if key.bytesize != key_size
|
||||||
|
@key = key
|
||||||
|
key
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a random key in a SecureBuffer.
|
||||||
|
def random_key
|
||||||
|
self.key = SecureBuffer.random key_size
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only used for encryption.
|
||||||
|
def header
|
||||||
|
raise "only call when encrypting" if @encrypt_decrypt != 1
|
||||||
|
buf = Bytes.new header_size
|
||||||
|
init_state buf
|
||||||
|
buf
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only used for decryption.
|
||||||
|
def header=(buf : Bytes)
|
||||||
|
raise "only call when decrypting" if @encrypt_decrypt != -1
|
||||||
|
init_state buf
|
||||||
|
buf
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(src : Bytes) : Bytes
|
||||||
|
update src, Bytes.new(src.bytesize + (auth_tag_size * @encrypt_decrypt))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Provided for compatibility with block ciphers.
|
||||||
|
# Stream ciphers don't have additional data.
|
||||||
|
def final
|
||||||
|
Bytes.new(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
abstract def update(src : Bytes, dst : Bytes)
|
||||||
|
abstract def init_state(header_buf : Bytes) : Nil
|
||||||
|
protected abstract def state_size : Int32
|
||||||
|
abstract def key_size : Int32
|
||||||
|
abstract def header_size : Int32
|
||||||
|
abstract def auth_tag_size : Int32
|
||||||
|
end
|
||||||
|
|
||||||
|
{% for key, val in {"XChaCha20Poly1305" => "xchacha20poly1305"} %}
|
||||||
|
# [Libsodium Secret Stream API](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream)
|
||||||
|
#
|
||||||
|
# This class mimicks the OpenSSL::Cipher interface with minor differences.
|
||||||
|
# * .header must be called for encryption before calling .update
|
||||||
|
# * .header= must be called for decryption with the data returned from .header before calling .update
|
||||||
|
# * every .update is it's own authenticated message. Unlike OpenSSL this class doesn't buffer data. You must handle the framing yourself.
|
||||||
|
# * A tag may be set before encrypting and is set after calling .update when decrypting.
|
||||||
|
# * .additional may be set before encrypting and must be set before decrypting.
|
||||||
|
#
|
||||||
|
# See `spec/sodium/cipher/secret_stream_spec.cr` for examples on how to use this class.
|
||||||
|
#
|
||||||
|
# WARNING: Not verified against test vectors.
|
||||||
|
class SecretStream::{{ key.id }} < SecretStream
|
||||||
|
def update(src : Bytes, dst : Bytes) : Bytes
|
||||||
|
raise Sodium::Error.new("must call .header or .header= first") unless @initialized
|
||||||
|
min_dst_size = src.bytesize + (auth_tag_size * @encrypt_decrypt)
|
||||||
|
raise ArgumentError.new("dst bytesize must at least #{min_dst_size}, got #{dst.bytesize}") if dst.bytesize < min_dst_size
|
||||||
|
|
||||||
|
ad, ad_size = if a = @additional
|
||||||
|
{a.to_unsafe, a.bytesize}
|
||||||
|
else
|
||||||
|
{Pointer(UInt8).null, 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
case @encrypt_decrypt
|
||||||
|
when 1
|
||||||
|
if LibSodium.crypto_secretstream_{{ val.id }}_push(@state.to_slice, dst.to_slice, out dst_size, src, src.bytesize, ad, ad_size, @tag) != 0
|
||||||
|
raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic")
|
||||||
|
end
|
||||||
|
@tag = 0
|
||||||
|
@additional = nil
|
||||||
|
dst[0, dst_size]
|
||||||
|
when -1
|
||||||
|
if LibSodium.crypto_secretstream_{{ val.id }}_pull(@state.to_slice, dst.to_slice, out dst_size2, out @tag, src, src.bytesize, ad, ad_size) != 0
|
||||||
|
raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic")
|
||||||
|
end
|
||||||
|
@additional = nil
|
||||||
|
dst[0, dst_size2]
|
||||||
|
else
|
||||||
|
abort "invalid encrypt_decrypt state #{@encrypt_decrypt}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
protected def init_state(header_buf : Bytes) : Nil
|
||||||
|
raise Sodium::Error.new("can't initialize more than once") if @initialized
|
||||||
|
|
||||||
|
if k = @key
|
||||||
|
case @encrypt_decrypt
|
||||||
|
when 1
|
||||||
|
if LibSodium.crypto_secretstream_xchacha20poly1305_init_push(@state.to_slice, header_buf.to_slice, k.to_slice) != 0
|
||||||
|
raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push")
|
||||||
|
end
|
||||||
|
when -1
|
||||||
|
if LibSodium.crypto_secretstream_xchacha20poly1305_init_pull(@state.to_slice, header_buf.to_slice, k.to_slice) != 0
|
||||||
|
raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push")
|
||||||
|
end
|
||||||
|
when 0
|
||||||
|
raise Sodium::Error.new("must call .encrypt or .decrypt first")
|
||||||
|
else
|
||||||
|
abort "invalid encrypt_decrypt state #{@encrypt_decrypt}"
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise Sodium::Error.new("must set an encryption/decryption key")
|
||||||
|
end
|
||||||
|
|
||||||
|
@initialized = true
|
||||||
|
end
|
||||||
|
|
||||||
|
protected def state_size : Int32
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_statebytes.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def key_size : Int32
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_keybytes.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def header_size : Int32
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_headerbytes.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def auth_tag_size : Int32
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_abytes.to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_push
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_tag_push
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_rekey
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_tag_rekey
|
||||||
|
end
|
||||||
|
|
||||||
|
def tag_final
|
||||||
|
LibSodium.crypto_secretstream_{{ val.id }}_tag_final
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
end
|
|
@ -72,6 +72,50 @@ module Sodium
|
||||||
key : Pointer(LibC::UChar)
|
key : Pointer(LibC::UChar)
|
||||||
) : LibC::Int
|
) : LibC::Int
|
||||||
|
|
||||||
|
{% for name in ["_xchacha20poly1305"] %}
|
||||||
|
{% for name2 in %w(keybytes headerbytes statebytes abytes) %}
|
||||||
|
fun crypto_secretstream{{ name.id }}_{{ name2.id }} : LibC::SizeT
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
{% for name2 in %w(tag_rekey tag_push tag_final) %}
|
||||||
|
fun crypto_secretstream{{ name.id }}_{{ name2.id }} : LibC::UChar
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
fun crypto_secretstream{{ name.id }}_init_push(
|
||||||
|
state : Pointer(LibC::UChar),
|
||||||
|
header : Pointer(LibC::UChar),
|
||||||
|
key : Pointer(LibC::UChar),
|
||||||
|
) : LibC::Int
|
||||||
|
|
||||||
|
fun crypto_secretstream{{ name.id }}_init_pull(
|
||||||
|
state : Pointer(LibC::UChar),
|
||||||
|
header : Pointer(LibC::UChar),
|
||||||
|
key : Pointer(LibC::UChar),
|
||||||
|
) : LibC::Int
|
||||||
|
|
||||||
|
fun crypto_secretstream{{ name.id }}_push(
|
||||||
|
state : Pointer(LibC::UChar),
|
||||||
|
c : Pointer(LibC::UChar),
|
||||||
|
clen : Pointer(LibC::ULongLong),
|
||||||
|
m : Pointer(LibC::UChar),
|
||||||
|
mlen : LibC::ULongLong,
|
||||||
|
ad : Pointer(LibC::UChar),
|
||||||
|
adlen : LibC::ULongLong,
|
||||||
|
tag : LibC::UChar,
|
||||||
|
) : LibC::Int
|
||||||
|
|
||||||
|
fun crypto_secretstream{{ name.id }}_pull(
|
||||||
|
state : Pointer(LibC::UChar),
|
||||||
|
m : Pointer(LibC::UChar),
|
||||||
|
mlen : Pointer(LibC::ULongLong),
|
||||||
|
tag : Pointer(LibC::UChar),
|
||||||
|
c : Pointer(LibC::UChar),
|
||||||
|
clen : LibC::ULongLong,
|
||||||
|
ad : Pointer(LibC::UChar),
|
||||||
|
adlen : LibC::ULongLong,
|
||||||
|
) : LibC::Int
|
||||||
|
{% end %}
|
||||||
|
|
||||||
# TODO: Add reduced round variants.
|
# TODO: Add reduced round variants.
|
||||||
{% for name in ["_chacha20", "_chacha20_ietf", "_xchacha20", "_salsa20", "_xsalsa20"] %}
|
{% for name in ["_chacha20", "_chacha20_ietf", "_xchacha20", "_salsa20", "_xsalsa20"] %}
|
||||||
fun crypto_stream{{ name.id }}_keybytes() : LibC::SizeT
|
fun crypto_stream{{ name.id }}_keybytes() : LibC::SizeT
|
||||||
|
|
Loading…
Reference in New Issue