From 0742fd9b3644f32599fb23df2a62bb1711ba3c5d Mon Sep 17 00:00:00 2001 From: Nazarii Date: Thu, 14 Dec 2017 17:10:07 +0200 Subject: [PATCH] non 32 byte keys supporting --- lib/rbnacl/hmac/sha256.rb | 53 +++++++++++-- lib/rbnacl/hmac/sha512.rb | 53 +++++++++++-- lib/rbnacl/test_vectors.rb | 17 ++++ lib/rbnacl/util.rb | 88 ++++++++++++++++++++- spec/rbnacl/authenticators/poly1305_spec.rb | 22 +++++- spec/rbnacl/hmac/sha256_spec.rb | 23 +++++- spec/rbnacl/hmac/sha512256_spec.rb | 22 +++++- spec/rbnacl/hmac/sha512_spec.rb | 23 +++++- spec/rbnacl/util_spec.rb | 51 ++++++++++++ spec/shared/authenticator.rb | 19 ----- 10 files changed, 330 insertions(+), 41 deletions(-) diff --git a/lib/rbnacl/hmac/sha256.rb b/lib/rbnacl/hmac/sha256.rb index 23bdf24..9d59736 100644 --- a/lib/rbnacl/hmac/sha256.rb +++ b/lib/rbnacl/hmac/sha256.rb @@ -22,23 +22,60 @@ class SHA256 < Auth sodium_constant :BYTES sodium_constant :KEYBYTES - sodium_function :auth_hmacsha256, - :crypto_auth_hmacsha256, - %i[pointer pointer ulong_long pointer] + sodium_function :auth_hmacsha256_init, + :crypto_auth_hmacsha256_init, + %i[pointer pointer size_t] - sodium_function :auth_hmacsha256_verify, - :crypto_auth_hmacsha256_verify, - %i[pointer pointer ulong_long pointer] + sodium_function :auth_hmacsha256_update, + :crypto_auth_hmacsha256_update, + %i[pointer pointer ulong_long] + + sodium_function :auth_hmacsha256_final, + :crypto_auth_hmacsha256_final, + %i[pointer pointer] + + # Create instance without checking key length + # + # RFC 2104 HMAC + # The key for HMAC can be of any length. + # + # see https://tools.ietf.org/html/rfc2104#section-3 + def initialize(key) + @key = Util.check_hmac_key(key, "#{self.class} key") + end private def compute_authenticator(authenticator, message) - self.class.auth_hmacsha256(authenticator, message, message.bytesize, key) + state = State.new + + self.class.auth_hmacsha256_init(state, key, key.bytesize) + self.class.auth_hmacsha256_update(state, message, message.bytesize) + self.class.auth_hmacsha256_final(state, authenticator) end + # libsodium crypto_auth_hmacsha256_verify works only for 32 byte keys + # ref: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_auth/hmacsha256/auth_hmacsha256.c#L109 def verify_message(authenticator, message) - self.class.auth_hmacsha256_verify(authenticator, message, message.bytesize, key) + correct = Util.zeros(BYTES) + compute_authenticator(correct, message) + Util.verify32(correct, authenticator) end end + + # The crupto_auth_hmacsha256_state struct representation + # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_auth_hmacsha256.h + class SHA256State < FFI::Struct + layout :state, [:uint32, 8], + :count, :uint64, + :buf, [:uint8, 64] + end + + # The crypto_hash_sha256_state struct representation + # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_hash_sha256.h + class State < FFI::Struct + layout :ictx, SHA256State, + :octx, SHA256State + end end end diff --git a/lib/rbnacl/hmac/sha512.rb b/lib/rbnacl/hmac/sha512.rb index 93ad3db..a56744b 100644 --- a/lib/rbnacl/hmac/sha512.rb +++ b/lib/rbnacl/hmac/sha512.rb @@ -22,23 +22,60 @@ class SHA512 < Auth sodium_constant :BYTES sodium_constant :KEYBYTES - sodium_function :auth_hmacsha512, - :crypto_auth_hmacsha512, - %i[pointer pointer ulong_long pointer] + sodium_function :auth_hmacsha512_init, + :crypto_auth_hmacsha512_init, + %i[pointer pointer size_t] - sodium_function :auth_hmacsha512_verify, - :crypto_auth_hmacsha512_verify, - %i[pointer pointer ulong_long pointer] + sodium_function :auth_hmacsha512_update, + :crypto_auth_hmacsha512_update, + %i[pointer pointer ulong_long] + + sodium_function :auth_hmacsha512_final, + :crypto_auth_hmacsha512_final, + %i[pointer pointer] + + # Create instance without checking key length + # + # RFC 2104 HMAC + # The key for HMAC can be of any length. + # + # see https://tools.ietf.org/html/rfc2104#section-3 + def initialize(key) + @key = Util.check_hmac_key(key, "#{self.class} key") + end private def compute_authenticator(authenticator, message) - self.class.auth_hmacsha512(authenticator, message, message.bytesize, key) + state = State.new + + self.class.auth_hmacsha512_init(state, key, key.bytesize) + self.class.auth_hmacsha512_update(state, message, message.bytesize) + self.class.auth_hmacsha512_final(state, authenticator) end + # libsodium crypto_auth_hmacsha512_verify works only for 32 byte keys + # ref: https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_auth/hmacsha512/auth_hmacsha512.c#L109 def verify_message(authenticator, message) - self.class.auth_hmacsha512_verify(authenticator, message, message.bytesize, key) + correct = Util.zeros(BYTES) + compute_authenticator(correct, message) + Util.verify64(correct, authenticator) end end + + # The crypto_auth_hmacsha512_state struct representation + # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_auth_hmacsha512.h + class SHA512State < FFI::Struct + layout :state, [:uint64, 8], + :count, [:uint64, 2], + :buf, [:uint8, 128] + end + + # The crypto_hash_sha512_state struct representation + # ref: jedisct1/libsodium/src/libsodium/include/sodium/crypto_hash_sha512.h + class State < FFI::Struct + layout :ictx, SHA512State, + :octx, SHA512State + end end end diff --git a/lib/rbnacl/test_vectors.rb b/lib/rbnacl/test_vectors.rb index 44faa1b..959f8d1 100644 --- a/lib/rbnacl/test_vectors.rb +++ b/lib/rbnacl/test_vectors.rb @@ -163,6 +163,23 @@ module RbNaCl auth_hmacsha512: "b2a31b8d4e01afcab2ee545b5caf4e3d212a99d7b3a116a97cec8e83c32e107d" \ "270e3921f69016c267a63ab4b226449a0dee0dc7dcb897a9bce9d27d788f8e8d", + # HMAC-SHA Identifiers and Test Vectors + # ref: https://tools.ietf.org/html/rfc4231#section-4.8 + # + auth_hmac_key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" \ + "aaaaaa", + auth_hmac_data: "5468697320697320612074657374207573696e672061206c6172676572207468" \ + "616e20626c6f636b2d73697a65206b657920616e642061206c61726765722074" \ + "68616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565" \ + "647320746f20626520686173686564206265666f7265206265696e6720757365" \ + "642062792074686520484d414320616c676f726974686d2e", + auth_hmacsha256_tag: "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", + auth_hmacsha512_tag: "e37b6a775dc87dbaa4dfa9f96e5e3ffddebd71f8867289865df5a32d20cdc944" \ + "b6022cac3c4982b10d5eeb55c3e4de15134676fb6de0446065c97440fa8c6a58", + # AEAD ChaCha20-Poly1305 original implementation test vectors # Taken from https://tools.ietf.org/html/draft-agl-tls-chacha20poly1305-04 aead_chacha20poly1305_orig_key: "4290bcb154173531f314af57f3be3b5006da371ece272afa1b5dbdd1100a1007", diff --git a/lib/rbnacl/util.rb b/lib/rbnacl/util.rb index 45d629b..25f6a4e 100644 --- a/lib/rbnacl/util.rb +++ b/lib/rbnacl/util.rb @@ -8,6 +8,7 @@ module Util sodium_function :c_verify16, :crypto_verify_16, %i[pointer pointer] sodium_function :c_verify32, :crypto_verify_32, %i[pointer pointer] + sodium_function :c_verify64, :crypto_verify_64, %i[pointer pointer] module_function @@ -81,6 +82,8 @@ def zero_pad(n, message) # @param description [String] Description of the string (used in the error) def check_length(string, length, description) if string.nil? + # code below is runs only in test cases + # nil can't be converted to str with #to_str method raise LengthError, "#{description} was nil (Expected #{length.to_int})", caller @@ -106,6 +109,57 @@ def check_length(string, length, description) # @param length [Integer] The only acceptable length of the string # @param description [String] Description of the string (used in the error) def check_string(string, length, description) + check_string_validation(string) + string = string.to_s + check_length(string, length, description) + + string + end + + # Check a passed in string, convertion if necessary + # + # This method will check the key, and raise error + # if argument is not a string, and if it's empty string. + # + # RFC 2104 HMAC + # The key for HMAC can be of any length (keys longer than B bytes are + # first hashed using H). However, less than L bytes is strongly + # discouraged as it would decrease the security strength of the + # function. Keys longer than L bytes are acceptable but the extra + # length would not significantly increase the function strength. (A + # longer key may be advisable if the randomness of the key is + # considered weak.) + # + # see https://tools.ietf.org/html/rfc2104#section-3 + # + # + # @raise [ArgumentError] If we cannot convert to a string with #to_str + # @raise [RbNaCl::LengthError] If the string is empty + # + # @param string [#to_str] The input string + def check_hmac_key(string, _description) + check_string_validation(string) + + string = string.to_str + + if string.bytesize.zero? + raise LengthError, + "#{Description} was #{string.bytesize} bytes (Expected more than 0)", + caller + end + + string + end + + # Check a passed string is it valid + # + # Raise an error if passed argument is invalid + # + # @raise [TypeError] If string cannot convert to a string with #to_str + # @raise [EncodingError] If string have wrong encoding + # + # @param string [#to_str] The input string + def check_string_validation(string) unless string.respond_to? :to_str raise TypeError, "can't convert #{string.class} into String with #to_str" end @@ -114,9 +168,39 @@ def check_string(string, length, description) unless string.encoding == Encoding::BINARY raise EncodingError, "strings must use BINARY encoding (got #{string.encoding})" end - check_length(string, length, description) + end - string + # Compare two 64 byte strings in constant time + # + # This should help to avoid timing attacks for string comparisons in your + # application. Note that many of the functions (such as HmacSha512#verify) + # use this method under the hood already. + # + # @param [String] one String #1 + # @param [String] two String #2 + # + # @return [Boolean] Well, are they equal? + def verify64(one, two) + return false unless two.bytesize == 64 && one.bytesize == 64 + c_verify64(one, two) + end + + # Compare two 64 byte strings in constant time + # + # This should help to avoid timing attacks for string comparisons in your + # application. Note that many of the functions (such as HmacSha512#verify) + # use this method under the hood already. + # + # @param [String] one String #1 + # @param [String] two String #2 + # + # @raise [ArgumentError] If the strings are not equal in length + # + # @return [Boolean] Well, are they equal? + def verify64!(one, two) + check_length(one, 64, "First message") + check_length(two, 64, "Second message") + c_verify64(one, two) end # Compare two 32 byte strings in constant time diff --git a/spec/rbnacl/authenticators/poly1305_spec.rb b/spec/rbnacl/authenticators/poly1305_spec.rb index d2c2e35..134ae95 100644 --- a/spec/rbnacl/authenticators/poly1305_spec.rb +++ b/spec/rbnacl/authenticators/poly1305_spec.rb @@ -2,7 +2,27 @@ # frozen_string_literal: true RSpec.describe RbNaCl::OneTimeAuth do - let(:tag) { vector :auth_onetime } + let(:key) { vector "auth_key_#{described_class.key_bytes}".to_sym } + let(:message) { vector :auth_message } + let(:tag) { vector :auth_onetime } + + context ".new" do + it "raises ArgumentError on a key which is too long" do + expect { described_class.new("\0" * described_class.key_bytes.succ) }.to raise_error(ArgumentError) + end + end + + context ".auth" do + it "raises ArgumentError on a key which is too long" do + expect { described_class.auth("\0" * described_class.key_bytes.succ, message) }.to raise_error(ArgumentError) + end + end + + context ".verify" do + it "raises ArgumentError on a key which is too long" do + expect { described_class.verify("\0" * described_class.key_bytes.succ, tag, message) }.to raise_error(ArgumentError) + end + end include_examples "authenticator" end diff --git a/spec/rbnacl/hmac/sha256_spec.rb b/spec/rbnacl/hmac/sha256_spec.rb index 7e46abc..1280f1a 100644 --- a/spec/rbnacl/hmac/sha256_spec.rb +++ b/spec/rbnacl/hmac/sha256_spec.rb @@ -2,7 +2,28 @@ # frozen_string_literal: true RSpec.describe RbNaCl::HMAC::SHA256 do - let(:tag) { vector :auth_hmacsha256 } + let(:key) { vector :auth_hmac_key } + let(:message) { vector :auth_hmac_data } + let(:tag) { vector :auth_hmacsha256_tag } + let(:wrong_key) { "key".encode("utf-8") } + + context ".new" do + it "raises EncodingError on a key with wrong encoding" do + expect { described_class.new(wrong_key) }.to raise_error(EncodingError) + end + end + + context ".auth" do + it "raises EncodingError on a key with wrong encoding " do + expect { described_class.auth(wrong_key, message) }.to raise_error(EncodingError) + end + end + + context ".verify" do + it "raises EncodingError on a key with wrong encoding" do + expect { described_class.verify(wrong_key, tag, message) }.to raise_error(EncodingError) + end + end include_examples "authenticator" end diff --git a/spec/rbnacl/hmac/sha512256_spec.rb b/spec/rbnacl/hmac/sha512256_spec.rb index 8a6811f..d1b940e 100644 --- a/spec/rbnacl/hmac/sha512256_spec.rb +++ b/spec/rbnacl/hmac/sha512256_spec.rb @@ -2,7 +2,27 @@ # frozen_string_literal: true RSpec.describe RbNaCl::HMAC::SHA512256 do - let(:tag) { vector :auth_hmacsha512256 } + let(:key) { vector "auth_key_#{described_class.key_bytes}".to_sym } + let(:message) { vector :auth_message } + let(:tag) { vector :auth_hmacsha512256 } + + context ".new" do + it "raises ArgumentError on a key which is too long" do + expect { described_class.new("\0" * described_class.key_bytes.succ) }.to raise_error(ArgumentError) + end + end + + context ".auth" do + it "raises ArgumentError on a key which is too long" do + expect { described_class.auth("\0" * described_class.key_bytes.succ, message) }.to raise_error(ArgumentError) + end + end + + context ".verify" do + it "raises ArgumentError on a key which is too long" do + expect { described_class.verify("\0" * described_class.key_bytes.succ, tag, message) }.to raise_error(ArgumentError) + end + end include_examples "authenticator" end diff --git a/spec/rbnacl/hmac/sha512_spec.rb b/spec/rbnacl/hmac/sha512_spec.rb index 8f8ba09..a877a79 100644 --- a/spec/rbnacl/hmac/sha512_spec.rb +++ b/spec/rbnacl/hmac/sha512_spec.rb @@ -2,7 +2,28 @@ # frozen_string_literal: true RSpec.describe RbNaCl::HMAC::SHA512 do - let(:tag) { vector :auth_hmacsha512 } + let(:key) { vector :auth_hmac_key } + let(:message) { vector :auth_hmac_data } + let(:tag) { vector :auth_hmacsha512_tag } + let(:wrong_key) { "key".encode("utf-8") } + + context ".new" do + it "raises EncodingError on a key with wrong encoding" do + expect { described_class.new(wrong_key) }.to raise_error(EncodingError) + end + end + + context ".auth" do + it "raises EncodingError on a key with wrong encoding " do + expect { described_class.auth(wrong_key, message) }.to raise_error(EncodingError) + end + end + + context ".verify" do + it "raises EncodingError on a key with wrong encoding" do + expect { described_class.verify(wrong_key, tag, message) }.to raise_error(EncodingError) + end + end include_examples "authenticator" end diff --git a/spec/rbnacl/util_spec.rb b/spec/rbnacl/util_spec.rb index 1e479a1..01fe843 100644 --- a/spec/rbnacl/util_spec.rb +++ b/spec/rbnacl/util_spec.rb @@ -2,6 +2,57 @@ # frozen_string_literal: true RSpec.describe RbNaCl::Util do + context ".verify64" do + let(:msg) { RbNaCl::Util.zeros(64) } + let(:identical_msg) { RbNaCl::Util.zeros(64) } + let(:other_msg) { RbNaCl::Util.zeros(63) + "\001" } + let(:short_msg) { RbNaCl::Util.zeros(63) } + let(:long_msg) { RbNaCl::Util.zeros(65) } + + it "confirms identical messages are identical" do + expect(RbNaCl::Util.verify64(msg, identical_msg)).to be true + end + + it "confirms non-identical messages are non-identical" do + expect(RbNaCl::Util.verify64(msg, other_msg)).to be false + expect(RbNaCl::Util.verify64(other_msg, msg)).to be false + expect(RbNaCl::Util.verify64(short_msg, msg)).to be false + expect(RbNaCl::Util.verify64(msg, short_msg)).to be false + expect(RbNaCl::Util.verify64(long_msg, msg)).to be false + expect(RbNaCl::Util.verify64(msg, long_msg)).to be false + end + end + + context ".verify64!" do + let(:msg) { RbNaCl::Util.zeros(64) } + let(:identical_msg) { RbNaCl::Util.zeros(64) } + let(:other_msg) { RbNaCl::Util.zeros(63) + "\001" } + let(:short_msg) { RbNaCl::Util.zeros(63) } + let(:long_msg) { RbNaCl::Util.zeros(65) } + + it "confirms identical messages are identical" do + expect(RbNaCl::Util.verify64!(msg, identical_msg)).to be true + end + + it "confirms non-identical messages are non-identical" do + expect(RbNaCl::Util.verify64!(msg, other_msg)).to be false + expect(RbNaCl::Util.verify64!(other_msg, msg)).to be false + end + + it "raises descriptively on a short message in position 1" do + expect { RbNaCl::Util.verify64!(short_msg, msg) }.to raise_error(RbNaCl::LengthError) + end + it "raises descriptively on a short message in position 2" do + expect { RbNaCl::Util.verify64!(msg, short_msg) }.to raise_error(RbNaCl::LengthError) + end + it "raises descriptively on a long message in position 1" do + expect { RbNaCl::Util.verify64!(long_msg, msg) }.to raise_error(RbNaCl::LengthError) + end + it "raises descriptively on a long message in position 2" do + expect { RbNaCl::Util.verify64!(msg, long_msg) }.to raise_error(RbNaCl::LengthError) + end + end + context ".verify32!" do let(:msg) { RbNaCl::Util.zeros(32) } let(:identical_msg) { RbNaCl::Util.zeros(32) } diff --git a/spec/shared/authenticator.rb b/spec/shared/authenticator.rb index 3559d58..341d03b 100644 --- a/spec/shared/authenticator.rb +++ b/spec/shared/authenticator.rb @@ -2,9 +2,6 @@ # frozen_string_literal: true RSpec.shared_examples "authenticator" do - let(:key) { vector "auth_key_#{described_class.key_bytes}".to_sym } - let(:message) { vector :auth_message } - context ".new" do it "accepts a key" do expect { described_class.new(key) }.to_not raise_error @@ -17,14 +14,6 @@ it "raises TypeError on a nil key" do expect { described_class.new(nil) }.to raise_error(TypeError) end - - it "raises ArgumentError on a key which is too long" do - expect { described_class.new("\0" * described_class.key_bytes.succ) }.to raise_error(ArgumentError) - end - - it "raises ArgumentError on a key which is too short" do - expect { described_class.new("\0" * described_class.key_bytes.pred) }.to raise_error(ArgumentError) - end end context ".auth" do @@ -35,10 +24,6 @@ it "raises TypeError on a nil key" do expect { described_class.auth(nil, message) }.to raise_error(TypeError) end - - it "raises ArgumentError on a key which is too long" do - expect { described_class.auth("\0" * described_class.key_bytes.succ, message) }.to raise_error(ArgumentError) - end end context ".verify" do @@ -50,10 +35,6 @@ expect { described_class.verify(nil, tag, message) }.to raise_error(TypeError) end - it "raises ArgumentError on a key which is too long" do - expect { described_class.verify("\0" * described_class.key_bytes.succ, tag, message) }.to raise_error(ArgumentError) - end - it "fails to validate an invalid authenticator" do expect { described_class.verify(key, tag, message + "\0") }.to raise_error(RbNaCl::BadAuthenticatorError) end