diff --git a/lib/rbnacl.rb b/lib/rbnacl.rb index d8ea1c6..561f56c 100644 --- a/lib/rbnacl.rb +++ b/lib/rbnacl.rb @@ -55,8 +55,9 @@ class BadAuthenticatorError < CryptoError; end # Sealed boxes require "rbnacl/boxes/sealed" - # Secret Key Encryption (SecretBox): XSalsa20Poly1305 + # Secret Key Encryption (SecretBox): XSalsa20Poly1305 and XChaCha20Poly1305 require "rbnacl/secret_boxes/xsalsa20poly1305" + require "rbnacl/secret_boxes/xchacha20poly1305" # Digital Signatures: Ed25519 require "rbnacl/signatures/ed25519" diff --git a/lib/rbnacl/secret_boxes/xchacha20poly1305.rb b/lib/rbnacl/secret_boxes/xchacha20poly1305.rb new file mode 100644 index 0000000..08503fb --- /dev/null +++ b/lib/rbnacl/secret_boxes/xchacha20poly1305.rb @@ -0,0 +1,141 @@ +# encoding: binary +# frozen_string_literal: true + +module RbNaCl + module SecretBoxes + # The XChaCha20Poly1305 class boxes and unboxes messages using the XChaCha20Poly1305 algorithm + # + # This class uses the given secret key to encrypt and decrypt messages. + # + # It is VITALLY important that the nonce is a nonce, i.e. it is a number used + # only once for any given pair of keys. If you fail to do this, you + # compromise the privacy of the messages encrypted. Give your nonces a + # different prefix, or have one side use an odd counter and one an even counter. + # Just make sure they are different. + # + # The ciphertexts generated by this class include a 16-byte authenticator which + # is checked as part of the decryption. An invalid authenticator will cause + # the unbox function to raise. The authenticator is not a signature. Once + # you've looked in the box, you've demonstrated the ability to create + # arbitrary valid messages, so messages you send are repudiable. For + # non-repudiable messages, sign them before or after encryption. + class XChaCha20Poly1305 + extend Sodium + + sodium_type :secretbox + sodium_primitive :xchacha20poly1305 + sodium_constant :KEYBYTES + sodium_constant :NONCEBYTES + sodium_constant :MACBYTES + + sodium_function :secretbox_xchacha20poly1305, + :crypto_secretbox_xchacha20poly1305_easy, + %i[pointer pointer ulong_long pointer pointer] + + sodium_function :secretbox_xchacha20poly1305_open, + :crypto_secretbox_xchacha20poly1305_open_easy, + %i[pointer pointer ulong_long pointer pointer] + + # Create a new XChaCha20Poly1305 Box. + # + # Sets up the Box with a secret key for encrypting and decrypting messages. + # + # @param key [String] The key to encrypt and decrypt with + # + # @raise [RbNaCl::LengthError] on invalid keys + # + # @return [RbNaCl::XChaCha20Poly1305] The new Box, ready to use + def initialize(key) + @key = Util.check_string(key, KEYBYTES, "Secret key") + end + + # Encrypts a message + # + # Encrypts the message with the given nonce to the key set up when + # initializing the class. Make sure the nonce is unique for any given + # key, or you might as well just send plain text. + # + # This function takes care of the padding required by the NaCL C API. + # + # @param nonce [String] A 24-byte string containing the nonce. + # @param message [String] The message to be encrypted. + # + # @raise [RbNaCl::LengthError] If the nonce is not valid + # + # @return [String] The ciphertext without the nonce prepended (BINARY encoded) + def box(nonce, message) + Util.check_length(nonce, nonce_bytes, "Nonce") + + # Libsodium appends the tag, so make space for the tag. + ct = Util.zeros(message.bytesize + MACBYTES) + + success = self.class.secretbox_xchacha20poly1305(ct, message, message.bytesize, nonce, @key) + raise CryptoError, "Encryption failed" unless success + ct + end + alias encrypt box + + # Decrypts a ciphertext + # + # Decrypts the ciphertext with the given nonce using the key setup when + # initializing the class. + # + # This function takes care of the padding required by the NaCL C API. + # + # @param nonce [String] A 24-byte string containing the nonce. + # @param ciphertext [String] The message to be decrypted. + # + # @raise [RbNaCl::LengthError] If the nonce is not valid + # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. + # + # @return [String] The decrypted message (BINARY encoded) + def open(nonce, ciphertext) + Util.check_length(nonce, nonce_bytes, "Nonce") + message = Util.zeros(ciphertext.bytesize) + + success = self.class.secretbox_xchacha20poly1305_open(message, ciphertext, ciphertext.bytesize, nonce, @key) + raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success + + # When the message was created it's length included the length of the tag. Slice it off + # now that it passed the tag check. + message.byteslice(0, message.bytesize - MACBYTES) + end + alias decrypt open + + # The crypto primitive for the XChaCha20Poly1305 instance + # + # @return [Symbol] The primitive used + def primitive + self.class.primitive + end + + # The nonce bytes for the XChaCha20Poly1305 class + # + # @return [Integer] The number of bytes in a valid nonce + def self.nonce_bytes + NONCEBYTES + end + + # The nonce bytes for the XChaCha20Poly1305 instance + # + # @return [Integer] The number of bytes in a valid nonce + def nonce_bytes + NONCEBYTES + end + + # The key bytes for the XChaCha20Poly1305 class + # + # @return [Integer] The number of bytes in a valid key + def self.key_bytes + KEYBYTES + end + + # The key bytes for the XChaCha20Poly1305 instance + # + # @return [Integer] The number of bytes in a valid key + def key_bytes + KEYBYTES + end + end + end +end \ No newline at end of file diff --git a/lib/rbnacl/test_vectors.rb b/lib/rbnacl/test_vectors.rb index e0b6347..585eb41 100644 --- a/lib/rbnacl/test_vectors.rb +++ b/lib/rbnacl/test_vectors.rb @@ -242,7 +242,18 @@ module RbNaCl "b89ad929530a1bb3ab5e69f24c7f6070c8f840c9abb4f69fbfc8a7ff5126faee" \ "bbb55805ee9c1cf2ce5a57263287aec5780f04ec324c3514122cfc3231fc1a8b" \ "718a62863730a2702bb76366116bed09e0fd5c6d84b6b0c1abaf249d5dd0f7f5" \ - "a7ea" + "a7ea", + + # XChaCha20 test vectors + # Taken from https://github.com/jedisct1/libsodium/blob/1.0.20-RELEASE/test/default/xchacha20.c + secretbox_xchacha20poly1305_key: "d3c71d54e6b13506e07aa2e7b412a17a7a1f34df3d3148cd3f45b91ccaa5f4d9", + secretbox_xchacha20poly1305_message: "76bd706e07741e713d90efdb34ad202067263f984942aae8bda159f30dfccc72" \ + "200f8093520b85c5ad124ff7c8b2d920946e5cfff4b819abf84c7b35a6205ca7" \ + "2c9f8747c3044dd73fb4bebda1b476", + secretbox_xchacha20poly1305_nonce: "943b454a853aa514c63cf99b1e197bbb99da24b2e2d93e47", + secretbox_xchacha20poly1305_ciphertext: "0384276f1cfa5c82c3e58f0f2acc1f821c6f526d2c19557cf8bd270fcde43fba" \ + "1d88890663f7b2f5c6b1d7deccf5c91b4df5865dc55cc7e04d6793fc2db8f9e3" \ + "b418f95cb796d67a7f3f7e097150cb607c435dacf82eac3d669866e5092ace" }.freeze end # rubocop:enable Metrics/ModuleLength diff --git a/spec/rbnacl/boxes/xchacha20poly1305_spec.rb b/spec/rbnacl/boxes/xchacha20poly1305_spec.rb new file mode 100644 index 0000000..984dff5 --- /dev/null +++ b/spec/rbnacl/boxes/xchacha20poly1305_spec.rb @@ -0,0 +1,28 @@ +# encoding: binary +# frozen_string_literal: true + +RSpec.describe RbNaCl::SecretBoxes::XChaCha20Poly1305 do + let(:key) { vector :secretbox_xchacha20poly1305_key } + let(:box) { described_class.new(key) } + + context "new" do + it "accepts strings" do + expect { described_class.new(key) }.to_not raise_error + end + + it "raises on a nil key" do + expect { described_class.new(nil) }.to raise_error(TypeError) + end + + it "raises on a short key" do + expect { described_class.new("hello") }.to raise_error RbNaCl::LengthError + end + end + + + include_examples "box" do + let(:nonce) { vector :secretbox_xchacha20poly1305_nonce } + let(:message) { vector :secretbox_xchacha20poly1305_message } + let(:ciphertext) { vector :secretbox_xchacha20poly1305_ciphertext } + end +end diff --git a/spec/shared/box.rb b/spec/shared/box.rb index a9aa181..8cd0441 100644 --- a/spec/shared/box.rb +++ b/spec/shared/box.rb @@ -39,6 +39,12 @@ end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) end + it "raises on a message too short to contain the authentication tag" do + expect do + box.open(nonce, ciphertext[0, 7]) + end.to raise_error(RbNaCl::CryptoError, /Decryption failed. Ciphertext failed verification./) + end + it "raises on a corrupt ciphertext" do expect do box.open(nonce, corrupt_ciphertext)