diff --git a/spec/cox/blake2b_spec.cr b/spec/cox/blake2b_spec.cr new file mode 100644 index 0000000..f858174 --- /dev/null +++ b/spec/cox/blake2b_spec.cr @@ -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 diff --git a/src/cox/blake2b.cr b/src/cox/blake2b.cr new file mode 100644 index 0000000..f290d14 --- /dev/null +++ b/src/cox/blake2b.cr @@ -0,0 +1,95 @@ +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 + + @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(@out_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, @out_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 + + def finish + data = Bytes.new @out_size + if LibSodium.crypto_generichash_blake2b_final(@state, data, data.bytesize) != 0 + raise Cox::Error.new("crypto_generichash_blake2b_final") + end + data + 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 + + diff --git a/src/cox/lib_sodium.cr b/src/cox/lib_sodium.cr index 33c78a4..9303fbe 100644 --- a/src/cox/lib_sodium.cr +++ b/src/cox/lib_sodium.cr @@ -21,6 +21,15 @@ module Cox fun crypto_pwhash_opslimit_sensitive() : LibC::SizeT fun crypto_pwhash_opslimit_max() : LibC::SizeT fun crypto_pwhash_strbytes() : LibC::SizeT + fun crypto_generichash_blake2b_statebytes : LibC::SizeT + fun crypto_generichash_blake2b_bytes : LibC::SizeT + fun crypto_generichash_blake2b_bytes_min : LibC::SizeT + fun crypto_generichash_blake2b_bytes_max : LibC::SizeT + fun crypto_generichash_blake2b_keybytes : LibC::SizeT + fun crypto_generichash_blake2b_keybytes_min : LibC::SizeT + fun crypto_generichash_blake2b_keybytes_max : LibC::SizeT + fun crypto_generichash_blake2b_saltbytes : LibC::SizeT + fun crypto_generichash_blake2b_personalbytes : LibC::SizeT PUBLIC_KEY_BYTES = crypto_box_publickeybytes() SECRET_KEY_BYTES = crypto_box_secretkeybytes() @@ -119,5 +128,26 @@ module Cox optslimit : LibC::ULongLong, memlimit : LibC::SizeT, ) : LibC::Int + + fun crypto_generichash_blake2b_init_salt_personal( + state : Pointer(LibC::UChar), + key : Pointer(LibC::UChar), + key_len : UInt8, + out_len : UInt8, + salt : Pointer(LibC::UChar), + personal : Pointer(LibC::UChar), + ) : LibC::Int + + fun crypto_generichash_blake2b_update( + state : Pointer(LibC::UChar), + in : Pointer(LibC::UChar), + in_len : UInt64, + ) : LibC::Int + + fun crypto_generichash_blake2b_final( + state : Pointer(LibC::UChar), + output : Pointer(LibC::UChar), + output_len : UInt64, + ) : LibC::Int end end