最近、情報漏洩や不正アクセスのニュースで「ハッシュ化されたパスワードが漏洩した可能性がある」といった表現を見ることがあります。
そこで気になったのが、ハッシュは不可逆なのに、ハッシュ値が漏れるとなぜ危ないのか? ということでした。
よく「パスワードはハッシュ化して保存する」と説明されますが、適切なパスワード保存では、単純にハッシュ関数へ通して保存するわけではありません。現在のパスワード保存では、salt と、意図的に計算を重くしたパスワード用ハッシュが重要になります。1
ハッシュは逆算できない。でも答え合わせはできる
ハッシュ値から元のパスワードを「復号」することはできません。しかし、ハッシュ値とsaltを入手した攻撃者は、候補となるパスワードを同じ方法でハッシュ化し、一致するかを確認できます。
候補のパスワード + salt
↓
同じ方式でハッシュ化
↓
保存されていたhashと一致する?
DBを入手して手元で解析するオフライン攻撃なら、ログイン画面にあるアカウントロックやレート制限も関係ありません。NISTでも、パスワード保存時にはこうしたオフライン攻撃を想定した対策が求められています。2
現在推奨されるパスワード保存では、単純な
SHA-256(password)
のような方式ではなく、Argon2id、bcrypt、PBKDF2などのパスワード保存向けの方式を使います。OWASPでは、新規システムではArgon2idが第一候補として推奨されています。1
なお、攻撃者がハッシュ値だけを見て必ず保存方式を特定できるとは限りません。bcryptやArgon2idのように保存値自体にアルゴリズムやパラメータが含まれる方式もありますが、単なる16進文字列だけでは断定できない場合もあります。その場合は、保存形式やsalt用カラム、使用しているフレームワークなどから方式を推測することもあるようです。
saltは何をしているのか
パスワード保存では、ユーザーごとに異なるランダムな salt を生成し、パスワードと組み合わせて計算します。
Aさん
Password123 + salt_A
→ hash_A
Bさん
Password123 + salt_B
→ hash_B
AさんとBさんが同じ Password123 を使っていても、saltが異なるので保存されるハッシュ値は別になります。salt自体は秘密にする必要はなく、通常はハッシュ値と一緒に保存されます。1
saltがない場合、たとえば攻撃者が一度
MD5("Password123") = abc...
を計算すれば、漏洩した大量のユーザーデータから abc... を探せます。同じパスワードを使っているユーザーは同じハッシュ値になるため、1回の計算結果を複数ユーザーに使い回せてしまうわけです。
saltがあれば Password123 を試すだけでも、
Password123 + Aさんのsalt
Password123 + Bさんのsalt
Password123 + Cさんのsalt
...
とユーザーごとに計算し直す必要があります。また、よく使われそうなパスワードとハッシュ値をあらかじめ大量に計算しておく、レインボーテーブルのような事前計算結果もそのまま使い回せなくなります。1
ただし、saltは1回の計算そのものを重くする仕組みではありません。そこで、パスワード用ハッシュでは別の対策も行います。
なぜわざと計算を遅くするのか
MD5やSHA-256は高速に計算できるよう設計されています。通常なら高速なのは長所ですが、パスワード保存では攻撃者もその速さを利用できます。
たとえば英大文字・小文字・数字の62種類から12文字を完全ランダムに選んだ場合、候補数は
62^12
になります。
これだけを見ると膨大ですが、実際の攻撃者はこの全パターンを最初から律儀に試すとは限りません。まずは次のような、人間が使いそうなパスワード候補を優先して試せます。
- 過去に漏洩したパスワード
- 辞書にある単語
- 名前やユーザー名
- 生年月日や西暦
- 末尾の
123 - 先頭だけ大文字
- 末尾に
! -
a→@のようなありがちな置換
たとえば、
fK7sP2QaX9Lm
と
Tanaka2026!!
はどちらも12文字ですが、攻撃者から見た難しさはまったく異なります。後者は「名字 + 西暦 + 記号」のような規則を使えば、62^12 通りを探索するよりずっと早い段階で候補に入る可能性があります。
MD5やSHA-256では1候補あたりの計算が非常に軽いため、こうした有望そうな候補を何億、何兆と試すコストが安いことが問題になります。
そこでbcryptやArgon2idでは、あえて1回の計算を重くします。正規ユーザーはログイン時に1回計算すればよい一方、攻撃者が1億個の候補を試すなら、その重い処理を1億回実行しなければなりません。
Argon2idはさらに、CPU時間だけでなく大量のメモリも要求できるようになっています。GPUで大量の候補を並列処理しようとした場合にも、メモリ容量や帯域がボトルネックになります。1
役割を整理すると、次のようになります。
- salt:計算結果を別ユーザーに使い回せなくする
- bcrypt / Argon2idなど:パスワード候補1個の答え合わせ自体を高価にする
pepperという追加策もある
saltはハッシュ値と一緒に保存してよいため、DB全体が漏洩すればsaltも攻撃者に渡ります。そのため追加の防御として、DBとは別の場所に秘密値 pepper を保持する方法もあります。1
pepperはsaltとは異なり秘密にする必要があり、Secrets ManagerやHSMなど、パスワードDBとは分離した場所に保存します。DBだけが漏洩した場合、攻撃者にはパスワード候補を正しく照合するための情報が足りないため、防御を一段増やせます。
必須の仕組みというよりは、多層防御(defense in depth)として追加できる対策という位置づけです。1
昔の方式と今の方式でどれくらい違う?
実際のHashcatの公開ベンチマーク34をもとに、英大文字・小文字・数字の62種類から完全ランダムに生成されたパスワードを総当たりした場合の時間を概算してみます。
攻撃環境は比較しやすいように RTX 4090を16枚 使用する想定としました。表の時間は全パターンを試し切る時間ではなく、総当たりで平均的に正解が見つかるまでの時間として、公開ベンチマーク値から筆者が概算しています。
| 方式 | 8文字 | 10文字 | 12文字 |
|---|---|---|---|
| MD5そのまま | 約42秒 | 約1.8日 | 約20年 |
| SHA-256そのまま | 約5分 | 約14日 | 約145年 |
| md5crypt | 約1.3日 | 約13年 | 約5.1万年 |
| bcrypt cost 10 | 約38年 | 約14.5万年 | 約5.6億年 |
| Argon2id(64MiB, t=3, p=1) | 約127年 | 約49万年 | 約19億年 |
RTX 4090 1枚のHashcatベンチマークでは、MD5は約1640億回/秒、SHA-256は約220億回/秒、md5cryptでも約6100万回/秒の試行が可能です。3
一方、Argon2idを m=64MiB, t=3, p=1 とした公開ベンチマークでは約1,700回/秒まで低下します。4
md5cryptはsaltと反復処理を持ち、Unix系で実際に使われてきたパスワード保存方式です。それでも現在のGPUでは高速に試行できるため、計算機性能の向上に合わせて、より高コストな方式へ移行してきたことが分かります。
なお、OWASPが現在示しているArgon2idの最低推奨設定の一つは、
m = 19 MiB
t = 2
p = 1
です。1
それぞれ、
-
m:1回の計算で使用するメモリ量 -
t:計算の繰り返し回数 -
p:並列度
を表します。
この記事で比較に使っている m=64MiB, t=3, p=1 はOWASPの最低推奨値そのものではなく、公開されているGPUベンチマークの条件に合わせたものです。
また、攻撃者の計算コストを高くするということは、認証サーバー側も同じ計算コストを負担するということでもあります。ログイン1回ごとにArgon2idの計算が実行されるため、m や t を大きくするほどCPU・メモリ負荷やレスポンス時間も増えます。
そのため、Argon2idのパラメータは高ければ高いほどよいわけではありません。実際のログイン頻度やサーバーの性能とのバランスを見ながら決める必要があります。1
表は公開ベンチマークをもとにした概算です。GPUを16枚に増やした場合に処理性能がほぼ比例して増えると仮定しているため、実際の攻撃環境や実装では結果が変わります。
完全総当たりの時間だけでは実態は分からない
上の表を見て、最初は正直、
12文字ならMD5でも20年かかるし、そこまで問題なのか?
と思いました。
しかし、これは完全ランダムな12文字を前提にした数字です。
攻撃者が1億個の「人間が使いそうな候補」を用意できるのであれば、探索対象は 62^12 通りではなく、その1億候補だけです。MD5やSHA-256のような高速な方式では、このような候補を大量に試すこと自体が安くなります。
一方、bcryptやArgon2idでは同じ1億候補でも、候補1個ごとに高い計算コストを支払わなければなりません。
つまりパスワード保存方式の目的は、完全総当たりを絶対に不可能にすることというより、どのような候補を試す場合でも、1回1回の答え合わせを高価にすることと考えると分かりやすそうです。
AI時代だと個人情報からパスワードを当てられる?
今ならAIに、
名前
生年月日
ユーザー名
趣味
過去に漏れたパスワード
などを与えて、「この人が使いそうなパスワード」を生成させることも考えられます。
実際に、個人情報(PII)や過去のパスワードを使って対象者向けのパスワード候補を生成し、推測効率を上げる研究も行われています。5
ただし、AIがハッシュを逆算できるようになったわけではありません。AIが改善するのは「どの候補から試せば当たりそうか」という部分であり、最終的にはその候補をbcryptやArgon2idに通して、保存されたハッシュ値と照合する必要があります。
逆にパスワードマネージャーで、
qR7!zm2@Pk9#vL4$
のような完全ランダムな文字列をサービスごとに生成しているのであれば、名前や生年月日をどれだけ分析されても、パスワードとの関係はありません。
AI時代だから突然パスワードハッシュが破られるようになったというより、人間が予測可能なパスワードを作るという従来からの弱点を、より効率的に突ける可能性が出てきたと考える方が近そうです。
まとめ
パスワードハッシュは「漏れたら絶対に解析できない」仕組みではありません。DBが漏洩することまで想定し、
- saltで計算結果の使い回しを防ぐ
- Argon2idなどで1回の推測を高価にする
- 必要に応じてpepperを追加する
- パスワード自体も長く予測困難にする
ことで、「答え合わせ自体はできるが、現実的な時間と計算資源では割に合わない」状態を作るのが目的です。
「ハッシュ化して保存しているから安全」だけではなく、どの方式を、どのパラメータで使っているのかまでが重要という話でした。
そして、ここまで調べてみて、結局のところ一番効くのは十分にランダムで、サービスごとに使い回さないパスワードを使うことなんだなと改めて思いました。saltやArgon2idは「漏れたときにどれだけ時間を稼げるか」を底上げする仕組みであって、パスワード自体が推測しやすければその効果は薄れます。
利用者側でできる対策としては、パスワードマネージャーなどで十分にランダムなパスワードを生成し、サービスごとに使い分けることが重要です。そうすれば、ハッシュが漏洩した際のオフライン推測に強くなり、仮に1サービスのパスワードが漏れても他サービスへの波及を防げます。