Skip to content

Commit

Permalink
Add support for argon2id digest, if libsodium >= 1.0.13.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
elijh committed Jan 23, 2018
1 parent 028b1a1 commit 028ec78
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 39 deletions.
35 changes: 33 additions & 2 deletions lib/rbnacl/password_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
43 changes: 28 additions & 15 deletions lib/rbnacl/password_hash/argon2.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion lib/rbnacl/sodium.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/rbnacl/sodium/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
29 changes: 22 additions & 7 deletions lib/rbnacl/test_vectors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
49 changes: 35 additions & 14 deletions spec/rbnacl/password_hash/argon2_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 028ec78

Please sign in to comment.