Madogiwa Blog

主に技術系の学習メモに使っていきます。

Ruby: よりセキュアなArgon2でパスワードをハッシュ化するメモ

パスワードのハッシュ化といえば、RailsのActiveModel::SecurePasswordでもBcryptが使われているので今まで自分で作るときもBcryptを使っていたのですが、Bcryptには以下のような仕様があり、多少気になりArgon2を使ってみたので使い方とかをメモ📝

bcryptは、PHPのpassword_hash関数のデフォルトアルゴリズムである他、他の言語でも安全なハッシュ保存機能として広く利用されていますが、パスワードが最大72文字で切り詰められるという実装上の特性があり、その点が気になる人もいるようです(この制限はDoS脆弱性回避が目的です)。 bcryptの72文字制限をSHA-512ハッシュで回避する方式の注意点 | 徳丸浩の日記

Argon2とは?

Argon2 は 2015年の Password Hashing Competition で1位を獲得した比較的新しいハッシュ関数です。

  • GPU や専用のハードウェア(FPGA/ASIC) 攻撃に強い Argon2d
  • サイドチャンネル攻撃に強い Argon2i
  • Argon2d と Argon2i のハイブリッド型の Argon2id

OWASPに学ぶパスワードの安全なハッシュ化 | DevelopersIO

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.

Password Storage - OWASP Cheat Sheet Series

Argon2をRubyで使う

Argon2をRubyで使用できる以下のRuby Argon2 Gemがあるのでそれを利用してみます💎

github.com

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