From 347bb4debc417a7aef8287a95f95e624dfcba779 Mon Sep 17 00:00:00 2001 From: Paul Mucur Date: Thu, 28 Nov 2024 20:09:40 +0000 Subject: [PATCH] Provide preset parameters for RFC 9106 GitHub: https://github.com/mudge/argon2id/issues/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. --- README.md | 37 +++++++++++++ lib/argon2id.rb | 71 ++++++++++++++++++++++-- test/test_argon2id.rb | 126 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 229 insertions(+), 5 deletions(-) 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..e9389f2 100644 --- a/lib/argon2id.rb +++ b/lib/argon2id.rb @@ -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 @@ -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 diff --git a/test/test_argon2id.rb b/test/test_argon2id.rb index 261c317..b866649 100644 --- a/test/test_argon2id.rb +++ b/test/test_argon2id.rb @@ -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