From 028ec78dff62473ef2158c82a5038a7cc6253943 Mon Sep 17 00:00:00 2001 From: elijah Date: Mon, 22 Jan 2018 23:29:49 -0800 Subject: [PATCH] Add support for argon2id digest, if libsodium >= 1.0.13. Note: for libsodium >= 1.0.15, the default digest algorithm for argon2 changed from argon2i to argon2id. This means RbNaCl::PasswordHash.argon2() will produce different results depending on which libsodium you have installed. You should now use RbNaCl::PasswordHash.argon2i() or RbNaCl::PasswordHash.argon2id() instead. RbNaCl::PasswordHash.argon_str() is unaffected. --- lib/rbnacl/password_hash.rb | 35 ++++++++++++++++- lib/rbnacl/password_hash/argon2.rb | 43 +++++++++++++-------- lib/rbnacl/sodium.rb | 6 ++- lib/rbnacl/sodium/version.rb | 2 + lib/rbnacl/test_vectors.rb | 29 ++++++++++---- spec/rbnacl/password_hash/argon2_spec.rb | 49 +++++++++++++++++------- 6 files changed, 125 insertions(+), 39 deletions(-) diff --git a/lib/rbnacl/password_hash.rb b/lib/rbnacl/password_hash.rb index b06b80c..1e95e1c 100644 --- a/lib/rbnacl/password_hash.rb +++ b/lib/rbnacl/password_hash.rb @@ -36,7 +36,8 @@ def self.scrypt(password, salt, opslimit, memlimit, digest_size = 64) SCrypt.new(opslimit, memlimit, digest_size).digest(password, salt) end - # argon2: state of the art in the design of memory-hard hashing functions. + # argon2: state of the art in the design of memory-hard hashing functions + # (default digest algorithm). # # @param [String] password to be hashed # @param [String] salt to make the digest unique @@ -46,11 +47,41 @@ def self.scrypt(password, salt, opslimit, memlimit, digest_size = 64) # # @raise [CryptoError] If calculating the digest fails for some reason. # - # @return [String] The argon2i digest as raw bytes + # @return [String] The argon2 digest as raw bytes def self.argon2(password, salt, opslimit, memlimit, digest_size = 64) argon2_supported? && Argon2.new(opslimit, memlimit, digest_size).digest(password, salt) end + # argon2i: argon2, using argon2i digest algorithm. + # + # @param [String] password to be hashed + # @param [String] salt to make the digest unique + # @param [Integer] opslimit the CPU cost (3..10) + # @param [Integer] memlimit the memory cost, in bytes + # @param [Integer] digest_size of the output + # + # @raise [CryptoError] If calculating the digest fails for some reason. + # + # @return [String] The argon2i digest as raw bytes + def self.argon2i(password, salt, opslimit, memlimit, digest_size = 64) + argon2_supported? && Argon2.new(opslimit, memlimit, digest_size).digest(password, salt, :argon2i) + end + + # argon2id: argon2, using argon2id digest algorithm. + # + # @param [String] password to be hashed + # @param [String] salt to make the digest unique + # @param [Integer] opslimit the CPU cost (3..10) + # @param [Integer] memlimit the memory cost, in bytes + # @param [Integer] digest_size of the output + # + # @raise [CryptoError] If calculating the digest fails for some reason. + # + # @return [String] The argon2id digest as raw bytes + def self.argon2id(password, salt, opslimit, memlimit, digest_size = 64) + argon2_supported? && Argon2.new(opslimit, memlimit, digest_size).digest(password, salt, :argon2id) + end + # argon2_str: crypt-style password digest # # @param [String] password to be hashed diff --git a/lib/rbnacl/password_hash/argon2.rb b/lib/rbnacl/password_hash/argon2.rb index ff9e85b..c262ac2 100644 --- a/lib/rbnacl/password_hash/argon2.rb +++ b/lib/rbnacl/password_hash/argon2.rb @@ -13,46 +13,45 @@ class Argon2 extend Sodium sodium_type :pwhash - sodium_primitive :argon2i + sodium_constant :ALG_DEFAULT sodium_constant :ALG_ARGON2I13 - sodium_constant :SALTBYTES - sodium_constant :STRBYTES + sodium_constant :ALG_ARGON2ID13 if Sodium::Version::ARGON2ID_SUPPORTED + + sodium_constant :SALTBYTES # 16 + sodium_constant :STRBYTES # 128 sodium_constant :OPSLIMIT_INTERACTIVE # 4 sodium_constant :MEMLIMIT_INTERACTIVE # 2 ** 25 (32mb) sodium_constant :OPSLIMIT_MODERATE # 6 sodium_constant :MEMLIMIT_MODERATE # 2 ** 27 (128mb) sodium_constant :OPSLIMIT_SENSITIVE # 8 sodium_constant :MEMLIMIT_SENSITIVE # 2 ** 29 (512mb) + sodium_constant :MEMLIMIT_MIN # 8192 + sodium_constant :MEMLIMIT_MAX # 4_294_967_296 + sodium_constant :OPSLIMIT_MIN # 3 + sodium_constant :OPSLIMIT_MAX # 10 ARGON2_MIN_OUTLEN = 16 ARGON2_MAX_OUTLEN = 0xFFFFFFFF - MEMLIMIT_MIN = 8192 - MEMLIMIT_MAX = 4_294_967_296 - OPSLIMIT_MIN = 3 - OPSLIMIT_MAX = 10 - sodium_function_with_return_code( :pwhash, - :crypto_pwhash_argon2i, + :crypto_pwhash, %i[pointer ulong_long pointer ulong_long pointer ulong_long size_t int] ) sodium_function( :pwhash_str, - :crypto_pwhash_argon2i_str, + :crypto_pwhash_str, %i[pointer pointer ulong_long ulong_long size_t] ) sodium_function( :pwhash_str_verify, - :crypto_pwhash_argon2i_str_verify, + :crypto_pwhash_str_verify, %i[pointer pointer ulong_long] ) - ALG_DEFAULT = ALG_ARGON2I13 - ARGON_ERROR_CODES = { -1 => "ARGON2_OUTPUT_PTR_NULL", -2 => "ARGON2_OUTPUT_TOO_SHORT", -3 => "ARGON2_OUTPUT_TOO_LONG", -4 => "ARGON2_PWD_TOO_SHORT", @@ -103,17 +102,31 @@ def initialize(opslimit, memlimit, digest_size = nil) # # @param [String] password to be hashed # @param [String] salt to make the digest unique + # @param [Symbol] digest algorithm to use (may be :argon2i or :argon2id) + # if nil, the default is determined by libsodium + # (argon2i for libsodium < 1.0.15, and argon2id for + # libsodium >= 1.0.15). # # @return [String] scrypt digest of the string as raw bytes - def digest(password, salt) + def digest(password, salt, algo = nil) raise ArgumentError, "digest_size is required" unless @digest_size digest = Util.zeros(@digest_size) salt = Util.check_string(salt, SALTBYTES, "salt") + if algo.nil? + algorithm = ALG_DEFAULT + elsif algo == :argon2i + algorithm = ALG_ARGON2I13 + elsif algo == :argon2id && Sodium::Version::ARGON2ID_SUPPORTED + algorithm = ALG_ARGON2ID13 + else + raise ArgumentError, "digest algorithm is not supported" + end + status = self.class.pwhash( digest, @digest_size, password, password.bytesize, salt, - @opslimit, @memlimit, ALG_DEFAULT + @opslimit, @memlimit, algorithm ) raise CryptoError, ARGON_ERROR_CODES[status] if status.nonzero? digest diff --git a/lib/rbnacl/sodium.rb b/lib/rbnacl/sodium.rb index 5e35be7..da76269 100644 --- a/lib/rbnacl/sodium.rb +++ b/lib/rbnacl/sodium.rb @@ -30,7 +30,11 @@ def primitive end def sodium_constant(constant, name = constant) - fn = "crypto_#{sodium_type}_#{sodium_primitive}_#{constant.to_s.downcase}" + fn = if sodium_primitive + "crypto_#{sodium_type}_#{sodium_primitive}_#{constant.to_s.downcase}" + else + "crypto_#{sodium_type}_#{constant.to_s.downcase}" + end attach_function fn, [], :size_t const_set(name, public_send(fn)) end diff --git a/lib/rbnacl/sodium/version.rb b/lib/rbnacl/sodium/version.rb index baa45ca..0f932f7 100644 --- a/lib/rbnacl/sodium/version.rb +++ b/lib/rbnacl/sodium/version.rb @@ -9,6 +9,7 @@ module Sodium module Version MINIMUM_LIBSODIUM_VERSION = [0, 4, 3].freeze MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2 = [1, 0, 9].freeze + MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2ID = [1, 0, 13].freeze extend Sodium attach_function :sodium_version_string, [], :string @@ -24,6 +25,7 @@ module Version end ARGON2_SUPPORTED = (INSTALLED_VERSION <=> MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2) != -1 + ARGON2ID_SUPPORTED = (INSTALLED_VERSION <=> MINIMUM_LIBSODIUM_VERSION_FOR_ARGON2ID) != -1 # Determine if a given feature is supported based on Sodium version def self.supported_version?(version) diff --git a/lib/rbnacl/test_vectors.rb b/lib/rbnacl/test_vectors.rb index 44faa1b..3fa823a 100644 --- a/lib/rbnacl/test_vectors.rb +++ b/lib/rbnacl/test_vectors.rb @@ -124,21 +124,36 @@ module RbNaCl "451a5c8ddbb4862c03d45c75bf91b7fb49265feb667ad5c899fdbf2ca19eac67", # argon2 vectors - # from libsodium/test/default/pwhash.c - argon2_password: "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ + # from libsodium/test/default/pwhash_argon2i.c + argon2i_password: "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d" \ "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5" \ "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6", - argon2_salt: "5541fbc995d5c197ba290346d2c559de", - argon2_outlen: 155, - argon2_opslimit: 5, - argon2_memlimit: 7_256_678, - argon2_digest: "23b803c84eaa25f4b44634cc1e5e37792c53fcd9b1eb20f865329c68e09cbfa9f19" \ + argon2i_salt: "5541fbc995d5c197ba290346d2c559de", + argon2i_outlen: 155, + argon2i_opslimit: 5, + argon2i_memlimit: 7_256_678, + argon2i_digest: "23b803c84eaa25f4b44634cc1e5e37792c53fcd9b1eb20f865329c68e09cbfa9f19" \ "68757901b383fce221afe27713f97914a041395bbe1fb70e079e5bed2c7145b1f61" \ "54046f5958e9b1b29055454e264d1f2231c316f26be2e3738e83a80315e9a0951ce" \ "4b137b52e7d5ee7b37f7d936dcee51362bcf792595e3c896ad5042734fc90c92cae" \ "572ce63ff659a2f7974a3bd730d04d525d253ccc38", + # from libsodium/test/default/pwhash_argon2id.c + argon2id_password: "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0" \ + "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d" \ + "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5" \ + "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6", + argon2id_salt: "5541fbc995d5c197ba290346d2c559de", + argon2id_outlen: 155, + argon2id_opslimit: 5, + argon2id_memlimit: 7_256_678, + argon2id_digest: "18acec5d6507739f203d1f5d9f1d862f7c2cdac4f19d2bdff64487e60d969e3ced6" \ + "15337b9eec6ac4461c6ca07f0939741e57c24d0005c7ea171a0ee1e7348249d135b" \ + "38f222e4dad7b9a033ed83f5ca27277393e316582033c74affe2566a2bea47f91f0" \ + "fd9fe49ece7e1f79f3ad6e9b23e0277c8ecc4b313225748dd2a80f5679534a0700e" \ + "246a79a49b3f74eb89ec6205fe1eeb941c73b1fcf1", + # argon2_str vectors # from libsodium/test/default/pwhash.c argon2_str_digest: "$argon2i$v=19$m=4096,t=3,p=2$b2RpZHVlamRpc29kaXNrdw" \ diff --git a/spec/rbnacl/password_hash/argon2_spec.rb b/spec/rbnacl/password_hash/argon2_spec.rb index 07ef361..a2c369f 100644 --- a/spec/rbnacl/password_hash/argon2_spec.rb +++ b/spec/rbnacl/password_hash/argon2_spec.rb @@ -3,26 +3,47 @@ if RbNaCl::Sodium::Version::ARGON2_SUPPORTED RSpec.describe RbNaCl::PasswordHash::Argon2 do - let(:reference_password) { vector :argon2_password } - let(:reference_salt) { vector :argon2_salt } - let(:reference_opslimit) { RbNaCl::TEST_VECTORS[:argon2_opslimit] } - let(:reference_memlimit) { RbNaCl::TEST_VECTORS[:argon2_memlimit] } - let(:reference_digest) { vector :argon2_digest } - let(:reference_outlen) { RbNaCl::TEST_VECTORS[:argon2_outlen] } + let(:argon2i_password) { vector :argon2i_password } + let(:argon2i_salt) { vector :argon2i_salt } + let(:argon2i_opslimit) { RbNaCl::TEST_VECTORS[:argon2i_opslimit] } + let(:argon2i_memlimit) { RbNaCl::TEST_VECTORS[:argon2i_memlimit] } + let(:argon2i_digest) { vector :argon2i_digest } + let(:argon2i_outlen) { RbNaCl::TEST_VECTORS[:argon2i_outlen] } + + let(:argon2id_password) { vector :argon2id_password } + let(:argon2id_salt) { vector :argon2id_salt } + let(:argon2id_opslimit) { RbNaCl::TEST_VECTORS[:argon2id_opslimit] } + let(:argon2id_memlimit) { RbNaCl::TEST_VECTORS[:argon2id_memlimit] } + let(:argon2id_digest) { vector :argon2id_digest } + let(:argon2id_outlen) { RbNaCl::TEST_VECTORS[:argon2id_outlen] } let(:str_ref_password) { RbNaCl::TEST_VECTORS[:argon2_str_passwd] } let(:str_ref_digest) { RbNaCl::TEST_VECTORS[:argon2_str_digest] } - it "calculates the correct digest for a reference password/salt" do - digest = RbNaCl::PasswordHash.argon2( - reference_password, - reference_salt, - reference_opslimit, - reference_memlimit, - reference_outlen + it "calculates the correct argon2i digest for a reference password/salt" do + digest = RbNaCl::PasswordHash.argon2i( + argon2i_password, + argon2i_salt, + argon2i_opslimit, + argon2i_memlimit, + argon2i_outlen ) - expect(digest).to eq reference_digest + expect(digest).to eq argon2i_digest + end + + if RbNaCl::Sodium::Version.supported_version?("1.0.13") + it "calculates the correct argon2id digest for a reference password/salt" do + digest = RbNaCl::PasswordHash.argon2id( + argon2id_password, + argon2id_salt, + argon2id_opslimit, + argon2id_memlimit, + argon2id_outlen + ) + + expect(digest).to eq argon2id_digest + end end it "verifies password" do