Skip to content

Commit 6312a8f

Browse files
authored
Make default costs RFC 9106's second preferred option; introduce named cost profiles (#62)
* Make default costs RFC 9106's second preferred option RFC 9106 is the formal standard for describing Argon2. It also gives the official recommended cost parameters that should be sufficient for all environments. This commit introduces the concept of named profiles for a set of cost parameters/values and changes the default costs to `:rfc_9106_low_memory`, the second preferred option in the RFC. The RFC's first choice can be quite computationally expensive and, mirroring Python's `argon2-cffi`, we leave that as an opt-in choice. A developer can use one of the named profiles, or continue to hand specify costs: ```ruby hasher = Argon2::Password.new(profile: :rfc_9106_high_memory) hasher.create("password") => "$argon2id$v=19$m=2097152,t=1,p=4$LvHa74Yax7uCWPN7P6/oQQ$V1dMt4dfuYSmLpwUTpKUzg+RrXjWzWHlE6NLowBzsAg" hasher = Argon2::Password.new(t_cost: 2, m_cost: 16, p_cost: 1) hasher.create("password") => "$argon2i$v=19$m=65536,t=2,p=1$jL7lLEAjDN+pY2cG1N8D2g$iwj1ueduCvm6B9YVjBSnAHu+6mKzqGmDW745ALR38Uo" ``` The list of named cost profiles are: * `:rfc_9106_high_memory`: the first recommended option but is expensive * `:rfc_9106_low_memory`: the second recommended option (default) * `:pre_rfc_9106`: the previous default costs for `ruby-argon2` <= v2.2.0, before offering RFC 9106 named profiles * `:unsafe_cheapest`: Strictly for testing, the minimum costs allowed by Argon2 for the fastest hashing speed A developer can see the list of profiles with `Argon2::Profiles.to_a` and the actual cost values with `.to_h` or `[name]`. As guidance changes over time (OWASP has its own recommended values), the list of profiles may expand or even change their values. * Satisfy rubocop
1 parent e4e74b8 commit 6312a8f

File tree

5 files changed

+144
-20
lines changed

5 files changed

+144
-20
lines changed

README.md

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,26 +27,50 @@ Require this in your Gemfile like a typical Ruby gem:
2727
require 'argon2'
2828
```
2929

30-
To generate a hash using specific time and memory cost:
30+
To utilise default costs ([RFC 9106](https://www.rfc-editor.org/rfc/rfc9106#name-parameter-choice)'s lower-memory, second recommended parameters):
3131

3232
```ruby
33-
hasher = Argon2::Password.new(t_cost: 2, m_cost: 16, p_cost: 1)
33+
hasher = Argon2::Password.new
3434
hasher.create("password")
35-
=> "$argon2i$v=19$m=65536,t=2,p=1$jL7lLEAjDN+pY2cG1N8D2g$iwj1ueduCvm6B9YVjBSnAHu+6mKzqGmDW745ALR38Uo"
3635
```
3736

38-
To utilise default costs:
37+
Alternatively, use this shortcut:
3938

4039
```ruby
41-
hasher = Argon2::Password.new
40+
Argon2::Password.create("password")
41+
=> "$argon2i$v=19$m=65536,t=2,p=1$61qkSyYNbUgf3kZH3GtHRw$4CQff9AZ0lWd7uF24RKMzqEiGpzhte1Hp8SO7X8bAew"
42+
```
43+
44+
If your use case can afford the higher memory consumption/cost, you can/should specify to use RFC 9106's first recommended parameters:
45+
46+
```ruby
47+
hasher = Argon2::Password.new(profile: :rfc_9106_high_memory)
4248
hasher.create("password")
49+
=> "$argon2id$v=19$m=2097152,t=1,p=4$LvHa74Yax7uCWPN7P6/oQQ$V1dMt4dfuYSmLpwUTpKUzg+RrXjWzWHlE6NLowBzsAg"
4350
```
4451

45-
Alternatively, use this shortcut:
52+
To generate a hash using one of the other `Argon::Profiles` names:
4653

4754
```ruby
48-
Argon2::Password.create("password")
49-
=> "$argon2i$v=19$m=65536,t=2,p=1$61qkSyYNbUgf3kZH3GtHRw$4CQff9AZ0lWd7uF24RKMzqEiGpzhte1Hp8SO7X8bAew"
55+
# Only use this profile in testing env, it's unsafe!
56+
hasher = Argon2::Password.new(profile: :unsafe_cheapest)
57+
hasher.create("password")
58+
=> "$argon2id$v=19$m=8,t=1,p=1$HZZHG3oTqptqgrxWxFic5g$EUokHMU6m6w2AVIEk1MpZBhVwW9Nj+ESRjPwTBVtWpY"
59+
```
60+
61+
The list of named cost profiles are:
62+
63+
* `:rfc_9106_high_memory`: the first recommended option but is expensive
64+
* `:rfc_9106_low_memory`: the second recommended option (default)
65+
* `:pre_rfc_9106`: the previous default costs for `ruby-argon2` <= v2.2.0, before offering RFC 9106 named profiles
66+
* `:unsafe_cheapest`: Strictly for testing, the minimum costs allowed by Argon2 for the fastest hashing speed
67+
68+
To generate a hash using specific time and memory cost:
69+
70+
```ruby
71+
hasher = Argon2::Password.new(t_cost: 2, m_cost: 16, p_cost: 1)
72+
hasher.create("password")
73+
=> "$argon2i$v=19$m=65536,t=2,p=1$jL7lLEAjDN+pY2cG1N8D2g$iwj1ueduCvm6B9YVjBSnAHu+6mKzqGmDW745ALR38Uo"
5074
```
5175

5276
You can then use this function to verify a password against a given hash. Will return either true or false.

lib/argon2.rb

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@
66
require 'argon2/errors'
77
require 'argon2/engine'
88
require 'argon2/hash_format'
9+
require 'argon2/profiles'
910

1011
module Argon2
1112
# Front-end API for the Argon2 module.
1213
class Password
1314
# Expose constants for the options supported and default used for passwords.
14-
DEFAULT_T_COST = 2
15-
DEFAULT_M_COST = 16
16-
DEFAULT_P_COST = 1
15+
DEFAULT_T_COST = Argon2::Profiles::RFC_9106_LOW_MEMORY[:t_cost]
16+
DEFAULT_M_COST = Argon2::Profiles::RFC_9106_LOW_MEMORY[:m_cost]
17+
DEFAULT_P_COST = Argon2::Profiles::RFC_9106_LOW_MEMORY[:p_cost]
1718
MIN_T_COST = 1
1819
MAX_T_COST = 750
1920
MIN_M_COST = 3
@@ -22,14 +23,9 @@ class Password
2223
MAX_P_COST = 8
2324

2425
def initialize(options = {})
25-
@t_cost = options[:t_cost] || DEFAULT_T_COST
26-
raise ArgonHashFail, "Invalid t_cost" if @t_cost < MIN_T_COST || @t_cost > MAX_T_COST
26+
options.update(Profiles[options[:profile]]) if options.key?(:profile)
2727

28-
@m_cost = options[:m_cost] || DEFAULT_M_COST
29-
raise ArgonHashFail, "Invalid m_cost" if @m_cost < MIN_M_COST || @m_cost > MAX_M_COST
30-
31-
@p_cost = options[:p_cost] || DEFAULT_P_COST
32-
raise ArgonHashFail, "Invalid p_cost" if @p_cost < MIN_P_COST || @p_cost > MAX_P_COST
28+
init_costs(options)
3329

3430
@salt_do_not_supply = options[:salt_do_not_supply]
3531
@secret = options[:secret]
@@ -61,5 +57,18 @@ def self.verify_password(pass, hash, secret = nil)
6157

6258
Argon2::Engine.argon2_verify(pass, hash, secret)
6359
end
60+
61+
protected
62+
63+
def init_costs(options = {})
64+
@t_cost = options[:t_cost] || DEFAULT_T_COST
65+
raise ArgonHashFail, "Invalid t_cost" if @t_cost < MIN_T_COST || @t_cost > MAX_T_COST
66+
67+
@m_cost = options[:m_cost] || DEFAULT_M_COST
68+
raise ArgonHashFail, "Invalid m_cost" if @m_cost < MIN_M_COST || @m_cost > MAX_M_COST
69+
70+
@p_cost = options[:p_cost] || DEFAULT_P_COST
71+
raise ArgonHashFail, "Invalid p_cost" if @p_cost < MIN_P_COST || @p_cost > MAX_P_COST
72+
end
6473
end
6574
end

lib/argon2/profiles.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
module Argon2
4+
# Contains named profiles of different common cost parameter sets
5+
class Profiles
6+
def self.[](name)
7+
name = name.upcase.to_sym
8+
raise NotImplementedError unless const_defined?(name)
9+
10+
const_get(name)
11+
end
12+
13+
def self.to_a
14+
constants.map(&:downcase)
15+
end
16+
17+
def self.to_h
18+
to_a.reduce({}) { |h, name| h.update(name => self[name]) }
19+
end
20+
21+
# https://datatracker.ietf.org/doc/html/rfc9106#name-argon2-algorithm
22+
# FIRST RECOMMENDED option per RFC 9106.
23+
RFC_9106_HIGH_MEMORY = {
24+
t_cost: 1,
25+
m_cost: 21, # 2 GiB
26+
p_cost: 4
27+
}.freeze
28+
29+
# SECOND RECOMMENDED option per RFC 9106.
30+
RFC_9106_LOW_MEMORY = {
31+
t_cost: 3,
32+
m_cost: 16, # 64 MiB
33+
p_cost: 4
34+
}.freeze
35+
36+
# The default values ruby-argon2 had before using RFC 9106 recommendations
37+
PRE_RFC_9106 = {
38+
t_cost: 2,
39+
m_cost: 16, # 64 MiB
40+
p_cost: 1
41+
}.freeze
42+
43+
# Only use for fast testing. Insecure otherwise!
44+
UNSAFE_CHEAPEST = {
45+
t_cost: 1,
46+
m_cost: 3, # 8 KiB
47+
p_cost: 1
48+
}.freeze
49+
end
50+
end

test/api_test.rb

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ def test_create_default
1414
assert pass = Argon2::Password.new
1515
assert_instance_of Argon2::Password, pass
1616
assert_equal 16, pass.m_cost
17-
assert_equal 2, pass.t_cost
18-
assert_equal 1, pass.p_cost
17+
assert_equal 3, pass.t_cost
18+
assert_equal 4, pass.p_cost
1919
assert_nil pass.secret
2020
end
2121

@@ -28,6 +28,15 @@ def test_create_args
2828
assert_nil pass.secret
2929
end
3030

31+
def test_create_profile_arg
32+
assert pass = Argon2::Password.new(profile: :rfc_9106_high_memory)
33+
assert_instance_of Argon2::Password, pass
34+
assert_equal 21, pass.m_cost
35+
assert_equal 1, pass.t_cost
36+
assert_equal 4, pass.p_cost
37+
assert_nil pass.secret
38+
end
39+
3140
def test_secret
3241
assert pass = Argon2::Password.new(secret: "A secret")
3342
assert_equal pass.secret, "A secret"

test/profiles_test.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
5+
class ProfilesTest < Minitest::Test
6+
def test_hash_access
7+
assert_equal Argon2::Profiles::RFC_9106_LOW_MEMORY, Argon2::Profiles[:RFC_9106_LOW_MEMORY]
8+
end
9+
10+
def test_to_a
11+
# rubocop:disable Naming/VariableNumber
12+
assert_equal %i[
13+
pre_rfc_9106
14+
rfc_9106_high_memory
15+
rfc_9106_low_memory
16+
unsafe_cheapest
17+
], Argon2::Profiles.to_a.sort
18+
# rubocop:enable Naming/VariableNumber
19+
end
20+
21+
def test_to_h
22+
hash = Argon2::Profiles.to_h
23+
assert_equal Argon2::Profiles::RFC_9106_HIGH_MEMORY, hash[:rfc_9106_high_memory]
24+
end
25+
26+
def test_structure
27+
Argon2::Profiles.to_h.values do |profile|
28+
assert_equal %i[t_cost m_cost p_cost], profile.keys
29+
assert(profile.values.all? { |v| v.instance_of?(Integer) })
30+
end
31+
end
32+
end

0 commit comments

Comments
 (0)