Skip to content
Merged
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
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
70 changes: 56 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,68 @@

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 argon2i_digest
end

if RbNaCl::Sodium::Version::ARGON2_SUPPORTED
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

it "calculates the correct argon2 default digest" do
if RbNaCl::Sodium::Version.supported_version?("1.0.15")
digest = RbNaCl::PasswordHash.argon2(
argon2id_password,
argon2id_salt,
argon2id_opslimit,
argon2id_memlimit,
argon2id_outlen
)
expect(digest).to eq argon2id_digest
else
digest = RbNaCl::PasswordHash.argon2(
argon2i_password,
argon2i_salt,
argon2i_opslimit,
argon2i_memlimit,
argon2i_outlen
)
expect(digest).to eq argon2i_digest
end
end

expect(digest).to eq reference_digest
end

it "verifies password" do
Expand Down