API changed all Key classes .bytes to .to_slice

Switched most custom Wipe implementation to libsodium guarded memory.
This commit is contained in:
Didactic Drunk 2019-07-03 17:56:02 -07:00
parent 769e02e4c7
commit d1c8829fcf
16 changed files with 262 additions and 132 deletions

View File

@ -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) - [ ] [One time auth](https://libsodium.gitbook.io/doc/advanced/poly1305)
- [ ] Padding - [ ] Padding
- Library features - Library features
- Faster builds by requiring what you need (`require "sodium/secret_box"`) - [x] Faster builds by requiring what you need (`require "sodium/secret_box"`)
- Controlled memory wiping (by calling `.close`) - [x] All SecretKey's held in libsodium guarded memory.
- Semi-automatic memory wiping (on GC). - [ ] Controlled memory wiping (by calling `.close`)
☑ Indicate specs are compared against test vectors from another source. ☑ Indicate specs are compared against test vectors from another source.
@ -96,38 +96,17 @@ dependencies:
## Usage ## 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. 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. Objects with a `.close` method also respond to `Class.open` and wipe when the block returns.
```crystal ```crystal
# TODO
Sodium::CryptoBox::SecretKey.open(sec_key, pub_key) do |secret_key| Sodium::CryptoBox::SecretKey.open(sec_key, pub_key) do |secret_key|
... Do crypto operations ... ... Do crypto operations ...
end end

View File

@ -2,30 +2,30 @@ require "../../spec_helper"
require "../../../src/sodium/crypto_box/secret_key" require "../../../src/sodium/crypto_box/secret_key"
private def new_key_bytes private def new_key_bytes
Sodium::CryptoBox::SecretKey.new.bytes Sodium::CryptoBox::SecretKey.new.to_slice
end end
describe Sodium::CryptoBox::SecretKey do describe Sodium::CryptoBox::SecretKey do
it "loads keys" do it "loads keys" do
key1 = Sodium::CryptoBox::SecretKey.new key1 = Sodium::CryptoBox::SecretKey.new
key2 = Sodium::CryptoBox::SecretKey.new key1.bytes, key1.public_key.bytes key2 = Sodium::CryptoBox::SecretKey.new key1.to_slice, key1.public_key.to_slice
key1.bytes.should eq key2.bytes key1.to_slice.should eq key2.to_slice
key1.public_key.bytes.should eq key2.public_key.bytes key1.public_key.to_slice.should eq key2.public_key.to_slice
end end
it "recomputes the public_key" do it "recomputes the public_key" do
key1 = Sodium::CryptoBox::SecretKey.new key1 = Sodium::CryptoBox::SecretKey.new
key2 = Sodium::CryptoBox::SecretKey.new key1.bytes key2 = Sodium::CryptoBox::SecretKey.new key1.to_slice
key1.bytes.should eq key2.bytes key1.to_slice.should eq key2.to_slice
key1.public_key.bytes.should eq key2.public_key.bytes key1.public_key.to_slice.should eq key2.public_key.to_slice
end end
it "seed keys" do it "seed keys" do
seed = Bytes.new Sodium::CryptoBox::SecretKey::SEED_SIZE seed = Bytes.new Sodium::CryptoBox::SecretKey::SEED_SIZE
key1 = Sodium::CryptoBox::SecretKey.new seed: seed key1 = Sodium::CryptoBox::SecretKey.new seed: seed
key2 = Sodium::CryptoBox::SecretKey.new seed: seed key2 = Sodium::CryptoBox::SecretKey.new seed: seed
key1.bytes.should eq key2.bytes key1.to_slice.should eq key2.to_slice
key1.public_key.bytes.should eq key2.public_key.bytes key1.public_key.to_slice.should eq key2.public_key.to_slice
end end
it "authenticated easy encrypt/decrypt" do it "authenticated easy encrypt/decrypt" do
@ -65,8 +65,4 @@ describe Sodium::CryptoBox::SecretKey do
String.new(decrypted).should eq(data) String.new(decrypted).should eq(data)
end end
it "wipes keys" do
check_wiped new_key_bytes
end
end end

View File

@ -8,7 +8,7 @@ describe Sodium::Kdf do
kdf1 = Sodium::Kdf.new kdf1 = Sodium::Kdf.new
# verify loading saved key # 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 # verify generated subkey's are the same after loading
key1_s1 = kdf1.derive CONTEXT, 0, 16 key1_s1 = kdf1.derive CONTEXT, 0, 16

View File

@ -71,18 +71,18 @@ describe Sodium::Pwhash do
it "key_derive fails without a mode" do it "key_derive fails without a mode" do
pwhash = pw_min pwhash = pw_min
expect_raises(ArgumentError) do expect_raises(ArgumentError) do
pwhash.key_derive pwhash.salt, "foo", 16 pwhash.key_derive pwhash.random_salt, "foo", 16
end end
end end
it "derives a key from a password" do it "derives a key from a password" do
pwhash = pw_min pwhash = pw_min
pwhash.mode = Sodium::Pwhash::Mode::Argon2id13 pwhash.mode = Sodium::Pwhash::Mode::Argon2id13
salt = pwhash.salt salt = pwhash.random_salt
key1 = pwhash.key_derive salt, "foo", 16 key1 = pwhash.key_derive salt, "foo", 16
key2 = pwhash.key_derive salt, "foo", 16 key2 = pwhash.key_derive salt, "foo", 16
key3 = pwhash.key_derive salt, "bar", 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.bytesize.should eq 16
key1.should eq key2 key1.should eq key2

View File

@ -1,31 +1,31 @@
require "../../spec_helper" require "../../spec_helper"
require "../../../src/sodium/sign/secret_key" require "../../../src/sodium/sign/secret_key"
private def new_sign_key_bytes private def new_sign_key_to_slice
Sodium::Sign::SecretKey.new.bytes Sodium::Sign::SecretKey.new.to_slice
end end
describe Sodium::Sign::SecretKey do describe Sodium::Sign::SecretKey do
it "loads keys" do it "loads keys" do
key1 = Sodium::Sign::SecretKey.new key1 = Sodium::Sign::SecretKey.new
key2 = Sodium::Sign::SecretKey.new key1.bytes, key1.public_key.bytes key2 = Sodium::Sign::SecretKey.new key1.to_slice, key1.public_key.to_slice
key1.bytes.should eq key2.bytes key1.to_slice.should eq key2.to_slice
key1.public_key.bytes.should eq key2.public_key.bytes key1.public_key.to_slice.should eq key2.public_key.to_slice
end end
it "recomputes the public key" do it "recomputes the public key" do
key1 = Sodium::Sign::SecretKey.new key1 = Sodium::Sign::SecretKey.new
key2 = Sodium::Sign::SecretKey.new key1.bytes key2 = Sodium::Sign::SecretKey.new key1.to_slice
key1.bytes.should eq key2.bytes key1.to_slice.should eq key2.to_slice
key1.public_key.bytes.should eq key2.public_key.bytes key1.public_key.to_slice.should eq key2.public_key.to_slice
end end
it "seed keys" do it "seed keys" do
seed = Bytes.new Sodium::Sign::SecretKey::SEED_SIZE seed = Bytes.new Sodium::Sign::SecretKey::SEED_SIZE
key1 = Sodium::Sign::SecretKey.new seed: seed key1 = Sodium::Sign::SecretKey.new seed: seed
key2 = Sodium::Sign::SecretKey.new seed: seed key2 = Sodium::Sign::SecretKey.new seed: seed
key1.bytes.should eq key2.bytes key1.to_slice.should eq key2.to_slice
key1.public_key.bytes.should eq key2.public_key.bytes key1.public_key.to_slice.should eq key2.public_key.to_slice
end end
it "signs and verifies" do it "signs and verifies" do
@ -45,8 +45,4 @@ describe Sodium::Sign::SecretKey do
skey.public_key.verify_detached "bar", sig skey.public_key.verify_detached "bar", sig
end end
end end
it "checks wiped" do
check_wiped new_sign_key_bytes
end
end end

View File

@ -7,7 +7,7 @@ module Sodium
class CryptoBox class CryptoBox
include Wipe include Wipe
MAC_SIZE = LibSodium.crypto_box_macbytes MAC_SIZE = LibSodium.crypto_box_macbytes.to_i
# BUG: precompute size # BUG: precompute size
@[Wipe::Var] @[Wipe::Var]

View File

@ -3,10 +3,10 @@ require "../key"
class Sodium::CryptoBox class Sodium::CryptoBox
class PublicKey < Key class PublicKey < Key
KEY_SIZE = LibSodium.crypto_box_publickeybytes KEY_SIZE = LibSodium.crypto_box_publickeybytes.to_i
SEAL_SIZE = LibSodium.crypto_box_sealbytes SEAL_SIZE = LibSodium.crypto_box_sealbytes
getter bytes : Bytes delegate to_slice, to: @bytes
# :nodoc: # :nodoc:
# Only used by SecretKey # Only used by SecretKey

View File

@ -6,55 +6,73 @@ require "../crypto_box"
class Sodium::CryptoBox class Sodium::CryptoBox
# Key used for encryption + authentication or encryption without authentication, not for unencrypted signing. # 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(). # If you don't want this behavior pass a duplicate of the key/seed to initialize().
class SecretKey < Key class SecretKey < Key
KEY_SIZE = LibSodium.crypto_box_secretkeybytes KEY_SIZE = LibSodium.crypto_box_secretkeybytes.to_i
SEED_SIZE = LibSodium.crypto_box_seedbytes SEED_SIZE = LibSodium.crypto_box_seedbytes.to_i
SEAL_SIZE = LibSodium.crypto_box_sealbytes SEAL_SIZE = LibSodium.crypto_box_sealbytes.to_i
getter public_key : PublicKey getter public_key : PublicKey
@[Wipe::Var] delegate to_slice, to: @sbuf
getter bytes : Bytes
@[Wipe::Var] @seed : SecureBuffer?
@seed : Bytes?
# Generate a new random secret/public key pair. # Generate a new random secret/public key pair.
def initialize def initialize
@bytes = Bytes.new(KEY_SIZE) @sbuf = SecureBuffer.new KEY_SIZE
@public_key = PublicKey.new @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") raise Sodium::Error.new("crypto_box_keypair")
end end
end end
# Use existing secret and public keys. # Use existing secret and public keys.
# Copies secret key to a SecureBuffer.
# Recomputes the public key from a secret key if missing. # 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 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 if pk = pkey
@public_key = PublicKey.new pk @public_key = PublicKey.new pk
else else
@public_key = PublicKey.new @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") raise Sodium::Error.new("crypto_scalarmult_base")
end end
end end
end end
# Derive a new secret/public key pair based on a consistent seed. # 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 raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE
@seed = seed @seed = seed
@bytes = Bytes.new(KEY_SIZE) @sbuf = SecureBuffer.new KEY_SIZE
@public_key = PublicKey.new @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") raise Sodium::Error.new("crypto_box_seed_keypair")
end end
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. # Return a Box containing a precomputed shared secret for use with authenticated encryption/decryption.
def box(public_key) : CryptoBox def box(public_key) : CryptoBox
CryptoBox.new self, public_key CryptoBox.new self, public_key
@ -77,7 +95,7 @@ class Sodium::CryptoBox
end end
def decrypt(src : Bytes, dst : Bytes = Bytes.new(src.bytesize - SEAL_SIZE)) : Bytes 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") raise Sodium::Error.new("crypto_box_seal_open")
end end
dst dst

View File

@ -1,4 +1,5 @@
require "./lib_sodium" require "./lib_sodium"
require "./secure_buffer"
require "./wipe" require "./wipe"
module Sodium module Sodium
@ -17,33 +18,38 @@ module Sodium
class Kdf class Kdf
include Wipe include Wipe
KEY_SIZE = LibSodium.crypto_kdf_keybytes KEY_SIZE = LibSodium.crypto_kdf_keybytes.to_i
CONTEXT_SIZE = LibSodium.crypto_kdf_contextbytes CONTEXT_SIZE = LibSodium.crypto_kdf_contextbytes
@[Wipe::Var] delegate to_slice, to: @sbuf
getter bytes : Bytes
delegate to_slice, to: @bytes
# Use an existing KDF key. # Use an existing KDF key.
# #
# WARNING: This class takes ownership of any key material passed to it. # Optionally erases bytes after copying if erase is set
# If you don't want this behavior pass a duplicate of the key to initialize(). def initialize(bytes : Bytes, erase = false)
def initialize(bytes : Bytes)
if bytes.bytesize != KEY_SIZE if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("bytes must be #{KEY_SIZE}, got #{bytes.bytesize}") raise ArgumentError.new("bytes must be #{KEY_SIZE}, got #{bytes.bytesize}")
end 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 end
# Generate a new random KDF key. # Generate a new random KDF key.
# #
# WARNING: This class takes ownership of any key material passed to it. # Make sure to save kdf.to_slice before kdf goes out of scope.
#
# Make sure to save kdf.bytes before kdf goes out of scope.
def initialize def initialize
@bytes = Random::Secure.random_bytes(KEY_SIZE) @sbuf = SecureBuffer.random KEY_SIZE
end end
# Derive a consistent subkey based on `context` and `subkey_id`. # Derive a consistent subkey based on `context` and `subkey_id`.
@ -52,17 +58,42 @@ module Sodium
# * context must be 8 bytes # * context must be 8 bytes
# * subkey_size must be 16..64 bytes as of libsodium 1.0.17 # * 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 context = context.to_slice
if context.bytesize != CONTEXT_SIZE if context.bytesize != CONTEXT_SIZE
raise ArgumentError.new("context must be #{CONTEXT_SIZE}, got #{context.bytesize}") raise ArgumentError.new("context must be #{CONTEXT_SIZE}, got #{context.bytesize}")
end end
subkey = Bytes.new subkey_size subkey = SecureBuffer.new subkey_size
if (ret = LibSodium.crypto_kdf_derive_from_key(subkey, subkey.bytesize, subkey_id, context, @bytes)) != 0 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)") raise Sodium::Error.new("crypto_kdf_derive_from_key returned #{ret} (subkey size is probably out of range)")
end end
subkey subkey
end 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
end end

View File

@ -1,15 +1,14 @@
require "./secure_buffer"
require "./wipe" require "./wipe"
module Sodium module Sodium
abstract class Key abstract class Key
include Sodium::Wipe include Sodium::Wipe
abstract def bytes abstract def to_slice : Bytes
delegate to_slice, to: @bytes
def to_base64 def to_base64
Base64.encode(bytes) Base64.encode(to_slice)
end end
def self.from_base64(encoded_key) def self.from_base64(encoded_key)

View File

@ -44,8 +44,16 @@ module Sodium
fun crypto_generichash_blake2b_keybytes_max : LibC::SizeT fun crypto_generichash_blake2b_keybytes_max : LibC::SizeT
fun crypto_generichash_blake2b_saltbytes : LibC::SizeT fun crypto_generichash_blake2b_saltbytes : LibC::SizeT
fun crypto_generichash_blake2b_personalbytes : LibC::SizeT fun crypto_generichash_blake2b_personalbytes : LibC::SizeT
fun sodium_memzero(Pointer(LibC::UChar), LibC::SizeT) : Nil 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() NONCE_SIZE = crypto_box_noncebytes()
fun crypto_secretbox_easy( fun crypto_secretbox_easy(

View File

@ -1,4 +1,5 @@
require "./lib_sodium" require "./lib_sodium"
require "./secure_buffer"
module Sodium module Sodium
# [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function) # [Argon2 Password Hashing](https://libsodium.gitbook.io/doc/password_hashing/the_argon2i_function)
@ -74,19 +75,19 @@ module Sodium
end end
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. # Must set a mode before calling.
def key_derive(salt, pass, key_bytes) def key_derive(salt, pass, key_bytes)
key_derive salt.to_slice, pass.to_slice, key_bytes key_derive salt.to_slice, pass.to_slice, key_bytes
end 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 raise "salt expected #{SALT_SIZE} bytes, got #{salt.bytesize} " if salt.bytesize != SALT_SIZE
if m = mode if m = mode
key = Bytes.new key_bytes key = SecureBuffer.new key_bytes
if LibSodium.crypto_pwhash(key, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0 if LibSodium.crypto_pwhash(key.to_slice, key.bytesize, pass, pass.bytesize, salt, @opslimit, @memlimit, m) != 0
raise Sodium::Error.new("crypto_pwhash_str") raise Sodium::Error.new("crypto_pwhash_str")
end end
key key
@ -95,8 +96,14 @@ module Sodium
end end
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 # Returns a random salt for use with #key_derive
def salt def random_salt
Random::Secure.random_bytes SALT_SIZE Random::Secure.random_bytes SALT_SIZE
end end
end end

View File

@ -18,23 +18,33 @@ module Sodium
# message = key.decrypt_easy encrypted, nonce # message = key.decrypt_easy encrypted, nonce
# ``` # ```
class SecretBox < Key class SecretBox < Key
KEY_SIZE = LibSodium.crypto_secretbox_keybytes KEY_SIZE = LibSodium.crypto_secretbox_keybytes.to_i
NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes.to_i
MAC_SIZE = LibSodium.crypto_secretbox_macbytes MAC_SIZE = LibSodium.crypto_secretbox_macbytes.to_i
@[Wipe::Var] delegate to_slice, to: @buf
getter bytes : Bytes
# Generate a new random key. # Generate a new random key held in a SecureBuffer.
def initialize def initialize
@bytes = Random::Secure.random_bytes(KEY_SIZE) @buf = SecureBuffer.random KEY_SIZE
end end
# Use an existing key from bytes. # Use an existing SecureBuffer.
def initialize(@bytes : Bytes) 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 if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")
end end
@buf = SecureBuffer.new bytes, erase: erase
end end
def encrypt_easy(data) def encrypt_easy(data)
@ -60,7 +70,7 @@ module Sodium
if dst.bytesize != (src.bytesize + MAC_SIZE) if dst.bytesize != (src.bytesize + MAC_SIZE)
raise ArgumentError.new("dst.bytesize must be src.bytesize + MAC_SIZE, got #{dst.bytesize}") raise ArgumentError.new("dst.bytesize must be src.bytesize + MAC_SIZE, got #{dst.bytesize}")
end 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") raise Sodium::Error.new("crypto_secretbox_easy")
end end
dst dst
@ -77,7 +87,7 @@ module Sodium
if dst.bytesize != (src.bytesize - MAC_SIZE) if dst.bytesize != (src.bytesize - MAC_SIZE)
raise ArgumentError.new("dst.bytesize must be src.bytesize - MAC_SIZE, got #{dst.bytesize}") raise ArgumentError.new("dst.bytesize must be src.bytesize - MAC_SIZE, got #{dst.bytesize}")
end 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") raise Sodium::Error::DecryptionFailed.new("crypto_secretbox_easy")
end end
dst dst

View File

@ -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

View File

@ -2,10 +2,10 @@ require "../lib_sodium"
module Sodium module Sodium
class Sign::PublicKey < Key class Sign::PublicKey < Key
KEY_SIZE = LibSodium.crypto_sign_publickeybytes KEY_SIZE = LibSodium.crypto_sign_publickeybytes.to_i
SIG_SIZE = LibSodium.crypto_sign_bytes SIG_SIZE = LibSodium.crypto_sign_bytes.to_i
getter bytes : Bytes delegate to_slice, to: @bytes
# :nodoc: # :nodoc:
# Only used by SecretKey # Only used by SecretKey

View File

@ -15,49 +15,57 @@ module Sodium
# key.public_key.verify_detached data # key.public_key.verify_detached data
# ``` # ```
class Sign::SecretKey < Sodium::Key class Sign::SecretKey < Sodium::Key
KEY_SIZE = LibSodium.crypto_sign_secretkeybytes KEY_SIZE = LibSodium.crypto_sign_secretkeybytes.to_i
SIG_SIZE = LibSodium.crypto_sign_bytes SIG_SIZE = LibSodium.crypto_sign_bytes.to_i
SEED_SIZE = LibSodium.crypto_sign_seedbytes SEED_SIZE = LibSodium.crypto_sign_seedbytes.to_i
getter public_key : PublicKey getter public_key : PublicKey
@[Wipe::Var] delegate to_slice, to: @sbuf
getter bytes : Bytes
@[Wipe::Var] @seed : SecureBuffer?
@seed : Bytes?
# Generates a new random secret/public key pair. # Generates a new random secret/public key pair.
def initialize def initialize
@bytes = Bytes.new(KEY_SIZE) @sbuf = SecureBuffer.new KEY_SIZE
@public_key = PublicKey.new @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") raise Sodium::Error.new("crypto_sign_keypair")
end end
end end
# Use existing secret and public keys. # Use existing secret and public keys.
# Copies secret key to a SecureBuffer.
# Recomputes the public key from a secret key if missing. # 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 sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") unless @bytes.bytesize == KEY_SIZE 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 if pk = pkey
@public_key = PublicKey.new pkey @public_key = PublicKey.new pkey
else else
@public_key = PublicKey.new @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") raise Sodium::Error.new("crypto_sign_ed25519_sk_to_pk")
end end
end end
end end
# Derive a new secret/public key pair based on a consistent seed. # 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 raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE
@seed = seed @seed = seed
@bytes = Bytes.new(KEY_SIZE) @sbuf = SecureBuffer.new KEY_SIZE
@public_key = PublicKey.new @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") raise Sodium::Error.new("crypto_sign_seed_keypair")
end end
end end
@ -70,7 +78,7 @@ module Sodium
def sign_detached(message : Bytes) def sign_detached(message : Bytes)
sig = Bytes.new(SIG_SIZE) 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") raise Error.new("crypto_sign_detached")
end end
sig sig