From 162cd72b0c4c1da7fe6b10297c79cf3b49dca6b9 Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Sat, 29 Jun 2019 17:21:00 -0700 Subject: [PATCH] Add seed support to CryptoBox and Sign. Wiping now supports multiple variables by Annotation. --- README.md | 2 +- spec/sodium/crypto_box/secret_key_spec.cr | 7 +++ spec/sodium/{ => digest}/blake2b_spec.cr | 2 +- .../{secret_key.cr => secret_key_spec.cr} | 11 +++-- src/sodium.cr | 4 -- src/sodium/crypto_box/box.cr | 1 + src/sodium/crypto_box/public_key.cr | 6 +++ src/sodium/crypto_box/secret_key.cr | 43 +++++++++++-------- src/sodium/digest/blake2b.cr | 19 ++++---- src/sodium/kdf.cr | 13 +++--- src/sodium/key.cr | 4 ++ src/sodium/lib_sodium.cr | 40 +++++++++++++---- src/sodium/pwhash.cr | 4 +- src/sodium/secret_box.cr | 1 + src/sodium/sign/public_key.cr | 6 +++ src/sodium/sign/secret_key.cr | 33 ++++++++------ src/sodium/wipe.cr | 19 +++++++- 17 files changed, 149 insertions(+), 66 deletions(-) rename spec/sodium/{ => digest}/blake2b_spec.cr (98%) rename spec/sodium/sign/{secret_key.cr => secret_key_spec.cr} (79%) diff --git a/README.md b/README.md index 251c76e..0c2a2a2 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/) ## Goals -* Provide an easy to use API based on reviewing most other [libsodium bindings](https://libsodium.gitbook.io/doc/bindings_for_other_languages). * Provide the most commonly used libsodium API's. +* Provide an easy to use API based on reviewing most other [libsodium bindings](https://libsodium.gitbook.io/doc/bindings_for_other_languages). * Test for compatibility against other libsodium bindings to ensure interoperability. * Always provide a stream interface to handle arbitrarily sized data when one is available. * Drop in replacement classes compatible with OpenSSL::{Digest,Cipher} when possible. diff --git a/spec/sodium/crypto_box/secret_key_spec.cr b/spec/sodium/crypto_box/secret_key_spec.cr index 82a22e7..f577d2a 100644 --- a/spec/sodium/crypto_box/secret_key_spec.cr +++ b/spec/sodium/crypto_box/secret_key_spec.cr @@ -12,6 +12,13 @@ describe Sodium::CryptoBox::SecretKey do key1.public_key.bytes.should eq key2.public_key.bytes end + it "recomputes the public_key" do + key1 = Sodium::CryptoBox::SecretKey.new + key2 = Sodium::CryptoBox::SecretKey.new key1.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 diff --git a/spec/sodium/blake2b_spec.cr b/spec/sodium/digest/blake2b_spec.cr similarity index 98% rename from spec/sodium/blake2b_spec.cr rename to spec/sodium/digest/blake2b_spec.cr index 9d22e0e..70238c7 100644 --- a/spec/sodium/blake2b_spec.cr +++ b/spec/sodium/digest/blake2b_spec.cr @@ -1,4 +1,4 @@ -require "../spec_helper" +require "../../spec_helper" libsodium_comparisons = [ { diff --git a/spec/sodium/sign/secret_key.cr b/spec/sodium/sign/secret_key_spec.cr similarity index 79% rename from spec/sodium/sign/secret_key.cr rename to spec/sodium/sign/secret_key_spec.cr index 1aa3f1f..4a36983 100644 --- a/spec/sodium/sign/secret_key.cr +++ b/spec/sodium/sign/secret_key_spec.cr @@ -1,7 +1,7 @@ require "../../spec_helper" require "../../../src/sodium/sign/secret_key" -private def new_key_bytes +private def new_sign_key_bytes Sodium::Sign::SecretKey.new.bytes end @@ -11,8 +11,13 @@ describe Sodium::Sign::SecretKey do 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 + end - # TODO: test loading when missing public_key + it "recomputes the public key" do + key1 = Sodium::Sign::SecretKey.new + key2 = Sodium::Sign::SecretKey.new key1.bytes + key1.bytes.should eq key2.bytes + key1.public_key.bytes.should eq key2.public_key.bytes end it "seed keys" do @@ -42,6 +47,6 @@ describe Sodium::Sign::SecretKey do end it "checks wiped" do - check_wiped new_key_bytes + check_wiped new_sign_key_bytes end end diff --git a/src/sodium.cr b/src/sodium.cr index 3ab1381..3d3646c 100644 --- a/src/sodium.cr +++ b/src/sodium.cr @@ -15,7 +15,3 @@ module Sodium end require "./sodium/**" - -if Sodium::LibSodium.sodium_init == -1 - abort "Failed to init libsodium" -end diff --git a/src/sodium/crypto_box/box.cr b/src/sodium/crypto_box/box.cr index 7d12faf..19e8dd5 100644 --- a/src/sodium/crypto_box/box.cr +++ b/src/sodium/crypto_box/box.cr @@ -5,6 +5,7 @@ module Sodium::CryptoBox include Wipe # BUG: precompute size + @[Wipe::Var] @bytes = Bytes.new(1) def initialize(@secret_key : SecretKey, @public_key : PublicKey) diff --git a/src/sodium/crypto_box/public_key.cr b/src/sodium/crypto_box/public_key.cr index ea02d54..9670038 100644 --- a/src/sodium/crypto_box/public_key.cr +++ b/src/sodium/crypto_box/public_key.cr @@ -7,6 +7,12 @@ module Sodium::CryptoBox getter bytes : Bytes + # :nodoc: + # Only used by SecretKey + def initialize + @bytes = Bytes.new KEY_SIZE + end + def initialize(@bytes : Bytes) if bytes.bytesize != KEY_SIZE raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") diff --git a/src/sodium/crypto_box/secret_key.cr b/src/sodium/crypto_box/secret_key.cr index 1e49c78..85c2355 100644 --- a/src/sodium/crypto_box/secret_key.cr +++ b/src/sodium/crypto_box/secret_key.cr @@ -1,34 +1,42 @@ require "../lib_sodium" module Sodium::CryptoBox + # WARNING: This class takes ownership of any key material passed to it. + # If you don't want this behavior pass a duplicate of the key/seed to initialize(). class SecretKey < Key include Wipe KEY_SIZE = LibSodium.crypto_box_secretkeybytes SEED_SIZE = LibSodium.crypto_box_seedbytes MAC_SIZE = LibSodium::MAC_SIZE - getter public_key + getter public_key : PublicKey + + @[Wipe::Var] getter bytes : Bytes + @[Wipe::Var] @seed : Bytes? # 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 - v = LibSodium.crypto_box_keypair(pkey, @bytes) - if v != 0 - raise Sodium::Error.new("crypto_box_keypair #{v}") + @public_key = PublicKey.new + if LibSodium.crypto_box_keypair(@public_key.bytes, @bytes) != 0 + raise Sodium::Error.new("crypto_box_keypair") 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}") + # Use existing secret and public keys. + # Recomputes the public key from a secret key if missing. + def initialize(@bytes : Bytes, pkey : Bytes? = nil) + raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") if bytes.bytesize != KEY_SIZE + if pk = pkey + @public_key = PublicKey.new pk + else + @public_key = PublicKey.new + if LibSodium.crypto_scalarmult_base(@public_key.bytes, @bytes) != 0 + raise Sodium::Error.new("crypto_scalarmult_base") + end end - @public_key = PublicKey.new pkey end # Derive a new secret/public key pair based on a consistent seed. @@ -36,10 +44,9 @@ module Sodium::CryptoBox 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 + @public_key = PublicKey.new + if LibSodium.crypto_box_seed_keypair(@public_key.bytes, @bytes, seed) != 0 raise Sodium::Error.new("crypto_box_seed_keypair") end end @@ -51,11 +58,11 @@ module Sodium::CryptoBox # Create a new box and automatically close when the block exits. def box(public_key) - pa = box public_key + b = box public_key begin - yield pa + yield b ensure - pa.close + b.close end end end diff --git a/src/sodium/digest/blake2b.cr b/src/sodium/digest/blake2b.cr index ef70e61..7a378f7 100644 --- a/src/sodium/digest/blake2b.cr +++ b/src/sodium/digest/blake2b.cr @@ -4,6 +4,7 @@ module Sodium::Digest class Blake2b # provides copying digest/hexdigest methods include OpenSSL::DigestBase + include Wipe KEY_SIZE = LibSodium.crypto_generichash_blake2b_keybytes KEY_SIZE_MIN = LibSodium.crypto_generichash_blake2b_keybytes_min @@ -19,42 +20,40 @@ module Sodium::Digest getter digest_size + @[Wipe::Var] @state = StaticArray(UInt8, 384).new 0 @key_size = 0 - @have_salt = false - @have_personal = false # implemented as static array's so clone works without jumping through hoops. + @[Wipe::Var] @key = StaticArray(UInt8, 64).new 0 @salt = StaticArray(UInt8, 16).new 0 @personal = StaticArray(UInt8, 16).new 0 def initialize(@digest_size : Int32 = OUT_SIZE, key : Bytes? = nil, salt : Bytes? = nil, personal : Bytes? = nil) if k = key - raise ArgumentError.new("key larger than KEY_SIZE_MAX, got #{k.bytesize}") if k.bytesize > KEY_SIZE_MAX + raise ArgumentError.new("key larger than KEY_SIZE_MAX(#{KEY_SIZE_MAX}), got #{k.bytesize}") if k.bytesize > KEY_SIZE_MAX @key_size = k.bytesize k.copy_to @key.to_slice end if sa = salt - raise ArgumentError.new("salt must be SALT_SIZE bytes, got #{sa.bytesize}") if sa.bytesize != SALT_SIZE + raise ArgumentError.new("salt must be SALT_SIZE(#{SALT_SIZE}) bytes, got #{sa.bytesize}") if sa.bytesize != SALT_SIZE sa.copy_to @salt.to_slice - @have_salt = true end if pe = personal - raise ArgumentError.new("personal must be PERSONAL_SIZE bytes, got #{pe.bytesize}") if pe.bytesize != PERSONAL_SIZE + raise ArgumentError.new("personal must be PERSONAL_SIZE(#{PERSONAL_SIZE}) bytes, got #{pe.bytesize}") if pe.bytesize != PERSONAL_SIZE pe.copy_to @personal.to_slice - @have_personal = true end reset end def reset - key = @key_size > 0 ? @key.to_unsafe : nil - salt = @have_salt ? @salt.to_unsafe : nil - personal = @have_personal ? @personal.to_unsafe : nil + key = @key.to_unsafe + salt = @salt.to_unsafe + personal = @personal.to_unsafe if LibSodium.crypto_generichash_blake2b_init_salt_personal(@state, key, @key_size, @digest_size, salt, personal) != 0 raise Sodium::Error.new("blake2b_init_key_salt_personal") diff --git a/src/sodium/kdf.cr b/src/sodium/kdf.cr index fac7b78..7a82e4c 100644 --- a/src/sodium/kdf.cr +++ b/src/sodium/kdf.cr @@ -1,26 +1,29 @@ module Sodium class Kdf + KDF_KEY_SIZE = LibSodium.crypto_kdf_keybytes + KDF_CONTEXT_SIZE = LibSodium.crypto_kdf_contextbytes + property bytes : Bytes delegate to_slice, to: @bytes def initialize(bytes : Bytes) - if bytes.bytesize != LibSodium::KDF_KEY_SIZE - raise ArgumentError.new("bytes must be #{LibSodium::KDF_KEY_SIZE}, got #{bytes.bytesize}") + if bytes.bytesize != KDF_KEY_SIZE + raise ArgumentError.new("bytes must be #{KDF_KEY_SIZE}, got #{bytes.bytesize}") end @bytes = bytes end def initialize - @bytes = Random::Secure.random_bytes(LibSodium::KDF_KEY_SIZE) + @bytes = Random::Secure.random_bytes(KDF_KEY_SIZE) end # context must be 8 bytes # subkey_size must be 16..64 bytes as of libsodium 1.0.17 def derive(context, subkey_id, subkey_size) - if context.bytesize != LibSodium::KDF_CONTEXT_SIZE - raise ArgumentError.new("context must be #{LibSodium::KDF_CONTEXT_SIZE}, got #{context.bytesize}") + if context.bytesize != KDF_CONTEXT_SIZE + raise ArgumentError.new("context must be #{KDF_CONTEXT_SIZE}, got #{context.bytesize}") end subkey = Bytes.new subkey_size diff --git a/src/sodium/key.cr b/src/sodium/key.cr index 4a6298a..d1e4a82 100644 --- a/src/sodium/key.cr +++ b/src/sodium/key.cr @@ -1,5 +1,9 @@ +require "./wipe" + module Sodium abstract class Key + include Sodium::Wipe + abstract def bytes delegate to_slice, to: @bytes diff --git a/src/sodium/lib_sodium.cr b/src/sodium/lib_sodium.cr index d041cc7..8797456 100644 --- a/src/sodium/lib_sodium.cr +++ b/src/sodium/lib_sodium.cr @@ -42,14 +42,9 @@ module Sodium fun crypto_generichash_blake2b_personalbytes : LibC::SizeT fun sodium_memzero(Pointer(LibC::UChar), LibC::SizeT) : Nil - PUBLIC_KEY_SIZE = crypto_box_publickeybytes() - SECRET_KEY_SIZE = crypto_box_secretkeybytes() - NONCE_SIZE = crypto_box_noncebytes() - MAC_SIZE = crypto_box_macbytes() - SIGNATURE_SIZE = crypto_sign_bytes() - KDF_KEY_SIZE = crypto_kdf_keybytes() - KDF_CONTEXT_SIZE = crypto_kdf_contextbytes() - PWHASH_STR_SIZE = crypto_pwhash_strbytes() + NONCE_SIZE = crypto_box_noncebytes() + MAC_SIZE = crypto_box_macbytes() + SIGNATURE_SIZE = crypto_sign_bytes() fun crypto_secretbox_easy( output : Pointer(LibC::UChar), @@ -93,6 +88,11 @@ module Sodium seed : Pointer(LibC::UChar) ) : LibC::Int + fun crypto_scalarmult_base( + public_key_output : Pointer(LibC::UChar), + secret_key_output : Pointer(LibC::UChar) + ) : LibC::Int + fun crypto_box_easy( output : Pointer(LibC::UChar), data : Pointer(LibC::UChar), @@ -111,6 +111,21 @@ module Sodium recipient_secret_key : Pointer(LibC::UChar) ) : LibC::Int + fun crypto_box_seal( + output : Pointer(LibC::UChar), + data : Pointer(LibC::UChar), + data_size : LibC::ULongLong, + recipient_public_key : Pointer(LibC::UChar) + ) : LibC::Int + + fun crypto_box_seal_open( + output : Pointer(LibC::UChar), + data : Pointer(LibC::UChar), + data_size : LibC::ULongLong, + recipient_public_key : Pointer(LibC::UChar), + recipient_secret_key : Pointer(LibC::UChar) + ) : LibC::Int + fun crypto_sign_keypair( public_key_output : Pointer(LibC::UChar), secret_key_output : Pointer(LibC::UChar) @@ -122,6 +137,11 @@ module Sodium seed : Pointer(LibC::UChar) ) : LibC::Int + fun crypto_sign_ed25519_sk_to_pk( + public_key_output : Pointer(LibC::UChar), + secret_key_output : Pointer(LibC::UChar) + ) : LibC::Int + fun crypto_sign_detached( signature_output : Pointer(LibC::UChar), signature_output_size : Pointer(LibC::ULongLong), @@ -198,6 +218,10 @@ module Sodium ) : LibC::Int end + if LibSodium.sodium_init != 0 + abort "Failed to init libsodium" + end + if LibSodium.crypto_secretbox_noncebytes != LibSodium.crypto_box_noncebytes raise "Assumptions in this library regarding nonce sizes may not be valid" end diff --git a/src/sodium/pwhash.cr b/src/sodium/pwhash.cr index 5b5fbdf..4cf1e02 100644 --- a/src/sodium/pwhash.cr +++ b/src/sodium/pwhash.cr @@ -18,6 +18,8 @@ module Sodium MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive + PWHASH_STR_SIZE = LibSodium.crypto_pwhash_strbytes + # Use the most recent algorithm Argon2id13 for new applications. enum Algorithm Argon2i13 = 1 @@ -37,7 +39,7 @@ module Sodium # * the automatically generated salt used for the previous computation # * the other parameters required to verify the password, including the algorithm identifier, its version, opslimit and memlimit. def store(pass) - outstr = Bytes.new LibSodium::PWHASH_STR_SIZE + outstr = Bytes.new PWHASH_STR_SIZE if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @opslimit, @memlimit) != 0 raise Sodium::Error.new("crypto_pwhash_str") end diff --git a/src/sodium/secret_box.cr b/src/sodium/secret_box.cr index 3327594..f8935fd 100644 --- a/src/sodium/secret_box.cr +++ b/src/sodium/secret_box.cr @@ -17,6 +17,7 @@ module Sodium NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes MAC_SIZE = LibSodium::MAC_SIZE + @[Wipe::Var] property bytes : Bytes # Generate a new random key. diff --git a/src/sodium/sign/public_key.cr b/src/sodium/sign/public_key.cr index 83529cc..b311bf2 100644 --- a/src/sodium/sign/public_key.cr +++ b/src/sodium/sign/public_key.cr @@ -7,6 +7,12 @@ module Sodium getter bytes : Bytes + # :nodoc: + # Only used by SecretKey + def initialize + @bytes = Bytes.new(KEY_SIZE) + end + def initialize(@bytes : Bytes) if bytes.bytesize != KEY_SIZE raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") diff --git a/src/sodium/sign/secret_key.cr b/src/sodium/sign/secret_key.cr index 30cf907..005dff2 100644 --- a/src/sodium/sign/secret_key.cr +++ b/src/sodium/sign/secret_key.cr @@ -8,32 +8,38 @@ module Sodium # key.public_key.verify_detached data # ``` class Sign::SecretKey < Sodium::Key - include Wipe KEY_SIZE = LibSodium.crypto_sign_secretkeybytes SEED_SIZE = LibSodium.crypto_sign_seedbytes + getter public_key : PublicKey + + @[Wipe::Var] getter bytes : Bytes - getter public_key + @[Wipe::Var] @seed : Bytes? # 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 - if LibSodium.crypto_sign_keypair(pkey, @bytes) != 0 + @public_key = PublicKey.new + if LibSodium.crypto_sign_keypair(@public_key.bytes, @bytes) != 0 raise Sodium::Error.new("crypto_sign_keypair") end end - # Use existing Secret and Public keys. + # Use existing secret and public keys. + # Recomputes the public key from a secret key if missing. 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 + + if pk = pkey + @public_key = PublicKey.new pkey + else + @public_key = PublicKey.new + if LibSodium.crypto_sign_ed25519_sk_to_pk(@public_key.bytes, @bytes) != 0 + raise Sodium::Error.new("crypto_sign_ed25519_sk_to_pk") + end + end end # Derive a new secret/public key pair based on a consistent seed. @@ -41,10 +47,9 @@ module Sodium 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 + @public_key = PublicKey.new + if LibSodium.crypto_sign_seed_keypair(@public_key.bytes, @bytes, seed) != 0 raise Sodium::Error.new("crypto_sign_seed_keypair") end end diff --git a/src/sodium/wipe.cr b/src/sodium/wipe.cr index 1735411..199c20b 100644 --- a/src/sodium/wipe.cr +++ b/src/sodium/wipe.cr @@ -1,4 +1,7 @@ module Sodium::Wipe + annotation Var + end + @closed = false def close @@ -9,7 +12,21 @@ module Sodium::Wipe protected def wipe return if @closed - Sodium.memzero @bytes + + {% for ivar in @type.instance_vars %} + {% if ann = ivar.annotation(Wipe::Var) %} + {% if ivar.type.id == StaticArray.id %} +#puts "wiping static {{ivar}}" +# Sodium.memzero @{{ ivar.id }}.to_slice + {% else %} + if var = @{{ ivar.id }} +#puts "wiping {{ivar}}" +# Sodium.memzero var + Sodium.memzero var.to_slice + end + {% end %} + {% end %} + {% end %} end def finalize