From 96b215cf547d1c3a41f6c988c46497e34cd84c74 Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Mon, 8 Jul 2019 23:39:00 -0700 Subject: [PATCH] Add Sodium::Cipher::SecretStream --- README.md | 3 +- spec/sodium/cipher/secret_stream_spec.cr | 82 +++++++++++ src/sodium/cipher/secret_stream.cr | 176 +++++++++++++++++++++++ src/sodium/lib_sodium.cr | 44 ++++++ 4 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 spec/sodium/cipher/secret_stream_spec.cr create mode 100644 src/sodium/cipher/secret_stream.cr diff --git a/README.md b/README.md index 73d06fa..1dcc99a 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/) - Secret Box - [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) + - [x] [Secret Stream](https://libsodium.gitbook.io/doc/secret-key_cryptography/secretstream) - [AEAD](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead) - [ ] AES256-GCM (Requires hardware acceleration) - [ ] 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. - [ ] [SipHash](https://libsodium.gitbook.io/doc/hashing/short-input_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) - Other - [x] [Key Derivation](https://libsodium.gitbook.io/doc/key_derivation) diff --git a/spec/sodium/cipher/secret_stream_spec.cr b/spec/sodium/cipher/secret_stream_spec.cr new file mode 100644 index 0000000..0982004 --- /dev/null +++ b/spec/sodium/cipher/secret_stream_spec.cr @@ -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 diff --git a/src/sodium/cipher/secret_stream.cr b/src/sodium/cipher/secret_stream.cr new file mode 100644 index 0000000..085e408 --- /dev/null +++ b/src/sodium/cipher/secret_stream.cr @@ -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 diff --git a/src/sodium/lib_sodium.cr b/src/sodium/lib_sodium.cr index a474d4e..23cb917 100644 --- a/src/sodium/lib_sodium.cr +++ b/src/sodium/lib_sodium.cr @@ -72,6 +72,50 @@ module Sodium key : Pointer(LibC::UChar) ) : 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. {% for name in ["_chacha20", "_chacha20_ietf", "_xchacha20", "_salsa20", "_xsalsa20"] %} fun crypto_stream{{ name.id }}_keybytes() : LibC::SizeT