MD5 と SHA-1 が破られた理由 — そして「破られた」の本当の意味
MD5 は 1991 年に Ron Rivest によって設計されました。SHA-1 は 1995 年に NSA によって設計されました。どちらも汎用の暗号学的ハッシュ関数であり、固定長のダイジェストを生成し、どちらも長年「破られた」と見なされてきました。しかし「破られた」という言葉は二つの大きく異なる攻撃を指すため、定義が必要です。
衝突攻撃(collision attack)とは、攻撃者が同じハッシュを生成する二つの異なる入力 A と B を見つけられることを意味します。第二原像攻撃(second-preimage attack)とは、固定された入力 A が与えられたとき、同じ値にハッシュされる別の B を攻撃者が見つけられることを意味します。後者ははるかに困難です。MD5 は 2004 年以降、衝突が破られており、今日ではノート PC で数分あれば実用的な衝突を作れます。SHA-1 の最初の実用的な衝突は 2017 年 2 月に Google の「SHAttered」攻撃で実証されました — 同一の SHA-1 ハッシュを持つ二つの異なる PDF ファイルを作成したもので、コストは約 6,500 CPU 年と AWS 上の GPU 計算で約 11 万ドルでしたが、2020 年の「Shambles」論文以降はおよそ 4.5 万ドルにまで下がっています。MD5 にも SHA-1 にも実用的な第二原像攻撃はまだ存在しませんが、その差は年々縮まっており、「攻撃者に固定ターゲットを与えることはできない」というのは依存するには脆弱な性質です。
実務への影響 — いかなるセキュリティ目的にも MD5 や SHA-1 の使用をやめてください。現実の事例は理論上のものではありません。Flame マルウェア(2012 年、国家主体によるものと推定)は、Microsoft Terminal Services の証明書署名チェーンにおける MD5 衝突を悪用し、有効な Windows コード署名証明書を偽造しました。Git は 2.13 の移行計画までコンテンツアドレッシングに SHA-1 を使用しており、SHAttered のデモはまさに、二つの異なるコミットが同じハッシュを共有し得ることを示すために構成されていました — Git は 2017 年に衝突検出を導入し、SHA-256 への移行を進めています。CA/Browser Forum は 2017 年に TLS 証明書から SHA-1 を強制的に排除しました。PGP、S/MIME、そしてほとんどのコード署名もこれに続いています。
では MD5 と SHA-1 はどこまで許容されるのでしょうか。非敵対的な完全性チェックです。ランダムな破損を検出するためにハッシュを計算する場合(CDN 上のファイルとオリジンの比較、ダウンロードした成果物と公開されたチェックサムの比較)で、攻撃者が衝突を偽造するインセンティブが存在しなければ、MD5 と SHA-1 は依然として機能します。また、衝突耐性がセキュリティモデルではないデータ構造内部のフィンガープリントとしても適切です — 2026 年においても Git の SHA-1 がほぼ生き残っているのは、Git の脅威モデルが生成者を信頼できるものと想定し、コンテンツアドレッシングを利便性として扱っているためです。
現代の推奨 — 新しいシステムには SHA-256 または SHA-3-256 を、スループットが必要な場合は高速でモダンな代替として BLAKE3 を選んでください。SHA-256 は 2001 年以来、意味のある弱体化もなく持ちこたえており、暗号コミュニティはこの先さらに 20 年以上の信頼を置くことに抵抗がありません。SHA-3(Keccak、2015 年標準化)は SHA-2 と構造的に異なるため、たとえ SHA-2 に欠陥が見つかっても、SHA-3 が同じ欠陥を共有する可能性は低いです。BLAKE3 はソフトウェア実装で SHA-256 の約 5 倍高速で、同等のセキュリティレベルを持ち、非常に大きなファイルに最適です。
// MD5 collision in seconds (e.g., HashClash, fastcoll)
fastcoll input.bin a.bin b.bin
md5sum a.bin b.bin
# 008ee33a9d58b51cfeb425b0959121c9 a.bin
# 008ee33a9d58b51cfeb425b0959121c9 b.bin <-- different files, same MD5
// Modern hashing in Node 20+
import { createHash } from 'node:crypto';
createHash('sha256').update(buf).digest('hex'); // recommended
createHash('sha3-256').update(buf).digest('hex'); // SHA-3 (Keccak)
createHash('blake2b512').update(buf).digest('hex'); // BLAKE2
// BLAKE3 — install @noble/hashes for portability
import { blake3 } from '@noble/hashes/blake3';
blake3(buf, { dkLen: 32 }); // 32-byte digest
// Decision:
// - Adversary involved (signatures, content auth)? → SHA-256 / SHA-3 / BLAKE3
// - Just detecting random corruption (CRC-style)? → MD5/SHA-1 still OK
// - Speed matters on huge files (TB-scale)? → BLAKE3Argon2id vs scrypt vs bcrypt — 現代のパスワードハッシュ
パスワードに素の SHA-256 を使うのは誤りです。SHA-256 は高速になるよう設計されています — GPU では毎秒数十億ハッシュを計算できます。パスワードハッシュの要点は、盗まれたデータベースに対するブルートフォースが無意味になるほど十分に遅いことにあります。2000 年代以降、三つのファミリーが主流であり、OWASP の 2026 年版 password storage cheat sheet は次の順で推奨しています。
Argon2id(2015 年 Password Hashing Competition 優勝、2021 年に RFC 9106 で標準化)。ノブは三つ — メモリコスト(m、KiB 単位)、時間コスト(t、反復回数)、並列度(p、スレッド数)。「id」変種はデータ依存パスとデータ非依存パスを組み合わせ、サイドチャネル攻撃と時間・メモリのトレードオフ攻撃の両方に耐性を持ちます。OWASP の 2026 年ベースライン — m=19456(19 MiB)、t=2、p=1、または m=12288、t=3、p=1、または m=7168、t=5、p=1 のうち、ピーク時のログイン負荷でサーバーが維持できるものを選択してください。新しいシステムには Argon2id が文句なしの第一選択です。主要ライブラリ — argon2(Node)、argon2-cffi(Python)、org.bouncycastle.crypto.generators.Argon2BytesGenerator(Java)。
scrypt(2009 年、Colin Percival)。ノブは二つ — N(CPU/メモリのコスト係数、2 のべき乗であること)、r(ブロックサイズ)、p(並列化)。推奨(2026 年)— N = 2^17(約 128 MiB)、r=8、p=1。scrypt はメモリハードですが、Argon2 のような耐性特性は持ちません。互換性の都合で使い続けざるを得ない場合は、依然として許容範囲です。
bcrypt(1999 年、Niels Provos)。ノブは一つ — コストファクター(work factor、1 増えるごとにコストが 2 倍)。推奨(2026 年)— ハードウェア予算に応じて cost = 12 または 13。2026 年において 10 未満は速すぎます。bcrypt の欠陥は、入力長に 72 バイトの上限があること(それより長いパスワードは黙って切り詰められます)、有意なメモリを使わないこと、専用ハードウェアへの耐性がないことです。レガシーシステムには問題ありませんが、今日新たに選ぶものではありません。PBKDF2 はさらに古く、さらに弱いです — FIPS 準拠が強制される場合にのみ許容されます。
三つすべてに共通する重要な実装上の注意 — bcrypt の比較処理を決して自分で書かないでください。verify 関数はタイミング攻撃を避けるために定数時間比較を使用します。hex エンコードされたダイジェストに対する素朴な == は、先頭から何文字一致したかという情報を漏らします。argon2.verify(hash, password) または bcrypt.compare(password, hash) を使い、ハッシュ自体に等価演算子を決して使わないでください。
より強いアルゴリズムへ移行する際の戦略 — 全員を一度に強制的に再ハッシュしないでください。代わりに、次回ログイン時に旧アルゴリズムで検証した後、新しいアルゴリズムで再ハッシュして保存します。これは「遅延移行(lazy migration)」パターンであり、Stack Exchange が 10 年かけて MD5 から bcrypt、そして Argon2 へと移行した際に採った方法そのものです。
// Argon2id with OWASP 2026 baseline
import argon2 from 'argon2';
const hash = await argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB
timeCost: 2,
parallelism: 1,
});
// Stored format embeds params:
// $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
const ok = await argon2.verify(hash, password); // constant-time
// Lazy migration on login
async function login(email, password) {
const user = await db.users.findOne({ email });
if (!user) return null;
if (user.hash.startsWith('$argon2')) {
if (!await argon2.verify(user.hash, password)) return null;
} else if (user.hash.startsWith('$2')) { // bcrypt
if (!await bcrypt.compare(password, user.hash)) return null;
// upgrade!
const newHash = await argon2.hash(password, ARGON2_OPTS);
await db.users.update({ id: user.id }, { hash: newHash });
}
return user;
}暗号学的ハッシュ vs 非暗号学的ハッシュ — 正しいクラスを使う
ハッシュ関数は設計目標が大きく異なる二つのファミリーに分かれており、一方が適する場面で他方を使うのはよくあるバグです。
暗号学的ハッシュ(SHA-256、SHA-3、BLAKE3、BLAKE2)は、敵対的な入力に対する衝突耐性、原像耐性、第二原像耐性を優先します。意図的に、可能な限りの最速にはなっていません。入力が悪意ある当事者に制御され得るあらゆる場面に適しています — コンテンツアドレッシング、署名、セキュリティに関わるコンテキストで識別子として使うハッシュ、パスワードハッシュ(前述の専用関数を使用)、HMAC、鍵導出。ハードウェア SHA 拡張を備えた最新の x86 では SHA-256 は約 1.5〜2 GB/s、BLAKE3 は 5〜10 GB/s で動作し、SHA-3 は SHA-2 より少し遅い程度です。
非暗号学的ハッシュ(xxHash、MurmurHash、CityHash、FNV、FarmHash)は、生の速度と良好な統計的分布を優先します。敵対的な入力に対しては一切の保証をしません — xxHash の設計上、攻撃者はすべてが同じバケットに衝突する入力を構築できます。最新の CPU では xxHash は約 30 GB/s 以上(SHA-256 の 10 倍)で、それこそが存在理由のすべてです。用途 — ハッシュテーブル、Bloom フィルター、非敵対的データの重複排除キー、ランダムな破損だけを心配すればよい完全性チェック(ネットワークパケット、メモリのビット反転)、クライアントごとのログのバケット分け。
バグのパターン — 敵対的なコンテキストで非暗号学的ハッシュを使うこと。古典的な 2003 年の「Crosby & Wallach」論文は、初期の Perl、Java、PHP のハッシュテーブルが辞書キーにそうしたハッシュを使っていたことを示しました。フォームパラメータを送信できるリモート攻撃者がすべて衝突する入力を作り込めば、O(1) の検索が O(n) になり、サーバーを DoS 状態に陥れられます。各言語が採用した修正(プロセスごとのランダム鍵を持つ SipHash アルゴリズム)自体が「鍵付き暗号学的ハッシュ」です — 非暗号ハッシュのように高速でありながら、攻撃者が鍵を知らない限り安全です。
逆のバグ — ハッシュテーブルに SHA-256 を使うこと — は、単に遅いだけです。正しく動作しますが、アプリケーションに不要な暗号学的保証に CPU を浪費します。100 倍の速度差は、内側のループや言語ランタイムでは重大です。
簡単な決定木 — 信頼できない入力で、かつ衝突が正確性やセキュリティに影響する → SHA-256 / SHA-3 / BLAKE3。信頼できるか低リスクのランダムデータで、かつ速度が重要 → xxHash / MurmurHash。任意のユーザー入力に対するハッシュテーブルのキー → SipHash(Python 3.4+、Ruby、Rust では通常、言語側が自動的に選択)。パスワード → Argon2id。
// CRYPTOGRAPHIC — adversary-resistant
import { createHash } from 'node:crypto';
createHash('sha256').update(buf).digest('hex');
// ~1.5 GB/s on modern x86
// NON-CRYPTOGRAPHIC — fast, NOT for security
import xxhash from 'xxhash-wasm';
const { h64 } = await xxhash();
h64(buf); // ~30+ GB/s, 64-bit digest
// Use for: hashtables, dedup, sharding non-adversarial data
// HASH TABLE BUG — never use plain non-crypto hash on attacker input
class BadCache {
constructor() { this.buckets = new Array(1024); }
set(k, v) {
const i = murmur(k) % 1024; // attacker can force all to same bucket
(this.buckets[i] ||= []).push([k, v]);
}
}
// CORRECT — Map / Object use the language's keyed/hardened hasher
const cache = new Map();
cache.set(userInput, value); // V8 uses random-keyed hashing
// Non-adversarial integrity (e.g., copy across rack)
const a = h64(file);
const b = h64(receivedFile);
if (a !== b) console.log('corrupted in transit');