パスワードのハッシュ化といえば、RailsのActiveModel::SecurePasswordでもBcryptが使われているので今まで自分で作るときもBcryptを使っていたのですが、Bcryptには以下のような仕様があり、多少気になりArgon2を使ってみたので使い方とかをメモ📝
bcryptは、PHPのpassword_hash関数のデフォルトアルゴリズムである他、他の言語でも安全なハッシュ保存機能として広く利用されていますが、パスワードが最大72文字で切り詰められるという実装上の特性があり、その点が気になる人もいるようです(この制限はDoS脆弱性回避が目的です)。 bcryptの72文字制限をSHA-512ハッシュで回避する方式の注意点 | 徳丸浩の日記
Argon2とは?
Argon2 は 2015年の Password Hashing Competition で1位を獲得した比較的新しいハッシュ関数です。
OWASAPでも基本的にはArgon2の使用が推奨されているようですね。
This cheat sheet provides guidance on the various areas that need to be considered related to storing passwords. In short
- Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
- If Argon2id is not available, use bcrypt with a work factor of 10 or more and with a password limit of 72 bytes.
Argon2をRubyで使う
Argon2をRubyで使用できる以下のRuby Argon2 Gemがあるのでそれを利用してみます💎
hasher = Argon2::Password.new Argon2::Password.create("password") => "$argon2i$v=19$m=65536,t=2,p=1$61qkSyYNbUgf3kZH3GtHRw$4CQff9AZ0lWd7uF24RKMzqEiGpzhte1Hp8SO7X8bAew"
Version 2.0 - Argon 2id Version 2.x upwards will now default to the Argon2id hash format. This is consistent with current recommendations regarding Argon2 usage. It remains capable of verifying existing hashes.
Version 2.0以降であれば Argon2id hash formatでハッシュ化できるようです。
Ruby Argon2 Gemではハッシュ化に関するコストまわりの設定を行えますが、デフォルトだと以下のような設定になるようです。
- m_cost (使用メモリ) : 16 ※2のm_cost乗のKiB値が使用されるため65536KiB(64MiB)がデフォルトの使用メモリ量になるようです。
- t_cost (反復回数) : 2
- p_cost(並列処理数) : 1
ref: ruby-argon2/test.c at b89f08dfffaf160dffcda1f9163e2e3e31076c9a · technion/ruby-argon2 · GitHub
OWASAPの推奨設定がデフォルトは以下のようなので、デフォルトは対象コストを上げているようですね👀
Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism.
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction
OWASAPの値に設定するなら以下のような感じになりそうです。
- m_cost (使用メモリ) : 14※16384KiB(16Mib)
- t_cost (反復回数) : 2
- p_cost(並列処理数) : 1
Argon2でパスワードのハッシュ化・検証を行う
Argon2でパスワードのハッシュ化・検証を行う以下のようなclassを作ってみました。 そこまでBcryptと使い勝手は変わらず良い感じですね✨
require "argon2" class Password def initialize( value:, pepper: "default-papper" ) @value = value @pepper = pepper end attr_reader :value def match?(digest:) verify_password?(password_with_pepper(value), digest) end def digest generate_hash(password_with_pepper(value)) end private def verify_password?(password, digest) Argon2::Password.verify_password(password, digest) end def password_with_pepper(password) "#{password}-#{@pepper}" end def generate_hash(password) # NOTE: Adjusted to be OWASAP's recommended value by default. # > Use Argon2id with a minimum configuration of 15 MiB of memory, an iteration count of 2, and 1 degree of parallelism. # > https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction argon = Argon2::Password.new(t_cost: 2, m_cost: 14, p_cost: 1) argon.create(password) end end password = Password.new(value: "password") # => #<Password:0x00000001097b2208 @pepper="default-papper", @value="password"> password.digest # => "$argon2id$v=19$m=16384,t=2,p=1$HyAICViZtYIldgmsu3mICw$7wfMkSRMnpyLzal1rdQ1YdnwsncfYceGXp4Cn7gDSSA" password.match?(digest: password.digest) # => true other_password = Password.new(value: "other_password") # => #<Password:0x0000000109f1f9a0 @pepper="default-papper", @value="other_password"> password.match?(digest: other_password.digest) # => false