👋

OWASPのチートシートを読んでパスワードの保存方法を確認する

2024/05/08に公開

はじめに

現在ログインアプリを作成しているのですが、ふとパスワードをハッシュ化する時のお作法は何か疑問に感じました。
そこで今回はOWASPが展開しているパスワード保存についてのチートシートを見ていきます。
私と同様な疑問を感じた方は見ていただけますと幸いです。
でははじめます。

パスワードは暗号化?ハッシュ化?

パスワードを保存する時の方法として、OWASP は以下のように太字で言及しています。

passwords should be hashed, NOT encrypted.

パスワードは暗号化したものを保存するのではなく、ハッシュ化したものを保存すべきだとしています。
これの理由については以下で記載があります。

Because hashing is a one-way function

(中略)

Since encryption is a two-way function, (略)

ハッシュは一方向関数です。
一方で、暗号は双方向関数のためパスワード管理に使用すべきではないと言っています。
どちらも元の文字列から変換されたものだけ見ても解読はできないように思います。
ではなぜ、双方向だとダメなのかは Introduction に記載していることから推測できます。

they must be protected from an attacker even if the application or database is compromised

OWASP としてはデータベースやアプリケーションに不正アクセスされ、データを盗み出されたとしてもパスワードについては安全に保たれる必要があると述べています。
暗号は鍵を用いて平文を分からないようにします。
一方で、必要な鍵さえあれば暗号文を平文に戻すことができます。
そのため、仮にデータベースやアプリケーションに不正アクセスされ鍵と暗号化したパスワードを取られたら、パスワードが分かってしまいます。
これは OWASP が言及している変換されたパスワードを取られたとしても、攻撃者は元のパスワードが分からないという要件を満たしません。
よって、双方向関数である暗号化によるパスワード保存は避けるべきだと言っています。
一方で、ハッシュ化はハッシュ化した文字列から元の平文に戻すことはできません。
これはハッシュ化した本人ですらできません。
なので、攻撃者もハッシュ化されたパスワードを取得したとしても、総当たりによる攻撃でしか元の平文を推測できません。
ゆえに、ハッシュ化による保存は求める保存方法として適しています。

パスワード保存のセキュリティを強化するものたち

Salt

Salt は各パスワードがハッシュ化される際に付与する一意でランダムな文字列です。
上記の一意とは各ユーザーごとを指します。
この Salt をハッシュ化する前のパスワードに付与してハッシュ化させることで、パスワードが同じでもユーザーごとに異なるハッシュ値を生成します。
そのため攻撃する側はソルト無しのハッシュ値を求めるよりも相当多くの計算を必要とします。
またソルトはレインボーテーブル攻撃にも有効となっています。
なので、ハッシュする時にソルトをすることはより安全性を強化します。
ただ、Argon2id, bcrypt, PBKDF2 はアルゴリズムとして、Salt を自動的に付与しているので、実装者側で特別何かをする必要はありません。

Pepper

Pepper は、パスワードハッシュの保護を強化するために使われるもう一つの方法です。Salting はパスワードごとにユニークな Salt 値を付与しますが、Pepper はすべてのパスワードに対して同一の値を付与する点が異なります。
Pepper の主な特徴は以下の通りです:

  1. Pepper はすべてのパスワードに対して共通の値を使用します。
  2. Pepper の値はデータベースには保存せず、アプリケーションのコードや設定ファイルなど別の場所に保管します。

Pepper を使うと、データベースが漏洩してパスワードハッシュが盗まれた場合でも、攻撃者は Pepper の値を知らないためハッシュの解読が困難になります。
Pepper はデータベースに保存しないので、SQL インジェクションなどの攻撃でデータベースの内容を盗まれても、Pepper の値は守られます。
Pepper の使用例としては、パスワードをハッシュ化した後に、そのハッシュ値と Pepper を組み合わせて再度ハッシュ化するといった方法があります。
ただし、Pepper はすべてのパスワードで共通の値となるため、Pepper が漏洩してしまうと全パスワードの保護が失われてしまう恐れがあります。
そのため、Pepper は定期的に変更するなどの対策が要求されているみたいです。

Using Work Factors

入力値に対してハッシュ関数を何回実行するか(Work Factors)を決めることです。
記載している文章からも、ベストプラクティスはアプリケーションの要件によって異なるので、一概に決めることができません。
ただ、一般的には認証時間は 1 秒以内に完了することが望まれるので、それよりは早くハッシュ関数の実行が完了すべきだそうです。

パスワードのハッシュ関数として適しているアルゴリズム達

前提

アルゴリズムについて見る前に、パスワードを保存する時に使用するハッシュ関数として求められている共通点は、以下の OWASP の記載のように処理が遅いということです。

Some modern hashing algorithms have been specifically designed to securely store passwords. This means that they should be slow (unlike algorithms such as MD5 and SHA-1, which were designed to be fast), and you can change how slow they are by changing the work factor.

処理が早いことはむしろ望まれていません。
なぜなら以下のデメリットがあるためです。

  • 処理が早いと試行回数が多くすることができる

これに尽きます。
処理が早いと、有限時間内でより多くのハッシュ関数を実行することができます。
これによって、一致するハッシュ値とそのもととなった文章を見つけ出す可能性が高くなります。
また、処理が早いということは必要なメモリ量が少なくてすみます。
そのため、マルチコア CPU や GPU での並列処理が可能となり、秒間でハッシュ関数の実行量を増やすことができます。
ゆえに、処理が早いハッシュ関数というのはパスワードをハッシュ化する際においては避けるべき事象となります。
一方で、パスワード保存に適しているハッシュ関数は総じて実行時間がかかります。
それは意図的により多くのメモリを占有する仕組みになっているからです。
これによって、単純な処理完了時間を延ばしつつ、マルチコア CPU や GPU での処理を不可にし並列実行を避けることができます。
以上のことから、OWASP としては処理が遅いハッシュ関数を使用することを求めています。

アルゴリズム ① Argon2id

2015 の Password Hashing Competition で勝者となった Argon2 アルゴリズムの一つです。
使用する場合、推奨する設定は以下の通りです。

  • m=47104 (46 MiB), t=1, p=1 (Do not use with Argon2i)
  • m=19456 (19 MiB), t=2, p=1 (Do not use with Argon2i)
  • m=12288 (12 MiB), t=3, p=1
  • m=9216 (9 MiB), t=4, p=1
  • m=7168 (7 MiB), t=5, p=1

m は実行時に使用するメモリ量です。
t は入力値に対して、何回 Argon2id でハッシュ化を行うかの回数です。
p は処理に使用するスレッド数です。
基本的に実行スレッドは 1 で、メモリを多く使うなら一回のハッシュ化でよいですが、小さくなればなるほどハッシュ関数を実行する回数は多くする必要があるみたいです。

アルゴリズム ② scrypt

scrypt はColin Percivalは作成したハッシュ関数です。
こちらも選択肢としてはありますが、以下 OWASP の記載のように何かしら事情で Argon2id が使用できない場合しか、使用すべきではないそうです。

While Argon2id should be the best choice for password hashing, scrypt should be used when the former is not available.

使用する場合、推奨設定は以下の通りです。

  • N=2^17 (128 MiB), r=8 (1024 bytes), p=1
  • N=2^16 (64 MiB), r=8 (1024 bytes), p=2
  • N=2^15 (32 MiB), r=8 (1024 bytes), p=3
  • N=2^14 (16 MiB), r=8 (1024 bytes), p=5
  • N=2^13 (8 MiB), r=8 (1024 bytes), p=10

N は CPU/メモリコストを表します。(これ調べても意味がわかりませんでした。すみません。)
r は入力値を分割する時の 1 ブロック当たりの大きさを表します。
p は Argon2id と同じで、並列処理の度合いを示します。

アルゴリズム ③ bcrypt

bcrypt は、レガシーシステムでのパスワード保存に最適です。
使用する場合の注意点は以下の通りです。

  • Work Factor(≒ ハッシュ関数の実行回数)はサーバーが許す限りの大きい値にする必要があります。最低値は 10 です。
  • bcrypt に渡す入力値の最大値は 72bytes です。
  • bcrypt(base64(sha256(data:$password, key:$pepper)), $salt, $cost)みたいに、bcrypt でのハッシュ化の前でパスワードを SHA-2 などの高速なハッシュ関数でハッシュ化したものを渡すのはパスワードシャッキングなどの危険があるので避ける必要があります。

アルゴリズム ④ PBKDF2

PBKDF2 は NIST によって推奨されており、FIPS-140 の検証を受けた実装が存在します。
FIPS-140 は’暗号モジュールに関するセキュリティ要件の仕様を規定する米国連邦標準規格’(ウィキペディアから引用)です。
なので、FIPS-140 の基準を満たす際には主な選択肢になります。
PBKDF2 は内 HMAC やその他のハッシュアルゴリズムを内部のハッシュアルゴリズムとして決定する必要があります。
なお、HMAC-SHA-256 が広くサポートされており NIST も推奨とのことです。
HMAC-SHA 系を使用した PBKDF2 によるハッシュ化の設定は以下の通りです。

  • PBKDF2-HMAC-SHA1: 1,300,000 iterations
  • PBKDF2-HMAC-SHA256: 600,000 iterations
  • PBKDF2-HMAC-SHA512: 210,000 iterations

ビット数が増えるほど繰り返しの回数が減少します。
並列で PBKDF2 を実行する場合、上記と同等のセキュリティ品質になる設定は以下の通りです。(2022 年 12 月時点の RTX 4000 GPU でのテストに基づく)

  • PPBKDF2-SHA512: cost 2
  • PPBKDF2-SHA256: cost 5
  • PPBKDF2-SHA1: cost 10

cost はメモリ使用量と CPU 時間のトレードオフを表すパラメータだと思います。
cost の値が大きければ大きいほどり多くのメモリが使用され、CPU 時間が短縮されるそうです。
逆に、cost の値が小さいほど、メモリ使用量が少なくなり、CPU 時間が長くなります。
ただ、この説明は正しくないかもしれないので、その時は指摘いただける嬉しいです。

Upgrading Legacy Hashes

ここはレガシーなハッシュ関数を使っていた際の置き換えについての説明です。
今回この記事を読む目的とは少し異なるので、省略します。

OWASP のチートシートを読んで今後どうするか

ここまで読んでみての方針を決めていきます。
基本的には Argon2id でハッシュ化したものを保存し、Pepper も設定していこうと思います。
自分で実装することはできないので、ライブラリ探しからします。

今後の展望

実装としては Argon2id によるハッシュ化をし、それを保存していくつもりです。
ただ、ライブラリの是非を今後判断できるようにするためArgon2 の仕様についても並行して勉強していきます。
なので、今後は Argon2 の RFC を読んだことについての記事を挙げていこうかなと思います。
全部理解してから投稿するのは時間がかかりすぎるので、都度呟きみたいなものをアップしていこうと思います。

おわりに

今回はパスワードの保存について OWASP のチートシートを確認しました。
ハッシュ関数=SHA-256 としか考えていなかった自分の甘さと危険さを存分に感じました。
また、パスワードの保存方法やハッシュのお作法について知ることもできました。
今後はこういったものを参考にしつつ実装を進め、仕様についても都度確認していこうと思います。
ここまで読んでいただきありがとうございました。

Discussion