From 566dc28756624108dbc289c9ae040c39b34cc84b Mon Sep 17 00:00:00 2001 From: "Robin H. Johnson" Date: Sat, 30 Dec 2017 23:26:07 -0800 Subject: [PATCH 1/2] Improve constant helper function naming Not all of the libsodium constants contain the primitive in their name. Introduce clearer functions for importing constants: - `sodium_type_primitive_constant` - `sodium_type_constant` The old `sodium_constant` function is preserved via alias, in case external consumers depended on the API. Signed-off-by: Robin H. Johnson --- lib/rbnacl/aead/chacha20poly1305_ietf.rb | 6 +++--- lib/rbnacl/aead/chacha20poly1305_legacy.rb | 6 +++--- lib/rbnacl/hash/blake2b.rb | 12 ++++++------ lib/rbnacl/hash/sha256.rb | 2 +- lib/rbnacl/hash/sha512.rb | 2 +- lib/rbnacl/hmac/sha256.rb | 4 ++-- lib/rbnacl/hmac/sha512.rb | 4 ++-- lib/rbnacl/hmac/sha512256.rb | 4 ++-- lib/rbnacl/one_time_auths/poly1305.rb | 4 ++-- lib/rbnacl/password_hash/argon2.rb | 18 +++++++++--------- lib/rbnacl/password_hash/scrypt.rb | 2 +- lib/rbnacl/secret_boxes/xsalsa20poly1305.rb | 8 ++++---- lib/rbnacl/sodium.rb | 20 +++++++++++++++++--- 13 files changed, 53 insertions(+), 39 deletions(-) diff --git a/lib/rbnacl/aead/chacha20poly1305_ietf.rb b/lib/rbnacl/aead/chacha20poly1305_ietf.rb index f5c3848..340bf82 100644 --- a/lib/rbnacl/aead/chacha20poly1305_ietf.rb +++ b/lib/rbnacl/aead/chacha20poly1305_ietf.rb @@ -11,9 +11,9 @@ class ChaCha20Poly1305IETF < RbNaCl::AEAD::Base sodium_type :aead sodium_primitive :chacha20poly1305_ietf - sodium_constant :KEYBYTES - sodium_constant :NPUBBYTES - sodium_constant :ABYTES + sodium_type_primitive_constant :KEYBYTES + sodium_type_primitive_constant :NPUBBYTES + sodium_type_primitive_constant :ABYTES sodium_function :aead_chacha20poly1305_ietf_encrypt, :crypto_aead_chacha20poly1305_ietf_encrypt, diff --git a/lib/rbnacl/aead/chacha20poly1305_legacy.rb b/lib/rbnacl/aead/chacha20poly1305_legacy.rb index d7218cb..3fabff0 100644 --- a/lib/rbnacl/aead/chacha20poly1305_legacy.rb +++ b/lib/rbnacl/aead/chacha20poly1305_legacy.rb @@ -10,9 +10,9 @@ class ChaCha20Poly1305Legacy < RbNaCl::AEAD::Base sodium_type :aead sodium_primitive :chacha20poly1305 - sodium_constant :KEYBYTES - sodium_constant :NPUBBYTES - sodium_constant :ABYTES + sodium_type_primitive_constant :KEYBYTES + sodium_type_primitive_constant :NPUBBYTES + sodium_type_primitive_constant :ABYTES sodium_function :aead_chacha20poly1305_encrypt, :crypto_aead_chacha20poly1305_encrypt, diff --git a/lib/rbnacl/hash/blake2b.rb b/lib/rbnacl/hash/blake2b.rb index 8d96579..121a171 100644 --- a/lib/rbnacl/hash/blake2b.rb +++ b/lib/rbnacl/hash/blake2b.rb @@ -17,12 +17,12 @@ class Blake2b sodium_type :generichash sodium_primitive :blake2b - sodium_constant :BYTES_MIN - sodium_constant :BYTES_MAX - sodium_constant :KEYBYTES_MIN - sodium_constant :KEYBYTES_MAX - sodium_constant :SALTBYTES - sodium_constant :PERSONALBYTES + sodium_type_primitive_constant :BYTES_MIN + sodium_type_primitive_constant :BYTES_MAX + sodium_type_primitive_constant :KEYBYTES_MIN + sodium_type_primitive_constant :KEYBYTES_MAX + sodium_type_primitive_constant :SALTBYTES + sodium_type_primitive_constant :PERSONALBYTES sodium_function :generichash_blake2b, :crypto_generichash_blake2b_salt_personal, diff --git a/lib/rbnacl/hash/sha256.rb b/lib/rbnacl/hash/sha256.rb index 9e721d8..aef3014 100644 --- a/lib/rbnacl/hash/sha256.rb +++ b/lib/rbnacl/hash/sha256.rb @@ -8,7 +8,7 @@ module SHA256 extend Sodium sodium_type :hash sodium_primitive :sha256 - sodium_constant :BYTES + sodium_type_primitive_constant :BYTES sodium_function :hash_sha256, :crypto_hash_sha256, %i[pointer pointer ulong_long] diff --git a/lib/rbnacl/hash/sha512.rb b/lib/rbnacl/hash/sha512.rb index facd114..9f5601d 100644 --- a/lib/rbnacl/hash/sha512.rb +++ b/lib/rbnacl/hash/sha512.rb @@ -8,7 +8,7 @@ module SHA512 extend Sodium sodium_type :hash sodium_primitive :sha512 - sodium_constant :BYTES + sodium_type_primitive_constant :BYTES sodium_function :hash_sha512, :crypto_hash_sha512, %i[pointer pointer ulong_long] diff --git a/lib/rbnacl/hmac/sha256.rb b/lib/rbnacl/hmac/sha256.rb index 23bdf24..ee9e98e 100644 --- a/lib/rbnacl/hmac/sha256.rb +++ b/lib/rbnacl/hmac/sha256.rb @@ -19,8 +19,8 @@ class SHA256 < Auth sodium_type :auth sodium_primitive :hmacsha256 - sodium_constant :BYTES - sodium_constant :KEYBYTES + sodium_type_primitive_constant :BYTES + sodium_type_primitive_constant :KEYBYTES sodium_function :auth_hmacsha256, :crypto_auth_hmacsha256, diff --git a/lib/rbnacl/hmac/sha512.rb b/lib/rbnacl/hmac/sha512.rb index 93ad3db..d5e65f0 100644 --- a/lib/rbnacl/hmac/sha512.rb +++ b/lib/rbnacl/hmac/sha512.rb @@ -19,8 +19,8 @@ class SHA512 < Auth sodium_type :auth sodium_primitive :hmacsha512 - sodium_constant :BYTES - sodium_constant :KEYBYTES + sodium_type_primitive_constant :BYTES + sodium_type_primitive_constant :KEYBYTES sodium_function :auth_hmacsha512, :crypto_auth_hmacsha512, diff --git a/lib/rbnacl/hmac/sha512256.rb b/lib/rbnacl/hmac/sha512256.rb index 7b4c5a7..bf2c654 100644 --- a/lib/rbnacl/hmac/sha512256.rb +++ b/lib/rbnacl/hmac/sha512256.rb @@ -19,8 +19,8 @@ class SHA512256 < Auth sodium_type :auth sodium_primitive :hmacsha512256 - sodium_constant :BYTES - sodium_constant :KEYBYTES + sodium_type_primitive_constant :BYTES + sodium_type_primitive_constant :KEYBYTES sodium_function :auth_hmacsha512256, :crypto_auth_hmacsha512256, diff --git a/lib/rbnacl/one_time_auths/poly1305.rb b/lib/rbnacl/one_time_auths/poly1305.rb index 6a277cf..4e1fa48 100644 --- a/lib/rbnacl/one_time_auths/poly1305.rb +++ b/lib/rbnacl/one_time_auths/poly1305.rb @@ -24,8 +24,8 @@ class Poly1305 < Auth sodium_type :onetimeauth sodium_primitive :poly1305 - sodium_constant :BYTES - sodium_constant :KEYBYTES + sodium_type_primitive_constant :BYTES + sodium_type_primitive_constant :KEYBYTES sodium_function :onetimeauth_poly1305, :crypto_onetimeauth_poly1305, diff --git a/lib/rbnacl/password_hash/argon2.rb b/lib/rbnacl/password_hash/argon2.rb index ff9e85b..4e26d98 100644 --- a/lib/rbnacl/password_hash/argon2.rb +++ b/lib/rbnacl/password_hash/argon2.rb @@ -15,15 +15,15 @@ class Argon2 sodium_type :pwhash sodium_primitive :argon2i - sodium_constant :ALG_ARGON2I13 - sodium_constant :SALTBYTES - sodium_constant :STRBYTES - sodium_constant :OPSLIMIT_INTERACTIVE # 4 - sodium_constant :MEMLIMIT_INTERACTIVE # 2 ** 25 (32mb) - sodium_constant :OPSLIMIT_MODERATE # 6 - sodium_constant :MEMLIMIT_MODERATE # 2 ** 27 (128mb) - sodium_constant :OPSLIMIT_SENSITIVE # 8 - sodium_constant :MEMLIMIT_SENSITIVE # 2 ** 29 (512mb) + sodium_type_primitive_constant :ALG_ARGON2I13 + sodium_type_primitive_constant :SALTBYTES + sodium_type_primitive_constant :STRBYTES + sodium_type_primitive_constant :OPSLIMIT_INTERACTIVE # 4 + sodium_type_primitive_constant :MEMLIMIT_INTERACTIVE # 2 ** 25 (32mb) + sodium_type_primitive_constant :OPSLIMIT_MODERATE # 6 + sodium_type_primitive_constant :MEMLIMIT_MODERATE # 2 ** 27 (128mb) + sodium_type_primitive_constant :OPSLIMIT_SENSITIVE # 8 + sodium_type_primitive_constant :MEMLIMIT_SENSITIVE # 2 ** 29 (512mb) ARGON2_MIN_OUTLEN = 16 ARGON2_MAX_OUTLEN = 0xFFFFFFFF diff --git a/lib/rbnacl/password_hash/scrypt.rb b/lib/rbnacl/password_hash/scrypt.rb index ada0557..00a507e 100644 --- a/lib/rbnacl/password_hash/scrypt.rb +++ b/lib/rbnacl/password_hash/scrypt.rb @@ -22,7 +22,7 @@ class SCrypt sodium_type :pwhash sodium_primitive :scryptsalsa208sha256 - sodium_constant :SALTBYTES + sodium_type_primitive_constant :SALTBYTES sodium_function :scrypt, :crypto_pwhash_scryptsalsa208sha256, diff --git a/lib/rbnacl/secret_boxes/xsalsa20poly1305.rb b/lib/rbnacl/secret_boxes/xsalsa20poly1305.rb index 0487964..f421024 100644 --- a/lib/rbnacl/secret_boxes/xsalsa20poly1305.rb +++ b/lib/rbnacl/secret_boxes/xsalsa20poly1305.rb @@ -24,10 +24,10 @@ class XSalsa20Poly1305 sodium_type :secretbox sodium_primitive :xsalsa20poly1305 - sodium_constant :KEYBYTES - sodium_constant :NONCEBYTES - sodium_constant :ZEROBYTES - sodium_constant :BOXZEROBYTES + sodium_type_primitive_constant :KEYBYTES + sodium_type_primitive_constant :NONCEBYTES + sodium_type_primitive_constant :ZEROBYTES + sodium_type_primitive_constant :BOXZEROBYTES sodium_function :secretbox_xsalsa20poly1305, :crypto_secretbox_xsalsa20poly1305, diff --git a/lib/rbnacl/sodium.rb b/lib/rbnacl/sodium.rb index 5e35be7..bd2081d 100644 --- a/lib/rbnacl/sodium.rb +++ b/lib/rbnacl/sodium.rb @@ -29,10 +29,14 @@ def primitive sodium_primitive end - def sodium_constant(constant, name = constant) + def sodium_type_primitive_constant(constant, name = constant) fn = "crypto_#{sodium_type}_#{sodium_primitive}_#{constant.to_s.downcase}" - attach_function fn, [], :size_t - const_set(name, public_send(fn)) + _generic_constant(fn, name) + end + + def sodium_type_constant(constant, name = constant) + fn = "crypto_#{sodium_type}_#{constant.to_s.downcase}" + _generic_constant(fn, name) end def sodium_function(name, function, arguments) @@ -53,5 +57,15 @@ def self.#{name}(*args) end RUBY end + + # this alias exists to ensure we have a stable API + # remove for 6.0 + alias sodium_constant sodium_type_primitive_constant + + private + def _generic_constant(fn, name) + attach_function fn, [], :size_t + const_set(name, public_send(fn)) + end end end From 1dba4a9f7026d890cef56a31fc094be5e41a7927 Mon Sep 17 00:00:00 2001 From: "Robin H. Johnson" Date: Sat, 30 Dec 2017 23:16:22 -0800 Subject: [PATCH 2/2] WIP: Implement Sealed Boxes Implement Sealed Boxes. Requires >=libsodium-1.0.3. TODO before merging: - finish docs - handle error case for too-old libsodium See: https://download.libsodium.org/doc/public-key_cryptography/sealed_boxes.html Closes: https://github.com/cryptosphere/rbnacl/issues/120 Signed-off-by: Robin H. Johnson --- lib/rbnacl.rb | 4 + lib/rbnacl/boxes/sealed.rb | 190 +++++++++++++++++++++++++++++++ spec/rbnacl/boxes/sealed_spec.rb | 63 ++++++++++ 3 files changed, 257 insertions(+) create mode 100644 lib/rbnacl/boxes/sealed.rb create mode 100644 spec/rbnacl/boxes/sealed_spec.rb diff --git a/lib/rbnacl.rb b/lib/rbnacl.rb index 228e1f2..95ecb06 100644 --- a/lib/rbnacl.rb +++ b/lib/rbnacl.rb @@ -46,6 +46,9 @@ class BadAuthenticatorError < CryptoError; end require "rbnacl/boxes/curve25519xsalsa20poly1305/private_key" require "rbnacl/boxes/curve25519xsalsa20poly1305/public_key" + # Sealed boxes + require "rbnacl/boxes/sealed" + # Secret Key Encryption (SecretBox): XSalsa20Poly1305 require "rbnacl/secret_boxes/xsalsa20poly1305" @@ -88,6 +91,7 @@ class BadAuthenticatorError < CryptoError; end Box = Boxes::Curve25519XSalsa20Poly1305 PrivateKey = Boxes::Curve25519XSalsa20Poly1305::PrivateKey PublicKey = Boxes::Curve25519XSalsa20Poly1305::PublicKey + SealedBox = Boxes::Sealed SecretBox = SecretBoxes::XSalsa20Poly1305 SigningKey = Signatures::Ed25519::SigningKey VerifyKey = Signatures::Ed25519::VerifyKey diff --git a/lib/rbnacl/boxes/sealed.rb b/lib/rbnacl/boxes/sealed.rb new file mode 100644 index 0000000..92b45c0 --- /dev/null +++ b/lib/rbnacl/boxes/sealed.rb @@ -0,0 +1,190 @@ +# encoding: binary +# frozen_string_literal: true + +module RbNaCl + module Boxes + # The Box class boxes and unboxes messages between a pair of keys + # + # This class uses the given public and secret keys to derive a shared key, + # which is used with the nonce given to encrypt the given messages and + # decrypt the given ciphertexts. The same shared key will generated from + # both pairing of keys, so given two keypairs belonging to alice (pkalice, + # skalice) and bob(pkbob, skbob), the key derived from (pkalice, skbob) with + # equal that from (pkbob, skalice). This is how the system works: + # + # @example + # # On bob's system + # bobkey = RbNaCl::PrivateKey.generate + # #=> # + # + # # send bobkey.public_key to alice + # # receive alice's public key, alicepk + # # NB: This is actually the hard part of the system. How to do it securely + # # is left as an exercise to for the reader. + # alice_pubkey = "..." + # + # # make a box + # alicebob_box = RbNaCl::Box.new(alice_pubkey, bobkey) + # #=> # + # + # # encrypt a message to alice + # cipher_text = alicebob_box.box("A bad example of a nonce", "Hello, Alice!") + # #=> "..." # a string of bytes, 29 bytes long + # + # # send ["A bad example of a nonce", cipher_text] to alice + # # note that nonces don't have to be secret + # # receive [nonce, cipher_text_to_bob] from alice + # + # # decrypt the reply + # # Alice has been a little more sensible than bob, and has a random nonce + # # that is too fiddly to type here. But there are other choices than just + # # random + # plain_text = alicebob_box.open(nonce, cipher_text_to_bob) + # #=> "Hey there, Bob!" + # + # # we have a new message! + # # But Eve has tampered with this message, by flipping some bytes around! + # # [nonce2, cipher_text_to_bob_honest_love_eve] + # alicebob_box.open(nonce2, cipher_text_to_bob_honest_love_eve) + # + # # BOOM! + # # Bob gets a RbNaCl::CryptoError to deal with! + # + # 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 the messages encrypted. Also, bear in mind + # the property mentioned just above. 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 Sealed + extend Sodium + + sodium_type :box + sodium_primitive :curve25519xsalsa20poly1305 + sodium_type_constant :SEALBYTES + + sodium_function :box_seal, + :crypto_box_seal, + %i[pointer pointer ulong_long pointer] + + sodium_function :box_seal_open, + :crypto_box_seal_open, + %i[pointer pointer ulong_long pointer pointer] + + # WARNING: you should strongly prefer the from_private_key/from_public_key class methods. + # + # Create a new Sealed Box + # + # Sets up the Box for deriving the shared key and encrypting and + # decrypting messages. + # + # @param public_key [String,RbNaCl::PublicKey] The public key to encrypt to + # @param private_key [String,RbNaCl::PrivateKey] The private key to decrypt with + # + # @raise [RbNaCl::LengthError] on invalid keys + # + # @return [RbNaCl::SealedBox] The new Box, ready to use + private + def initialize(public_key, private_key = nil) + unless private_key.nil? + @private_key = private_key.is_a?(PrivateKey) ? private_key : PrivateKey.new(private_key) + raise IncorrectPrimitiveError unless @private_key.primitive == primitive + if public_key.nil? + public_key = @private_key.public_key + end + end + + @public_key = public_key.is_a?(PublicKey) ? public_key : PublicKey.new(public_key) + raise IncorrectPrimitiveError unless @public_key.primitive == primitive + end + + public + # Create a new Sealed Box for encrypting + # + # Sets up the Box for encryoption of new messages. + # + # @param private_key [String,RbNaCl::PrivateKey] The private key to decrypt with + # + # @raise [RbNaCl::LengthError] on invalid keys + # + # @return [RbNaCl::SealedBox] The new Box, ready to use + def self.from_private_key(private_key) + new(nil, private_key) + end + + # Create a new Sealed Box for decrypting + # + # Sets up the Box for decrytoption of new messages. + # + # @param public_key [String,RbNaCl::PublicKey] The public key to encrypt to + # + # @raise [RbNaCl::LengthError] on invalid keys + # + # @return [RbNaCl::SealedBox] The new Box, ready to use + def self.from_public_key(public_key) + new(public_key, nil) + end + + # Encrypts a message + # + # TODO + # + # @param message [String] The message to be encrypted. + # + # @raise [RbNaCl::CryptoError] If the encrytion fails. + # + # @return [String] The ciphertext (BINARY encoded) + def box(message) + # No padding needed. + msg = message # variable name to match other RbNaCl code. + # ensure enough space in result + ct = Util.zeros(msg.bytesize + SEALBYTES) + + success = self.class.box_seal(ct, msg, msg.bytesize, @public_key.to_s) + raise CryptoError, "Encryption failed" unless success + + ct + end + alias encrypt box + + # Decrypts a ciphertext + # + # TODO + # + # @param ciphertext [String] The message to be decrypted. + # + # @raise [RbNaCl::CryptoError] If no private key is available. + # @raise [RbNaCl::CryptoError] If the ciphertext cannot be authenticated. + # + # @return [String] The decrypted message (BINARY encoded) + def open(ciphertext) + raise CryptoError, "Decryption failed. No private key." unless @private_key + ct = ciphertext + message = Util.zeros(ct.bytesize - SEALBYTES) + + success = self.class.box_seal_open(message, ct, ct.bytesize, @public_key.to_s, @private_key.to_s) + raise CryptoError, "Decryption failed. Ciphertext failed verification." unless success + + message + end + alias decrypt open + + # The crypto primitive for the box class + # + # @return [Symbol] The primitive used + def primitive + self.class.primitive + end + + private + + end + end +end diff --git a/spec/rbnacl/boxes/sealed_spec.rb b/spec/rbnacl/boxes/sealed_spec.rb new file mode 100644 index 0000000..c48a70e --- /dev/null +++ b/spec/rbnacl/boxes/sealed_spec.rb @@ -0,0 +1,63 @@ +# encoding: binary +# frozen_string_literal: true + +RSpec.describe RbNaCl::Box do + let(:alicepk) { vector :alice_public } + let(:bobsk) { vector :bob_private } + let(:alice_key) { RbNaCl::PublicKey.new(alicepk) } + let(:bob_key) { RbNaCl::PrivateKey.new(bobsk) } + + context "new" do + it "accepts public key strings" do + expect do + RbNaCl::SealedBox.from_public_key(alicepk) + end.to_not raise_error + end + + it "accepts public KeyPairs" do + expect do + RbNaCl::SealedBox.from_public_key(alice_key) + end.to_not raise_error + end + + it "accepts private key strings" do + expect do + RbNaCl::SealedBox.from_private_key(bobpk) + end.to_not raise_error + end + + it "accepts private KeyPairs" do + expect do + RbNaCl::SealedBox.from_private_key(bob_key) + end.to_not raise_error + end + + it "raises TypeError on a nil public key" do + expect do + RbNaCl::SealedBox.from_public_key(nil) + end.to raise_error(TypeError) + end + + it "raises RbNaCl::LengthError on an invalid public key" do + expect do + RbNaCl::SealedBox.from_public_key("hello") + end.to raise_error(RbNaCl::LengthError, /Public key was 5 bytes \(Expected 32\)/) + end + + it "raises TypeError on a nil secret key" do + expect do + RbNaCl::SealedBox.from_secret_key(nil) + end.to raise_error(TypeError) + end + + it "raises RbNaCl::LengthError on an invalid secret key" do + expect do + RbNaCl::SealedBox.from_secret_key("hello") + end.to raise_error(RbNaCl::LengthError, /Private key was 5 bytes \(Expected 32\)/) + end + end + + include_examples "box" do + let(:box) { RbNaCl::SealedBox.from_public_key(alicepk) } + end +end