パスワードはどう保存されるのか:平文からArgon2まで

サービスはパスワードをそのまま保存しない。平文→ハッシュ→ソルト→遅いKDFへと続いた30年の歩みと、2012年のLinkedIn流出が残した教訓を整理する。

everytooleverywhereallatonce · 公開

ログイン時にサービスが確かめる必要があるのは、「この人が知っているパスワードが登録時に決めたものと同じか」だけです。ところがそれを確かめるためにパスワードをそのままデータベースに書いておくと、そのデータベースが流出した瞬間に全アカウントが開いてしまいます。だから30年をかけて「元を保存せずに正しさを確かめる」方法が発達してきました。その階段を一段ずつ上がってみます。

0段:平文——なぜだめか

パスワードをそのまま保存すると2つが崩れます。第一に、内部者や侵入者がデータベースを見れば即座にすべて分かります。第二に、人は複数のサイトで同じパスワードを使い回すため、1か所が破られるとその一覧で銀行やメールにも試すクレデンシャルスタッフィング攻撃につながります。どのサービスも利用者の元のパスワードを知る「必要」はない、というのが出発点です。

1段:ハッシュ——元を保存しない

ハッシュ関数は入力を固定長の値に変え、その値から元を戻せません。だから登録時にハッシュだけを保存し、ログイン時に入力を同じ方式でハッシュして比べれば、元を持たずに確認できます。初期のUnixから使われてきた方法です。

問題は「速い」ハッシュを使った点です。MD5やSHA-1はファイル検証のためにできるだけ速く設計されています。攻撃者が流出したハッシュ一覧を手にすると、ありふれたパスワード候補を毎秒数十億件ハッシュして照合できます。当サイトのハッシュ逆算ツールで短い原文が一瞬で出るのを見ると、この方式の限界が実感できます。さらに同じパスワードは常に同じハッシュになるので、事前計算した対応表(レインボーテーブル)で一括照会もできました。

2段:ソルト——事前計算を封じる

ソルトはパスワードごとに付ける異なる乱数です。ハッシュ前にソルトを混ぜると、同じパスワードの2人でも保存値が変わり、レインボーテーブルが無効になります。特定のソルト向けの表を作り直す必要があり、利用者ごとにソルトが違えば1つの表で多数のアカウントを破れません。

ソルトは秘密ではなく、通常ハッシュと一緒に保存されます。目的は「一度の計算で複数アカウントを同時に破る」ことを防ぐ点にあり、隠すことではありません。この性質はハッシュ生成器で、同じテキストでも前に別の値を付けると結果が全く変わることから確認できます。

2012年LinkedIn:ソルトがないとどうなるか

2012年6月、LinkedInの約650万人分のパスワードハッシュがオンラインに現れました。ソルトなしのSHA-1でした。ソルトがないため公開のレインボーテーブルと辞書攻撃がそのまま通用し、数十万件が数日で破られました。2016年には同じ流出由来とされる1億1,700万件超のアカウント情報が再び売りに出されました。LinkedInはこの件の後にソルトと遅いハッシュへ移行し、集団訴訟で和解金を払いました。「ハッシュはしたがソルトはしていない」保存がなぜ不十分かを示す代表例です。

3段:遅いKDF——速いことが問題だった

ソルトを付けても根本の問題が残ります。ハッシュ自体が速すぎることです。そこで「わざと遅い」パスワード専用の関数(鍵導出関数、KDF)が登場しました。

  • bcrypt(1999):Niels ProvosとDavid Mazièresが発表。コスト係数を持ち、1上げるごとに計算が2倍遅くなります。ハードウェアが速くなればコストを上げて追随できるので「将来適応型」と呼ばれます。実際の動きはbcryptハッシュ生成でコストを変えて確かめられます。
  • scrypt(2009):Colin Percivalが設計し、後にRFC 7914として整理。計算だけでなく多くのメモリを使うよう作られ、メモリの少ない専用ハードウェア(ASIC・GPU)での大量並列攻撃を難しくします。
  • Argon2(2015):公開のPassword Hashing Competitionで優勝。メモリ・時間・並列度を個別に調整でき、サイドチャネルに強いArgon2id変種が今の新規システムの推奨既定値です。

要は非対称性です。一般利用者のログインは0.25秒ほどで十分ですが、攻撃者はその0.25秒を流出ハッシュ数億件に掛ける必要があり、事実上コストを賄えなくなります。

現在の基準:NISTとOWASP

NISTのデジタルID指針(SP 800-63B、2025年改訂)は、保存パスワードはソルトを付け適切なパスワードハッシュ方式でハッシュすること、ソルトは最低32ビット、コスト係数は性能が許す限り高くとしています。さらに検証側だけが知る秘密鍵でもう一段処理すること(ペッパー)を勧めます。OWASPのパスワード保存チートシートは具体値を示します。Argon2idは最低メモリ19MiB・反復2・並列1、難しければscrypt(コスト2^17)、レガシーならbcryptコスト10以上です。

利用者が選ぶパスワード側の勧告も変わりました。文字種を強制する複雑さの規則より長さを重視し、定期的な強制変更を求めないことです。なぜ長さが重要かはパスワード生成器で、エントロピー(ビット)が長さとともに増える様子を見ると分かりやすいです。

遅くすることがなぜそれほど効くのか——数字で見る

コストの非対称を具体的な数字で描いてみます。速いハッシュなら、ありふれたグラフィックカード1枚でもSHA-1で毎秒数十億件を試せます。辞書にある一般的なパスワードや短い組み合わせは事実上即座に破れます。一方、ログイン検証が0.25秒かかるよう調整した遅いKDFなら、同じカードは毎秒4件しか試せません。候補1件を試す時間が10億倍近く伸びた計算です。

正常な利用者に0.25秒はほとんど感じられません。しかし攻撃者は流出ハッシュ1件ごとにその0.25秒を掛けなければなりません。アカウント100万件に対し利用者あたり候補1,000件だけ試しても、100万 × 1,000 × 0.25秒 ≈ 約8年かかります。同じ作業を速いハッシュでやれば数秒で終わります。遅いKDFの目的が「不可能にすること」ではなく「賄えないほど高価にすること」だと示す計算です。だからコスト係数をハードウェア性能に合わせて上げ続けることが重要です。

ソルト、ペッパー、そしてよくある混同

よく混ざる3つの概念があります。ソルトはパスワードごとに異なり、ハッシュの隣に保存され、事前計算と複数アカウントへの同時攻撃を無効にする役目です。ペッパーはすべてのパスワードが共有する1つの秘密値ですが、データベースの外(アプリ設定やハードウェアセキュリティモジュール)に置きます。最も多いデータベースのみの流出で、攻撃者に必要なものを全部渡さないためです。両方が流出すればペッパーは意味がありません。だからデータベースのダンプが届かない場所に置く必要があります。

もう一つは「ハッシュ」と「暗号化」の混同です。暗号化は設計上戻せます。鍵があれば平文を復元できます。ハッシュは一方向で、この戻せなさこそパスワードでは核心です。サービスは利用者の以前のパスワードをメールで送れてはならず、リセットだけができるべきです。あるサイトが現在のパスワードをそのまま表示できるなら、戻せる形で保存したということで、それが0段で述べた危険信号です。

パスワードのリセットなしで古いハッシュを上げる

遅いKDFへ変えるには全員にパスワードのリセットを強制しなければならないと心配しがちですが、そうではありません。標準的な移行は既に保存されたハッシュを新しい関数で再ハッシュすることです。たとえばソルトなしの古いSHA-1値を新しいソルトとコストで bcrypt(sha1_hash) の形で包みます。保存された全レコードが即座に強くなり、利用者が次にログインしたときに実際のパスワードを現代方式で静かに再ハッシュし、包みを外せます。bcryptのコストを後で上げるのも同じで、ログイン時に再ハッシュして置き換えます。コスト・ソルト・アルゴリズムといった設定が保存文字列の中に一緒に入っているため、検証側は各レコードがどう作られたかを常に把握でき、移行中も正しく比較できます。

まとめ

パスワード保存の歴史は「元を持たない→事前計算を封じる→計算を高価にする」に要約できます。各段は前段が残した穴を埋め、2012年のLinkedInのように一段を飛ばしたサービスが代償を払いました。今何かを作るなら答えは明確です。Argon2id(状況によりbcrypt・scrypt)に利用者ごとのソルトを付け、利用者には長くランダムなパスワードを使ってもらうことです。

出典