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` (and corresponding `Argon2id.defaults`) method
for setting multiple parameters at once.
  • Loading branch information
mudge committed Nov 28, 2024
1 parent e08609b commit 347bb4d
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 5 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
71 changes: 66 additions & 5 deletions lib/argon2id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,53 @@
require "argon2id/version"

module Argon2id
# OWASP Password Storage Cheat Sheet second recommended parameters.
#
# See https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
OWASP_2 = {
t_cost: 2,
m_cost: 19_456,
parallelism: 1,
salt_len: 16,
output_len: 32
}.freeze

# RFC 9106 first recommended parameters.
#
# 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.
#
# 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

# The default "time cost" of 2 iterations recommended by OWASP.
DEFAULT_T_COST = 2
DEFAULT_T_COST = OWASP_2[:t_cost]

# The default "memory cost" of 19 mebibytes recommended by OWASP.
DEFAULT_M_COST = 19_456
DEFAULT_M_COST = OWASP_2[:m_cost]

# The default 1 thread and compute lane recommended by OWASP.
DEFAULT_PARALLELISM = 1
DEFAULT_PARALLELISM = OWASP_2[:parallelism]

# The default salt length of 16 bytes.
DEFAULT_SALT_LEN = 16
DEFAULT_SALT_LEN = OWASP_2[:salt_len]

# The default desired hash length of 32 bytes.
DEFAULT_OUTPUT_LEN = 32
DEFAULT_OUTPUT_LEN = OWASP_2[:output_len]

@t_cost = DEFAULT_T_COST
@m_cost = DEFAULT_M_COST
Expand All @@ -41,5 +74,33 @@ 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
#
# 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

defaults
end

# Return all default parameters used by Argon2id::Password.create
#
# Argon2id.defaults
# #=> {:t_cost=>2, :m_cost=>19456, :parallelism=>1, :salt_len=>16, :output_len=>32}
def defaults
{
t_cost: t_cost,
m_cost: m_cost,
parallelism: parallelism,
salt_len: salt_len,
output_len: output_len
}
end
end
end
126 changes: 126 additions & 0 deletions test/test_argon2id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,130 @@ 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_all_defaults
assert_equal(
{ t_cost: 2, m_cost: 19_456, parallelism: 1, salt_len: 16, output_len: 16 },
Argon2id.set_defaults(output_len: 16)
)
ensure
Argon2id.output_len = Argon2id::DEFAULT_OUTPUT_LEN
end

def test_defaults_returns_all_parameters
assert_equal(
{ t_cost: 2, m_cost: 19_456, parallelism: 1, salt_len: 16, output_len: 32 },
Argon2id.defaults
)
end
end

0 comments on commit 347bb4d

Please sign in to comment.