From d1c8829fcf68dae4256cea49db30a88ebc12a2b7 Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Wed, 3 Jul 2019 17:56:02 -0700 Subject: [PATCH] API changed all Key classes .bytes to .to_slice Switched most custom Wipe implementation to libsodium guarded memory. --- README.md | 33 ++-------- spec/sodium/crypto_box/secret_key_spec.cr | 22 +++---- spec/sodium/kdf_spec.cr | 2 +- spec/sodium/pwhash_spec.cr | 6 +- spec/sodium/sign/secret_key_spec.cr | 24 +++---- src/sodium/crypto_box.cr | 2 +- src/sodium/crypto_box/public_key.cr | 4 +- src/sodium/crypto_box/secret_key.cr | 50 ++++++++++----- src/sodium/kdf.cr | 63 +++++++++++++----- src/sodium/key.cr | 7 +- src/sodium/lib_sodium.cr | 8 +++ src/sodium/pwhash.cr | 17 +++-- src/sodium/secret_box.cr | 32 ++++++---- src/sodium/secure_buffer.cr | 78 +++++++++++++++++++++++ src/sodium/sign/public_key.cr | 6 +- src/sodium/sign/secret_key.cr | 40 +++++++----- 16 files changed, 262 insertions(+), 132 deletions(-) create mode 100644 src/sodium/secure_buffer.cr diff --git a/README.md b/README.md index 2a413ef..73d06fa 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/) - [ ] [One time auth](https://libsodium.gitbook.io/doc/advanced/poly1305) - [ ] Padding - Library features - - Faster builds by requiring what you need (`require "sodium/secret_box"`) - - Controlled memory wiping (by calling `.close`) - - Semi-automatic memory wiping (on GC). + - [x] Faster builds by requiring what you need (`require "sodium/secret_box"`) + - [x] All SecretKey's held in libsodium guarded memory. + - [ ] Controlled memory wiping (by calling `.close`) ☑ Indicate specs are compared against test vectors from another source. @@ -96,38 +96,17 @@ dependencies: ## Usage -The `specs` provide the best examples of how to use or misuse this shard. +See `examples` for help on using these classes in a complete application. -### Warning +The `specs` provide the best examples of how to use or misuse individual classes. -This library uses automatic memory wiping. **Make sure you write your keys to disk or send them over a network -before losing references to objects that contain keys.** You may also call `.dup`. -```crystal -# Returns Bytes representation and loses reference to SecretKey -def new_key - secret_key = Sodium::CryptoBox::SecretKey.new - secret_key.bytes -end - -saved_key = new_key - -p saved_key[0, 8] -GC.collect -p saved_key[0, 8] -``` - -``` -# Before GC -Bytes[175, 134, 134, 159, 149, 208, 171, 251] -# After GC -Bytes[0, 0, 0, 0, 0, 0, 0, 0] -``` You may call `.close` on any object that retains keying material to wipe it's key(s) earlier. Objects with a `.close` method also respond to `Class.open` and wipe when the block returns. ```crystal +# TODO Sodium::CryptoBox::SecretKey.open(sec_key, pub_key) do |secret_key| ... Do crypto operations ... end diff --git a/spec/sodium/crypto_box/secret_key_spec.cr b/spec/sodium/crypto_box/secret_key_spec.cr index 3fada64..0f69379 100644 --- a/spec/sodium/crypto_box/secret_key_spec.cr +++ b/spec/sodium/crypto_box/secret_key_spec.cr @@ -2,30 +2,30 @@ require "../../spec_helper" require "../../../src/sodium/crypto_box/secret_key" private def new_key_bytes - Sodium::CryptoBox::SecretKey.new.bytes + Sodium::CryptoBox::SecretKey.new.to_slice 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 + key2 = Sodium::CryptoBox::SecretKey.new key1.to_slice, key1.public_key.to_slice + key1.to_slice.should eq key2.to_slice + key1.public_key.to_slice.should eq key2.public_key.to_slice 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 + key2 = Sodium::CryptoBox::SecretKey.new key1.to_slice + key1.to_slice.should eq key2.to_slice + key1.public_key.to_slice.should eq key2.public_key.to_slice 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 + key1.to_slice.should eq key2.to_slice + key1.public_key.to_slice.should eq key2.public_key.to_slice end it "authenticated easy encrypt/decrypt" do @@ -65,8 +65,4 @@ describe Sodium::CryptoBox::SecretKey do String.new(decrypted).should eq(data) end - - it "wipes keys" do - check_wiped new_key_bytes - end end diff --git a/spec/sodium/kdf_spec.cr b/spec/sodium/kdf_spec.cr index f8d86b2..4185a6b 100644 --- a/spec/sodium/kdf_spec.cr +++ b/spec/sodium/kdf_spec.cr @@ -8,7 +8,7 @@ describe Sodium::Kdf do kdf1 = Sodium::Kdf.new # verify loading saved key - kdf2 = Sodium::Kdf.new kdf1.bytes + kdf2 = Sodium::Kdf.new kdf1.to_slice.dup # verify generated subkey's are the same after loading key1_s1 = kdf1.derive CONTEXT, 0, 16 diff --git a/spec/sodium/pwhash_spec.cr b/spec/sodium/pwhash_spec.cr index 2df53dc..45048d1 100644 --- a/spec/sodium/pwhash_spec.cr +++ b/spec/sodium/pwhash_spec.cr @@ -71,18 +71,18 @@ describe Sodium::Pwhash do it "key_derive fails without a mode" do pwhash = pw_min expect_raises(ArgumentError) do - pwhash.key_derive pwhash.salt, "foo", 16 + pwhash.key_derive pwhash.random_salt, "foo", 16 end end it "derives a key from a password" do pwhash = pw_min pwhash.mode = Sodium::Pwhash::Mode::Argon2id13 - salt = pwhash.salt + salt = pwhash.random_salt key1 = pwhash.key_derive salt, "foo", 16 key2 = pwhash.key_derive salt, "foo", 16 key3 = pwhash.key_derive salt, "bar", 16 - key4 = pwhash.key_derive pwhash.salt, "foo", 16 + key4 = pwhash.key_derive pwhash.random_salt, "foo", 16 key1.bytesize.should eq 16 key1.should eq key2 diff --git a/spec/sodium/sign/secret_key_spec.cr b/spec/sodium/sign/secret_key_spec.cr index 4a36983..82cee5e 100644 --- a/spec/sodium/sign/secret_key_spec.cr +++ b/spec/sodium/sign/secret_key_spec.cr @@ -1,31 +1,31 @@ require "../../spec_helper" require "../../../src/sodium/sign/secret_key" -private def new_sign_key_bytes - Sodium::Sign::SecretKey.new.bytes +private def new_sign_key_to_slice + Sodium::Sign::SecretKey.new.to_slice 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 + key2 = Sodium::Sign::SecretKey.new key1.to_slice, key1.public_key.to_slice + key1.to_slice.should eq key2.to_slice + key1.public_key.to_slice.should eq key2.public_key.to_slice end 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 + key2 = Sodium::Sign::SecretKey.new key1.to_slice + key1.to_slice.should eq key2.to_slice + key1.public_key.to_slice.should eq key2.public_key.to_slice 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 + key1.to_slice.should eq key2.to_slice + key1.public_key.to_slice.should eq key2.public_key.to_slice end it "signs and verifies" do @@ -45,8 +45,4 @@ describe Sodium::Sign::SecretKey do skey.public_key.verify_detached "bar", sig end end - - it "checks wiped" do - check_wiped new_sign_key_bytes - end end diff --git a/src/sodium/crypto_box.cr b/src/sodium/crypto_box.cr index 1d49607..2bed26b 100644 --- a/src/sodium/crypto_box.cr +++ b/src/sodium/crypto_box.cr @@ -7,7 +7,7 @@ module Sodium class CryptoBox include Wipe - MAC_SIZE = LibSodium.crypto_box_macbytes + MAC_SIZE = LibSodium.crypto_box_macbytes.to_i # BUG: precompute size @[Wipe::Var] diff --git a/src/sodium/crypto_box/public_key.cr b/src/sodium/crypto_box/public_key.cr index 4639f3c..22fab3b 100644 --- a/src/sodium/crypto_box/public_key.cr +++ b/src/sodium/crypto_box/public_key.cr @@ -3,10 +3,10 @@ require "../key" class Sodium::CryptoBox class PublicKey < Key - KEY_SIZE = LibSodium.crypto_box_publickeybytes + KEY_SIZE = LibSodium.crypto_box_publickeybytes.to_i SEAL_SIZE = LibSodium.crypto_box_sealbytes - getter bytes : Bytes + delegate to_slice, to: @bytes # :nodoc: # Only used by SecretKey diff --git a/src/sodium/crypto_box/secret_key.cr b/src/sodium/crypto_box/secret_key.cr index 7813c7b..9c6b00f 100644 --- a/src/sodium/crypto_box/secret_key.cr +++ b/src/sodium/crypto_box/secret_key.cr @@ -6,55 +6,73 @@ require "../crypto_box" class Sodium::CryptoBox # Key used for encryption + authentication or encryption without authentication, not for unencrypted signing. # - # 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 - KEY_SIZE = LibSodium.crypto_box_secretkeybytes - SEED_SIZE = LibSodium.crypto_box_seedbytes - SEAL_SIZE = LibSodium.crypto_box_sealbytes + KEY_SIZE = LibSodium.crypto_box_secretkeybytes.to_i + SEED_SIZE = LibSodium.crypto_box_seedbytes.to_i + SEAL_SIZE = LibSodium.crypto_box_sealbytes.to_i getter public_key : PublicKey - @[Wipe::Var] - getter bytes : Bytes - @[Wipe::Var] - @seed : Bytes? + delegate to_slice, to: @sbuf + + @seed : SecureBuffer? # Generate a new random secret/public key pair. def initialize - @bytes = Bytes.new(KEY_SIZE) + @sbuf = SecureBuffer.new KEY_SIZE @public_key = PublicKey.new - if LibSodium.crypto_box_keypair(@public_key.bytes, @bytes) != 0 + if LibSodium.crypto_box_keypair(@public_key.to_slice, self.to_slice) != 0 raise Sodium::Error.new("crypto_box_keypair") end end # Use existing secret and public keys. + # Copies secret key to a SecureBuffer. # Recomputes the public key from a secret key if missing. - def initialize(@bytes : Bytes, pkey : Bytes? = nil) + 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 + @sbuf = SecureBuffer.new bytes if pk = pkey @public_key = PublicKey.new pk else @public_key = PublicKey.new - if LibSodium.crypto_scalarmult_base(@public_key.bytes, @bytes) != 0 + if LibSodium.crypto_scalarmult_base(@public_key.to_slice, self.to_slice) != 0 raise Sodium::Error.new("crypto_scalarmult_base") end end end # Derive a new secret/public key pair based on a consistent seed. - def initialize(*, seed : Bytes) + # Copies seed to a SecureBuffer. + def initialize(*, seed : Bytes, erase = false) + raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE + @seed = SecureBuffer.new seed, erase: erase + + @sbuf = SecureBuffer.new KEY_SIZE + @public_key = PublicKey.new + if LibSodium.crypto_box_seed_keypair(@public_key.to_slice, self.to_slice, seed) != 0 + raise Sodium::Error.new("crypto_box_seed_keypair") + end + end + + # Derive a new secret/public key pair based on a consistent seed. + def initialize(*, seed : SecureBuffer) raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE @seed = seed - @bytes = Bytes.new(KEY_SIZE) + @sbuf = SecureBuffer.new KEY_SIZE @public_key = PublicKey.new - if LibSodium.crypto_box_seed_keypair(@public_key.bytes, @bytes, seed) != 0 + if LibSodium.crypto_box_seed_keypair(@public_key.to_slice, self.to_slice, seed) != 0 raise Sodium::Error.new("crypto_box_seed_keypair") end end + def seed + # BUG: Generate seed if not set. + @seed.not_nil!.to_slice + end + # Return a Box containing a precomputed shared secret for use with authenticated encryption/decryption. def box(public_key) : CryptoBox CryptoBox.new self, public_key @@ -77,7 +95,7 @@ class Sodium::CryptoBox end def decrypt(src : Bytes, dst : Bytes = Bytes.new(src.bytesize - SEAL_SIZE)) : Bytes - if LibSodium.crypto_box_seal_open(dst, src, src.bytesize, @public_key.bytes, @bytes) != 0 + if LibSodium.crypto_box_seal_open(dst, src, src.bytesize, @public_key.to_slice, self.to_slice) != 0 raise Sodium::Error.new("crypto_box_seal_open") end dst diff --git a/src/sodium/kdf.cr b/src/sodium/kdf.cr index 2cc0430..a8fc754 100644 --- a/src/sodium/kdf.cr +++ b/src/sodium/kdf.cr @@ -1,4 +1,5 @@ require "./lib_sodium" +require "./secure_buffer" require "./wipe" module Sodium @@ -17,33 +18,38 @@ module Sodium class Kdf include Wipe - KEY_SIZE = LibSodium.crypto_kdf_keybytes + KEY_SIZE = LibSodium.crypto_kdf_keybytes.to_i CONTEXT_SIZE = LibSodium.crypto_kdf_contextbytes - @[Wipe::Var] - getter bytes : Bytes - - delegate to_slice, to: @bytes + delegate to_slice, to: @sbuf # Use an existing KDF key. # - # 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 to initialize(). - def initialize(bytes : Bytes) + # Optionally erases bytes after copying if erase is set + def initialize(bytes : Bytes, erase = false) if bytes.bytesize != KEY_SIZE raise ArgumentError.new("bytes must be #{KEY_SIZE}, got #{bytes.bytesize}") end - @bytes = bytes + @sbuf = SecureBuffer.new bytes, erase + end + + # Use an existing KDF SecureBuffer key. + # + # 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 to initialize(). + def initialize(@sbuf : SecureBuffer) + if @sbuf.bytesize != KEY_SIZE + raise ArgumentError.new("bytes must be #{KEY_SIZE}, got #{sbuf.bytesize}") + end + @sbuf.readonly end # Generate a new random KDF key. # - # WARNING: This class takes ownership of any key material passed to it. - # - # Make sure to save kdf.bytes before kdf goes out of scope. + # Make sure to save kdf.to_slice before kdf goes out of scope. def initialize - @bytes = Random::Secure.random_bytes(KEY_SIZE) + @sbuf = SecureBuffer.random KEY_SIZE end # Derive a consistent subkey based on `context` and `subkey_id`. @@ -52,17 +58,42 @@ module Sodium # * 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) + # Returns a SecureBuffer. May transfer ownership to SecretBox or SecretKey without copying. + def derive(context, subkey_id, subkey_size) : SecureBuffer context = context.to_slice if context.bytesize != CONTEXT_SIZE raise ArgumentError.new("context must be #{CONTEXT_SIZE}, got #{context.bytesize}") end - subkey = Bytes.new subkey_size - if (ret = LibSodium.crypto_kdf_derive_from_key(subkey, subkey.bytesize, subkey_id, context, @bytes)) != 0 + subkey = SecureBuffer.new subkey_size + if (ret = LibSodium.crypto_kdf_derive_from_key(subkey, subkey.bytesize, subkey_id, context, self.to_slice)) != 0 raise Sodium::Error.new("crypto_kdf_derive_from_key returned #{ret} (subkey size is probably out of range)") end subkey end + + # Convenience method to create a new CryptoBox::Secret without handling the key. + # + # See derive() for further information on context and subkey_id. + def derive_cryptobox(context, subkey_id) : CryptoBox::SecretKey + subkey = derive context, subkey_id, CryptoBox::SecretKey::SEED_SIZE + CryptoBox::SecretKey.new seed: subkey + end + + # Convenience method to create a new Sign::Secret without handling the key. + # + # See derive() for further information on context and subkey_id. + def derive_sign(context, subkey_id) : Sign::SecretKey + subkey = derive context, subkey_id, Sign::SecretKey::SEED_SIZE + Sign::SecretKey.new seed: subkey + end + + # Convenience method to create a new SecretBox without handling the key. + # + # See derive() for further information on context and subkey_id. + def derive_secretbox(context, subkey_id) : SecretBox + subkey = derive context, subkey_id, SecretBox::KEY_SIZE + SecretBox.new subkey + end end end diff --git a/src/sodium/key.cr b/src/sodium/key.cr index d1e4a82..ab65faa 100644 --- a/src/sodium/key.cr +++ b/src/sodium/key.cr @@ -1,15 +1,14 @@ +require "./secure_buffer" require "./wipe" module Sodium abstract class Key include Sodium::Wipe - abstract def bytes - - delegate to_slice, to: @bytes + abstract def to_slice : Bytes def to_base64 - Base64.encode(bytes) + Base64.encode(to_slice) end def self.from_base64(encoded_key) diff --git a/src/sodium/lib_sodium.cr b/src/sodium/lib_sodium.cr index 44bd0e9..a474d4e 100644 --- a/src/sodium/lib_sodium.cr +++ b/src/sodium/lib_sodium.cr @@ -44,8 +44,16 @@ module Sodium fun crypto_generichash_blake2b_keybytes_max : LibC::SizeT fun crypto_generichash_blake2b_saltbytes : LibC::SizeT fun crypto_generichash_blake2b_personalbytes : LibC::SizeT + fun sodium_memzero(Pointer(LibC::UChar), LibC::SizeT) : Nil + fun sodium_malloc(LibC::SizeT) : Pointer(LibC::UChar) + fun sodium_free(Pointer(LibC::UChar)) : Nil + + fun sodium_mprotect_noaccess(Pointer(LibC::UChar)) : LibC::Int + fun sodium_mprotect_readonly(Pointer(LibC::UChar)) : LibC::Int + fun sodium_mprotect_readwrite(Pointer(LibC::UChar)) : LibC::Int + NONCE_SIZE = crypto_box_noncebytes() fun crypto_secretbox_easy( diff --git a/src/sodium/pwhash.cr b/src/sodium/pwhash.cr index 01ac764..b1d9a17 100644 --- a/src/sodium/pwhash.cr +++ b/src/sodium/pwhash.cr @@ -1,4 +1,5 @@ require "./lib_sodium" +require "./secure_buffer" module Sodium # [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) @@ -74,19 +75,19 @@ module Sodium end end - # Returns a consistent key based on [salt, pass, key_bytes, mode, ops_limit, mem_limit] + # Returns a consistent key based on [salt, pass, key_bytes, mode, ops_limit, mem_limit] in a SecureBuffer # # Must set a mode before calling. def key_derive(salt, pass, key_bytes) key_derive salt.to_slice, pass.to_slice, key_bytes end - def key_derive(salt : Bytes, pass : Bytes, key_bytes) : Bytes + def key_derive(salt : Bytes, pass : Bytes, key_bytes) : SecureBuffer raise "salt expected #{SALT_SIZE} bytes, got #{salt.bytesize} " if salt.bytesize != SALT_SIZE if m = mode - key = Bytes.new key_bytes - if LibSodium.crypto_pwhash(key, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0 + key = SecureBuffer.new key_bytes + if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0 raise Sodium::Error.new("crypto_pwhash_str") end key @@ -95,8 +96,14 @@ module Sodium end end + # Derives a key using key_derive and returns KDF.new(key) + def kdf_derive(salt, pass, key_bytes) : Kdf + key = key_derive salt, pass, key_bytes + Kdf.new key + end + # Returns a random salt for use with #key_derive - def salt + def random_salt Random::Secure.random_bytes SALT_SIZE end end diff --git a/src/sodium/secret_box.cr b/src/sodium/secret_box.cr index 27187a8..c97e21c 100644 --- a/src/sodium/secret_box.cr +++ b/src/sodium/secret_box.cr @@ -18,23 +18,33 @@ module Sodium # message = key.decrypt_easy encrypted, nonce # ``` class SecretBox < Key - KEY_SIZE = LibSodium.crypto_secretbox_keybytes - NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes - MAC_SIZE = LibSodium.crypto_secretbox_macbytes + KEY_SIZE = LibSodium.crypto_secretbox_keybytes.to_i + NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes.to_i + MAC_SIZE = LibSodium.crypto_secretbox_macbytes.to_i - @[Wipe::Var] - getter bytes : Bytes + delegate to_slice, to: @buf - # Generate a new random key. + # Generate a new random key held in a SecureBuffer. def initialize - @bytes = Random::Secure.random_bytes(KEY_SIZE) + @buf = SecureBuffer.random KEY_SIZE end - # Use an existing key from bytes. - def initialize(@bytes : Bytes) + # Use an existing SecureBuffer. + protected def initialize(@buf : SecureBuffer) + if @buf.bytesize != KEY_SIZE + raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{@buf.bytesize}") + end + @buf.readonly + end + + # Copy bytes to a new SecureBuffer + # + # Optionally erases bytes after copying if erase is set + protected def initialize(bytes : Bytes, erase = false) if bytes.bytesize != KEY_SIZE raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") end + @buf = SecureBuffer.new bytes, erase: erase end def encrypt_easy(data) @@ -60,7 +70,7 @@ module Sodium if dst.bytesize != (src.bytesize + MAC_SIZE) raise ArgumentError.new("dst.bytesize must be src.bytesize + MAC_SIZE, got #{dst.bytesize}") end - if LibSodium.crypto_secretbox_easy(dst, src, src.bytesize, nonce.to_slice, @bytes) != 0 + if LibSodium.crypto_secretbox_easy(dst, src, src.bytesize, nonce.to_slice, self.to_slice) != 0 raise Sodium::Error.new("crypto_secretbox_easy") end dst @@ -77,7 +87,7 @@ module Sodium if dst.bytesize != (src.bytesize - MAC_SIZE) raise ArgumentError.new("dst.bytesize must be src.bytesize - MAC_SIZE, got #{dst.bytesize}") end - if LibSodium.crypto_secretbox_open_easy(dst, src, src.bytesize, nonce.to_slice, @bytes) != 0 + if LibSodium.crypto_secretbox_open_easy(dst, src, src.bytesize, nonce.to_slice, self.to_slice) != 0 raise Sodium::Error::DecryptionFailed.new("crypto_secretbox_easy") end dst diff --git a/src/sodium/secure_buffer.cr b/src/sodium/secure_buffer.cr new file mode 100644 index 0000000..377b70d --- /dev/null +++ b/src/sodium/secure_buffer.cr @@ -0,0 +1,78 @@ +require "./lib_sodium" +require "./wipe" + +module Sodium + class SecureBuffer + getter bytesize + + # Allocate guarded memory using [sodium_malloc](https://libsodium.gitbook.io/doc/memory_management) + def initialize(@bytesize : Int32) + @ptr = LibSodium.sodium_malloc @bytesize + end + + # Returns a **readonly** random SecureBuffer. + def self.random(size) + buf = new(size) + Random::Secure.random_bytes buf.to_slice + buf.readonly + end + + # Copies bytes to a **readonly** SecureBuffer. + # Optionally erases bytes after copying if erase is set + def initialize(bytes : Bytes, erase = false) + initialize bytes.bytesize + bytes.copy_to self.to_slice + Sodium.memzero(bytes) if erase + readonly + end + + def wipe + readwrite + Sodium.memzero self.to_slice + end + + def finalize + LibSodium.sodium_free @ptr + end + + def to_slice + Slice(UInt8).new @ptr, @bytesize + end + + def to_unsafe + @ptr + end + + # Makes a region allocated using sodium_malloc() or sodium_allocarray() inaccessible. It cannot be read or written, but the data are preserved. + def noaccess + if LibSodium.sodium_mprotect_noaccess(@ptr) != 0 + raise "sodium_mprotect_noaccess" + end + self + end + + # Marks a region allocated using sodium_malloc() or sodium_allocarray() as read-only. + def readonly + if LibSodium.sodium_mprotect_readonly(@ptr) != 0 + raise "sodium_mprotect_readonly" + end + self + end + + # Marks a region allocated using sodium_malloc() or sodium_allocarray() as readable and writable, after having been protected using sodium_mprotect_readonly() or sodium_mprotect_noaccess(). + def readwrite + if LibSodium.sodium_mprotect_readwrite(@ptr) != 0 + raise "sodium_mprotect_readwrite" + end + self + end + + def ==(other : self) + self.to_slice == other.to_slice + end + + def ==(other : Bytes) + self.to_slice == other + end + end +end diff --git a/src/sodium/sign/public_key.cr b/src/sodium/sign/public_key.cr index 940f0b0..d4113bd 100644 --- a/src/sodium/sign/public_key.cr +++ b/src/sodium/sign/public_key.cr @@ -2,10 +2,10 @@ require "../lib_sodium" module Sodium class Sign::PublicKey < Key - KEY_SIZE = LibSodium.crypto_sign_publickeybytes - SIG_SIZE = LibSodium.crypto_sign_bytes + KEY_SIZE = LibSodium.crypto_sign_publickeybytes.to_i + SIG_SIZE = LibSodium.crypto_sign_bytes.to_i - getter bytes : Bytes + delegate to_slice, to: @bytes # :nodoc: # Only used by SecretKey diff --git a/src/sodium/sign/secret_key.cr b/src/sodium/sign/secret_key.cr index a2e68cb..fb730fd 100644 --- a/src/sodium/sign/secret_key.cr +++ b/src/sodium/sign/secret_key.cr @@ -15,49 +15,57 @@ module Sodium # key.public_key.verify_detached data # ``` class Sign::SecretKey < Sodium::Key - KEY_SIZE = LibSodium.crypto_sign_secretkeybytes - SIG_SIZE = LibSodium.crypto_sign_bytes - SEED_SIZE = LibSodium.crypto_sign_seedbytes + KEY_SIZE = LibSodium.crypto_sign_secretkeybytes.to_i + SIG_SIZE = LibSodium.crypto_sign_bytes.to_i + SEED_SIZE = LibSodium.crypto_sign_seedbytes.to_i getter public_key : PublicKey - @[Wipe::Var] - getter bytes : Bytes - @[Wipe::Var] - @seed : Bytes? + delegate to_slice, to: @sbuf + + @seed : SecureBuffer? # Generates a new random secret/public key pair. def initialize - @bytes = Bytes.new(KEY_SIZE) + @sbuf = SecureBuffer.new KEY_SIZE @public_key = PublicKey.new - if LibSodium.crypto_sign_keypair(@public_key.bytes, @bytes) != 0 + if LibSodium.crypto_sign_keypair(@public_key.to_slice, self.to_slice) != 0 raise Sodium::Error.new("crypto_sign_keypair") end end # Use existing secret and public keys. + # Copies secret key to a SecureBuffer. # Recomputes the public key from a secret key if missing. - def initialize(@bytes : Bytes, pkey : Bytes? = nil) - raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") unless @bytes.bytesize == KEY_SIZE + def initialize(bytes : Bytes, pkey : Bytes? = nil) + raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{bytes.bytesize}") unless bytes.bytesize == KEY_SIZE + @sbuf = SecureBuffer.new bytes 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 + if LibSodium.crypto_sign_ed25519_sk_to_pk(@public_key.to_slice, self.to_slice) != 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. - def initialize(*, seed : Bytes) + # Copies seed to a SecureBuffer. + def initialize(*, seed : Bytes, erase = false) + raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE + initialize(seed: SecureBuffer.new(seed, erase: erase)) + end + + # Derive a new secret/public key pair based on a consistent seed. + def initialize(*, seed : SecureBuffer) raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE @seed = seed - @bytes = Bytes.new(KEY_SIZE) + @sbuf = SecureBuffer.new KEY_SIZE @public_key = PublicKey.new - if LibSodium.crypto_sign_seed_keypair(@public_key.bytes, @bytes, seed) != 0 + if LibSodium.crypto_sign_seed_keypair(@public_key.to_slice, self.to_slice, seed.to_slice) != 0 raise Sodium::Error.new("crypto_sign_seed_keypair") end end @@ -70,7 +78,7 @@ module Sodium def sign_detached(message : Bytes) sig = Bytes.new(SIG_SIZE) - if LibSodium.crypto_sign_detached(sig, out sig_len, message, message.bytesize, @bytes) != 0 + if LibSodium.crypto_sign_detached(sig, out sig_len, message, message.bytesize, self.to_slice) != 0 raise Error.new("crypto_sign_detached") end sig