Add seed support to CryptoBox and Sign.

Wiping now supports multiple variables by Annotation.
master
Didactic Drunk 2019-06-29 17:21:00 -07:00
parent 41a55a9593
commit 162cd72b0c
17 changed files with 149 additions and 66 deletions

View File

@ -6,8 +6,8 @@ Crystal bindings for the [libsodium API](https://libsodium.gitbook.io/doc/)
## Goals
* Provide an easy to use API based on reviewing most other [libsodium bindings](https://libsodium.gitbook.io/doc/bindings_for_other_languages).
* Provide the most commonly used libsodium API's.
* Provide an easy to use API based on reviewing most other [libsodium bindings](https://libsodium.gitbook.io/doc/bindings_for_other_languages).
* Test for compatibility against other libsodium bindings to ensure interoperability.
* Always provide a stream interface to handle arbitrarily sized data when one is available.
* Drop in replacement classes compatible with OpenSSL::{Digest,Cipher} when possible.

View File

@ -12,6 +12,13 @@ describe Sodium::CryptoBox::SecretKey do
key1.public_key.bytes.should eq key2.public_key.bytes
end
it "recomputes the public_key" do
key1 = Sodium::CryptoBox::SecretKey.new
key2 = Sodium::CryptoBox::SecretKey.new key1.bytes
key1.bytes.should eq key2.bytes
key1.public_key.bytes.should eq key2.public_key.bytes
end
it "seed keys" do
seed = Bytes.new Sodium::CryptoBox::SecretKey::SEED_SIZE
key1 = Sodium::CryptoBox::SecretKey.new seed: seed

View File

@ -1,4 +1,4 @@
require "../spec_helper"
require "../../spec_helper"
libsodium_comparisons = [
{

View File

@ -1,7 +1,7 @@
require "../../spec_helper"
require "../../../src/sodium/sign/secret_key"
private def new_key_bytes
private def new_sign_key_bytes
Sodium::Sign::SecretKey.new.bytes
end
@ -11,8 +11,13 @@ describe Sodium::Sign::SecretKey do
key2 = Sodium::Sign::SecretKey.new key1.bytes, key1.public_key.bytes
key1.bytes.should eq key2.bytes
key1.public_key.bytes.should eq key2.public_key.bytes
end
# TODO: test loading when missing public_key
it "recomputes the public key" do
key1 = Sodium::Sign::SecretKey.new
key2 = Sodium::Sign::SecretKey.new key1.bytes
key1.bytes.should eq key2.bytes
key1.public_key.bytes.should eq key2.public_key.bytes
end
it "seed keys" do
@ -42,6 +47,6 @@ describe Sodium::Sign::SecretKey do
end
it "checks wiped" do
check_wiped new_key_bytes
check_wiped new_sign_key_bytes
end
end

View File

@ -15,7 +15,3 @@ module Sodium
end
require "./sodium/**"
if Sodium::LibSodium.sodium_init == -1
abort "Failed to init libsodium"
end

View File

@ -5,6 +5,7 @@ module Sodium::CryptoBox
include Wipe
# BUG: precompute size
@[Wipe::Var]
@bytes = Bytes.new(1)
def initialize(@secret_key : SecretKey, @public_key : PublicKey)

View File

@ -7,6 +7,12 @@ module Sodium::CryptoBox
getter bytes : Bytes
# :nodoc:
# Only used by SecretKey
def initialize
@bytes = Bytes.new KEY_SIZE
end
def initialize(@bytes : Bytes)
if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")

View File

@ -1,34 +1,42 @@
require "../lib_sodium"
module Sodium::CryptoBox
# WARNING: This class takes ownership of any key material passed to it.
# If you don't want this behavior pass a duplicate of the key/seed to initialize().
class SecretKey < Key
include Wipe
KEY_SIZE = LibSodium.crypto_box_secretkeybytes
SEED_SIZE = LibSodium.crypto_box_seedbytes
MAC_SIZE = LibSodium::MAC_SIZE
getter public_key
getter public_key : PublicKey
@[Wipe::Var]
getter bytes : Bytes
@[Wipe::Var]
@seed : Bytes?
# Generate a new random secret/public key pair.
def initialize
pkey = Bytes.new(PublicKey::KEY_SIZE)
@bytes = Bytes.new(KEY_SIZE)
@public_key = PublicKey.new pkey
v = LibSodium.crypto_box_keypair(pkey, @bytes)
if v != 0
raise Sodium::Error.new("crypto_box_keypair #{v}")
@public_key = PublicKey.new
if LibSodium.crypto_box_keypair(@public_key.bytes, @bytes) != 0
raise Sodium::Error.new("crypto_box_keypair")
end
end
# Use existing Secret and Public keys.
def initialize(@bytes : Bytes, pkey : Bytes)
# TODO: finish regenerating public_key
if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")
# Use existing secret and public keys.
# Recomputes the public key from a secret key if missing.
def initialize(@bytes : Bytes, pkey : Bytes? = nil)
raise ArgumentError.new("Secret key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}") if bytes.bytesize != KEY_SIZE
if pk = pkey
@public_key = PublicKey.new pk
else
@public_key = PublicKey.new
if LibSodium.crypto_scalarmult_base(@public_key.bytes, @bytes) != 0
raise Sodium::Error.new("crypto_scalarmult_base")
end
end
@public_key = PublicKey.new pkey
end
# Derive a new secret/public key pair based on a consistent seed.
@ -36,10 +44,9 @@ module Sodium::CryptoBox
raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE
@seed = seed
pkey = Bytes.new(PublicKey::KEY_SIZE)
@bytes = Bytes.new(KEY_SIZE)
@public_key = PublicKey.new pkey
if LibSodium.crypto_box_seed_keypair(pkey, @bytes, seed) != 0
@public_key = PublicKey.new
if LibSodium.crypto_box_seed_keypair(@public_key.bytes, @bytes, seed) != 0
raise Sodium::Error.new("crypto_box_seed_keypair")
end
end
@ -51,11 +58,11 @@ module Sodium::CryptoBox
# Create a new box and automatically close when the block exits.
def box(public_key)
pa = box public_key
b = box public_key
begin
yield pa
yield b
ensure
pa.close
b.close
end
end
end

View File

@ -4,6 +4,7 @@ module Sodium::Digest
class Blake2b
# provides copying digest/hexdigest methods
include OpenSSL::DigestBase
include Wipe
KEY_SIZE = LibSodium.crypto_generichash_blake2b_keybytes
KEY_SIZE_MIN = LibSodium.crypto_generichash_blake2b_keybytes_min
@ -19,42 +20,40 @@ module Sodium::Digest
getter digest_size
@[Wipe::Var]
@state = StaticArray(UInt8, 384).new 0
@key_size = 0
@have_salt = false
@have_personal = false
# implemented as static array's so clone works without jumping through hoops.
@[Wipe::Var]
@key = StaticArray(UInt8, 64).new 0
@salt = StaticArray(UInt8, 16).new 0
@personal = StaticArray(UInt8, 16).new 0
def initialize(@digest_size : Int32 = OUT_SIZE, key : Bytes? = nil, salt : Bytes? = nil, personal : Bytes? = nil)
if k = key
raise ArgumentError.new("key larger than KEY_SIZE_MAX, got #{k.bytesize}") if k.bytesize > KEY_SIZE_MAX
raise ArgumentError.new("key larger than KEY_SIZE_MAX(#{KEY_SIZE_MAX}), got #{k.bytesize}") if k.bytesize > KEY_SIZE_MAX
@key_size = k.bytesize
k.copy_to @key.to_slice
end
if sa = salt
raise ArgumentError.new("salt must be SALT_SIZE bytes, got #{sa.bytesize}") if sa.bytesize != SALT_SIZE
raise ArgumentError.new("salt must be SALT_SIZE(#{SALT_SIZE}) bytes, got #{sa.bytesize}") if sa.bytesize != SALT_SIZE
sa.copy_to @salt.to_slice
@have_salt = true
end
if pe = personal
raise ArgumentError.new("personal must be PERSONAL_SIZE bytes, got #{pe.bytesize}") if pe.bytesize != PERSONAL_SIZE
raise ArgumentError.new("personal must be PERSONAL_SIZE(#{PERSONAL_SIZE}) bytes, got #{pe.bytesize}") if pe.bytesize != PERSONAL_SIZE
pe.copy_to @personal.to_slice
@have_personal = true
end
reset
end
def reset
key = @key_size > 0 ? @key.to_unsafe : nil
salt = @have_salt ? @salt.to_unsafe : nil
personal = @have_personal ? @personal.to_unsafe : nil
key = @key.to_unsafe
salt = @salt.to_unsafe
personal = @personal.to_unsafe
if LibSodium.crypto_generichash_blake2b_init_salt_personal(@state, key, @key_size, @digest_size, salt, personal) != 0
raise Sodium::Error.new("blake2b_init_key_salt_personal")

View File

@ -1,26 +1,29 @@
module Sodium
class Kdf
KDF_KEY_SIZE = LibSodium.crypto_kdf_keybytes
KDF_CONTEXT_SIZE = LibSodium.crypto_kdf_contextbytes
property bytes : Bytes
delegate to_slice, to: @bytes
def initialize(bytes : Bytes)
if bytes.bytesize != LibSodium::KDF_KEY_SIZE
raise ArgumentError.new("bytes must be #{LibSodium::KDF_KEY_SIZE}, got #{bytes.bytesize}")
if bytes.bytesize != KDF_KEY_SIZE
raise ArgumentError.new("bytes must be #{KDF_KEY_SIZE}, got #{bytes.bytesize}")
end
@bytes = bytes
end
def initialize
@bytes = Random::Secure.random_bytes(LibSodium::KDF_KEY_SIZE)
@bytes = Random::Secure.random_bytes(KDF_KEY_SIZE)
end
# context must be 8 bytes
# subkey_size must be 16..64 bytes as of libsodium 1.0.17
def derive(context, subkey_id, subkey_size)
if context.bytesize != LibSodium::KDF_CONTEXT_SIZE
raise ArgumentError.new("context must be #{LibSodium::KDF_CONTEXT_SIZE}, got #{context.bytesize}")
if context.bytesize != KDF_CONTEXT_SIZE
raise ArgumentError.new("context must be #{KDF_CONTEXT_SIZE}, got #{context.bytesize}")
end
subkey = Bytes.new subkey_size

View File

@ -1,5 +1,9 @@
require "./wipe"
module Sodium
abstract class Key
include Sodium::Wipe
abstract def bytes
delegate to_slice, to: @bytes

View File

@ -42,14 +42,9 @@ module Sodium
fun crypto_generichash_blake2b_personalbytes : LibC::SizeT
fun sodium_memzero(Pointer(LibC::UChar), LibC::SizeT) : Nil
PUBLIC_KEY_SIZE = crypto_box_publickeybytes()
SECRET_KEY_SIZE = crypto_box_secretkeybytes()
NONCE_SIZE = crypto_box_noncebytes()
MAC_SIZE = crypto_box_macbytes()
SIGNATURE_SIZE = crypto_sign_bytes()
KDF_KEY_SIZE = crypto_kdf_keybytes()
KDF_CONTEXT_SIZE = crypto_kdf_contextbytes()
PWHASH_STR_SIZE = crypto_pwhash_strbytes()
NONCE_SIZE = crypto_box_noncebytes()
MAC_SIZE = crypto_box_macbytes()
SIGNATURE_SIZE = crypto_sign_bytes()
fun crypto_secretbox_easy(
output : Pointer(LibC::UChar),
@ -93,6 +88,11 @@ module Sodium
seed : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_scalarmult_base(
public_key_output : Pointer(LibC::UChar),
secret_key_output : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_box_easy(
output : Pointer(LibC::UChar),
data : Pointer(LibC::UChar),
@ -111,6 +111,21 @@ module Sodium
recipient_secret_key : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_box_seal(
output : Pointer(LibC::UChar),
data : Pointer(LibC::UChar),
data_size : LibC::ULongLong,
recipient_public_key : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_box_seal_open(
output : Pointer(LibC::UChar),
data : Pointer(LibC::UChar),
data_size : LibC::ULongLong,
recipient_public_key : Pointer(LibC::UChar),
recipient_secret_key : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_sign_keypair(
public_key_output : Pointer(LibC::UChar),
secret_key_output : Pointer(LibC::UChar)
@ -122,6 +137,11 @@ module Sodium
seed : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_sign_ed25519_sk_to_pk(
public_key_output : Pointer(LibC::UChar),
secret_key_output : Pointer(LibC::UChar)
) : LibC::Int
fun crypto_sign_detached(
signature_output : Pointer(LibC::UChar),
signature_output_size : Pointer(LibC::ULongLong),
@ -198,6 +218,10 @@ module Sodium
) : LibC::Int
end
if LibSodium.sodium_init != 0
abort "Failed to init libsodium"
end
if LibSodium.crypto_secretbox_noncebytes != LibSodium.crypto_box_noncebytes
raise "Assumptions in this library regarding nonce sizes may not be valid"
end

View File

@ -18,6 +18,8 @@ module Sodium
MEMLIMIT_MAX = LibSodium.crypto_pwhash_memlimit_max
MEMLIMIT_INTERACTIVE = LibSodium.crypto_pwhash_memlimit_interactive
PWHASH_STR_SIZE = LibSodium.crypto_pwhash_strbytes
# Use the most recent algorithm Argon2id13 for new applications.
enum Algorithm
Argon2i13 = 1
@ -37,7 +39,7 @@ module Sodium
# * the automatically generated salt used for the previous computation
# * the other parameters required to verify the password, including the algorithm identifier, its version, opslimit and memlimit.
def store(pass)
outstr = Bytes.new LibSodium::PWHASH_STR_SIZE
outstr = Bytes.new PWHASH_STR_SIZE
if LibSodium.crypto_pwhash_str(outstr, pass, pass.bytesize, @opslimit, @memlimit) != 0
raise Sodium::Error.new("crypto_pwhash_str")
end

View File

@ -17,6 +17,7 @@ module Sodium
NONCE_SIZE = LibSodium.crypto_secretbox_noncebytes
MAC_SIZE = LibSodium::MAC_SIZE
@[Wipe::Var]
property bytes : Bytes
# Generate a new random key.

View File

@ -7,6 +7,12 @@ module Sodium
getter bytes : Bytes
# :nodoc:
# Only used by SecretKey
def initialize
@bytes = Bytes.new(KEY_SIZE)
end
def initialize(@bytes : Bytes)
if bytes.bytesize != KEY_SIZE
raise ArgumentError.new("Public key must be #{KEY_SIZE} bytes, got #{bytes.bytesize}")

View File

@ -8,32 +8,38 @@ module Sodium
# key.public_key.verify_detached data
# ```
class Sign::SecretKey < Sodium::Key
include Wipe
KEY_SIZE = LibSodium.crypto_sign_secretkeybytes
SEED_SIZE = LibSodium.crypto_sign_seedbytes
getter public_key : PublicKey
@[Wipe::Var]
getter bytes : Bytes
getter public_key
@[Wipe::Var]
@seed : Bytes?
# Generates a new random secret/public key pair.
def initialize
pkey = Bytes.new(Sign::PublicKey::KEY_SIZE)
@bytes = Bytes.new(KEY_SIZE)
@public_key = PublicKey.new pkey
if LibSodium.crypto_sign_keypair(pkey, @bytes) != 0
@public_key = PublicKey.new
if LibSodium.crypto_sign_keypair(@public_key.bytes, @bytes) != 0
raise Sodium::Error.new("crypto_sign_keypair")
end
end
# Use existing Secret and Public keys.
# Use existing secret and public keys.
# Recomputes the public key from a secret key if missing.
def initialize(@bytes : Bytes, pkey : Bytes? = nil)
pkey ||= Bytes.new(Sign::PublicKey::KEY_SIZE).tap do |pk|
# BUG: Finish regenerating public_key
raise "Needs crypto_sign_ed25519_sk_to_pk"
end
raise ArgumentError.new("Secret sign key must be #{KEY_SIZE}, got #{@bytes.bytesize}") unless @bytes.bytesize == KEY_SIZE
@public_key = PublicKey.new pkey
if pk = pkey
@public_key = PublicKey.new pkey
else
@public_key = PublicKey.new
if LibSodium.crypto_sign_ed25519_sk_to_pk(@public_key.bytes, @bytes) != 0
raise Sodium::Error.new("crypto_sign_ed25519_sk_to_pk")
end
end
end
# Derive a new secret/public key pair based on a consistent seed.
@ -41,10 +47,9 @@ module Sodium
raise ArgumentError.new("Secret sign seed must be #{SEED_SIZE}, got #{seed.bytesize}") unless seed.bytesize == SEED_SIZE
@seed = seed
pkey = Bytes.new(Sign::PublicKey::KEY_SIZE)
@bytes = Bytes.new(KEY_SIZE)
@public_key = PublicKey.new pkey
if LibSodium.crypto_sign_seed_keypair(pkey, @bytes, seed) != 0
@public_key = PublicKey.new
if LibSodium.crypto_sign_seed_keypair(@public_key.bytes, @bytes, seed) != 0
raise Sodium::Error.new("crypto_sign_seed_keypair")
end
end

View File

@ -1,4 +1,7 @@
module Sodium::Wipe
annotation Var
end
@closed = false
def close
@ -9,7 +12,21 @@ module Sodium::Wipe
protected def wipe
return if @closed
Sodium.memzero @bytes
{% for ivar in @type.instance_vars %}
{% if ann = ivar.annotation(Wipe::Var) %}
{% if ivar.type.id == StaticArray.id %}
#puts "wiping static {{ivar}}"
# Sodium.memzero @{{ ivar.id }}.to_slice
{% else %}
if var = @{{ ivar.id }}
#puts "wiping {{ivar}}"
# Sodium.memzero var
Sodium.memzero var.to_slice
end
{% end %}
{% end %}
{% end %}
end
def finalize