Skip to content
Draft
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
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,35 @@ password.to_s
#=> "$argon2id$v=19$m=12288,t=3,p=1$uukIsLS6y6etvsgoN20kVg$exMvDX/P9exvEPmnZL2gZClRyMdrnqjqyysLMP/VUWA"
```

For convenience, several sets of parameters are available as constants:

1. The first recommended option from [RFC
9106](https://datatracker.ietf.org/doc/rfc9106/):

```ruby
password = Argon2id::Password.create("opensesame", **Argon2id::RFC_9106_HIGH_MEMORY)
password.to_s
#=> "$argon2id$v=19$m=2097152,t=1,p=4$6mZF5heTzNrztem0+ICjpg$ftqgeGJ0Hfsqymu1aeb4cXL11pjgbcIuIjYwFJOOUVM"
```

2. The second recommended option from RFC 9106:

```ruby
password = Argon2id::Password.create("opensesame", **Argon2id::RFC_9106_LOW_MEMORY)
password.to_s
#=> "$argon2id$v=19$m=65536,t=3,p=4$RSoUjYKa5Xg8zoPtv/LJgQ$wKGeEUJXaoG4yRCX5SyINyKWO1a78IL6nVToraNwwqY"
```

3. The second recommended option from the [OWASP Password Storage Cheat
Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id)
(this is the default if no keyword arguments are passed):

```ruby
password = Argon2id::Password.create("opensesame", **Argon2id::OWASP_2)
password.to_s
#=> "$argon2id$v=19$m=19456,t=2,p=1$CG+LJTSf0ghYGvPtUYdyqA$cynug5xL6dRN4YOrG4MCzc/3EWkJxwg+D0gZkoyPeH8"
```

If you want to override the parameters for all calls to
`Argon2id::Password.create`, you can set them on `Argon2id` directly:

Expand All @@ -121,6 +150,14 @@ Argon2id.salt_len = 16
Argon2id.output_len = 32
```

To set multiple parameters at once or use one of the constants, you can use
`Argon2id.set_defaults`:

```ruby
Argon2id.set_defaults(t_cost: 3, m_cost: 12288)
Argon2id.set_defaults(**Argon2id::RFC_9106_HIGH_MEMORY)
```

### Verifying passwords

To verify a password against a hash, use `Argon2id::Password#==`:
Expand Down
68 changes: 68 additions & 0 deletions lib/argon2id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,52 @@ module Argon2id
# The default desired hash length of 32 bytes.
DEFAULT_OUTPUT_LEN = 32

# OWASP Password Storage Cheat Sheet second recommended parameters.
#
# "m=19456 (19 MiB), t=2, p=1"
#
# These are the defaults used by Argon2id::Password.create if no other
# keyword arguments are given.
#
# See https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
OWASP_2 = {
t_cost: DEFAULT_T_COST,
m_cost: DEFAULT_M_COST,
parallelism: DEFAULT_PARALLELISM,
salt_len: DEFAULT_SALT_LEN,
output_len: DEFAULT_OUTPUT_LEN
}.freeze

# RFC 9106 first recommended parameters.
#
# "If a uniformly safe option that is not tailored to your application or
# hardware is acceptable, select Argon2id with t=1 iteration, p=4 lanes,
# m=2^(21) (2 GiB of RAM), 128-bit salt, and 256-bit tag size."
#
# See 4. Parameter Choice in https://datatracker.ietf.org/doc/rfc9106/
RFC_9106_HIGH_MEMORY = {
t_cost: 1,
parallelism: 4,
m_cost: 2_097_152,
salt_len: 16,
output_len: 32
}.freeze

# RFC 9106 second recommended parameters.
#
# "If much less memory is available, a uniformly safe option is Argon2id with
# t=3 iterations, p=4 lanes, m=2^(16) (64 MiB of RAM), 128-bit salt, and
# 256-bit tag size."
#
# See 4. Parameter Choice in https://datatracker.ietf.org/doc/rfc9106/
RFC_9106_LOW_MEMORY = {
t_cost: 3,
parallelism: 4,
m_cost: 65_536,
salt_len: 16,
output_len: 32
}.freeze

@t_cost = DEFAULT_T_COST
@m_cost = DEFAULT_M_COST
@parallelism = DEFAULT_PARALLELISM
Expand All @@ -41,5 +87,27 @@ class << self

# The default desired length of the hash in bytes used by Argon2id::Password.create
attr_accessor :output_len

# Set default parameters used by Argon2id::Password.create
#
# - +:t_cost+: integer (default Argon2id.t_cost) the "time cost" given as a number of iterations
# - +:m_cost+: integer (default Argon2id.m_cost) the "memory cost" given in kibibytes
# - +:parallelism+: integer (default Argon2id.parallelism) the number of threads and compute lanes to use
# - +:salt_len+: integer (default Argon2id.salt_len) the salt size in bytes
# - +:output_len+: integer (default Argon2id.output_len) the desired length of the hash in bytes
#
# For example:
#
# Argon2id.set_defaults(t_cost: 1, m_cost: 47104, parallelism: 1)
# Argon2id.set_defaults(**Argon2id::RFC_9106_HIGH_MEMORY)
def set_defaults(t_cost: self.t_cost, m_cost: self.m_cost, parallelism: self.parallelism, salt_len: self.salt_len, output_len: self.output_len)
@t_cost = t_cost
@m_cost = m_cost
@parallelism = parallelism
@salt_len = salt_len
@output_len = output_len

nil
end
end
end
116 changes: 116 additions & 0 deletions test/test_argon2id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,120 @@ def test_output_len_can_be_overridden
ensure
Argon2id.output_len = Argon2id::DEFAULT_OUTPUT_LEN
end

def test_owasp_2_uses_t_cost_of_2
assert_equal 2, Argon2id::OWASP_2[:t_cost]
end

def test_owasp_2_uses_parallelism_of_1
assert_equal 1, Argon2id::OWASP_2[:parallelism]
end

def test_owasp_2_uses_m_cost_of_19_mib
assert_equal 19_456, Argon2id::OWASP_2[:m_cost]
end

def test_owasp_2_uses_salt_len_of_128_bits
assert_equal 128/8, Argon2id::OWASP_2[:salt_len]
end

def test_owasp_2_uses_output_len_of_256_bits
assert_equal 256/8, Argon2id::OWASP_2[:output_len]
end

def test_rfc_9106_high_memory_uses_t_cost_of_1
assert_equal 1, Argon2id::RFC_9106_HIGH_MEMORY[:t_cost]
end

def test_rfc_9106_high_memory_uses_parallelism_of_4
assert_equal 4, Argon2id::RFC_9106_HIGH_MEMORY[:parallelism]
end

def test_rfc_9106_high_memory_uses_m_cost_of_2_gib
assert_equal 2**21, Argon2id::RFC_9106_HIGH_MEMORY[:m_cost]
end

def test_rfc_9106_high_memory_uses_salt_len_of_128_bits
assert_equal 128/8, Argon2id::RFC_9106_HIGH_MEMORY[:salt_len]
end

def test_rfc_9106_high_memory_uses_output_len_of_256_bits
assert_equal 256/8, Argon2id::RFC_9106_HIGH_MEMORY[:output_len]
end

def test_rfc_9106_low_memory_uses_t_cost_of_3
assert_equal 3, Argon2id::RFC_9106_LOW_MEMORY[:t_cost]
end

def test_rfc_9106_low_memory_uses_parallelism_of_4
assert_equal 4, Argon2id::RFC_9106_LOW_MEMORY[:parallelism]
end

def test_rfc_9106_low_memory_uses_m_cost_of_64_mib
assert_equal 2**16, Argon2id::RFC_9106_LOW_MEMORY[:m_cost]
end

def test_rfc_9106_low_memory_uses_salt_len_of_128_bits
assert_equal 128/8, Argon2id::RFC_9106_LOW_MEMORY[:salt_len]
end

def test_rfc_9106_low_memory_uses_output_len_of_256_bits
assert_equal 256/8, Argon2id::RFC_9106_LOW_MEMORY[:output_len]
end

def test_set_defaults_sets_t_cost
Argon2id.set_defaults(t_cost: 1)

assert_equal 1, Argon2id.t_cost
ensure
Argon2id.t_cost = Argon2id::DEFAULT_T_COST
end

def test_set_defaults_does_not_change_missing_parameters
Argon2id.m_cost = 47_014
Argon2id.set_defaults(t_cost: 1)

assert_equal 47_014, Argon2id.m_cost
ensure
Argon2id.t_cost = Argon2id::DEFAULT_T_COST
Argon2id.m_cost = Argon2id::DEFAULT_M_COST
end

def test_set_defaults_sets_m_cost
Argon2id.set_defaults(m_cost: 47_104)

assert_equal 47_104, Argon2id.m_cost
ensure
Argon2id.m_cost = Argon2id::DEFAULT_M_COST
end

def test_set_defaults_sets_parallelism
Argon2id.set_defaults(parallelism: 4)

assert_equal 4, Argon2id.parallelism
ensure
Argon2id.parallelism = Argon2id::DEFAULT_PARALLELISM
end

def test_set_defaults_sets_salt_len
Argon2id.set_defaults(salt_len: 32)

assert_equal 32, Argon2id.salt_len
ensure
Argon2id.salt_len = Argon2id::DEFAULT_SALT_LEN
end

def test_set_defaults_sets_output_len
Argon2id.set_defaults(output_len: 32)

assert_equal 32, Argon2id.output_len
ensure
Argon2id.output_len = Argon2id::DEFAULT_OUTPUT_LEN
end

def test_set_defaults_returns_nil
assert_nil Argon2id.set_defaults(output_len: 32)
ensure
Argon2id.output_len = Argon2id::DEFAULT_OUTPUT_LEN
end
end