From 41a55a9593b229346a987d9ea2a512c72b668ebd Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Sat, 29 Jun 2019 12:44:47 -0700 Subject: [PATCH] Sodum::CryptoBox::SecretKey may derive keys from a seed. Sodum::Sign::SecretKey may derive keys from a seed. Sodum::CryptoBox::Pair renamed to Sodum::CryptoBox::Box --- README.md | 6 +-- spec/sodium/crypto_box/secret_key_spec.cr | 31 ++++++++++++++-- spec/sodium/sign/secret_key.cr | 25 +++++++++++++ spec/spec_helper.cr | 7 ++++ src/sodium/crypto_box/{pair.cr => box.cr} | 2 +- src/sodium/crypto_box/public_key.cr | 2 +- src/sodium/crypto_box/secret_key.cr | 39 +++++++++++++++----- src/sodium/lib_sodium.cr | 29 +++++++++++++-- src/sodium/secret_box.cr | 5 ++- src/sodium/sign/public_key.cr | 4 +- src/sodium/sign/secret_key.cr | 45 ++++++++++++++++------- 11 files changed, 155 insertions(+), 40 deletions(-) rename src/sodium/crypto_box/{pair.cr => box.cr} (98%) diff --git a/README.md b/README.md index bba84aa..251c76e 100644 --- a/README.md +++ b/README.md @@ -144,14 +144,14 @@ alice = Sodium::CryptoBox::SecretKey.new bob = Sodium::CryptoBox::SecretKey.new # Precompute a shared secret between alice and bob. -pair = alice.pair bob.public_key +box = alice.box bob.public_key # Encrypt a message for Bob using his public key, signing it with Alice's # secret key -nonce, encrypted = pair.encrypt data +nonce, encrypted = box.encrypt data # Precompute within a block. The shared secret is wiped when the block exits. -bob.pair alice.public_key do |pair| +bob.box alice.public_key do |box| # Decrypt the message using Bob's secret key, and verify its signature against # Alice's public key decrypted = Sodium.decrypt(encrypted, nonce, alice.public, bob.secret) diff --git a/spec/sodium/crypto_box/secret_key_spec.cr b/spec/sodium/crypto_box/secret_key_spec.cr index 542c9b5..82a22e7 100644 --- a/spec/sodium/crypto_box/secret_key_spec.cr +++ b/spec/sodium/crypto_box/secret_key_spec.cr @@ -1,6 +1,25 @@ require "../../spec_helper" +private def new_key_bytes + Sodium::CryptoBox::SecretKey.new.bytes +end + describe Sodium::CryptoBox::SecretKey do + it "loads keys" do + key1 = Sodium::CryptoBox::SecretKey.new + key2 = Sodium::CryptoBox::SecretKey.new key1.bytes, key1.public_key.bytes + key1.bytes.should eq key2.bytes + key1.public_key.bytes.should eq key2.public_key.bytes + end + + it "seed keys" do + seed = Bytes.new Sodium::CryptoBox::SecretKey::SEED_SIZE + key1 = Sodium::CryptoBox::SecretKey.new seed: seed + key2 = Sodium::CryptoBox::SecretKey.new seed: seed + key1.bytes.should eq key2.bytes + key1.public_key.bytes.should eq key2.public_key.bytes + end + it "easy encrypt/decrypt" do data = "Hello World!" @@ -12,15 +31,19 @@ describe Sodium::CryptoBox::SecretKey do # Encrypt a message for Bob using his public key, signing it with Alice's # secret key - pair = alice.pair bob.public_key - nonce, encrypted = pair.encrypt_easy data + box = alice.box bob.public_key + nonce, encrypted = box.encrypt_easy data # Decrypt the message using Bob's secret key, and verify its signature against # Alice's public key - bob.pair alice.public_key do |pair| - decrypted = pair.decrypt_easy encrypted, nonce: nonce + bob.box alice.public_key do |box| + decrypted = box.decrypt_easy encrypted, nonce: nonce String.new(decrypted).should eq(data) end end + + it "wipes keys" do + check_wiped new_key_bytes + end end diff --git a/spec/sodium/sign/secret_key.cr b/spec/sodium/sign/secret_key.cr index 5c3299e..1aa3f1f 100644 --- a/spec/sodium/sign/secret_key.cr +++ b/spec/sodium/sign/secret_key.cr @@ -1,7 +1,28 @@ require "../../spec_helper" require "../../../src/sodium/sign/secret_key" +private def new_key_bytes + Sodium::Sign::SecretKey.new.bytes +end + describe Sodium::Sign::SecretKey do + it "loads keys" do + key1 = Sodium::Sign::SecretKey.new + key2 = Sodium::Sign::SecretKey.new key1.bytes, key1.public_key.bytes + key1.bytes.should eq key2.bytes + key1.public_key.bytes.should eq key2.public_key.bytes + + # TODO: test loading when missing public_key + end + + it "seed keys" do + seed = Bytes.new Sodium::Sign::SecretKey::SEED_SIZE + key1 = Sodium::Sign::SecretKey.new seed: seed + key2 = Sodium::Sign::SecretKey.new seed: seed + key1.bytes.should eq key2.bytes + key1.public_key.bytes.should eq key2.public_key.bytes + end + it "signs and verifies" do message = "foo" skey = Sodium::Sign::SecretKey.new @@ -19,4 +40,8 @@ describe Sodium::Sign::SecretKey do skey.public_key.verify_detached "bar", sig end end + + it "checks wiped" do + check_wiped new_key_bytes + end end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 4ee79af..0d1fccc 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,2 +1,9 @@ require "spec" require "../src/sodium" + +def check_wiped(buf : Bytes) + GC.collect + buf.each do |b| + raise "not wiped #{buf.inspect}" if b != 0_u8 + end +end diff --git a/src/sodium/crypto_box/pair.cr b/src/sodium/crypto_box/box.cr similarity index 98% rename from src/sodium/crypto_box/pair.cr rename to src/sodium/crypto_box/box.cr index 485d24f..7d12faf 100644 --- a/src/sodium/crypto_box/pair.cr +++ b/src/sodium/crypto_box/box.cr @@ -1,7 +1,7 @@ require "../lib_sodium" module Sodium::CryptoBox - class Pair + class Box include Wipe # BUG: precompute size diff --git a/src/sodium/crypto_box/public_key.cr b/src/sodium/crypto_box/public_key.cr index 64b095e..ea02d54 100644 --- a/src/sodium/crypto_box/public_key.cr +++ b/src/sodium/crypto_box/public_key.cr @@ -3,7 +3,7 @@ require "../lib_sodium" module Sodium::CryptoBox class PublicKey < Key include Wipe - KEY_SIZE = LibSodium::PUBLIC_KEY_SIZE + KEY_SIZE = LibSodium.crypto_box_publickeybytes getter bytes : Bytes diff --git a/src/sodium/crypto_box/secret_key.cr b/src/sodium/crypto_box/secret_key.cr index 98e012a..1e49c78 100644 --- a/src/sodium/crypto_box/secret_key.cr +++ b/src/sodium/crypto_box/secret_key.cr @@ -3,36 +3,55 @@ require "../lib_sodium" module Sodium::CryptoBox class SecretKey < Key include Wipe - KEY_SIZE = LibSodium::SECRET_KEY_SIZE - MAC_SIZE = LibSodium::MAC_SIZE + KEY_SIZE = LibSodium.crypto_box_secretkeybytes + SEED_SIZE = LibSodium.crypto_box_seedbytes + MAC_SIZE = LibSodium::MAC_SIZE getter public_key getter bytes : Bytes + @seed : Bytes? - # Generate a new secret/public key pair. + # Generate a new random secret/public key pair. def initialize pkey = Bytes.new(PublicKey::KEY_SIZE) @bytes = Bytes.new(KEY_SIZE) @public_key = PublicKey.new pkey - LibSodium.crypto_box_keypair(pkey, @bytes) + v = LibSodium.crypto_box_keypair(pkey, @bytes) + if v != 0 + raise Sodium::Error.new("crypto_box_keypair #{v}") + end end # Use existing Secret and Public keys. def initialize(@bytes : Bytes, pkey : Bytes) + # TODO: finish regenerating public_key if bytes.bytesize != KEY_SIZE raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") end @public_key = PublicKey.new pkey end - # Return a Pair containing a precomputed shared secret for use with encryption/decryption. - def pair(public_key) : Pair - Pair.new self, public_key + # Derive a new secret/public key pair based on a consistent seed. + def initialize(*, seed : Bytes) + raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE + @seed = seed + + pkey = Bytes.new(PublicKey::KEY_SIZE) + @bytes = Bytes.new(KEY_SIZE) + @public_key = PublicKey.new pkey + if LibSodium.crypto_box_seed_keypair(pkey, @bytes, seed) != 0 + raise Sodium::Error.new("crypto_box_seed_keypair") + end end - # Create a new pair and automatically close when the block exits. - def pair(public_key) - pa = pair public_key + # Return a Box containing a precomputed shared secret for use with encryption/decryption. + def box(public_key) : Box + Box.new self, public_key + end + + # Create a new box and automatically close when the block exits. + def box(public_key) + pa = box public_key begin yield pa ensure diff --git a/src/sodium/lib_sodium.cr b/src/sodium/lib_sodium.cr index c30d9ec..d041cc7 100644 --- a/src/sodium/lib_sodium.cr +++ b/src/sodium/lib_sodium.cr @@ -5,11 +5,16 @@ module Sodium fun crypto_box_publickeybytes : LibC::SizeT fun crypto_box_secretkeybytes : LibC::SizeT + fun crypto_box_seedbytes : LibC::SizeT fun crypto_box_noncebytes : LibC::SizeT fun crypto_box_macbytes : LibC::SizeT fun crypto_sign_publickeybytes : LibC::SizeT fun crypto_sign_secretkeybytes : LibC::SizeT fun crypto_sign_bytes : LibC::SizeT + fun crypto_sign_seedbytes : LibC::SizeT + fun crypto_secretbox_keybytes : LibC::SizeT + fun crypto_secretbox_noncebytes : LibC::SizeT + fun crypto_secretbox_macbytes : LibC::SizeT fun crypto_kdf_keybytes : LibC::SizeT fun crypto_kdf_contextbytes : LibC::SizeT fun crypto_pwhash_memlimit_min : LibC::SizeT @@ -41,8 +46,6 @@ module Sodium SECRET_KEY_SIZE = crypto_box_secretkeybytes() NONCE_SIZE = crypto_box_noncebytes() MAC_SIZE = crypto_box_macbytes() - PUBLIC_SIGN_SIZE = crypto_sign_publickeybytes() - SECRET_SIGN_SIZE = crypto_sign_secretkeybytes() SIGNATURE_SIZE = crypto_sign_bytes() KDF_KEY_SIZE = crypto_kdf_keybytes() KDF_CONTEXT_SIZE = crypto_kdf_contextbytes() @@ -82,7 +85,13 @@ module Sodium fun crypto_box_keypair( public_key_output : Pointer(LibC::UChar), secret_key_output : Pointer(LibC::UChar) - ) + ) : LibC::Int + + fun crypto_box_seed_keypair( + public_key_output : Pointer(LibC::UChar), + secret_key_output : Pointer(LibC::UChar), + seed : Pointer(LibC::UChar) + ) : LibC::Int fun crypto_box_easy( output : Pointer(LibC::UChar), @@ -107,6 +116,12 @@ module Sodium secret_key_output : Pointer(LibC::UChar) ) : LibC::Int + fun crypto_sign_seed_keypair( + public_key_output : Pointer(LibC::UChar), + secret_key_output : Pointer(LibC::UChar), + seed : Pointer(LibC::UChar) + ) : LibC::Int + fun crypto_sign_detached( signature_output : Pointer(LibC::UChar), signature_output_size : Pointer(LibC::ULongLong), @@ -182,4 +197,12 @@ module Sodium output_len : UInt64 ) : LibC::Int end + + if LibSodium.crypto_secretbox_noncebytes != LibSodium.crypto_box_noncebytes + raise "Assumptions in this library regarding nonce sizes may not be valid" + end + + if LibSodium.crypto_secretbox_macbytes != LibSodium.crypto_box_macbytes + raise "Assumptions in this library regarding mac sizes may not be valid" + end end diff --git a/src/sodium/secret_box.cr b/src/sodium/secret_box.cr index 53adf65..3327594 100644 --- a/src/sodium/secret_box.cr +++ b/src/sodium/secret_box.cr @@ -13,8 +13,9 @@ module Sodium # message = key.decrypt_easy encrypted, nonce # ``` class SecretBox < Key - KEY_SIZE = LibSodium::SECRET_KEY_SIZE - MAC_SIZE = LibSodium::MAC_SIZE + KEY_SIZE = LibSodium.crypto_secretbox_keybytes + NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes + MAC_SIZE = LibSodium::MAC_SIZE property bytes : Bytes diff --git a/src/sodium/sign/public_key.cr b/src/sodium/sign/public_key.cr index 8b063f2..83529cc 100644 --- a/src/sodium/sign/public_key.cr +++ b/src/sodium/sign/public_key.cr @@ -3,7 +3,7 @@ require "../lib_sodium" module Sodium class Sign::PublicKey < Key include Wipe - KEY_SIZE = LibSodium::PUBLIC_SIGN_SIZE + KEY_SIZE = LibSodium.crypto_sign_publickeybytes getter bytes : Bytes @@ -20,7 +20,7 @@ module Sodium end def verify_detached(message : Bytes, sig : Bytes) - raise ArgumentError.new("Signature must be #{LibSodium::SIGNATURE_SIZE} bytes, got #{sig.bytesize}") + raise ArgumentError.new("Signature must be #{LibSodium::SIGNATURE_SIZE} bytes, got #{sig.bytesize}") if sig.bytesize != LibSodium::SIGNATURE_SIZE v = LibSodium.crypto_sign_verify_detached sig, message, message.bytesize, @bytes if v != 0 diff --git a/src/sodium/sign/secret_key.cr b/src/sodium/sign/secret_key.cr index 2f5b6e8..30cf907 100644 --- a/src/sodium/sign/secret_key.cr +++ b/src/sodium/sign/secret_key.cr @@ -1,36 +1,53 @@ require "../lib_sodium" module Sodium + # Usage: + # ``` + # key = SecretKey.new + # sig = key.sign_detached data + # key.public_key.verify_detached data + # ``` class Sign::SecretKey < Sodium::Key include Wipe - KEY_SIZE = LibSodium::SECRET_SIGN_SIZE + KEY_SIZE = LibSodium.crypto_sign_secretkeybytes + SEED_SIZE = LibSodium.crypto_sign_seedbytes getter bytes : Bytes getter public_key + @seed : Bytes? - # Generates a new secret/public key pair. + # Generates a new random secret/public key pair. def initialize pkey = Bytes.new(Sign::PublicKey::KEY_SIZE) @bytes = Bytes.new(KEY_SIZE) @public_key = PublicKey.new pkey - LibSodium.crypto_sign_keypair pkey, @bytes + if LibSodium.crypto_sign_keypair(pkey, @bytes) != 0 + raise Sodium::Error.new("crypto_sign_keypair") + end end # Use existing Secret and Public keys. - def initialize(@bytes : Bytes, pkey : Bytes) - raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") + def initialize(@bytes : Bytes, pkey : Bytes? = nil) + pkey ||= Bytes.new(Sign::PublicKey::KEY_SIZE).tap do |pk| + # BUG: Finish regenerating public_key + raise "Needs crypto_sign_ed25519_sk_to_pk" + end + raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") unless @bytes.bytesize == KEY_SIZE @public_key = PublicKey.new pkey end - # def initialize(@bytes : Bytes) - # if bytes.bytesize != KEY_SIZE - # raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") - # end - # BUG: fix - # @public_key = PublicKey.new Bytes.new(100) - # raise "Needs crypto_sign_ed25519_sk_to_pk" - # Also needs to differentiate from seed as a single parameter - # end + # Derive a new secret/public key pair based on a consistent seed. + def initialize(*, seed : Bytes) + raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE + @seed = seed + + pkey = Bytes.new(Sign::PublicKey::KEY_SIZE) + @bytes = Bytes.new(KEY_SIZE) + @public_key = PublicKey.new pkey + if LibSodium.crypto_sign_seed_keypair(pkey, @bytes, seed) != 0 + raise Sodium::Error.new("crypto_sign_seed_keypair") + end + end # Signs message and returns a detached signature. # Verify using `secret_key.public_key.verify_detached(message, sig)`