Skip to content

Commit

Permalink
Provide preset parameters for RFC 9106
Browse files Browse the repository at this point in the history
GitHub: #8

Allow users to choose from recommended parameters from RFC 9106 and the
OWASP Password Storage Cheat Sheet by providing them as constants on
`Argon2id`.

To allow users to use this as defaults without having to pass them to
every call to `Argon2id::Password.create`, add a new
`Argon2id.set_defaults` method for setting multiple parameters at once.
Note that `Argon2id.set_defaults` returns `nil` as it is purely for
side-effects only.
  • Loading branch information
mudge committed Nov 29, 2024
1 parent e08609b commit b01ecae
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 0 deletions.
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

0 comments on commit b01ecae

Please sign in to comment.