From 0e1b64b1bfebca2f77161accf317d12b70980b07 Mon Sep 17 00:00:00 2001 From: Didactic Drunk <1479616+didactic-drunk@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:50:26 -0700 Subject: [PATCH] Partial internal switch to Crypto::Secret API --- spec/sodium/cipher/chalsa_spec.cr | 11 +---- spec/sodium/secure_buffer_spec.cr | 10 ++--- spec/sodium/sign/secret_key_spec.cr | 32 +++++++++------ src/sodium/cipher/aead/chalsa.cr | 12 +++--- src/sodium/cipher/chalsa.cr | 33 +++++++++------ src/sodium/cipher/secret_stream.cr | 57 +++++++++++++++---------- src/sodium/kdf.cr | 6 ++- src/sodium/password/key.cr | 8 ++-- src/sodium/secret_box.cr | 12 +++--- src/sodium/secure_buffer.cr | 20 ++++----- src/sodium/sign/secret_key.cr | 64 +++++++++++++++++++---------- 11 files changed, 153 insertions(+), 112 deletions(-) diff --git a/spec/sodium/cipher/chalsa_spec.cr b/spec/sodium/cipher/chalsa_spec.cr index d13db83..d48f1ae 100644 --- a/spec/sodium/cipher/chalsa_spec.cr +++ b/spec/sodium/cipher/chalsa_spec.cr @@ -7,15 +7,8 @@ require "../../../src/sodium/cipher/chalsa" it "xors" do data = Bytes.new(100) - cipher1 = Sodium::Cipher::{{ name.id }}.new - cipher2 = Sodium::Cipher::{{ name.id }}.new - - key = cipher1.random_key - cipher2.key = key - - nonce = cipher1.random_nonce - cipher2.nonce = nonce - + cipher1 = Sodium::Cipher::{{ name.id }}.random + cipher2 = Sodium::Cipher::{{ name.id }}.new key: cipher1.key, nonce: cipher1.nonce output = cipher1.update data output.should_not eq data # Verify encryption did something. diff --git a/spec/sodium/secure_buffer_spec.cr b/spec/sodium/secure_buffer_spec.cr index 58dfe8f..af1d2c0 100644 --- a/spec/sodium/secure_buffer_spec.cr +++ b/spec/sodium/secure_buffer_spec.cr @@ -1,9 +1,12 @@ require "../spec_helper" require "../../src/sodium/secure_buffer" +require "crypto-secret/test" class FakeError < Exception end +test_secret_class Sodium::SecureBuffer + describe Sodium::SecureBuffer do it "allocates empty" do buf = Sodium::SecureBuffer.new 5 @@ -18,13 +21,6 @@ describe Sodium::SecureBuffer do buf.readwrite end - it "allocates random" do - buf1 = Sodium::SecureBuffer.random 5 - buf2 = Sodium::SecureBuffer.random 5 - (buf1 == buf2).should be_false - buf1.wipe - end - it "copies and erases" do bytes = Bytes.new(5) { 1_u8 } diff --git a/spec/sodium/sign/secret_key_spec.cr b/spec/sodium/sign/secret_key_spec.cr index 563b3a3..116ce45 100644 --- a/spec/sodium/sign/secret_key_spec.cr +++ b/spec/sodium/sign/secret_key_spec.cr @@ -20,40 +20,48 @@ detached_test_vectors = [ private def sign_from_vec(vec) seckey = Sodium::Sign::SecretKey.new seed: vec[:seed].hexbytes - seckey.to_slice.should eq vec[:secret_key].hexbytes + seckey.key.readonly do |sslice| + sslice.should eq vec[:secret_key].hexbytes + end seckey.public_key.to_slice.should eq vec[:public_key].hexbytes plaintext = vec[:plaintext].hexbytes signature = vec[:signature].hexbytes {seckey, plaintext, signature} end -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.to_slice, key1.public_key.to_slice - key1.to_slice.should eq key2.to_slice + key2 = key1.key.readonly do |kslice| + Sodium::Sign::SecretKey.new kslice, key1.public_key.to_slice + end + key1.key.should eq key2.key 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.to_slice - key1.to_slice.should eq key2.to_slice + key2 = key1.key.readonly do |kslice| + Sodium::Sign::SecretKey.new kslice + end + key1.key.should eq key2.key key1.public_key.to_slice.should eq key2.public_key.to_slice end - it "seed keys" do + it "loading seed -> key -> seed" do seed = Bytes.new Sodium::Sign::SecretKey::SEED_SIZE key1 = Sodium::Sign::SecretKey.new seed: seed - key2 = Sodium::Sign::SecretKey.new seed: Sodium::Sign::SecretKey.new(key1.to_slice).seed - key1.to_slice.should eq key2.to_slice + key2 = key1.key.readonly do |kslice| + Sodium::Sign::SecretKey.new kslice + end + key3 = Sodium::Sign::SecretKey.new seed: key2.seed + key1.key.should eq key2.key + key1.key.should eq key3.key key1.public_key.to_slice.should eq key2.public_key.to_slice + key1.public_key.to_slice.should eq key3.public_key.to_slice key1.seed.should eq seed key1.seed.should eq key2.seed + key1.seed.should eq key3.seed end it "signs and verifies" do diff --git a/src/sodium/cipher/aead/chalsa.cr b/src/sodium/cipher/aead/chalsa.cr index 98edc08..92bd1bf 100644 --- a/src/sodium/cipher/aead/chalsa.cr +++ b/src/sodium/cipher/aead/chalsa.cr @@ -12,7 +12,7 @@ module Sodium::Cipher::Aead @key = SecureBuffer.random key_size end - # Initializes with a reference to an existing ky. + # Initializes with a reference to an existing key. def initialize(@key : SecureBuffer) raise ArgumentError.new("key size mismatch, got #{@key.bytesize}, wanted #{key_size}") if @key.bytesize != key_size @key.readonly @@ -30,6 +30,7 @@ module Sodium::Cipher::Aead offset = src.bytesize dst ||= Bytes.new (offset + mac_size) mac = dst[offset, mac_size] + _, _, nonce = encrypt_detached src.to_slice, dst[0, offset], mac: mac, nonce: nonce, additional: additional {dst, nonce} end @@ -114,8 +115,8 @@ module Sodium::Cipher::Aead ad_len = additional.try(&.bytesize) || 0 nonce.used! - @key.readonly do - r = LibSodium.crypto_aead{{ val.id }}_encrypt_detached(dst, mac, out mac_len, src, src.bytesize, additional, ad_len, nil, nonce.to_slice, @key.to_slice) + @key.readonly do |kslice| + r = LibSodium.crypto_aead{{ val.id }}_encrypt_detached(dst, mac, out mac_len, src, src.bytesize, additional, ad_len, nil, nonce.to_slice, kslice) raise Sodium::Error.new("crypto_aead_{{ val.id }}_encrypt_detached") if r != 0 raise Sodium::Error.new("crypto_aead_{{ val.id }}_encrypt_detached mac size mismatch") if mac_len != MAC_SIZE end @@ -134,8 +135,9 @@ module Sodium::Cipher::Aead ad_len = additional.try(&.bytesize) || 0 - r = @key.readonly do - LibSodium.crypto_aead{{ val.id }}_decrypt_detached(dst, nil, src, src.bytesize, mac, additional, ad_len, nonce.to_slice, @key.to_slice) + r = @key.readonly do |kslice| + LibSodium.crypto_aead{{ val.id }}_decrypt_detached(dst, nil, src, src.bytesize, mac, additional, ad_len, nonce. + to_slice, kslice) end raise Sodium::Error::DecryptionFailed.new("crypto_aead_{{ val.id }}_decrypt_detached") if r != 0 dst diff --git a/src/sodium/cipher/chalsa.cr b/src/sodium/cipher/chalsa.cr index 89ca9be..ee0ac61 100644 --- a/src/sodium/cipher/chalsa.cr +++ b/src/sodium/cipher/chalsa.cr @@ -6,27 +6,29 @@ module Sodium::Cipher # # What? They're both dance? abstract class Chalsa - @key : Bytes | SecureBuffer | Nil - @nonce : Bytes? + getter key : Crypto::Secret + getter! nonce : Bytes? # Advanced usage. Don't touch. property offset = 0 - getter! key - getter! nonce - - def initialize - end - - def initialize(key = nil, nonce = nil) - self.key = key if key + def initialize(key : Crypto::Secret | Bytes, nonce = nil) + raise ArgumentError.new("key must be #{key_size} bytes, got #{key.bytesize}") if key.bytesize != key_size + @key = key.is_a?(Crypto::Secret) ? key : Sodium::SecureBuffer.new(key) +# self.key = key if key self.nonce = nonce if nonce end - def key=(key : Bytes | SecureBuffer) + @[Deprecated("Use constructor to set key")] + def key=(key : Bytes | Crypto::Secret) : Crypto::Secret raise ArgumentError.new("key must be #{key_size} bytes, got #{key.bytesize}") if key.bytesize != key_size - @key = key - key + @key = case key + in Crypto::Secret + key + in Bytes + Sodium::SecureBuffer.new key + end + @key.not_nil! end def nonce=(nonce : Bytes) @@ -35,6 +37,7 @@ module Sodium::Cipher nonce end + @[Deprecated("Use constructor to set key")] def random_key self.key = SecureBuffer.random key_size end @@ -103,6 +106,10 @@ module Sodium::Cipher KEY_SIZE = LibSodium.crypto_stream_chacha20_{{ ietf ? "ietf_".id : "".id }}keybytes.to_i32 NONCE_SIZE = LibSodium.crypto_stream_chacha20_{{ ietf ? "ietf_".id : "".id }}noncebytes.to_i32 + def self.random + new key: Sodium::SecureBuffer.random(KEY_SIZE), nonce: Random::Secure.random_bytes(NONCE_SIZE) + end + # Xor's src with the cipher output and places in dst. # # src and dst may be the same object but should not overlap. diff --git a/src/sodium/cipher/secret_stream.cr b/src/sodium/cipher/secret_stream.cr index 5525de6..6273508 100644 --- a/src/sodium/cipher/secret_stream.cr +++ b/src/sodium/cipher/secret_stream.cr @@ -3,7 +3,7 @@ require "../secure_buffer" module Sodium::Cipher abstract class SecretStream - @state : SecureBuffer + @state : Crypto::Secret @encrypt_decrypt = 0 @initialized = false @@ -17,7 +17,7 @@ module Sodium::Cipher # * This property is set to nil after calling .update. property additional : Bytes? = nil - @key : Bytes | SecureBuffer | Nil = nil + @key : Crypto::Secret | Nil = nil def initialize @state = SecureBuffer.new state_size @@ -31,15 +31,18 @@ module Sodium::Cipher @encrypt_decrypt = -1 end - def key=(key : Bytes | SecureBuffer) + def key=(key : Bytes | Crypto::Secret) : Crypto::Secret raise ArgumentError.new("key must be #{key_size} bytes, got #{key.bytesize}") if key.bytesize != key_size + key = key.is_a?(Crypto::Secret) ? key : Sodium::SecureBuffer.new(key) @key = key key end # Returns a random key in a SecureBuffer. - def random_key - self.key = SecureBuffer.random key_size + def random_key : Crypto::Secret + k = SecureBuffer.random key_size + self.key = k + k end # Only used for encryption. @@ -102,15 +105,21 @@ module Sodium::Cipher case @encrypt_decrypt when 1 - if LibSodium.crypto_secretstream_{{ val.id }}_push(@state.to_slice, dst.to_slice, out dst_size, src, src.bytesize, ad, ad_size, @tag) != 0 - raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic") + dst_size = @state.readwrite do |stslice| + if LibSodium.crypto_secretstream_{{ val.id }}_push(stslice, dst.to_slice, out dsize, src, src.bytesize, ad, ad_size, @tag) != 0 + raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic") + end + dsize end @tag = 0 @additional = nil dst[0, dst_size] when -1 - if LibSodium.crypto_secretstream_{{ val.id }}_pull(@state.to_slice, dst.to_slice, out dst_size2, out @tag, src, src.bytesize, ad, ad_size) != 0 - raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic") + dst_size2 = @state.readwrite do |stslice| + if LibSodium.crypto_secretstream_{{ val.id }}_pull(stslice, dst.to_slice, out dsize2, out @tag, src, src.bytesize, ad, ad_size) != 0 + raise Sodium::Error.new("crypto_streamsecret_{{ val.id }}_xor_ic") + end + dsize2 end @additional = nil dst[0, dst_size2] @@ -123,19 +132,25 @@ module Sodium::Cipher raise Sodium::Error.new("can't initialize more than once") if @initialized if k = @key - case @encrypt_decrypt - when 1 - if LibSodium.crypto_secretstream_xchacha20poly1305_init_push(@state.to_slice, header_buf.to_slice, k.to_slice) != 0 - raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push") + k.readonly do |kslice| + case @encrypt_decrypt + when 1 + @state.readwrite do |stslice| + if LibSodium.crypto_secretstream_xchacha20poly1305_init_push(stslice, header_buf.to_slice, kslice) != 0 + raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push") + end + end + when -1 + @state.readwrite do |stslice| + if LibSodium.crypto_secretstream_xchacha20poly1305_init_pull(stslice, header_buf.to_slice, kslice) != 0 + raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push") + end + end + when 0 + raise Sodium::Error.new("must call .encrypt or .decrypt first") + else + abort "invalid encrypt_decrypt state #{@encrypt_decrypt}" end - when -1 - if LibSodium.crypto_secretstream_xchacha20poly1305_init_pull(@state.to_slice, header_buf.to_slice, k.to_slice) != 0 - raise Sodium::Error.new("crypto_secretstream_xchacha20poly1305_init_push") - end - when 0 - raise Sodium::Error.new("must call .encrypt or .decrypt first") - else - abort "invalid encrypt_decrypt state #{@encrypt_decrypt}" end else raise Sodium::Error.new("must set an encryption/decryption key") diff --git a/src/sodium/kdf.cr b/src/sodium/kdf.cr index 71fb459..1be3e90 100644 --- a/src/sodium/kdf.cr +++ b/src/sodium/kdf.cr @@ -38,6 +38,8 @@ module Sodium # Returns key delegate_to_slice to: @sbuf + @sbuf : Crypto::Secret + # Use an existing KDF key. # # * Copies key to a new SecureBuffer @@ -50,8 +52,8 @@ module Sodium @sbuf = SecureBuffer.new(bytes, erase).noaccess end - # Use an existing KDF SecureBuffer key. - def initialize(@sbuf : SecureBuffer) + # Use an existing KDF Crypto::Secret key. + def initialize(@sbuf : Crypto::Secret) if @sbuf.bytesize != KEY_SIZE raise ArgumentError.new("bytes must be #{KEY_SIZE}, got #{sbuf.bytesize}") end diff --git a/src/sodium/password/key.cr b/src/sodium/password/key.cr index 8017c13..bfd2fac 100644 --- a/src/sodium/password/key.cr +++ b/src/sodium/password/key.cr @@ -25,7 +25,7 @@ module Sodium::Password end # :nodoc: - def derive_key(key : SecureBuffer, pass : Bytes | String, *, salt : Bytes? = nil) : Nil + def derive_key(key : Crypto::Secret, pass : Bytes | String, *, salt : Bytes? = nil) : Nil m = mode || raise ArgumentError.new("mode not set") salt ||= @salt @@ -33,8 +33,10 @@ module Sodium::Password salt = salt.not_nil! raise "salt expected #{SALT_SIZE} bytes, got #{salt.bytesize} " if salt.bytesize != SALT_SIZE - if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass.to_slice, pass.bytesize, salt.to_slice, @ops, @mem, m) != 0 - raise Sodium::Error.new("crypto_pwhash") + key.readwrite do |key_slice| + if LibSodium.crypto_pwhash(key_slice, key_slice.bytesize, pass.to_slice, pass.bytesize, salt.to_slice, @ops, @mem, m) != 0 + raise Sodium::Error.new("crypto_pwhash") + end end end diff --git a/src/sodium/secret_box.cr b/src/sodium/secret_box.cr index 4a137b1..9cca6ab 100644 --- a/src/sodium/secret_box.cr +++ b/src/sodium/secret_box.cr @@ -19,19 +19,19 @@ module Sodium NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes.to_i MAC_SIZE = LibSodium.crypto_secretbox_macbytes.to_i - # Returns key + @[Deprecated("Use `key.readonly` or `key.readwrite`")] delegate_to_slice to: @key # Encryption key - getter key : SecureBuffer + getter key : Crypto::Secret # Generate a new random key held in a SecureBuffer. def initialize @key = SecureBuffer.random KEY_SIZE end - # Use an existing SecureBuffer. - def initialize(@key : SecureBuffer) + # Use an existing Crypto::Secret. + def initialize(@key : Crypto::Secret) if @key.bytesize != KEY_SIZE raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{@key.bytesize}") end @@ -91,8 +91,8 @@ module Sodium dst ||= Bytes.new dst_size raise ArgumentError.new("dst.bytesize must be src.bytesize - MAC_SIZE, got #{dst.bytesize}") if dst.bytesize != (src.bytesize - MAC_SIZE) - r = @key.readonly do - LibSodium.crypto_secretbox_open_easy(dst, src, src.bytesize, nonce.to_slice, @key) + r = @key.readonly do |kslice| + LibSodium.crypto_secretbox_open_easy(dst, src, src.bytesize, nonce.to_slice, kslice) end raise Sodium::Error::DecryptionFailed.new("crypto_secretbox_easy") if r != 0 dst diff --git a/src/sodium/secure_buffer.cr b/src/sodium/secure_buffer.cr index 883ceb6..de3f07a 100644 --- a/src/sodium/secure_buffer.cr +++ b/src/sodium/secure_buffer.cr @@ -10,8 +10,6 @@ module Sodium class SecureBuffer include Crypto::Secret::Stateful - # @state = State::Readwrite - getter bytesize : Int32 def initialize(@bytesize : Int32) @@ -31,13 +29,13 @@ module Sodium # :nodoc: # For .dup - def initialize(sbuf : self) + def initialize(sbuf : Crypto::Secret) initialize sbuf.bytesize # Maybe not thread safe - sbuf.readonly do |s1| - self.to_slice do |s2| - s1.copy_to s2.to_slice + sbuf.readonly do |sslice| + readwrite do |dslice| + s1.copy_to s2 end end @@ -63,8 +61,9 @@ module Sodium Slice(UInt8).new @ptr, @bytesize end - def to_slice(& : Bytes -> Nil) - yield Bytes.new @ptr, @bytesize + protected def to_slice(& : Bytes -> Nil) + ro = @state < State::Readonly + yield Bytes.new(@ptr, @bytesize, read_only: ro) end # :nodoc: @@ -72,11 +71,6 @@ module Sodium @ptr end - # WARNING: Not thread safe unless this object is readonly or readwrite - def dup - self.class.new self - end - protected def readwrite_impl : Nil if LibSodium.sodium_mprotect_readwrite(@ptr) != 0 raise "sodium_mprotect_readwrite" diff --git a/src/sodium/sign/secret_key.cr b/src/sodium/sign/secret_key.cr index 50ef624..4ca2de1 100644 --- a/src/sodium/sign/secret_key.cr +++ b/src/sodium/sign/secret_key.cr @@ -20,15 +20,20 @@ module Sodium getter public_key : PublicKey - # Returns key - delegate_to_slice to: @sbuf + @[Deprecated("Switching to Crypto::Secret. Use `key.readonly` or `key.readwrite`")] + delegate_to_slice to: @key + + getter key : Crypto::Secret + @seed : Crypto::Secret? # Generates a new random secret/public key pair. def initialize - @sbuf = SecureBuffer.new KEY_SIZE + @key = SecureBuffer.new KEY_SIZE @public_key = PublicKey.new - if LibSodium.crypto_sign_keypair(@public_key.to_slice, self.to_slice) != 0 - raise Sodium::Error.new("crypto_sign_keypair") + @key.readwrite do |kslice| + if LibSodium.crypto_sign_keypair(@public_key.to_slice, kslice) != 0 + raise Sodium::Error.new("crypto_sign_keypair") + end end end @@ -38,13 +43,15 @@ module Sodium def initialize(bytes : Bytes, pkey : Bytes? = nil, *, erase = false) raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{bytes.bytesize}") unless bytes.bytesize == KEY_SIZE - @sbuf = SecureBuffer.new bytes, erase: erase + @key = SecureBuffer.new bytes, erase: erase if pk = pkey @public_key = PublicKey.new pk else @public_key = PublicKey.new - 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") + @key.readwrite do |kslice| + if LibSodium.crypto_sign_ed25519_sk_to_pk(@public_key.to_slice, kslice) != 0 + raise Sodium::Error.new("crypto_sign_ed25519_sk_to_pk") + end end end end @@ -61,17 +68,25 @@ module Sodium raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE @seed = seed - @sbuf = SecureBuffer.new KEY_SIZE + @key = SecureBuffer.new KEY_SIZE @public_key = PublicKey.new - 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") + seed.readonly do |seed_slice| + @key.readwrite do |kslice| + if LibSodium.crypto_sign_seed_keypair(@public_key.to_slice, kslice, seed_slice) != 0 + raise Sodium::Error.new("crypto_sign_seed_keypair") + end + end end end - getter seed : SecureBuffer? do - SecureBuffer.new(SEED_SIZE).tap do |s| - if LibSodium.crypto_sign_ed25519_sk_to_seed(s.to_slice, self.to_slice) != 0 - raise Sodium::Error.new("crypto_sign_ed25519_sk_to_seed") + getter seed : Crypto::Secret? do + SecureBuffer.new(SEED_SIZE).tap do |seed_buf| + @key.readonly do |kslice| + seed_buf.readwrite do |seed_slice| + if LibSodium.crypto_sign_ed25519_sk_to_seed(seed_slice, kslice) != 0 + raise Sodium::Error.new("crypto_sign_ed25519_sk_to_seed") + end + end end end.readonly end @@ -84,17 +99,24 @@ module Sodium def sign_detached(message : Bytes) sig = Bytes.new(SIG_SIZE) - if LibSodium.crypto_sign_detached(sig, out sig_len, message, message.bytesize, self.to_slice) != 0 - raise Error.new("crypto_sign_detached") + @key.readonly do |kslice| + if LibSodium.crypto_sign_detached(sig, out sig_len, message, message.bytesize, kslice) != 0 + raise Error.new("crypto_sign_detached") + end + raise "expected #{sig.bytesize}, got #{sig_len}" if sig.bytesize != sig_len end sig end def to_curve25519 : CryptoBox::SecretKey - key = SecureBuffer.new CryptoBox::SecretKey::KEY_SIZE - LibSodium.crypto_sign_ed25519_sk_to_curve25519 key.to_slice, @sbuf.to_slice - key.readonly - CryptoBox::SecretKey.new key + sbuf = SecureBuffer.new CryptoBox::SecretKey::KEY_SIZE + sbuf.readwrite do |sbuf_slice| + @key.readonly do |kslice| + LibSodium.crypto_sign_ed25519_sk_to_curve25519 sbuf_slice, kslice + end + end + sbuf.readonly + CryptoBox::SecretKey.new sbuf end end end