From 94d116c40fb3a798417a2630ce1dc98da69cb386 Mon Sep 17 00:00:00 2001 From: Jonathan Stott Date: Wed, 13 Mar 2013 13:00:23 +0000 Subject: [PATCH] Expose access to XSalsa20Poly1305 primitive This is to start to fix #43 Explicitly expose access to the XSalsa20Poly1305 primitive from the library. The primitive class handles all of the input checking, and the encryption methods. At the moment, the code has been more or less just copied across. Eventually, it might make sense to factor this out into the SecretBox module. Also, it's possible that one or other of the method calls might return something other than a string, augmented with additional information and methods. --- lib/rbnacl/nacl.rb | 10 +- .../nacl/secret_box/xsalsa20_poly1305_box.rb | 102 ++++++++++++++++++ lib/rbnacl/secret_box.rb | 22 ++-- .../secret_box/xsalsa20_poly1305_box_spec.rb | 24 +++++ spec/rbnacl/secret_box_spec.rb | 4 +- 5 files changed, 141 insertions(+), 21 deletions(-) create mode 100644 lib/rbnacl/nacl/secret_box/xsalsa20_poly1305_box.rb create mode 100644 spec/rbnacl/nacl/secret_box/xsalsa20_poly1305_box_spec.rb diff --git a/lib/rbnacl/nacl.rb b/lib/rbnacl/nacl.rb index 7d38c2e..e7f4feb 100644 --- a/lib/rbnacl/nacl.rb +++ b/lib/rbnacl/nacl.rb @@ -29,6 +29,9 @@ def self.#{name}(*args) end eos end + + module SecretBox + end SHA256BYTES = 32 wrap_nacl_function :crypto_hash_sha256, @@ -63,12 +66,13 @@ def self.#{name}(*args) :crypto_box_curve25519xsalsa20poly1305_ref_open_afternm, [:pointer, :pointer, :long_long, :pointer, :pointer] - SECRETBOX_KEYBYTES = 32 - wrap_nacl_function :crypto_secretbox, + SecretBox::XSALSA20_POLY1305_KEYBYTES = 32 + SecretBox::XSALSA20_POLY1305_NONCEBYTES = 24 + wrap_nacl_function :crypto_secretbox_xsalsa20poly1305, :crypto_secretbox_xsalsa20poly1305_ref, [:pointer, :pointer, :long_long, :pointer, :pointer] - wrap_nacl_function :crypto_secretbox_open, + wrap_nacl_function :crypto_secretbox_xsalsa20poly1305_open, :crypto_secretbox_xsalsa20poly1305_ref_open, [:pointer, :pointer, :long_long, :pointer, :pointer] diff --git a/lib/rbnacl/nacl/secret_box/xsalsa20_poly1305_box.rb b/lib/rbnacl/nacl/secret_box/xsalsa20_poly1305_box.rb new file mode 100644 index 0000000..49e1c34 --- /dev/null +++ b/lib/rbnacl/nacl/secret_box/xsalsa20_poly1305_box.rb @@ -0,0 +1,102 @@ +# encoding: binary +module Crypto + module NaCl + module SecretBox + # The XSalsa20Poly1305Box class boxes and unboxes messages + # + # 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 + # repudiatable. For non-repudiatable messages, sign them before or after + # encryption. + # + # * Encryption: XSalsa20, a fast stream cipher primitive + # * Authentication: Poly1305, a fast one time authentication primitive + # + # **WARNING**: This class provides direct access to a low-level + # cryptographic method. You should not use this class without good + # reason and instead use the Crypto::SecretBox box class which will + # always point to the best primitive the library provides bindings for. + # It also provides a nicer interface, with e.g. encoding of the resulting + # messages as base64. + class XSalsa20Poly1305Box + # Number of bytes for a secret key + KEYBYTES = NaCl::SecretBox::XSALSA20_POLY1305_KEYBYTES + # Number of bytes for a nonce + NONCEBYTES = NaCl::SecretBox::XSALSA20_POLY1305_NONCEBYTES + + # Create a new SecretBox + # + # Sets up the Box with a secret key fro encrypting and decrypting messages. + # + # @param key [String] The key to encrypt and decrypt with + # + # @raise [Crypto::LengthError] on invalid keys + # + # @return [Crypto::SecretBox] The new Box, ready to use + def initialize(key) + @key = key + Util.check_length(@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 [Crypto::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, NONCEBYTES, "Nonce") + msg = Util.prepend_zeros(NaCl::ZEROBYTES, message) + ct = Util.zeros(msg.bytesize) + + NaCl.crypto_secretbox_xsalsa20poly1305(ct, msg, msg.bytesize, nonce, @key) || raise(CryptoError, "Encryption failed") + Util.remove_zeros(NaCl::BOXZEROBYTES, 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 [Crypto::LengthError] If the nonce is not valid + # @raise [Crypto::CryptoError] If the ciphertext cannot be authenticated. + # + # @return [String] The decrypted message (BINARY encoded) + def open(nonce, ciphertext) + Util.check_length(nonce, NONCEBYTES, "Nonce") + ct = Util.prepend_zeros(NaCl::BOXZEROBYTES, ciphertext) + message = Util.zeros(ct.bytesize) + + NaCl.crypto_secretbox_xsalsa20poly1305_open(message, ct, ct.bytesize, nonce, @key) || raise(CryptoError, "Decryption failed. Ciphertext failed verification.") + Util.remove_zeros(NaCl::ZEROBYTES, message) + end + alias decrypt open + end + end + end +end diff --git a/lib/rbnacl/secret_box.rb b/lib/rbnacl/secret_box.rb index c8c8631..541c15e 100644 --- a/lib/rbnacl/secret_box.rb +++ b/lib/rbnacl/secret_box.rb @@ -1,4 +1,5 @@ # encoding: binary +require 'rbnacl/nacl/secret_box/xsalsa20_poly1305_box' module Crypto # The SecretBox class boxes and unboxes messages # @@ -17,8 +18,7 @@ module Crypto # arbitrary valid messages, so messages you send are repudiatable. For # non-repudiatable messages, sign them before or after encryption. class SecretBox - # Number of bytes for a secret key - KEYBYTES = NaCl::SECRETBOX_KEYBYTES + attr_reader :primitive # Create a new SecretBox # @@ -30,9 +30,9 @@ class SecretBox # @raise [Crypto::LengthError] on invalid keys # # @return [Crypto::SecretBox] The new Box, ready to use - def initialize(key, encoding = :raw) + def initialize(key, encoding = :raw, primitive = NaCl::SecretBox::XSalsa20Poly1305Box) @key = Encoder[encoding].decode(key) if key - Util.check_length(@key, KEYBYTES, "Secret key") + @primitive = primitive.new(@key) end # Encrypts a message @@ -50,12 +50,7 @@ def initialize(key, encoding = :raw) # # @return [String] The ciphertext without the nonce prepended (BINARY encoded) def box(nonce, message) - Util.check_length(nonce, Crypto::NaCl::NONCEBYTES, "Nonce") - msg = Util.prepend_zeros(NaCl::ZEROBYTES, message) - ct = Util.zeros(msg.bytesize) - - NaCl.crypto_secretbox(ct, msg, msg.bytesize, nonce, @key) || raise(CryptoError, "Encryption failed") - Util.remove_zeros(NaCl::BOXZEROBYTES, ct) + self.primitive.box(nonce, message) end alias encrypt box @@ -74,12 +69,7 @@ def box(nonce, message) # # @return [String] The decrypted message (BINARY encoded) def open(nonce, ciphertext) - Util.check_length(nonce, Crypto::NaCl::NONCEBYTES, "Nonce") - ct = Util.prepend_zeros(NaCl::BOXZEROBYTES, ciphertext) - message = Util.zeros(ct.bytesize) - - NaCl.crypto_secretbox_open(message, ct, ct.bytesize, nonce, @key) || raise(CryptoError, "Decryption failed. Ciphertext failed verification.") - Util.remove_zeros(NaCl::ZEROBYTES, message) + self.primitive.open(nonce, ciphertext) end alias decrypt open end diff --git a/spec/rbnacl/nacl/secret_box/xsalsa20_poly1305_box_spec.rb b/spec/rbnacl/nacl/secret_box/xsalsa20_poly1305_box_spec.rb new file mode 100644 index 0000000..426e266 --- /dev/null +++ b/spec/rbnacl/nacl/secret_box/xsalsa20_poly1305_box_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Crypto::NaCl::SecretBox::XSalsa20Poly1305Box do + + let (:key) { hex2bytes(Crypto::TestVectors[:secret_key]) } + + context "new" do + it "accepts strings" do + expect { described_class.new(key) }.to_not raise_error(Exception) + end + + it "raises on a nil key" do + expect { described_class.new(nil) }.to raise_error(Crypto::LengthError, "Secret key was nil \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)") + end + + it "raises on a short key" do + expect { described_class.new("hello") }.to raise_error(Crypto::LengthError, "Secret key was 5 bytes \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)") + end + end + + include_examples "box" do + let(:box) { described_class.new(key) } + end +end diff --git a/spec/rbnacl/secret_box_spec.rb b/spec/rbnacl/secret_box_spec.rb index 3d9b23a..041f4ab 100644 --- a/spec/rbnacl/secret_box_spec.rb +++ b/spec/rbnacl/secret_box_spec.rb @@ -10,11 +10,11 @@ end it "raises on a nil key" do - expect { Crypto::SecretBox.new(nil) }.to raise_error(Crypto::LengthError, "Secret key was nil \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)") + expect { Crypto::SecretBox.new(nil) }.to raise_error(Crypto::LengthError, "Secret key was nil \(Expected #{Crypto::NaCl::SecretBox::XSALSA20_POLY1305_KEYBYTES}\)") end it "raises on a short key" do - expect { Crypto::SecretBox.new("hello") }.to raise_error(Crypto::LengthError, "Secret key was 5 bytes \(Expected #{Crypto::NaCl::SECRETKEYBYTES}\)") + expect { Crypto::SecretBox.new("hello") }.to raise_error(Crypto::LengthError, "Secret key was 5 bytes \(Expected #{Crypto::NaCl::SecretBox::XSALSA20_POLY1305_KEYBYTES}\)") end end