# A practical applied-cryptography guide [2026 edition]: using password hashing (Argon2id), encryption (AES-GCM), and key management correctly

> A practical guide for app developers to 'use cryptography correctly.' Faithful to official sources, it explains, in type-safe code: the difference between hashing, encryption, and encoding; Argon2id password hashing with OWASP-recommended parameters; the latest NIST 800-63B-4 password policy; authenticated encryption with AES-256-GCM; and key management and rotation.

- Published: 2026-06-28
- Author: 友田 陽大
- Tags: セキュリティ, 暗号, パスワードハッシュ, Argon2, セキュリティエンジニア
- URL: https://tomodahinata.com/en/blog/password-hashing-argon2-encryption-key-management-applied-cryptography-guide
- Category: Security engineering & career
- Pillar guide: https://tomodahinata.com/en/blog/security-engineer-how-to-become-roadmap-skills-certification-guide

## Key points

- The first principle of cryptography is 'don't implement it yourself.' What you should do is 'use a battle-tested library with the correct parameters.' The moment you roll your own algorithm, it almost certainly becomes vulnerable.
- Hashing, encryption, and encoding are different. Passwords go in a 'non-decryptable' hash (a slow KDF), confidential data in 'decryptable' encryption, and Base64 is not crypto but just a transformation — this confusion is a breeding ground for accidents.
- Hash passwords with Argon2id. OWASP recommends m=19456 (19MiB), t=2, p=1. Never use MD5/SHA-1/SHA-256 alone (too fast, brute-forceable). Legacy bcrypt is acceptable at cost≥10.
- NIST SP 800-63B-4 (2025 final) overhauled password policy: emphasize length (8+ chars, 15+ for single-factor), don't 'impose' periodic changes, don't force character classes, and block leaked passwords with a blocklist.
- Encrypt confidential data with 'authenticated encryption (AEAD)' like AES-256-GCM. Don't write keys in code; manage them in KMS/Secrets Manager and rotate. Envelope encryption is the realistic solution.

---

One of the most common accidents in the security field is **"misuse of cryptography."** Hashing passwords with MD5, just Base64-ing confidential data and believing it's "encrypted," encryption without authentication so tampering goes unnoticed, writing keys in source code — all of these arise from **knowing the individual techniques but not the "correct combination."**

This article isn't a textbook of cryptographic theory. It's **a practical guide for app developers to "use cryptography correctly."** There's one grand principle — **"Don't implement cryptography yourself. Use a battle-tested library with the correct parameters."** It shows those "correct parameters" in type-safe code, from the official primary sources of the [OWASP Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) and [NIST SP 800-63B](https://pages.nist.gov/800-63-4/sp800-63b.html).

> **The positioning of this article**: a core skill carrying NIST CSF 2.0's **"Protect."** For the whole occupation, see [how to become a security engineer](/blog/security-engineer-how-to-become-roadmap-skills-certification-guide); for defense generally like input validation and authorization, [secure coding practices](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide); and for implementing authentication foundations like JWT, the [authentication/authorization cluster](/blog/auth-platform-selection-2026-cognito-auth0-clerk-supabase).

---

## 0. First, untangle three confusions — hashing, encryption, encoding

Half of accidents arise from mixing up these three.

| Operation | Decryptable? | Purpose | Example | Typical misuse |
|---|---|---|---|---|
| **Hash (one-way)** | ❌ No | Verifying identity (matching) | Password storage, tamper detection | Storing passwords with a fast hash (MD5/SHA) |
| **Encryption (two-way)** | ✅ Yes (a key is needed) | Protecting confidentiality | Storing PII, tokens | Unauthenticated encryption, hardcoded keys |
| **Encoding** | ✅ Anyone can reverse | Format transformation (not crypto) | Base64, URL encoding | "It's safe because I Base64'd it" |

**Base64 is not cryptography.** Anyone can reverse it without a key. Using Base64 or encoding "for secrecy" is the most common beginner error. If secrecy is needed, encryption; if matching alone suffices, hashing — choose the means from the purpose.

---

## 1. Hash passwords with Argon2id

### Why a "fast hash" is dangerous

MD5, SHA-1, and SHA-256 are **fast.** For password storage, that's a **fatal flaw**, because an attacker can try billions of brute-force attempts per second on a GPU against a leaked hash. So for passwords, use **a password-dedicated algorithm (a KDF: key derivation function) deliberately made "slow."**

The [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)'s priority is clear.

1. **Argon2id** (first recommendation)
2. scrypt (when Argon2 can't be used)
3. bcrypt (legacy; OK for new use too, but mind the 72-byte limit)
4. PBKDF2 (when FIPS compliance is needed)

### Implementing Argon2id (OWASP-recommended parameters)

OWASP's recommended minimum configuration for Argon2id is **memory m=19456 KiB (19 MiB), iterations t=2, parallelism p=1.** The `argon2` library safely handles salt generation, parameter embedding, and matching.

```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; // 不正な形式のハッシュ等は「不一致」に倒す（例外を漏らさない）
  }
}
```

The point is **not to manage the salt yourself.** `argon2.hash` generates a random salt and returns the algorithm, parameters, salt, and hash all packed into a single string (PHC format). At matching, it restores the parameters from there, so **strengthening the parameters later doesn't break matching for existing users.**

### The latest NIST 800-63B-4 (2025 final) password policy

As important as the hash implementation is the policy of **"what passwords to allow."** The latest revision of [NIST SP 800-63B](https://pages.nist.gov/800-63-4/sp800-63b.html) (Rev.4, finalized 2025) overturned the conventional "common sense."

| Item | NIST 800-63B-4 policy | Common conventional error |
|---|---|---|
| **Length** | Require 8+ chars, 15+ for single-factor. Allow 64+ chars | Capping at "exactly 8 chars" |
| **Forcing character classes** | **SHALL NOT** | "Require upper/lower/digit/symbol" |
| **Periodic change** | **SHALL NOT impose.** Change only on a leak | "Change every 90 days" |
| **Leak check** | Block known leaked passwords with a blocklist | No check |
| **Character set** | Allow all printable characters including spaces, and Unicode | Banning symbols |

In other words — **"long, free, and block only what's leaked."** Forcing complex character classes and periodic changes actually encourages users to reuse weak passwords, so they're now **discouraged.** For matching leaked passwords, you can use [Have I Been Pwned](https://haveibeenpwned.com/Passwords)'s k-anonymity API, etc.

---

## 2. Encrypt confidential data with "authenticated encryption (AEAD)"

Passwords may be "non-reversible" (hash). But **confidential data you need to decrypt and use later** — API tokens, PII, payment-related identifiers, etc. — needs reversible **encryption.**

The iron rule here is to **use "authenticated encryption (AEAD)."** Merely encrypting **cannot detect tampering.** AEAD (e.g., AES-256-GCM, ChaCha20-Poly1305) attaches an authentication tag that, simultaneously with encryption, guarantees "it hasn't been tampered with."

```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");
}
```

What you absolutely must not do is **fix or reuse the IV (initialization vector).** With GCM, using the same IV twice with the same key collapses its security. **Generate a random IV every time and store it together with the ciphertext** — that's the standard (the IV need not be secret). Also, **never use ECB mode** (the same plaintext becomes the same ciphertext, leaking the pattern).

---

## 3. Key management — the security of cryptography is decided by "how you guard the key"

No matter how strong the cryptography, **it's meaningless if the key is written in source code or the repository.** Key management is the hardest and most important part of applied cryptography.

- **Don't write the key in code.** Manage it in environment variables, or a dedicated [KMS (Key Management Service) / Secrets Manager](/blog/dynamodb-security-iam-fine-grained-access-control-encryption-vpc-endpoint-guide). Typed boundaries for secrets are covered in [secure coding practices](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide).
- **Envelope Encryption.** Encrypt data with a random "data key," and store that data key itself encrypted with KMS's "master key." The master key never leaves KMS. It's the standard pattern of AWS KMS / Google Cloud KMS / Azure Key Vault.
- **Key rotation.** Design so keys can be swapped periodically, and immediately if a leak is suspected. KMS can automate rotation.
- **Least privilege.** Narrow the subjects (IAM roles, etc.) that can use a key to the minimum necessary.

```text
[平文] ──データ鍵で暗号化──▶ [暗号文]（DBに保存）
  │
  └─[データ鍵] ──KMSマスター鍵で暗号化──▶ [暗号化済みデータ鍵]（一緒に保存）
                                              ▲
                          マスター鍵はKMSの中から出ない（漏えい面を最小化）
```

---

## 4. What not to do (anti-pattern collection)

| Anti-pattern | Why it's dangerous | Correct response |
|---|---|---|
| Storing passwords with MD5/SHA-1/SHA-256 alone | Too fast, brute-forceable | Use Argon2id (a KDF) |
| Base64 as if it were encryption | Anyone can decrypt without a key | Real encryption (AES-GCM) |
| Unauthenticated encryption (AES-CBC only, etc.) | Can't detect tampering | AEAD (AES-GCM / ChaCha20-Poly1305) |
| Fixing/reusing the IV | GCM's security collapses | A random IV every time |
| Writing keys in code/the repository | A leak decrypts all data | KMS / Secrets Manager + rotation |
| Implementing a custom crypto algorithm | Almost certainly becomes vulnerable | A battle-tested standard library |
| JWT verification that allows `alg: none` | Accepts an unsigned token | Fix and verify the algorithm ([JWT verification](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide)) |

The last one, JWT's `alg: none`, is a frequent accident in authentication foundations. Details are collected in [implementing Cognito JWT (RS256) verification](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide).

---

## 5. Frequently asked questions (FAQ)

**Q. Should an existing system using bcrypt migrate to Argon2?**
A. There's no need to rush. bcrypt is still safe at cost≥10 (12 recommended). If you migrate, the standard is a staged migration that **re-hashes when the user next logs in.** For a new system, choose Argon2id.

**Q. What's the difference between salt and pepper?**
A. A salt is a random value per hash, managed automatically by the library (preventing reuse per item). A pepper is a single shared secret value, managed in KMS, etc., separately from the hash, and added. The salt is required; the pepper is an optional additional defense.

**Q. Is it wrong to impose character-count/symbol restrictions on passwords?**
A. NIST 800-63B-4 **discourages forcing character classes**, because it encourages weak reuse by users. Instead, defending with "length (8+, ideally 15+ chars)" and "blocking leaked passwords" is the current correct answer.

**Q. Is it safe if I encrypt on the client side?**
A. The client is outside the trust boundary. Client-side encryption can be supplementary, but **if you put the key on the client, it's easily extracted.** The basis is to ensure confidentiality with server-side encryption and access control.

**Q. In the end, which library should I use for crypto?**
A. The language standard (`crypto` for Node.js, `cryptography` for Python), or a widely audited implementation like Argon2/libsodium. **If "you're doing bit operations in code you wrote yourself," that's a red flag.**

---

## 6. Conclusion

The practice of applied cryptography isn't difficult math but **the discipline of "using the right tools correctly."**

- **Don't implement it yourself.** Use a battle-tested library with officially recommended parameters.
- **Choose the means from the purpose.** Hashing for matching only, encryption for secrecy; Base64 is not crypto.
- **Passwords are Argon2id.** OWASP-recommended m=19456, t=2, p=1. Per NIST 800-63B-4, "long, free, block what's leaked."
- **Confidential data is AEAD.** AES-256-GCM for encryption and tamper detection at once. A random IV every time.
- **Key management is everything.** Don't write keys in code; use KMS, envelope encryption, rotation, and least privilege.

The frightening thing about cryptographic misuse is that, since it appears to work, **you can't notice until an accident happens.** That's exactly why it's worth having an expert inspect "is the use of cryptography correct" before release. If you want to take inventory of your product's password storage, data encryption, and key management once, feel free to consult me.

---

### References (official primary sources)

- Password storage: [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)
- Authentication policy: [NIST SP 800-63B Digital Identity Guidelines](https://pages.nist.gov/800-63-4/sp800-63b.html)
- Leak check: [Have I Been Pwned — Pwned Passwords](https://haveibeenpwned.com/Passwords)
- Related: [secure coding practices](/blog/secure-coding-practices-nist-ssdf-owasp-asvs-engineer-guide) / [Cognito JWT (RS256) verification](/blog/aws-cognito-jwt-rs256-verification-jwks-security-guide)
