Add seed support to CryptoBox and Sign.
Wiping now supports multiple variables by Annotation.
This commit is contained in:
parent
41a55a9593
commit
162cd72b0c
@ -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.
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
require "../spec_helper"
|
||||
require "../../spec_helper"
|
||||
|
||||
libsodium_comparisons = [
|
||||
{
|
@ -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
|
@ -15,7 +15,3 @@ module Sodium
|
||||
end
|
||||
|
||||
require "./sodium/**"
|
||||
|
||||
if Sodium::LibSodium.sodium_init == -1
|
||||
abort "Failed to init libsodium"
|
||||
end
|
||||
|
@ -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)
|
||||
|
@ -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}")
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,9 @@
|
||||
require "./wipe"
|
||||
|
||||
module Sodium
|
||||
abstract class Key
|
||||
include Sodium::Wipe
|
||||
|
||||
abstract def bytes
|
||||
|
||||
delegate to_slice, to: @bytes
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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}")
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user