API changed all Key classes .bytes to .to_slice
Switched most custom Wipe implementation to libsodium guarded memory.
This commit is contained in:
parent
769e02e4c7
commit
d1c8829fcf
33
README.md
33
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)
|
- [ ] [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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
78
src/sodium/secure_buffer.cr
Normal file
78
src/sodium/secure_buffer.cr
Normal 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
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user