Skip to main content
友田 陽大
Security engineering & career
セキュリティ
暗号
パスワードハッシュ
Argon2
セキュリティエンジニア

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
Reading time
10 min read
Author
友田 陽大
Share

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 and NIST SP 800-63B.

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; for defense generally like input validation and authorization, secure coding practices; and for implementing authentication foundations like JWT, the authentication/authorization cluster.


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

Half of accidents arise from mixing up these three.

OperationDecryptable?PurposeExampleTypical misuse
Hash (one-way)❌ NoVerifying identity (matching)Password storage, tamper detectionStoring passwords with a fast hash (MD5/SHA)
Encryption (two-way)✅ Yes (a key is needed)Protecting confidentialityStoring PII, tokensUnauthenticated encryption, hardcoded keys
Encoding✅ Anyone can reverseFormat 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'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)

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.

// 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 (Rev.4, finalized 2025) overturned the conventional "common sense."

ItemNIST 800-63B-4 policyCommon conventional error
LengthRequire 8+ chars, 15+ for single-factor. Allow 64+ charsCapping at "exactly 8 chars"
Forcing character classesSHALL NOT"Require upper/lower/digit/symbol"
Periodic changeSHALL NOT impose. Change only on a leak"Change every 90 days"
Leak checkBlock known leaked passwords with a blocklistNo check
Character setAllow all printable characters including spaces, and UnicodeBanning 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'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."

// 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. Typed boundaries for secrets are covered in secure coding practices.
  • 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.
[平文] ──データ鍵で暗号化──▶ [暗号文](DBに保存)
  │
  └─[データ鍵] ──KMSマスター鍵で暗号化──▶ [暗号化済みデータ鍵](一緒に保存)
                                              ▲
                          マスター鍵はKMSの中から出ない(漏えい面を最小化)

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

Anti-patternWhy it's dangerousCorrect response
Storing passwords with MD5/SHA-1/SHA-256 aloneToo fast, brute-forceableUse Argon2id (a KDF)
Base64 as if it were encryptionAnyone can decrypt without a keyReal encryption (AES-GCM)
Unauthenticated encryption (AES-CBC only, etc.)Can't detect tamperingAEAD (AES-GCM / ChaCha20-Poly1305)
Fixing/reusing the IVGCM's security collapsesA random IV every time
Writing keys in code/the repositoryA leak decrypts all dataKMS / Secrets Manager + rotation
Implementing a custom crypto algorithmAlmost certainly becomes vulnerableA battle-tested standard library
JWT verification that allows alg: noneAccepts an unsigned tokenFix and verify the algorithm (JWT verification)

The last one, JWT's alg: none, is a frequent accident in authentication foundations. Details are collected in implementing Cognito JWT (RS256) verification.


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)

友田

友田 陽大

Developer of a METI Minister's Award–winning product. With TypeScript + Python + AWS, I deliver SaaS, industry DX, and production-grade generative AI (RAG) end to end — from requirements to infrastructure and operations — single-handedly.

I can take on the implementation from this article as an engagement

Security engineering, from design to implementation and operations

Design reviews via threat modeling, correct implementation of crypto and authn/authz, log design and detection (detection engineering), and building an incident-response capability. With experience building — solo × generative AI — a METI Minister's Award B2B SaaS and a payments platform with zero double charges in production, I help get your product to a state where it ships fast and can be defended. Vertical risks that only design can address, I also take on as an audit.

Available for both project-based (contract) and advisory engagements. Start with a free 30-minute consult.

Also worth reading