diff --git a/README.md b/README.md index 7f774c2..e42eb6a 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/) - [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 - - [ ] ChaCha20-Poly1305-IETF - - [ ] ChaCha20-Poly1305 + - [x] [XChaCha20-Poly1305-IETF](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/xchacha20-poly1305_construction) + - [ ] [ChaCha20-Poly1305-IETF](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305/ietf_chacha20-poly1305_construction) + - [ ] [ChaCha20-Poly1305](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead/chacha20-poly1305) - [Hashing](https://libsodium.gitbook.io/doc/hashing) - [x] ☑ [Blake2b](https://libsodium.gitbook.io/doc/hashing/generic_hashing) - [x] Complete libsodium implementation including `key`, `salt`, `personal` and fully selectable output sizes. diff --git a/spec/sodium/cipher/aead/chalsa_spec.cr b/spec/sodium/cipher/aead/chalsa_spec.cr new file mode 100644 index 0000000..f436959 --- /dev/null +++ b/spec/sodium/cipher/aead/chalsa_spec.cr @@ -0,0 +1,96 @@ +require "../../../spec_helper" +require "../../../../src/sodium/cipher/aead/chalsa" + +combined_test_vectors = [ + { + key: "1b27556473e985d462cd51197a9a46c76009549eac6474f206c4ee0844f68389", + nonce: "69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37", + plaintext: "be075fc53c81f2d5cf141316ebeb0c7b5228c52a4c62cbd44b66849b64244ffce5e" \ + "cbaaf33bd751a1ac728d45e6c61296cdc3c01233561f41db66cce314adb310e3be8" \ + "250c46f06dceea3a7fa1348057e2f6556ad6b1318a024a838f21af1fde048977eb4" \ + "8f59ffd4924ca1c60902e52f0a089bc76897040e082f937763848645e0705", + ciphertext: "f3ffc7703f9400e52a7dfb4b3d3305d98e993b9f48681273c29650ba32fc76ce483" \ + "32ea7164d96a4476fb8c531a1186ac0dfc17c98dce87b4da7f011ec48c97271d2c2" \ + "0f9b928fe2270d6fb863d51738b48eeee314a7cc8ab932164548e526ae902243685" \ + "17acfeabd6bb3732bc0e9da99832b61ca01b6de56244a9e88d5f9b37973f622a43d" \ + "14a6599b1f654cb45a74e355a5", + }, +] + +private def box_from_test_vector(vec) + box = Sodium::SecretBox.new vec[:key].hexbytes + nonce = Sodium::Nonce.new vec[:nonce].hexbytes + plaintext = vec[:plaintext].hexbytes + ciphertext = vec[:ciphertext].hexbytes + + {box, nonce, plaintext, ciphertext} +end + +{% for name in %w(Xchacha20Poly1305Ietf) %} +# TODO: verify against test vectors. + describe Sodium::Cipher::Aead::{{ name.id }} do + it "encrypts/decrypts" do + box = Sodium::Cipher::Aead::{{ name.id }}.new + + message = "foobar" + additional = "foo" + mac, encrypted, nonce = box.encrypt_detached message, additional: additional + decrypted = box.decrypt_detached encrypted, nonce: nonce, mac: mac, additional: additional + message.should eq String.new(decrypted) + + # Wrong additional. + expect_raises(Sodium::Error::DecryptionFailed) do + box.decrypt_detached encrypted, nonce: nonce, mac: mac, additional: "bar".to_slice + end + + # Missing additional. + expect_raises(Sodium::Error::DecryptionFailed) do + box.decrypt_detached encrypted, nonce: nonce, mac: mac + end + + # Wrong data. + expect_raises(Sodium::Error::DecryptionFailed) do + box.decrypt_detached "badmsgbadmsgbadmsgbadmsgbadmsg".to_slice, nonce: nonce, mac: mac + end + end + + it "can't encrypt twice using the same nonce" do + box = Sodium::Cipher::Aead::{{ name.id }}.new + + message = "foobar" + mac, encrypted, nonce = box.encrypt_detached message + + expect_raises(Sodium::Nonce::Error::Reused) do + box.encrypt_detached message.to_slice, nonce: nonce + end + end + end +{% end %} + +{% if false %} + describe Sodium::Cipher::Aead do + it "PyNaCl combined test vectors" do + combined_test_vectors.each do |vec| + box, nonce, plaintext, ciphertext = box_from_test_vector vec + + encrypted, _ = box.encrypt plaintext, nonce: nonce + encrypted.should eq ciphertext + + decrypted = box.decrypt encrypted, nonce: nonce + plaintext.should eq decrypted + end + end + + pending "detached test vectors" do + detached_test_vectors.each do |vec| + box, nonce, plaintext, ciphertext = box_from_test_vector vec + + encrypted = box.encrypt_detached plaintext, nonce + encrypted.should eq ciphertext + + decrypted = box.decrypt_detached encrypted, nonce + plaintext.should eq decrypted + end + end + end +{% end %} diff --git a/src/sodium/cipher/aead/chalsa.cr b/src/sodium/cipher/aead/chalsa.cr new file mode 100644 index 0000000..7c98b56 --- /dev/null +++ b/src/sodium/cipher/aead/chalsa.cr @@ -0,0 +1,98 @@ +require "../../lib_sodium" +require "../../secure_buffer" +require "../../nonce" + +module Sodium::Cipher::Aead + abstract class Chalsa + @key : Bytes | SecureBuffer + + def initialize + @key = SecureBuffer.random key_size + end + + def initialize(@key) + raise ArgumentError.new("key size mismatch, got #{@key.bytesize}, wanted #{key_size}") if @key.bytesize != key_size + end + + # Encrypts data and returns {ciphertext, nonce} + def encrypt(data) + encrypt data.to_slice + end + + # Encrypts data and returns {mac, ciphertext, nonce} + def encrypt_detached(data, dst : Bytes? = nil, *, mac : Bytes? = nil, additional = nil) + encrypt_detached data.to_slice, mac: mac, additional: additional + end + + # Decrypts data and returns plaintext + # Must supply `mac` and `nonce` + # Must supply `additional` if supplied to #encrypt + def decrypt_detached(data, dst : Bytes? = nil, *, mac : Bytes? = nil, additional = nil) + encrypt_detached data.to_slice, mac: mac, additional: additional + end + + abstract def encrypt_detached(src : Bytes, dst : Bytes? = nil, *, nonce : Sodium::Nonce? = nil, mac : Bytes? = nil, additional : String | Bytes | Nil = nil) : {Bytes, Bytes, Sodium::Nonce} + abstract def decrypt_detached(src : Bytes, dst : Bytes? = nil, *, nonce : Sodium::Nonce, mac : Bytes, additional : String | Bytes | Nil = nil) : Bytes + protected abstract def key_size : Int32 + end + + {% for key, val in {"Xchacha20Poly1305Ietf" => "_xchacha20poly1305_ietf"} %} + # Use like `SecretBox` with optional additional authenticated data. + # + # See [https://libsodium.gitbook.io/doc/secret-key_cryptography/aead](https://libsodium.gitbook.io/doc/secret-key_cryptography/aead) + # + # See `spec/sodium/cipher/aead/chalsa_spec.cr` for examples on how to use this class. + # + # WARNING: Not validated against test vectors. You should probably write some before using this class. + class {{ key.id }} < Chalsa + KEY_SIZE = LibSodium.crypto_aead{{ val.id }}_keybytes.to_i32 + MAC_SIZE = LibSodium.crypto_aead{{ val.id }}_abytes.to_i32 + NONCE_SIZE = LibSodium.crypto_aead{{ val.id }}_npubbytes.to_i32 + + # `src` and `dst` may be the same object but should not overlap. + # May supply `mac`, otherwise a new one is returned. + # May supply `additional` + def encrypt_detached(src : Bytes, dst : Bytes? = nil, nonce : Sodium::Nonce? = nil, *, mac : Bytes? = nil, additional : String | Bytes | Nil = nil) : {Bytes, Bytes, Sodium::Nonce} + dst ||= Bytes.new(src.bytesize) + nonce ||= Sodium::Nonce.random + mac ||= Bytes.new MAC_SIZE + + raise ArgumentError.new("src and dst bytesize must be identical") if src.bytesize != dst.bytesize + raise ArgumentError.new("nonce size mismatch, got #{nonce.bytesize}, wanted #{NONCE_SIZE}") unless nonce.bytesize == NONCE_SIZE + raise ArgumentError.new("mac size mismatch, got #{mac.bytesize}, wanted #{MAC_SIZE}") unless mac.bytesize == MAC_SIZE + + additional = additional.try &.to_slice + ad_len = additional.try(&.bytesize) || 0 + + nonce.used! + if LibSodium.crypto_aead{{ val.id }}_encrypt_detached(dst, mac, out mac_len, src, src.bytesize, additional, ad_len, nil, nonce.to_slice, @key.to_slice) != 0 + raise Sodium::Error.new("crypto_aead_{{ val.id }}_encrypt_detached") + end + raise Sodium::Error.new("crypto_aead_{{ val.id }}_encrypt_detached mac size mismatch") if mac_len != MAC_SIZE + + {mac, dst, nonce} + end + + # src and dst may be the same object but should not overlap. + # Must supply `mac` and `nonce` + # Must supply `additional` if supplied to #encrypt_detached + def decrypt_detached(src : Bytes, dst : Bytes? = nil, *, nonce : Sodium::Nonce, mac : Bytes, additional : String | Bytes | Nil = nil) : Bytes + dst ||= Bytes.new(src.bytesize) + raise ArgumentError.new("src and dst bytesize must be identical") if src.bytesize != dst.bytesize + raise ArgumentError.new("nonce size mismatch, got #{nonce.bytesize}, wanted #{NONCE_SIZE}") unless nonce.bytesize == NONCE_SIZE + raise ArgumentError.new("mac size mismatch, got #{mac.bytesize}, wanted #{MAC_SIZE}") unless mac.bytesize == MAC_SIZE + + ad_len = additional.try(&.bytesize) || 0 + + if LibSodium.crypto_aead{{ val.id }}_decrypt_detached(dst, nil, src, src.bytesize, mac, additional, ad_len, nonce.to_slice, @key.to_slice) != 0 + raise Sodium::Error::DecryptionFailed.new("crypto_aead_{{ val.id }}_decrypt_detached") + end + dst + end + + protected def key_size + KEY_SIZE + end + end + {% end %} +end diff --git a/src/sodium/lib_sodium.cr b/src/sodium/lib_sodium.cr index 91d2c0d..b79ee2d 100644 --- a/src/sodium/lib_sodium.cr +++ b/src/sodium/lib_sodium.cr @@ -121,6 +121,38 @@ module Sodium ) : LibC::Int {% end %} + # AEAD + {% for name in ["_chacha20poly1305", "_chacha20poly1305_ietf", "_xchacha20poly1305_ietf"] %} + fun crypto_aead{{ name.id }}_keybytes() : LibC::SizeT + fun crypto_aead{{ name.id }}_abytes() : LibC::SizeT + fun crypto_aead{{ name.id }}_npubbytes() : LibC::SizeT # Nonce + + fun crypto_aead{{ name.id }}_encrypt_detached( + c : Pointer(LibC::UChar), + mac : Pointer(LibC::UChar), + mac_len : Pointer(LibC::ULongLong), + m : Pointer(LibC::UChar), + len : LibC::ULongLong, + ad : Pointer(LibC::UChar), + ad_lenlen : LibC::ULongLong, + nsec : Pointer(LibC::UChar), + nonce : Pointer(LibC::UChar), + key : Pointer(LibC::UChar) + ) : LibC::Int + + fun crypto_aead{{ name.id }}_decrypt_detached( + m : Pointer(LibC::UChar), + nsec : Pointer(LibC::UChar), + c : Pointer(LibC::UChar), + len : LibC::ULongLong, + mac : Pointer(LibC::UChar), + ad : Pointer(LibC::UChar), + ad_lenlen : LibC::ULongLong, + nonce : Pointer(LibC::UChar), + key : Pointer(LibC::UChar) + ) : 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 diff --git a/src/sodium/nonce.cr b/src/sodium/nonce.cr index 67156cd..9c045f0 100644 --- a/src/sodium/nonce.cr +++ b/src/sodium/nonce.cr @@ -18,6 +18,8 @@ module Sodium # Returns bytes delegate to_slice, to: @bytes + delegate bytesize, to: @bytes + def initialize(@bytes : Bytes) if bytes.bytesize != NONCE_SIZE raise ArgumentError.new("Nonce must be #{NONCE_SIZE} bytes, got #{bytes.bytesize}")