パスワードのハッシュ化と暗号化の違いは、事故が起きたときに初めて差が出ます。暗号化は鍵で元に戻せる処理なので、鍵と復号の仕組みを盗まれると、保存していた全件が平文に戻ります。ハッシュ化は戻す手段を持たない処理で、攻撃者には推測を1件ずつ試す道しか残りません。

連載「事故から逆算するWebセキュリティ」の第1回は、2026年10月のソースネクストの情報漏洩を入口に、パスワードを何で保存するかを整理します。

STORE, DON'T DECRYPT保存のしかた

鍵で戻せる保存は、鍵と一緒に全件が戻る

暗号化して保存

AES などの可逆な方式

  • 鍵と復号の仕組みが盗まれると全件が平文に
  • 実測: 10万件の復号に0.07秒
  • サーバー側がパスワードを知れてしまう

遅いハッシュ+ソルトで保存

Argon2id / scrypt / bcrypt / PBKDF2

  • 元に戻す仕組みがそもそも無い
  • 推測を1件ずつ試すしかない
  • 1回の計算を重くして試行を遅らせる
実測: Python 3.13・1コア(2026-10-17、当サイト)。方式の推奨は OWASP Password Storage Cheat Sheet

30秒でわかる(2026年10月17日時点)

  • ・暗号化:鍵があれば戻る。当サイトの実測で、AES-GCMの10万件は0.07秒で全件復号できた
  • ・ハッシュ化:照合は入力を同じ条件で計算して比べるだけ。元に戻す必要がない
  • ・それでも足りない:SHA-256は1コアで1秒に約210万回計算できる。ソルトとコストを持つArgon2idは約39回
  • ・対策:Argon2id(19MiB・2回・並列1)を第一に。暗号化で保存しているなら全件を復号してハッシュ化し、鍵を消す

事案の経緯は ソースネクスト情報漏洩の記事 に、インシデントの読み方は サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説 にまとめています。

ソースネクストの説明とPCI DSSの読み方のタイムライン

日付 出来事
2026-10-15 ソースネクストが会員情報の流出の可能性(最大22,519,960件)を公表。項目にパスワードを含む
10-15以降 報道で、同社の説明として「パスワードは暗号化して保存していたが、暗号化を解除するプログラムもハッキングされた」と伝えられる
10-15以降 X上で、PCI DSS 3.2.1 要件8.2.1の文言が「暗号化」を求めているように読める点が議論になる

この説明の文言は、当サイトが検索結果に載った報道の本文で確認したもので、同社の発表本文では確認できていません。発表本文は保存形式を説明していないとされます。ここでは「暗号化して保存し、復号の仕組みごと取られた」という説明を前提に、保存方式の側から考えます。

X上の議論では、PCI DSS 3.2.1 の要件8.2.1「強力な暗号化を使用して、すべてのシステムコンポーネントで、送信と保存中に認証情報(パスワードなど)をすべて読み取り不能にする」(当サイト訳)を字面どおり読むと「パスワードを暗号化せよ」に見え、可逆な暗号化を選ぶ理由になったのではないか、という見方が出ていました。

規格の側を確かめると、PCI DSS v4.0 では同じ趣旨が要件8.3.2に移り、対象は「認証要素」に広がっています。PCI SSC の FAQ 1253(2025年7月)は、強力な暗号を使ってパスワードをハッシュ化すれば8.3.2の意図を満たすと答えています。規格が求めるのは「読み取り不能」であって、戻せる暗号化ではありません。

・要件の文言は、規格の対応表を載せた複数のサイトの本文で確認した
・FAQ 1253 の回答は、PCI SSC のページが開けず検索結果の本文で確認した
・v4.0.1 の8.3.2のガイダンスがソルトに触れているかは未検証

なお v4.0 で加わった「鍵付きハッシュ」の要件は、カード番号(PAN)を保存するときのもので、パスワードの要件ではありません(PCI SSC の FAQ を検索結果で確認)。

パスワードのハッシュ化と暗号化の違い

X上では、初心者向けにこんな説明が広く読まれていました。暗号化は鍵で元に戻せるが、ハッシュ化は戻せない。password123 をハッシュ化すると全く違う文字列になる。戻せないのにログインをどう判定するのかといえば、入力されたパスワードを登録時と同じ方法でハッシュ化し、保存値と照合すればよい。

ここまでは正しい出発点です。ただ「ハッシュ化したから安全」で止まると、事故は防げません。足りないのは2点です。

・ソルト:パスワードごとに違う乱数を混ぜる。ないと同じパスワードは同じ値になり、事前計算の表(レインボーテーブル)で一度に引かれる。使い回している会員も値の一致で一度に分かる
・コスト:1回の計算をわざと重くする。SHA-256 のような速いハッシュは、総当たりや辞書攻撃で1秒に何百万回も試せる

HOW LOGIN CHECKS照合のしかた

元に戻さず、同じ計算をして比べる

  1. 入力を受け取る

    ログイン画面から

  2. 保存値を読む

    方式・コスト・ソルト入り

  3. 同じ条件で計算

    登録時と同じ設定

  4. 一定時間で比べる

    verify 関数に任せる

  5. 一致ならログイン

    古いコストは作り直す

保存値の例: $argon2id$v=19$m=19456,t=2,p=1$<ソルト>$<ハッシュ>(argon2-cffi 25.1.0 の出力)

「登録時と同じ方法で」の中身も、実装では注意が要ります。Argon2id や bcrypt の保存値には、方式・コスト・ソルト・ハッシュがひと続きの文字列で入っています。照合では、この文字列と入力をライブラリの verify 関数に渡すだけで、ソルトを自分で取り出して計算し == で比べる実装は書きません。比べ終わるまでの時間が一致した位置で変わると、手がかりを与えるからです。argon2-cffi が使う libargon2 の比較関数は、全バイトを最後まで比べる一定時間の実装でした(ソースで確認)。

方式 鍵やDBを盗まれたとき 同じパスワードが分かるか 1回の計算
平文 そのまま読める 分かる 不要
暗号化(AES など) 鍵と復号の仕組みがあれば全件が戻る 分かる場合がある ごく軽い
速いハッシュ(SHA-256 単発) 推測を高速に試せる ソルトなしなら分かる ごく軽い
遅いハッシュ+ソルト 1件ずつ重い計算で試すしかない 分からない 数十ミリ秒〜
上に加えてペッパー DBだけでは推測も試せない 分からない 同上+鍵付き処理

速いハッシュでは足りない理由を実測する

1コアの Python で、パスワード1件のハッシュ計算が1秒に何回できるかを測りました。2026年10月17日、Xeon 2.1GHz・Python 3.13.16・argon2-cffi 25.1.0・bcrypt 5.0.0 です。実際に実行したコードは次のとおりです(pip install argon2-cffi==25.1.0 bcrypt==5.0.0 の後に python3 rate.py)。

import hashlib, time, bcrypt
from argon2 import PasswordHasher

def rate(f, n):
    t = time.perf_counter()
    for _ in range(n):
        f()
    return n / (time.perf_counter() - t)

pw = b"password123"
ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
print("SHA-256      :", round(rate(lambda: hashlib.sha256(pw).digest(), 1_000_000)), "回/秒")
print("Argon2id     :", round(rate(lambda: ph.hash(pw), 20), 1), "回/秒")
print("bcrypt(10)   :", round(rate(lambda: bcrypt.hashpw(pw, bcrypt.gensalt(10)), 20), 1), "回/秒")
print(hashlib.sha256(pw).hexdigest() == hashlib.sha256(pw).hexdigest())  # True: 同じ値
print(ph.hash(pw) == ph.hash(pw))                                        # False: ソルトで毎回違う

MEASURED実測

SHA-256 は 1 秒に約210万回試せる。Argon2id は約39回

210万回/秒

SHA-256(ソルトなし)

速いハッシュ。総当たりに向いてしまう

39回/秒

Argon2id m=19MiB t=2 p=1

OWASP の最小推奨値。約5万分の1

0.07秒

AES-GCM 10万件の復号

鍵があれば暗号化は一瞬で戻る

実測: Python 3.13.16・Xeon 2.1GHz の1コア、2026-10-17(当サイト)。攻撃者の GPU 環境の速度ではなく、比の目安
方式(パラメータ) 1秒あたりの計算回数(2〜3回の実行)
SHA-256(ソルトなし) 約202万〜219万回
Argon2id(m=19MiB, t=2, p=1) 37.2〜39.4回
bcrypt(cost=10) 12.7〜13.1回
bcrypt(cost=12) 3.2〜3.3回
PBKDF2-HMAC-SHA256(60万回) 5.5〜5.9回
scrypt(N=2^17, r=8, p=1) 1.9〜2.1回

SHA-256 と Argon2id の差は約5万倍でした。これは1コアの CPU の数字で、GPU を並べる攻撃者の速度ではありません。それでも、同じ機械で試せる推測の数が5万分の1になる、という比は変わりません。陽性対照として、SHA-256 は同じ入力で毎回同じ値、Argon2id はソルトで毎回違う値になることも確かめています。

暗号化の側も測りました。AES-256-GCM で10万件を暗号化しておき、同じ鍵で全件を復号すると0.07秒でした。暗号の強さは関係なく、鍵と復号の手順が手に入れば、この速さで全件が戻ります。

パスワードのハッシュ化の推奨値:OWASPとNIST

OWASP Password Storage Cheat Sheet(2026年10月17日時点の公式リポジトリ)は、「ほぼすべての場合、パスワードは暗号化でも平文でもなく、現代的な適応型ハッシュでハッシュ化する」としています。暗号化を使うのは、他のシステムへのログインに元のパスワードが要るような例外だけで、その場合も OpenID Connect などで保存しない設計を先に探すよう書いています。

方式 OWASP の推奨(最小) 位置づけ
Argon2id m=19MiB・t=2・p=1(ほか46MiB・t=1など同等の5通り) 第一候補
scrypt N=2^17・r=8・p=1(ほか4通り) Argon2id が使えないとき
bcrypt cost 10以上、入力は72バイトまで 既存システム向け
PBKDF2 HMAC-SHA256で60万回(SHA512は22万回) FIPS-140 が必要なとき

NIST SP 800-63B-4(2025年8月)の3.1.1.2は、パスワードを「オフライン攻撃に耐える形」で保存することを求めています。公式の原稿リポジトリで確認した要点は次の3つです。

・ソルトを付け、パスワードハッシュ方式でハッシュ化する(SHALL)。ソルトは32ビット以上
・コストは性能を損なわない範囲でできるだけ高くし、計算機の進歩に合わせて上げる(SHOULD)
・検証側だけが知る秘密鍵で鍵付きハッシュか暗号化をもう1段かける(SHOULD)。鍵はハッシュと別に、HSM など保護された領域に置く

3つ目がペッパーです。OWASP も、ペッパーはハッシュと同じ場所に置かず、シークレット保管庫か HSM に置くとしています。DB だけが漏れた場合は、推測を試すことすらできなくなります。ペッパーを替えるには全員のパスワード再設定が要る点も同じく書かれています。鍵をどこに置けば守れるかは、連載の次回で扱います。

対策:照合の実装と、暗号化からの移行

照合と作り直しは、ライブラリの関数に任せます。次のコードは実行して出力を確かめたものです(argon2-cffi 25.1.0)。

from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError

ph = PasswordHasher(time_cost=2, memory_cost=19456, parallelism=1)
stored = ph.hash("password123")          # 登録時:この文字列だけを保存する
print(stored)

try:
    ph.verify(stored, "password124")     # ログイン時:保存値と入力を渡すだけ
except VerifyMismatchError:
    print("不一致")
print(ph.verify(stored, "password123"))

new_ph = PasswordHasher(time_cost=3, memory_cost=19456, parallelism=1)
print(new_ph.check_needs_rehash(stored)) # コストを上げたら True → 次のログインで作り直す

出力は $argon2id$v=19$m=19456,t=2,p=1$<ソルト>$<ハッシュ>・不一致・True・True でした。コストを上げたら、ログインに成功したときに check_needs_rehash を見て、その場でハッシュを作り直して保存します。

今のシステムの保存方式ごとに、移行の手順は変わります。

今の保存方式 移行の手順
可逆な暗号化 全件を復号して、その場で Argon2id にする(ログインを待たなくてよい)。終わったら暗号文と鍵を、バックアップも含めて消す
速いハッシュ(MD5・SHA-1・SHA-256 単発) 既存の値を Argon2id で包み、次のログインで入力から直接ハッシュし直す。長く使われていない会員は期限切れにして再設定を求める
bcrypt・PBKDF2 の低いコスト 保存値のコストを見て、ログイン成功時に新しいコストで作り直す
すでに漏えいした可能性がある 上に加えて、全員にパスワードの再設定を求める

漏えいの告知を受けた利用者の側の備え(使い回しの洗い出し)は、メルカリ情報漏洩の主張の記事 にまとめています。

包む方式(OWASP の言う Layering)は、元のパスワードを知らずに全件を一度に強くできますが、二重のハッシュは単体より割れやすくなることがあるため、次のログインで置き換えるまでのつなぎです。暗号化からの一括変換は、平文が一時的にメモリに載るので、ログを残さない隔離した処理で行うのが無難です(当サイトの判断)。

影響を受けるシステムの見分け方

保存値の形で、影響範囲をおおよそ見分けられます。

保存値の形 方式 判断
$argon2id$v=19$m=… Argon2id パラメータが推奨値以上か見る
$2b$10$…($2a$・$2y$) bcrypt cost が10以上か見る
pbkdf2_sha256$600000$… PBKDF2(Django 形式) 回数が60万以上か見る
32・40・64桁の16進数だけ MD5・SHA-1・SHA-256 の可能性 ソルトの有無を確認し、包んで移行
長さがばらつく Base64 で、復号処理がコードにある 可逆な暗号化 一括変換して鍵を消す

コード側では、保存処理の周りに encrypt・decrypt・AES・Cipher があるか、パスワードを画面やメールで本人に返す機能があるかを見ます。パスワードを「教えられる」システムは、元に戻せる形で持っています。

参照ソース