なぜハッシュ関数が重要なのか

ユーザーがアカウントを作成するたびに、システムは根本的な問いに直面します。パスワードをどう保存するか、です。素朴な答え(平文で保存)は、歴史上最大級の情報漏洩を引き起こしてきました。ハッシュ関数は、正しい答えの土台です。

ハッシュ化・ソルト・HMAC:安全なデータ取り扱いの実践ガイド

暗号学的ハッシュ関数 は、任意長の入力を固定長の出力(ダイジェスト/ハッシュ値)に変換します。重要な性質が3つあります。

  1. 決定的:同じ入力からは常に同じ出力が得られる。
  2. 一方向性:出力から入力を復元することは計算上不可能。
  3. 雪崩効果:入力がわずかに(1ビットでも)変わると、出力は全く別物になる。

これらの性質により、パスワードそのものではなくハッシュ値だけを保存できます。ログイン時には入力をハッシュ化して保存済みのハッシュと比較し、一致すればパスワードは正しい——元のパスワードを一切知ることなく、です。

ハッシュアルゴリズムの比較

セキュリティ用途では、すべてのハッシュアルゴリズムが同等というわけではありません。

アルゴリズム出力速度パスワード用?現状
MD5128bit非常に速い不可破られている(衝突発見済み)
SHA-1160bit速い不可非推奨(衝突発見済み)
SHA-256256bit速い不可(速すぎる)完全性チェックには安全
SHA-512512bit速い不可(速すぎる)完全性チェックには安全
bcrypt192bit遅い(調整可)推奨推奨
scrypt可変遅い+メモリ依存推奨推奨
Argon2id可変遅い+メモリ依存最推奨現時点の最適解

なぜ「速すぎる」がパスワードに不向きなのか

SHA-256は速度を重視して設計されており、現代のハードウェアでは毎秒数十億回のハッシュ計算が可能です。これはファイルの完全性チェックには最適ですが、パスワード保存には最悪です。データベースを盗んだ攻撃者が毎秒数十億回の総当たりを試せてしまうからです。

bcryptやArgon2idといったパスワードハッシュ用アルゴリズムは、意図的に遅く作られています。コストファクター(処理にどれだけの計算を要するかを制御する値) を持ち、ハードウェアが速くなったらコストを上げて実効的なセキュリティを維持します。

SHA-256: 約10億ハッシュ/秒(GPU)
bcrypt (cost 12): 約200ハッシュ/秒 — 500万倍遅い

ソルト(Salt):省略できない理由

遅いアルゴリズムを使っても、もう一つの攻撃があります。レインボーテーブル——よくあるパスワードとそのハッシュ値を事前計算したデータベースです。ソルトがないと、同じパスワード「hunter2」を使う2人のユーザーのSHA-256ハッシュは同一になり、パスワードを共有していることが露見します。

ソルト は、ユーザーごとに固有のランダム値で、ハッシュ化前にパスワードと結合します。ソルトはハッシュと一緒に平文で保存します(秘密ではなく、ランダム性を与えるのが目的です)。

ユーザーA: salt = "x7kP9mQ3", password = "hunter2"
  → hash("x7kP9mQ3hunter2") → 固有のハッシュA

ユーザーB: salt = "r2Tz8wN5", password = "hunter2"
  → hash("r2Tz8wN5hunter2") → 全く別のハッシュB

現代のパスワードハッシュライブラリはソルト生成を自動で行います。自前でソルト処理を実装してはいけません。

実装例

// JavaScript / Node.js — bcrypt
import bcrypt from 'bcrypt';

// ハッシュ化(ソルトは自動生成され埋め込まれる)
const hash = await bcrypt.hash('myPassword123', 12); // 12 = コストファクター

// ログイン時の検証
const isValid = await bcrypt.compare('myPassword123', hash); // true
# Python — argon2-cffi(Argon2id)
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=2)
hash = ph.hash('myPassword123')  # ソルトは自動で含まれる

try:
    ph.verify(hash, 'myPassword123')  # 正しければTrue、誤りなら例外
except Exception:
    print("無効なパスワード")
// Go — golang.org/x/crypto/bcrypt
hash, _ := bcrypt.GenerateFromPassword([]byte("myPassword123"), 12)
err := bcrypt.CompareHashAndPassword(hash, []byte("myPassword123"))

HMAC:認証付きの完全性検証

通常のハッシュは「このデータは以前と同じか?」に答えます。
HMAC(Hash-based Message Authentication Code) は「このデータは改ざんされておらず、かつシークレットを知る相手から来たか?」に答えます。

HMACはハッシュ関数と共有シークレットを組み合わせます。鍵を知らなければ、メッセージを読めても有効なHMACを生成できません。

よくある用途

Webhook署名の検証: StripeやGitHubはHMAC-SHA256でWebhookペイロードに署名しており、リクエストが本当にそのサービスから来たかを検証できます。

import crypto from 'crypto';

function verifyWebhookSignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  // タイミング攻撃を防ぐため timingSafeEqual を使う
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'hex'),
    Buffer.from(expected, 'hex')
  );
}

APIリクエストの署名: リクエストパラメータをHMACで署名し、改ざんやリプレイ攻撃を防ぎます。

SHA-256はデータ完全性に(パスワードには不可)

パスワード以外の用途——ファイル完全性の検証、コンテンツのフィンガープリント、チェックサム——にはSHA-256が適しています。高速で広くサポートされ、64文字の16進数文字列を生成します。

// ブラウザ — Web Crypto API
async function sha256(text) {
  const data = new TextEncoder().encode(text);
  const buf = await crypto.subtle.digest('SHA-256', data);
  return Array.from(new Uint8Array(buf))
    .map(b => b.toString(16).padStart(2, '0')).join('');
}
// sha256('Hello, World!')
// → "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986d"
import hashlib
hashlib.sha256(b"Hello, World!").hexdigest()

避けるべきよくあるミス

1. パスワードにMD5やSHA-1を使う

これらは暗号学的に破られており、レインボーテーブルで逆引きされます。セキュリティ用途には使わないでください。

2. 自前で暗号を実装する

ハッシュ実装には微妙な落とし穴(タイミング攻撃、パディング、鍵導出の誤り)が多くあります。必ず十分に監査されたライブラリを使いましょう。

3. 比較に timingSafeEqual を使わない

素朴な文字列比較(===)は最初の不一致で早期終了します。これは タイミングサイドチャネル を生み、攻撃者が処理時間を測って正しい文字を1文字ずつ推測できてしまいます。セキュリティに関わる値の比較には定時間比較を使いましょう。

4. ソルトを使い回す

全ユーザーで同じソルトを使うと、同じパスワードのユーザーは同じハッシュになります。各ユーザーに固有のランダムソルトが必要です。

体感してみる

「ハッシュ値ってどんな見た目?」「キーを変えるとHMACはどう変わる?」。そんな疑問は ハッシュ・HMAC生成器 で確かめられます。データを入力するとSHA-256の文字列が即座に現れ、キーを入れるとHMACがガラリと変わります。

よくある質問

ハッシュ化と暗号化の違いは?

ハッシュ化は一方向で、ハッシュから元データを復元できません。暗号化は双方向で、鍵があれば復号して元データを取り出せます。パスワードにはハッシュ化(元を「見る」必要がない)、後で取り出す必要があるデータ(クレジットカード番号など)には暗号化を使います。

bcryptのコストファクターはどう選ぶ?

本番ハードウェアでハッシュ化に約100〜300ミリ秒かかるように設定します。コスト12から始めてベンチマークし、ハードウェアの向上に合わせて上げていきましょう。

SHA-256とSHA-3はどちらを使う?

SHA-256(SHA-2系)は安全で、最も広く使われています。SHA-3(Keccak)は設計が根本的に異なり安全ですが、ライブラリのサポートは少なめです。特別な要件がなければSHA-256が実用的なデフォルトです。

パスワード以外の機密データにハッシュは使える?

後で取り出す必要があるデータ(マイナンバー、決済カード番号)には暗号化を使います。検証だけが必要なデータ(パスワード、文書のフィンガープリント)にはハッシュ化を使います。サーバー側で保存するAPIトークンは、元々高エントロピーなランダム値なので、SHA-256でのハッシュ化(bcryptなし)でも許容されます。