diff --git a/README.md b/README.md index d880dd1..2a9e4a9 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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#==`: diff --git a/lib/argon2id.rb b/lib/argon2id.rb index b21937c..72777db 100644 --- a/lib/argon2id.rb +++ b/lib/argon2id.rb @@ -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 @@ -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 diff --git a/test/test_argon2id.rb b/test/test_argon2id.rb index 261c317..2b6cd23 100644 --- a/test/test_argon2id.rb +++ b/test/test_argon2id.rb @@ -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