# 実務のための応用暗号ガイド【2026年版】パスワードハッシュ(Argon2id)・暗号化(AES-GCM)・鍵管理を正しく使う

> アプリ開発者が暗号を“正しく使う”ための実践ガイド。ハッシュ・暗号化・エンコードの違いから、OWASP推奨パラメータでのArgon2idパスワードハッシュ、NIST 800-63B-4の最新パスワード方針、AES-256-GCMによる認証付き暗号、鍵管理とローテーションまでを、公式情報に忠実な型安全コードで解説します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: セキュリティ, 暗号, パスワードハッシュ, Argon2, セキュリティエンジニア
- URL: https://tomodahinata.com/blog/password-hashing-argon2-encryption-key-management-applied-cryptography-guide
- カテゴリ: セキュリティエンジニア・キャリア
- 総合ガイド: https://tomodahinata.com/blog/security-engineer-how-to-become-roadmap-skills-certification-guide

## 要点

- 暗号の第一原則は『自前で実装しない』。やるべきは“枯れたライブラリを、正しいパラメータで使う”こと。アルゴリズムを自作した瞬間に、ほぼ確実に脆弱になる
- ハッシュ・暗号化・エンコードは別物。パスワードは“復号できない”ハッシュ（遅いKDF）で、機密データは“復号できる”暗号化で、Base64は暗号ではなくただの変換——この混同が事故の温床
- パスワードはArgon2idでハッシュする。OWASP推奨は m=19456(19MiB), t=2, p=1。MD5/SHA-1/SHA-256単体は絶対に使わない（速すぎて総当たりされる）。レガシーのbcryptはcost≥10で許容
- NIST SP 800-63B-4（2025年最終版）はパスワード方針を刷新：長さ重視（8文字以上、単一要素なら15文字以上）、定期変更を“課すな”、文字種の強制をするな、漏えいパスワードはブロックリストで弾け
- 機密データはAES-256-GCMなど“認証付き暗号(AEAD)”で。鍵はコードに書かずKMS/Secrets Managerで管理しローテーションする。封筒暗号化（envelope encryption）が現実解

---

セキュリティの現場で最も多い事故の一つが、**「暗号の誤用」**です。パスワードをMD5でハッシュする、機密データをBase64しただけで「暗号化した」と思い込む、暗号化に認証が無くて改ざんに気づけない、鍵をソースコードに書く——どれも、**個々の技術は知っていても“正しい組み合わせ”を知らない**ことから起きます。

この記事は、暗号理論の教科書ではありません。**アプリ開発者が、暗号を“正しく使う”ための実務ガイド**です。大原則は一つ——**「暗号を自前で実装するな。枯れたライブラリを、正しいパラメータで使え」**。その「正しいパラメータ」を、[OWASP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)と[NIST SP 800-63B](https://pages.nist.gov/800-63-4/sp800-63b.html)という公式の一次情報から、型安全なコードで示します。

> **この記事の位置づけ：** NIST CSF 2.0の**「防御（Protect）」**を担う中核技能です。職種全体は[セキュリティエンジニアになるには](/blog/security-engineer-how-to-become-roadmap-skills-certification-guide)、入力検証や認可など防御全般は[セキュアコーディング実践](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide)、JWTなど認証基盤の実装は[認証・認可クラスタ](/blog/auth-platform-selection-2026-cognito-auth0-clerk-supabase)を参照してください。

---

## 0. まず3つの混同を解く — ハッシュ・暗号化・エンコード

事故の半分は、この3つの取り違えから起きます。

| 操作 | 復号できるか | 目的 | 例 | 典型的な誤用 |
|---|---|---|---|---|
| **ハッシュ（一方向）** | ❌ できない | 同一性の検証（照合） | パスワード保存、改ざん検知 | 速いハッシュ(MD5/SHA)でパスワード保存 |
| **暗号化（双方向）** | ✅ できる（鍵が必要） | 機密性の保護 | 個人情報・トークンの保存 | 認証なし暗号、鍵のハードコード |
| **エンコード** | ✅ 誰でも戻せる | 形式変換（暗号ではない） | Base64、URLエンコード | 「Base64したから安全」 |

**Base64は暗号ではありません。** 鍵なしで誰でも元に戻せます。「秘匿のため」にBase64やエンコードを使うのは、最も多い初歩的な誤りです。秘匿が必要なら暗号化、照合だけで十分ならハッシュ——目的から手段を選びます。

---

## 1. パスワードは Argon2id でハッシュする

### なぜ「速いハッシュ」が危険か

MD5・SHA-1・SHA-256は**高速**です。それはパスワード保存にとっては**致命的な欠点**になります。攻撃者は流出したハッシュに対し、GPUで毎秒数十億回の総当たりを試せるからです。だからパスワードには、**意図的に“遅く”作られたパスワード専用のアルゴリズム（KDF：鍵導出関数）**を使います。

[OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)の推奨順位は明確です。

1. **Argon2id**（第一推奨）
2. scrypt（Argon2が使えない場合）
3. bcrypt（レガシー。新規でも可だが72バイト制限に注意）
4. PBKDF2（FIPS準拠が必要な場合）

### Argon2id の実装（OWASP推奨パラメータ）

OWASPが推奨するArgon2idの最小構成は **メモリ m=19456 KiB（19 MiB）、反復 t=2、並列度 p=1** です。`argon2` ライブラリは、ソルト生成・パラメータの埋め込み・照合をすべて安全に行ってくれます。

```ts
// 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](https://pages.nist.gov/800-63-4/sp800-63b.html)の最新改訂（Rev.4、2025年最終化）は、従来の“常識”を覆しました。

| 項目 | NIST 800-63B-4 の方針 | 従来のよくある誤り |
|---|---|---|
| **長さ** | 8文字以上を必須、単一要素なら15文字以上。64文字以上を許可 | 「8文字ちょうど」で頭打ち |
| **文字種の強制** | **してはならない（SHALL NOT）** | 「大小英数記号を必須」 |
| **定期変更** | **課してはならない（SHALL NOT）**。漏えい時のみ変更 | 「90日ごとに変更」 |
| **漏えいチェック** | 既知の漏えいパスワードをブロックリストで弾く | チェックなし |
| **文字種** | 空白を含む印字可能な全文字・Unicodeを許可 | 記号を禁止 |

つまり——**「長く・自由に・漏れたものだけ弾く」**。複雑な文字種の強制や定期変更は、むしろユーザーに弱いパスワードの使い回しを促すため、現在は**非推奨**です。漏えいパスワードの照合には[Have I Been Pwned](https://haveibeenpwned.com/Passwords)のk-匿名性APIなどが使えます。

---

## 2. 機密データは「認証付き暗号（AEAD）」で暗号化する

パスワードは“戻せなくて”いい（ハッシュ）。しかし、**後で復号して使う必要がある機密データ**——APIトークン、個人情報、決済関連の識別子など——は、可逆的な**暗号化**が必要です。

ここでの鉄則は、**「認証付き暗号（AEAD）」を使う**ことです。単に暗号化するだけでは、**改ざんを検知できません**。AEAD（例：AES-256-GCM、ChaCha20-Poly1305）は、暗号化と同時に「改ざんされていないこと」を保証する認証タグを付けます。

```ts
// 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](/blog/dynamodb-security-iam-fine-grained-access-control-encryption-vpc-endpoint-guide)で管理する。秘密情報の型付き境界は[セキュアコーディング実践](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide)で扱っています。
- **封筒暗号化（Envelope Encryption）。** データはランダムな「データ鍵」で暗号化し、そのデータ鍵自体をKMSの「マスター鍵」で暗号化して保存する。マスター鍵はKMSから出ない。AWS KMS / Google Cloud KMS / Azure Key Vault の標準パターンです。
- **鍵のローテーション。** 鍵は定期的に、また漏えい疑いがあれば即座に交換できる設計にする。KMSはローテーションを自動化できます。
- **最小権限。** 鍵を使える主体（IAMロール等）を必要最小限に絞る。

```text
[平文] ──データ鍵で暗号化──▶ [暗号文]（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検証](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide)） |

最後のJWTの`alg: none`は、認証基盤で頻出の事故です。詳細は[Cognito JWT(RS256)検証の実装](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide)にまとめています。

---

## 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・封筒暗号化・ローテーション・最小権限。

暗号の誤用は、見た目には動いてしまうため、**事故が起きるまで気づけない**のが恐ろしいところです。だからこそ、リリース前に「暗号の使い方は正しいか」を専門家の目で点検する価値があります。あなたのプロダクトのパスワード保存・データ暗号化・鍵管理を一度棚卸ししたい場合は、お気軽にご相談ください。

---

### 参考（公式一次情報）

- パスワード保存：[OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)／[OWASP Cryptographic Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html)
- 認証方針：[NIST SP 800-63B Digital Identity Guidelines](https://pages.nist.gov/800-63-4/sp800-63b.html)
- 漏えいチェック：[Have I Been Pwned — Pwned Passwords](https://haveibeenpwned.com/Passwords)
- 関連：[セキュアコーディング実践](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide)／[Cognito JWT(RS256)検証](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide)
