セキュリティの現場で最も多い事故の一つが、**「暗号の誤用」**です。パスワードをMD5でハッシュする、機密データをBase64しただけで「暗号化した」と思い込む、暗号化に認証が無くて改ざんに気づけない、鍵をソースコードに書く——どれも、個々の技術は知っていても“正しい組み合わせ”を知らないことから起きます。
この記事は、暗号理論の教科書ではありません。アプリ開発者が、暗号を“正しく使う”ための実務ガイドです。大原則は一つ——「暗号を自前で実装するな。枯れたライブラリを、正しいパラメータで使え」。その「正しいパラメータ」を、OWASP Cheat SheetとNIST SP 800-63Bという公式の一次情報から、型安全なコードで示します。
この記事の位置づけ: NIST CSF 2.0の**「防御(Protect)」**を担う中核技能です。職種全体はセキュリティエンジニアになるには、入力検証や認可など防御全般はセキュアコーディング実践、JWTなど認証基盤の実装は認証・認可クラスタを参照してください。
0. まず3つの混同を解く — ハッシュ・暗号化・エンコード
事故の半分は、この3つの取り違えから起きます。
| 操作 | 復号できるか | 目的 | 例 | 典型的な誤用 |
|---|---|---|---|---|
| ハッシュ(一方向) | ❌ できない | 同一性の検証(照合) | パスワード保存、改ざん検知 | 速いハッシュ(MD5/SHA)でパスワード保存 |
| 暗号化(双方向) | ✅ できる(鍵が必要) | 機密性の保護 | 個人情報・トークンの保存 | 認証なし暗号、鍵のハードコード |
| エンコード | ✅ 誰でも戻せる | 形式変換(暗号ではない) | Base64、URLエンコード | 「Base64したから安全」 |
Base64は暗号ではありません。 鍵なしで誰でも元に戻せます。「秘匿のため」にBase64やエンコードを使うのは、最も多い初歩的な誤りです。秘匿が必要なら暗号化、照合だけで十分ならハッシュ——目的から手段を選びます。
1. パスワードは Argon2id でハッシュする
なぜ「速いハッシュ」が危険か
MD5・SHA-1・SHA-256は高速です。それはパスワード保存にとっては致命的な欠点になります。攻撃者は流出したハッシュに対し、GPUで毎秒数十億回の総当たりを試せるからです。だからパスワードには、**意図的に“遅く”作られたパスワード専用のアルゴリズム(KDF:鍵導出関数)**を使います。
OWASP Password Storage Cheat Sheetの推奨順位は明確です。
- Argon2id(第一推奨)
- scrypt(Argon2が使えない場合)
- bcrypt(レガシー。新規でも可だが72バイト制限に注意)
- PBKDF2(FIPS準拠が必要な場合)
Argon2id の実装(OWASP推奨パラメータ)
OWASPが推奨するArgon2idの最小構成は メモリ m=19456 KiB(19 MiB)、反復 t=2、並列度 p=1 です。argon2 ライブラリは、ソルト生成・パラメータの埋め込み・照合をすべて安全に行ってくれます。
// password.ts — Argon2id でパスワードを安全にハッシュ・照合する。
// 自前でソルトやパラメータを管理しない。ライブラリに任せるのが正解。
import argon2 from "argon2";
// OWASP推奨パラメータ(2026年時点)。ハードウェアに余裕があれば memoryCost を上げる。
const ARGON2_OPTIONS = {
type: argon2.argon2id,
memoryCost: 19456, // 19 MiB(KiB単位)
timeCost: 2, // 反復回数
parallelism: 1, // 並列度
} as const;
/** パスワードをハッシュする。ソルトはライブラリが自動生成し、結果に埋め込む。 */
export async function hashPassword(plain: string): Promise<string> {
return argon2.hash(plain, ARGON2_OPTIONS);
}
/**
* 照合する。argon2.verify はハッシュ文字列からパラメータを読み取るため、
* 将来パラメータを強化しても、既存ハッシュの照合は壊れない(前方互換)。
*/
export async function verifyPassword(hash: string, plain: string): Promise<boolean> {
try {
return await argon2.verify(hash, plain);
} catch {
return false; // 不正な形式のハッシュ等は「不一致」に倒す(例外を漏らさない)
}
}
ポイントは、ソルトを自分で管理しないことです。argon2.hash はランダムなソルトを生成し、アルゴリズム・パラメータ・ソルト・ハッシュをすべて1つの文字列(PHC形式)に詰めて返します。照合時はそこからパラメータを復元するので、後でパラメータを強化しても既存ユーザーの照合は壊れません。
NIST 800-63B-4(2025年最終版)の最新パスワード方針
ハッシュの実装と同じくらい重要なのが、**「どんなパスワードを許すか」**のポリシーです。NIST SP 800-63Bの最新改訂(Rev.4、2025年最終化)は、従来の“常識”を覆しました。
| 項目 | NIST 800-63B-4 の方針 | 従来のよくある誤り |
|---|---|---|
| 長さ | 8文字以上を必須、単一要素なら15文字以上。64文字以上を許可 | 「8文字ちょうど」で頭打ち |
| 文字種の強制 | してはならない(SHALL NOT) | 「大小英数記号を必須」 |
| 定期変更 | 課してはならない(SHALL NOT)。漏えい時のみ変更 | 「90日ごとに変更」 |
| 漏えいチェック | 既知の漏えいパスワードをブロックリストで弾く | チェックなし |
| 文字種 | 空白を含む印字可能な全文字・Unicodeを許可 | 記号を禁止 |
つまり——「長く・自由に・漏れたものだけ弾く」。複雑な文字種の強制や定期変更は、むしろユーザーに弱いパスワードの使い回しを促すため、現在は非推奨です。漏えいパスワードの照合にはHave I Been Pwnedのk-匿名性APIなどが使えます。
2. 機密データは「認証付き暗号(AEAD)」で暗号化する
パスワードは“戻せなくて”いい(ハッシュ)。しかし、後で復号して使う必要がある機密データ——APIトークン、個人情報、決済関連の識別子など——は、可逆的な暗号化が必要です。
ここでの鉄則は、「認証付き暗号(AEAD)」を使うことです。単に暗号化するだけでは、改ざんを検知できません。AEAD(例:AES-256-GCM、ChaCha20-Poly1305)は、暗号化と同時に「改ざんされていないこと」を保証する認証タグを付けます。
// crypto.ts — AES-256-GCM(AEAD)で機密データを暗号化・復号する。
// Node.js標準の crypto を使う。アルゴリズムは自作しない。
import { randomBytes, createCipheriv, createDecipheriv } from "node:crypto";
const ALGORITHM = "aes-256-gcm";
const IV_LENGTH = 12; // GCMの推奨IV長は96ビット(12バイト)
const KEY_LENGTH = 32; // AES-256 → 32バイト鍵
/**
* 暗号化。毎回ランダムなIV(初期化ベクトル)を使うのが必須——
* 同じ鍵・同じIVの再利用はGCMの安全性を完全に破壊する。
* 出力は base64( iv | authTag | ciphertext ) を連結して持ち運ぶ。
*/
export function encrypt(plaintext: string, key: Buffer): string {
if (key.length !== KEY_LENGTH) throw new Error("鍵は32バイト(AES-256)である必要があります");
const iv = randomBytes(IV_LENGTH); // ① 毎回新しいIV
const cipher = createCipheriv(ALGORITHM, key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const authTag = cipher.getAuthTag(); // ② 改ざん検知用の認証タグ
return Buffer.concat([iv, authTag, ciphertext]).toString("base64");
}
/** 復号。認証タグが合わなければ(=改ざんされていれば)例外で失敗する。 */
export function decrypt(payload: string, key: Buffer): string {
const data = Buffer.from(payload, "base64");
const iv = data.subarray(0, IV_LENGTH);
const authTag = data.subarray(IV_LENGTH, IV_LENGTH + 16); // GCMタグは16バイト
const ciphertext = data.subarray(IV_LENGTH + 16);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag); // ③ タグを検証——改ざんがあればここで失敗する
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
}
絶対にやってはいけないのが、IV(初期化ベクトル)の固定や使い回しです。GCMでは、同じ鍵で同じIVを二度使うと安全性が崩壊します。毎回ランダムなIVを生成し、暗号文と一緒に保存する——これが定石です(IVは秘密でなくてよい)。また、ECBモードは絶対に使わない(同じ平文が同じ暗号文になり、パターンが漏れる)。
3. 鍵管理 — 暗号の安全性は「鍵の守り方」で決まる
どれだけ強い暗号を使っても、鍵がソースコードやリポジトリに書かれていたら無意味です。鍵管理が、応用暗号で最も難しく、最も重要な部分です。
- 鍵をコードに書かない。 環境変数、または専用のKMS(Key Management Service)/Secrets Managerで管理する。秘密情報の型付き境界はセキュアコーディング実践で扱っています。
- 封筒暗号化(Envelope Encryption)。 データはランダムな「データ鍵」で暗号化し、そのデータ鍵自体をKMSの「マスター鍵」で暗号化して保存する。マスター鍵はKMSから出ない。AWS KMS / Google Cloud KMS / Azure Key Vault の標準パターンです。
- 鍵のローテーション。 鍵は定期的に、また漏えい疑いがあれば即座に交換できる設計にする。KMSはローテーションを自動化できます。
- 最小権限。 鍵を使える主体(IAMロール等)を必要最小限に絞る。
[平文] ──データ鍵で暗号化──▶ [暗号文](DBに保存)
│
└─[データ鍵] ──KMSマスター鍵で暗号化──▶ [暗号化済みデータ鍵](一緒に保存)
▲
マスター鍵はKMSの中から出ない(漏えい面を最小化)
4. やってはいけないこと(アンチパターン集)
| アンチパターン | なぜ危険か | 正しい対応 |
|---|---|---|
| MD5/SHA-1/SHA-256単体でパスワード保存 | 速すぎて総当たりされる | Argon2id(KDF)を使う |
| 暗号化のつもりでBase64 | 鍵なしで誰でも復号できる | 本物の暗号化(AES-GCM) |
| 認証なし暗号(AES-CBCのみ等) | 改ざんを検知できない | AEAD(AES-GCM / ChaCha20-Poly1305) |
| IVの固定・使い回し | GCMの安全性が崩壊 | 毎回ランダムIV |
| 鍵をコード/リポジトリに記述 | 漏えいで全データが復号される | KMS / Secrets Manager + ローテーション |
| 独自の暗号アルゴリズム実装 | ほぼ確実に脆弱になる | 枯れた標準ライブラリ |
alg: none を許すJWT検証 | 署名なしトークンを受理してしまう | アルゴリズムを固定して検証(JWT検証) |
最後のJWTのalg: noneは、認証基盤で頻出の事故です。詳細はCognito JWT(RS256)検証の実装にまとめています。
5. よくある質問(FAQ)
Q. bcryptを使っている既存システムは、Argon2に移行すべきですか? A. 急ぐ必要はありません。bcryptはcost≥10(推奨12)なら今も安全です。移行する場合は、ユーザーが次にログインした時に再ハッシュする段階移行が定石です。新規システムならArgon2idを選びましょう。
Q. ソルトとペッパーの違いは? A. ソルトはハッシュごとにランダムな値で、ライブラリが自動管理します(個別の使い回しを防ぐ)。ペッパーは全体共通の秘密値で、ハッシュとは別にKMS等で管理し追加します。ソルトは必須、ペッパーは任意の追加防御です。
Q. パスワードに文字数・記号の制限を課すのは間違い? A. NIST 800-63B-4は文字種の強制を非推奨としています。ユーザーに弱い使い回しを促すためです。代わりに「長さ(8文字以上、できれば15文字以上)」と「漏えいパスワードのブロック」で守るのが現在の正解です。
Q. クライアント側で暗号化すれば安全ですか? A. クライアントは信頼境界の外です。クライアント側の暗号化は補助にはなりますが、鍵をクライアントに置けば容易に抽出されます。機密性はサーバー側の暗号化とアクセス制御で担保するのが基本です。
Q. 暗号は結局どのライブラリを使えばいい?
A. 言語標準(Node.jsならcrypto、Pythonならcryptography)か、Argon2/libsodiumなど広く監査された実装。「自分で書いたコードでビット演算している」なら、それは赤信号です。
6. まとめ
応用暗号の実務は、難しい数学ではなく、「正しい道具を、正しく使う」という規律です。
- 自前実装しない。 枯れたライブラリを、公式推奨のパラメータで使う。
- 目的から手段を選ぶ。 照合だけならハッシュ、秘匿なら暗号化、Base64は暗号ではない。
- パスワードはArgon2id。 OWASP推奨 m=19456, t=2, p=1。NIST 800-63B-4に沿い「長く・自由に・漏れたものを弾く」。
- 機密データはAEAD。 AES-256-GCMで暗号化と改ざん検知を同時に。IVは毎回ランダム。
- 鍵管理がすべて。 コードに鍵を書かず、KMS・封筒暗号化・ローテーション・最小権限。
暗号の誤用は、見た目には動いてしまうため、事故が起きるまで気づけないのが恐ろしいところです。だからこそ、リリース前に「暗号の使い方は正しいか」を専門家の目で点検する価値があります。あなたのプロダクトのパスワード保存・データ暗号化・鍵管理を一度棚卸ししたい場合は、お気軽にご相談ください。