Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ signature = Cox.sign_detached(message, signing_pair.secret)
Cox.verify_detached(signature, message, signing_pair.public) # => true
```

# Key derivation
kdf = Cox::Kdf.new

# kdf.derive(8_byte_context, subkey_size, subkey_id)
subkey1 = kdf.derive "context1", 16, 0
subkey2 = kdf.derive "context1", 16, 1
subkey3 = kdf.derive "context2", 32, 0
subkey4 = kdf.derive "context2", 64, 1

## Contributing

1. Fork it ( https://github.com/andrewhamon/cox/fork )
Expand Down
89 changes: 89 additions & 0 deletions spec/cox/blake2b_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
require "../spec_helper"

libsodium_comparisons = [
{
key: nil,
input: "",
output: "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8",
out_size: 32,
},
]

# from https://github.com/BLAKE2/BLAKE2/tree/master/testvectors
test_vectors = [
{
key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
input: "",
output: "10ebb67700b1868efb4417987acf4690ae9d972fb7a590c2f02871799aaa4786b5e996e8f0f4eb981fc214b005f42d2ff4233499391653df7aefcbc13fc51568",
},
{
key: "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
input: "00",
output: "961f6dd1e4dd30f63901690c512e78e4b45e4742ed197c3c5e45c549fd25f2e4187b0bc9fe30492b16b0d0bc4ef9b0f34c7003fac09a5ef1532e69430234cebd",
},
]


describe Cox::Blake2b do
it "libsodium comparisons" do
libsodium_comparisons.each do |vec|
d = Cox::Blake2b.new vec[:out_size], key: vec[:key].try(&.hexbytes)
d.update vec[:input].hexbytes
d.hexdigest.should eq vec[:output]
end
end

it "test vectors" do
test_vectors.each do |vec|
d = Cox::Blake2b.new 64, key: vec[:key].hexbytes
d.update vec[:input].hexbytes
d.hexdigest.should eq vec[:output]
end
end

it "produces different output with different salt or personal params" do
key = Bytes.new Cox::Blake2b::KEY_SIZE
salt = Bytes.new Cox::Blake2b::SALT_SIZE
salt2 = Bytes.new Cox::Blake2b::SALT_SIZE
salt2 = salt.dup
salt2[0] = 1
personal = Bytes.new Cox::Blake2b::PERSONAL_SIZE
personal2 = personal.dup
personal2[0] = 1


d = Cox::Blake2b.new key: key, salt: salt, personal: personal
d.update "foo".to_slice
output = d.hexdigest

d = Cox::Blake2b.new key: key, salt: salt2, personal: personal
d.update "foo".to_slice
saltout = d.hexdigest

d = Cox::Blake2b.new key: key, salt: salt, personal: personal2
d.update "foo".to_slice
personalout = d.hexdigest

output.should_not eq saltout
output.should_not eq personalout
saltout.should_not eq personalout
end

it "raises on invalid " do
expect_raises ArgumentError do
Cox::Blake2b.new key: Bytes.new(128)
end

expect_raises ArgumentError do
Cox::Blake2b.new salt: Bytes.new(1)
end

expect_raises ArgumentError do
Cox::Blake2b.new salt: Bytes.new(128)
end

expect_raises ArgumentError do
Cox::Blake2b.new personal: Bytes.new(128)
end
end
end
26 changes: 26 additions & 0 deletions spec/cox/kdf_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require "../spec_helper"

CONTEXT = "8_bytess"

describe Cox::Kdf do
it "generates master key" do
kdf1 = Cox::Kdf.new

# verify loading saved key
kdf2 = Cox::Kdf.from_base64 kdf1.to_base64

# verify generated subkey's are the same after loading
key1_s1 = kdf1.derive CONTEXT, 16, 0
key2_s1 = kdf2.derive CONTEXT, 16, 0
key1_s1.should eq key2_s1
end

it "generates different keys" do
kdf1 = Cox::Kdf.new
subkey1 = kdf1.derive CONTEXT, 16, 0
subkey2 = kdf1.derive CONTEXT, 16, 1
subkey1.should_not eq subkey2
end

# TODO: test exceptions
end
22 changes: 22 additions & 0 deletions spec/cox/pwhash_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
require "../spec_helper"

describe Cox::Pwhash do
it "hashes and verifies a password" do
pwhash = Cox::Pwhash.new

# set to minimum to speed up tests
pwhash.memlimit = Cox::Pwhash::MEMLIMIT_MIN
pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MIN

pass = "1234"
hash = pwhash.hash_str pass
pwhash.verify hash, pass
expect_raises(Cox::Pwhash::PasswordVerifyError) do
pwhash.verify hash, "5678"
end

pwhash.needs_rehash?(hash).should be_false
pwhash.opslimit = Cox::Pwhash::OPSLIMIT_MAX
pwhash.needs_rehash?(hash).should be_true
end
end
16 changes: 16 additions & 0 deletions spec/cox/secret_key_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
require "../spec_helper"

describe Cox::SecretKey do
it "encrypts/decrypts" do
key = Cox::SecretKey.random

message = "foobar"
encrypted, nonce = key.encrypt_easy message
decrypted = key.decrypt_easy encrypted, nonce
message.should eq String.new(decrypted)

expect_raises(Cox::DecryptionFailed) do
key.decrypt_easy "badmsgbadmsgbadmsgbadmsgbadmsg".to_slice, nonce
end
end
end
24 changes: 21 additions & 3 deletions src/cox.cr
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
require "random/secure"

module Cox
class Error < ::Exception
end
class VerificationFailed < Error
end
class DecryptionFailed < Error
end
end


require "./cox/*"

module Cox
def self.encrypt(data, nonce : Nonce, recipient_public_key : PublicKey, sender_secret_key : SecretKey)
data_buffer = data.to_slice
data_size = data_buffer.bytesize
output_buffer = Bytes.new(data_buffer.bytesize + LibSodium::MAC_BYTES)
LibSodium.crypto_box_easy(output_buffer.to_unsafe, data_buffer, data_size, nonce.pointer, recipient_public_key.pointer, sender_secret_key.pointer)
if LibSodium.crypto_box_easy(output_buffer.to_unsafe, data_buffer, data_size, nonce.pointer, recipient_public_key.pointer, sender_secret_key.pointer) != 0
raise Error.new("crypto_box_easy")
end
output_buffer
end

Expand All @@ -18,7 +32,9 @@ module Cox
data_buffer = data.to_slice
data_size = data_buffer.bytesize
output_buffer = Bytes.new(data_buffer.bytesize - LibSodium::MAC_BYTES)
LibSodium.crypto_box_open_easy(output_buffer.to_unsafe, data_buffer.to_unsafe, data_size, nonce.pointer, sender_public_key.pointer, recipient_secret_key.pointer)
if LibSodium.crypto_box_open_easy(output_buffer.to_unsafe, data_buffer.to_unsafe, data_size, nonce.pointer, sender_public_key.pointer, recipient_secret_key.pointer) != 0
raise DecryptionFailed.new("crypto_box_open_easy")
end
output_buffer
end

Expand All @@ -27,7 +43,9 @@ module Cox
message_buffer_size = message_buffer.bytesize
signature_output_buffer = Bytes.new(LibSodium::SIGNATURE_BYTES)

LibSodium.crypto_sign_detached(signature_output_buffer.to_unsafe, 0, message_buffer.to_unsafe, message_buffer_size, secret_key.pointer)
if LibSodium.crypto_sign_detached(signature_output_buffer.to_unsafe, 0, message_buffer.to_unsafe, message_buffer_size, secret_key.pointer) != 0
raise Error.new("crypto_sign_detached")
end
signature_output_buffer
end

Expand Down
108 changes: 108 additions & 0 deletions src/cox/blake2b.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
require "openssl/digest/digest_base"

module Cox
class Blake2b
# provides copying digest/hexdigest methods
include OpenSSL::DigestBase

KEY_SIZE = LibSodium.crypto_generichash_blake2b_keybytes
KEY_SIZE_MIN = LibSodium.crypto_generichash_blake2b_keybytes_min
KEY_SIZE_MAX = LibSodium.crypto_generichash_blake2b_keybytes_max

SALT_SIZE = LibSodium.crypto_generichash_blake2b_saltbytes

PERSONAL_SIZE = LibSodium.crypto_generichash_blake2b_personalbytes

OUT_SIZE = LibSodium.crypto_generichash_blake2b_bytes.to_i32
OUT_SIZE_MIN = LibSodium.crypto_generichash_blake2b_bytes_min.to_i32
OUT_SIZE_MAX = LibSodium.crypto_generichash_blake2b_bytes_max.to_i32

getter digest_size

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

if LibSodium.crypto_generichash_blake2b_init_salt_personal(@state, key, @key_size, @digest_size, salt, personal) != 0
raise Cox::Error.new("blake2b_init_key_salt_personal")
end
end

def update(data : Bytes)
if LibSodium.crypto_generichash_blake2b_update(@state, data, data.bytesize) != 0
raise Cox::Error.new("crypto_generichash_blake2b_update")
end

self
end

# Destructive operation. Assumes you know what you are doing.
# Use .digest or .hexdigest instead.
def finish
dst = Bytes.new @digest_size
finish dst
dst
end

# Destructive operation. Assumes you know what you are doing.
# Use .digest or .hexdigest instead.
def finish(dst : Bytes) : Bytes
if LibSodium.crypto_generichash_blake2b_final(@state, dst, dst.bytesize) != 0
raise Cox::Error.new("crypto_generichash_blake2b_final")
end

dst
end

def clone
dup
end

# :nodoc:
def __validate_sizes__
state_size = LibSodium.crypto_generichash_blake2b_statebytes
abort "@state.bytesize doesn't match library version #{@state.to_slice.bytesize} #{state_size}" if @state.to_slice.bytesize < state_size
abort "@key.bytesize doesn't match library version" if @key.to_slice.bytesize != KEY_SIZE_MAX
abort "@salt.bytesize doesn't match library version #{@salt.to_slice.bytesize} #{SALT_SIZE}" if @salt.to_slice.bytesize != SALT_SIZE
abort "@personal.bytesize doesn't match library version #{@personal.to_slice.bytesize} #{PERSONAL_SIZE}" if @personal.to_slice.bytesize != SALT_SIZE
end
end

Blake2b.new.__validate_sizes__
end


47 changes: 47 additions & 0 deletions src/cox/kdf.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
module Cox
class Kdf
property bytes : Bytes

def initialize(bytes : Bytes)
if bytes.bytesize != LibSodium::KDF_KEY_BYTES
raise ArgumentError.new("bytes must be #{LibSodium::KDF_KEY_BYTES}, got #{bytes.bytesize}")
end

@bytes = bytes
end

def initialize
@bytes = Random::Secure.random_bytes(LibSodium::KDF_KEY_BYTES)
end

# context must be 8 bytes
# subkey_size must be 16..64 bytes as of libsodium 1.0.17
def derive(context, subkey_size, subkey_id = 0)
if context.bytesize != LibSodium::KDF_CONTEXT_BYTES
raise ArgumentError.new("context must be #{LibSodium::KDF_CONTEXT_BYTES}, got #{context.bytesize}")
end

subkey = Bytes.new subkey_size
if (ret = LibSodium.crypto_kdf_derive_from_key(subkey, subkey.bytesize, subkey_id, context, @bytes)) != 0
raise Cox::Error.new("crypto_kdf_derive_from_key returned #{ret} (subkey size is probably out of range)")
end
subkey
end

def pointer
bytes.to_unsafe
end

def pointer(size)
bytes.pointer(size)
end

def to_base64
Base64.encode(bytes)
end

def self.from_base64(encoded_key)
new(Base64.decode(encoded_key))
end
end
end
Loading