---
key: "crypto-quick-reference"
title: "Choose cryptographic algorithms and parameters by job"
url: "https://softwaresecurity.com/domains/identity-crypto/encryption/crypto-quick-reference/"
api: "https://softwaresecurity.com/api/v1/articles/crypto-quick-reference.json"
domain: "identity-crypto"
topic: "Identity \u0026 cryptography"
type: "Reference"
identifiers: ["CWE-327","CWE-326","CWE-328","CWE-916","CWE-338","CWE-323","CWE-347","CWE-757","A04:2025","V11.2","V11.3","V11.4","V11.5","V11.6","V12.1","CAPEC-97","CAPEC-20","CAPEC-55","CAPEC-463","CAPEC-620"]
tags: ["Password storage","Encryption","Key management","PKI and certificates","TLS","Post-quantum cryptography"]
stages: ["design","build"]
roles: ["developer"]
license: "CC-BY-4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
code_license: "MIT"
---

# Choose cryptographic algorithms and parameters by job

A sourced quick reference for October 2026: which algorithm and parameters to use for encryption, hashing, passwords, MACs, signatures, key agreement, key derivation, randomness and TLS, the matching APIs in Node.js, Python, Java, Go and .NET, and where post-quantum migration stands.

Canonical URL: https://softwaresecurity.com/domains/identity-crypto/encryption/crypto-quick-reference/

## In short

Encrypt with AES-256-GCM or ChaCha20-Poly1305 and never reuse a nonce under one key. Hash with SHA-256 or stronger and authenticate messages with HMAC-SHA-256. Store passwords with Argon2id (at least 19 MiB, 2 iterations, 1 lane). Sign with Ed25519, ECDSA P-256 or 3072-bit RSA-PSS. Agree keys with X25519 and derive them with HKDF. Take randomness only from the OS CSPRNG. Use TLS 1.3, with hybrid X25519MLKEM768 key exchange where supported.

- Use an AEAD, not a bare cipher mode, and never repeat a nonce under the same key.
- Use a password hashing function for passwords, never a fast hash or reversible encryption.
- Compare MACs and secret tokens in constant time, never with ==.
- Derive one key per purpose with HKDF; never use a raw shared secret or a password as a key.
- Prefer complete constructions (TLS 1.3, HPKE, your KMS) over primitives you assemble yourself.
- Inventory RSA and elliptic-curve uses now: hybrid ML-KEM key exchange is standardized and on by default in major stacks.

## Recommended defaults

| Setting | Value | Note | Source |
| --- | --- | --- | --- |
| AES-256-GCM | 256-bit key, 96-bit nonce, 128-bit tag | Random nonces: at most 2^32 encryptions per key. At most about 64 GiB per message. | https://csrc.nist.gov/pubs/sp/800/38/d/final |
| ChaCha20-Poly1305 | 256-bit key, 96-bit nonce, 128-bit tag | Nonce must be unique per key, ideally a counter. XChaCha20-Poly1305 has a 192-bit nonce that can be random. | https://www.rfc-editor.org/rfc/rfc8439.html |
| Argon2id | m = 19 MiB (19456 KiB), t = 2, p = 1 minimum | Equivalent floors: 46 MiB with t = 1, 12 MiB with t = 3, 9 MiB with t = 4, 7 MiB with t = 5. | https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id |
| scrypt | N = 2^17, r = 8, p = 1 minimum | About 128 MiB per hash; Node.js and Python need `maxmem` raised above the 32 MiB default. | https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#scrypt |
| bcrypt | Cost 10 or more; input of at most 72 bytes | Legacy systems only. | https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#bcrypt |
| PBKDF2 | HMAC-SHA-256: 600,000 iterations; HMAC-SHA-512: 220,000 | Use when FIPS 140 validation is required. ASVS 5.0.0 still lists 210,000 for SHA-512. | https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 |
| Password salt | 16 bytes or more, random, unique per hash | RFC 9106 recommends 16 bytes for Argon2 password hashing. | https://www.rfc-editor.org/rfc/rfc9106.html |
| HMAC key | At least 32 random bytes for HMAC-SHA-256 | RFC 2104 strongly discourages keys shorter than the hash output. | https://www.rfc-editor.org/rfc/rfc2104.html |
| Unguessable tokens and IDs | At least 128 bits of CSPRNG output; 256 bits costs nothing extra | ASVS 5.0 requirement 11.5.1; UUIDs do not qualify. | https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x20-V11-Cryptography.md |
| RSA key size | 3072 bits for 128-bit security | 2048 bits gives 112-bit security, which NIST's draft IR 8547 deprecates after 2030. | https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final |
| Elliptic curves | P-256, X25519, Ed25519 (about 128-bit security) | P-384 for a larger classical margin; none of these resist a quantum computer. | https://csrc.nist.gov/pubs/sp/800/57/pt1/r5/final |
| TLS | TLS 1.3; TLS 1.2 only with ECDHE, AEAD suites and extended master secret | TLS 1.0 and 1.1 must not be negotiated (RFC 8996). | https://www.rfc-editor.org/rfc/rfc9325.html |
| Post-quantum key exchange in TLS 1.3 | X25519MLKEM768 (group `0x11EC`) | RFC 10024; offered by default in Chrome 131+, OpenSSL 3.5+, Go 1.24+ and JDK 27. | https://www.rfc-editor.org/rfc/rfc10024.html |

## Verify it

1. Search security code paths for MD5, SHA1, DES, RC4, ECB, CBC, `PKCS1v15`, `Math.random`, `random.random`, `java.util.Random` and `math/rand`; each hit needs a replacement or a written reason.
2. Add tests that flip a byte of ciphertext, tag and nonce and change the associated data, expecting decryption to fail, and that encrypt one plaintext twice expecting different output.
3. Read a sample of stored password hashes: each must carry its algorithm and parameters, and those must meet the floors in the table.
4. Run `openssl s_client -brief` against each endpoint (OpenSSL 3.5+ prints the negotiated group) and a scanner such as `testssl.sh` or SSLyze to confirm no TLS 1.0 or 1.1 and no non-AEAD suites.
5. Run Project Wycheproof test vectors against custom wrappers or less common libraries.

## What you will learn

- Choose an algorithm and parameters for each cryptographic job.
- Spot nonce, comparison and parameter mistakes in code review.
- Map each job to the right API in your language.
- Explain what is and is not post-quantum ready in October 2026.

Audience: Developers and reviewers who choose or check cryptographic primitives in application code

Before you start:

- Know the difference between symmetric encryption, hashing and public-key cryptography.
- Read Node.js or Python code.
- For the snippets: Node.js 22+ and Python 3.11+ with cryptography 47+ and argon2-cffi.

Time: About 41 minutes: 20 to read and 21 for the exercise

## Pick the primitive by the job

Start from the job, then use the complete construction a maintained library already provides: an AEAD rather than a cipher mode plus a separate MAC, HPKE rather than a home-made ECIES, TLS rather than a custom channel. Most cryptographic vulnerabilities come from how primitives are combined and used, not from broken primitives.

| Job | Use | Avoid |
| --- | --- | --- |
| Encrypt data with a shared key | AES-256-GCM or ChaCha20-Poly1305; XChaCha20-Poly1305 when nonces must be random at high volume | ECB; CBC or CTR without a MAC; DES, 3DES, RC4, Blowfish |
| Hash for integrity or fingerprints | SHA-256, SHA-384, SHA-512, SHA3-256, BLAKE2 | MD5, SHA-1 |
| Store passwords | Argon2id; scrypt; PBKDF2-HMAC-SHA-256 where FIPS 140 is required; bcrypt only in legacy systems | Fast or unsalted hashes; reversible encryption |
| Authenticate a message with a shared key | HMAC-SHA-256 with a 32-byte random key; KMAC | hash(key + message); comparing tags with == |
| Sign data | Ed25519; ECDSA P-256 with SHA-256; RSA-PSS with 3072-bit or larger keys | DSA; RSA under 2048 bits; SHA-1 signatures; RSA PKCS#1 v1.5 in new designs |
| Agree on a key | X25519 or ECDH P-256, then HKDF; X25519MLKEM768 inside TLS 1.3 | Static RSA key transport; finite-field DH under 2048 bits; custom DH groups |
| Encrypt to a public key | HPKE (RFC 9180) | Raw RSA; RSA PKCS#1 v1.5 encryption; home-made ECIES |
| Derive keys from a key | HKDF-SHA-256 with a distinct info label per purpose | One key for two purposes or algorithms; a bare hash as a KDF |
| Derive a key from a password | Argon2id, scrypt or PBKDF2 with a random salt | HKDF or a single hash over the password |
| Generate keys, nonces and tokens | The OS CSPRNG through the language API | `Math.random`, Python random, `java.util.Random`, `math/rand`, time-based seeds |
| Protect traffic | TLS 1.3; TLS 1.2 only with ECDHE and AEAD suites | SSL, TLS 1.0 and 1.1, static RSA suites, disabled certificate checks |

## What is the difference between hashing, encryption and a MAC?

Encryption is reversible with a key and keeps data confidential. Hashing is one-way and keyless: it turns any input into a fixed-size fingerprint that anyone can recompute, so it proves integrity against accidents but not against an attacker who can recompute it. A MAC such as HMAC is a keyed hash: only holders of the key can produce or check it, so it proves a message came from someone with the key and was not changed. Passwords need a fourth tool, a deliberately slow, salted password hash.

The common mistakes follow from mixing them up: encrypting passwords (anyone with the key reads them all), hashing passwords with a fast hash (an attacker tries billions of guesses per second), using a plain hash as a signature for data an attacker can change, and assuming a hash or a base64 encoding hides anything. AEAD encryption, the default for new designs, combines encryption with a MAC so a modified ciphertext fails to decrypt.

| Operation | Reversible | Key | Use it for | Examples |
| --- | --- | --- | --- | --- |
| Encryption (AEAD) | Yes, with the key | Secret key, or public key for encryption to a recipient | Confidentiality and integrity of stored or sent data | AES-256-GCM, ChaCha20-Poly1305, HPKE |
| Hash | No | None | Fingerprints, deduplication, integrity against accidental change | SHA-256, SHA-3, BLAKE2 |
| MAC | No | Shared secret key | Proving a message came from a key holder: webhooks, cookies, API signing | HMAC-SHA-256, KMAC |
| Password hash | No | None, but a random salt per password | Storing passwords and deriving keys from them | Argon2id, scrypt, PBKDF2, bcrypt |
| Signature | No | Private key signs, public key verifies | Proof of origin that anyone can check | Ed25519, ECDSA P-256, RSA-PSS |

## Encrypt with an AEAD and never reuse a nonce

AES-256-GCM and ChaCha20-Poly1305 both encrypt and authenticate: decryption fails as a whole if the ciphertext, tag, nonce or associated data changed. Use AES-GCM where the CPU has AES instructions, which covers most servers and phones. Without them prefer ChaCha20-Poly1305: RFC 8439 notes it runs about three times faster than AES in software and is not exposed to the cache-timing attacks that affect many software AES implementations. ASVS 5.0 lists AES-256 first and AES-128 as legacy; NIST still approves AES-128 and expects it to stay secure for decades even against quantum attacks, so AES-256 buys margin rather than fixing a weakness.

A 96-bit nonce must never repeat under the same key. With GCM a repeat reveals the XOR of the two plaintexts and opens the door to tag forgery; with ChaCha20-Poly1305 it reveals the XOR of the plaintexts and reuses the one-time Poly1305 key. NIST SP 800-38D permits random 96-bit GCM nonces but then caps a key at 2^32 encryptions. A counter avoids the cap but must never repeat across restarts, replicas or restored snapshots, and RFC 8439 expects counter-style nonces. For random nonces at high volume, use XChaCha20-Poly1305 (192-bit nonce, available in libsodium and Go's x/crypto but specified only in an expired IETF draft) or rotate keys well before the limit.

Keep the full 16-byte tag, release no plaintext until decryption succeeds, and pass the record's context (type, ID, tenant, version) as associated data so a valid ciphertext cannot be moved to another record. One message may be at most about 64 GiB under GCM and 256 GiB under ChaCha20-Poly1305. Before Node.js 26, `createDecipheriv` accepted truncated GCM tags unless `authTagLength` was set, so the example sets it on both sides. The AEAD record guide at /domains/identity-crypto/encryption/authenticated-record-design/ covers associated data and nonce limits in depth.

```js
import { randomBytes, createCipheriv, createDecipheriv } from 'node:crypto';

const IV_LEN = 12;   // 96-bit nonce, the size GCM is designed for
const TAG_LEN = 16;  // full 128-bit tag; never accept shorter tags

// key: 32 random bytes (AES-256). aad: context to bind, e.g. "invoice:v1:tenant-42".
export function seal(key, plaintext, aad) {
  const iv = randomBytes(IV_LEN); // fresh per message; random nonces cap a key at 2^32 messages
  const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: TAG_LEN });
  cipher.setAAD(aad);
  const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
  return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]); // iv | tag | ciphertext
}

export function open(key, sealed, aad) {
  if (sealed.length < IV_LEN + TAG_LEN) throw new Error('ciphertext too short');
  const iv = sealed.subarray(0, IV_LEN);
  const tag = sealed.subarray(IV_LEN, IV_LEN + TAG_LEN);
  const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: TAG_LEN });
  decipher.setAAD(aad);
  decipher.setAuthTag(tag);
  const body = decipher.update(sealed.subarray(IV_LEN + TAG_LEN));
  return Buffer.concat([body, decipher.final()]); // final() throws if anything was altered
}
```

## AES-GCM or ChaCha20-Poly1305: which should you use?

Either is a sound choice: both are AEADs with 256-bit keys, 96-bit nonces and 128-bit tags, and both are TLS 1.3 cipher suites. Choose AES-256-GCM when the CPU has AES instructions, which covers most servers and phones, or when you need a FIPS 140-validated, NIST-approved algorithm. Choose ChaCha20-Poly1305 when software may run without AES hardware, because it is fast and constant-time in plain software. If you must draw nonces at random for many messages under one key, use XChaCha20-Poly1305; if a nonce might repeat by accident, use AES-GCM-SIV (RFC 8452).

| Property | AES-256-GCM | ChaCha20-Poly1305 |
| --- | --- | --- |
| Speed | Fastest with AES instructions (AES-NI, ARMv8 Cryptography Extensions) | About three times faster than software AES, per RFC 8439; no special hardware needed |
| Timing side channels | Safe with hardware instructions; table-based software AES can leak through the cache | Constant-time in software |
| Approval | NIST-approved (SP 800-38D); available in FIPS 140 modules | Not NIST-approved |
| Nonce reuse | Reveals the XOR of plaintexts and lets an attacker forge tags | Reveals the XOR of plaintexts and reuses the one-time Poly1305 key |
| Random nonces | At most 2^32 messages per key (SP 800-38D) | Use a counter (RFC 8439), or XChaCha20-Poly1305 with 192-bit nonces |
| Maximum message | About 64 GiB | About 256 GiB |
| Misuse-resistant variant | AES-GCM-SIV (RFC 8452) | None standardized; XChaCha20 widens the nonce instead |

## Hash with SHA-2 or SHA-3, authenticate with HMAC

For integrity checks, fingerprints and content addressing use SHA-256, SHA-384, SHA-512, SHA3-256 or BLAKE2. MD5 and SHA-1 have practical collision attacks: ASVS 5.0 forbids MD5 for any cryptographic purpose, and NIST is phasing SHA-1 out of its last uses by 31 December 2030. Where collision resistance matters, use at least 256 bits of output.

A hash is not a MAC: an attacker who does not know the key can extend SHA-256(key || message) into a valid tag for a longer message. Use HMAC-SHA-256 with a random key of at least 32 bytes (RFC 2104 discourages keys shorter than the hash output) or KMAC. Verify by recomputing the tag and comparing in constant time, because == can stop at the first differing byte and leak timing. Do not truncate tags unless a standard tells you to.

```python
import hashlib
import hmac
import secrets

MAC_KEY = secrets.token_bytes(32)  # 256-bit random key; store it like any other secret

def tag(message: bytes) -> str:
    return hmac.new(MAC_KEY, message, hashlib.sha256).hexdigest()

def verify(message: bytes, received_hex: str) -> bool:
    # Constant-time comparison of equal-type values; never use == for MACs.
    return hmac.compare_digest(tag(message).encode(), received_hex.encode())
```

## How do you compare tokens and MACs without leaking timing?

Use a timing-safe, constant-time comparison for every secret value a caller supplies: API keys, session and reset tokens, HMAC tags and webhook signatures. An ordinary `==` can stop at the first differing byte, so response times reveal how much of a guess was right, and an attacker who can resubmit freely may recover a valid MAC byte by byte.

Compare values of equal length: hash both sides first, or check lengths before calling functions such as Node.js `crypto.timingSafeEqual`, which throws when lengths differ. Better still, look a record up by a public ID and compare only the secret part, or store a hash of the token and compare hashes, so the secret never drives a database index lookup. The functions are `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node.js, `MessageDigest.isEqual` in Java, `subtle.ConstantTimeCompare` or `hmac.Equal` in Go and `CryptographicOperations.FixedTimeEquals` in .NET.

## Hash passwords with Argon2id and current parameters

Passwords need a deliberately slow function with a random salt of 16 bytes or more per hash. The table gives the OWASP Password Storage Cheat Sheet minimums as of October 2026. Benchmark on production hardware and go higher if latency and memory allow, store the algorithm and parameters with each hash (the PHC string format does this), and rehash after the next successful login when the policy changes, as the password storage guide at /domains/identity-crypto/authentication/password-storage/ shows.

Check library defaults against these floors. ASP.NET Core Identity hashes with PBKDF2-HMAC-SHA512 at 100,000 iterations unless you raise `PasswordHasherOptions.IterationCount`, after which it rehashes on login. Spring Security's `Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()` uses 16 MiB and 2 iterations, below the 19 MiB floor. The scrypt row needs about 128 MiB per hash, so Node.js and Python reject it until you raise `maxmem` above its 32 MiB default. ASVS 5.0.0 (May 2025) still prints 210,000 iterations for PBKDF2-HMAC-SHA512; OWASP raised the figure to 220,000 in March 2026.

```python
from argon2 import PasswordHasher  # pip install argon2-cffi
from argon2.exceptions import VerifyMismatchError

# Argon2id with the RFC 9106 low-memory profile (m=64 MiB, t=3, p=4).
# OWASP's floor is m=19 MiB, t=2, p=1; benchmark on production hardware.
ph = PasswordHasher(time_cost=3, memory_cost=64 * 1024, parallelism=4)

def register(password: str) -> str:
    return ph.hash(password)  # "$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>"

def login(stored: str, password: str) -> tuple[bool, str]:
    try:
        ph.verify(stored, password)
    except VerifyMismatchError:
        return False, stored
    if ph.check_needs_rehash(stored):  # parameters older than the current policy
        stored = ph.hash(password)     # upgrade only after a successful login
    return True, stored
```

| Algorithm | Minimum parameters (OWASP, October 2026) | Notes |
| --- | --- | --- |
| Argon2id | m = 19 MiB (19456 KiB), t = 2, p = 1; or 46 MiB with t = 1, 12 MiB with t = 3, 9 MiB with t = 4, 7 MiB with t = 5 | First choice. m is total memory in KiB; p splits it into lanes and is not a multiplier. |
| scrypt | N = 2^17 (128 MiB), r = 8, p = 1; or N = 2^16 with p = 2, N = 2^15 with p = 3 | Use when Argon2id is unavailable. |
| bcrypt | Cost 10 or more | Legacy only. Most implementations read at most 72 bytes, so enforce that limit. |
| PBKDF2-HMAC-SHA256 | 600,000 iterations | Choose when FIPS 140 validation is required. |
| PBKDF2-HMAC-SHA512 | 220,000 iterations | ASVS 5.0.0 lists the older 210,000. |
| PBKDF2-HMAC-SHA1 | 1,400,000 iterations | Legacy only; do not choose it for new systems. |

## Sign with Ed25519, ECDSA P-256 or RSA-PSS

Ed25519 (RFC 8032, approved in FIPS 186-5) is the easiest to use safely: fixed parameters, 64-byte signatures and deterministic signing. ECDSA with P-256 and SHA-256 has the widest support, including hardware tokens and publicly trusted certificates, but every signature needs a fresh secret nonce, and a repeated or biased nonce reveals the private key. Use a library that derives nonces deterministically (RFC 6979, also in FIPS 186-5) or draws them from a vetted CSPRNG. For RSA use PSS with keys of at least 3072 bits for 128-bit security; 2048-bit keys give 112 bits, which NIST's draft transition plan, IR 8547, deprecates after 2030. The cryptography appendix of ASVS 5.0 marks PKCS#1 v1.5 signatures as disallowed, although FIPS 186-5 still approves them.

Most signature bugs live in verification: trusting the algorithm or key named inside the message (the JWT algorithm-confusion family), verifying different bytes from those that were signed, or failing open on an error. Pin the algorithm and key for each use, sign a canonical encoding, and reject anything that does not verify. Publicly trusted TLS certificates may carry only RSA keys of 2048 bits or more or ECDSA P-256, P-384 or P-521 keys under the CA/Browser Forum Baseline Requirements, so Ed25519 and ML-DSA belong in your own protocols for now.

```js
import { generateKeyPairSync, sign, verify } from 'node:crypto';

// Ed25519: deterministic signatures, 32-byte keys, no parameters to get wrong.
const { privateKey, publicKey } = generateKeyPairSync('ed25519');
const message = Buffer.from('release=v2.4.1;sha256=9f2c...');

const signature = sign(null, message, privateKey);        // algorithm is null for Ed25519
const ok = verify(null, message, publicKey, signature);    // true
const forged = verify(null, Buffer.from('release=v2.4.2;sha256=9f2c...'), publicKey, signature); // false

// Distribute only the public key, for example as SPKI PEM:
const pem = publicKey.export({ type: 'spki', format: 'pem' });
```

## RSA or ECDSA (or Ed25519): which should you use?

For new designs, sign with Ed25519 or ECDSA P-256; use RSA where compatibility requires it, with PSS padding and 3072-bit keys. All three give about 128-bit security at those sizes, but the elliptic-curve keys and signatures are a sixth of the size or less and signing is much faster. ECDSA needs a fresh secret nonce for every signature, and a repeated or biased one reveals the private key, so use a library that derives it deterministically (RFC 6979) or prefer Ed25519, which is deterministic by design. RSA keeps two advantages: verification is very fast, and every platform supports it. None of them resists a quantum computer; ML-DSA (FIPS 204) is the planned replacement.

Constraints often decide. Publicly trusted TLS certificates accept RSA and ECDSA P-256, P-384 or P-521, not Ed25519; hardware tokens, HSMs and WebAuthn authenticators widely support ECDSA P-256; JWTs can use any of them (`RS256` or `PS256`, `ES256`, `Ed25519`, formerly `EdDSA`). For key exchange the same choice is X25519 or ECDH P-256 rather than RSA key transport, which offers no forward secrecy.

| Property | RSA-3072 with PSS | ECDSA P-256 | Ed25519 |
| --- | --- | --- | --- |
| Security level | About 128 bits | About 128 bits | About 128 bits |
| Public key | 384-byte modulus | 65 bytes uncompressed, 33 compressed | 32 bytes |
| Signature | 384 bytes | 64 bytes raw, up to 72 in DER | 64 bytes |
| Signing and verifying | Slow to sign, very fast to verify | Fast to sign and verify | Fast to sign and verify |
| Per-signature randomness | A random salt; a bad one does not leak the key | A secret nonce; reuse or bias leaks the private key | None needed; deterministic |
| Support | Universal, including old systems | Universal: Web PKI, HSMs, WebAuthn, JOSE | Modern libraries, SSH, JOSE; not in publicly trusted TLS certificates |

## Agree keys with X25519, then derive them with HKDF

X25519 (RFC 7748) is the default key agreement; use ECDH on P-256 where FIPS-approved curves are required. Never use the raw shared secret as a key. Run it through HKDF (RFC 5869) with both public keys or the protocol transcript in the info input, and derive one key per purpose with distinct labels. RFC 7748 allows rejecting the all-zero result that small-order public keys produce, and Node.js, pyca/cryptography, Go and Java all reject it.

HKDF only works on input that already has high entropy, such as a shared secret or a random master key. It cannot make a password safe to use as a key; that needs Argon2id, scrypt or PBKDF2. The HKDF salt is optional and public, but a random salt strengthens the extraction step.

```js
import { hkdfSync, randomBytes } from 'node:crypto';

// HKDF turns one high-entropy secret into independent keys, one per purpose.
// Never feed it a password: use Argon2id, scrypt or PBKDF2 for those.
const master = randomBytes(32);
const salt = randomBytes(16); // not secret; store it with whatever the keys protect

const encKey = Buffer.from(hkdfSync('sha256', master, salt, 'invoices:v1:aes-256-gcm', 32));
const macKey = Buffer.from(hkdfSync('sha256', master, salt, 'invoices:v1:hmac-sha256', 32));
```

## Encrypt to a public key with HPKE

To encrypt for a recipient's public key, use HPKE (RFC 9180) instead of a home-made ECIES. It packages key agreement, HKDF and an AEAD behind one call and binds an application context string. It ships in pyca/cryptography 47+, Go 1.26+ (`crypto/hpke`), Java 26+ (`Cipher` `"HPKE"`) and OpenSSL 3.2+; Node.js and .NET have no built-in API. Base mode does not authenticate the sender, so sign the payload when the recipient must know who sent it. Avoid raw RSA and PKCS#1 v1.5 encryption; if RSA encryption is unavoidable, use OAEP.

```python
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.hpke import AEAD, KDF, KEM, Suite  # cryptography 47+

# RFC 9180 HPKE: X25519 key agreement, HKDF and an AEAD in one reviewed construction.
suite = Suite(KEM.X25519, KDF.HKDF_SHA256, AEAD.CHACHA20_POLY1305)

recipient = X25519PrivateKey.generate()   # keep private; publish recipient.public_key()
sealed = suite.encrypt(b"refund approved: in_42", recipient.public_key(), info=b"billing-v1")

# Raises cryptography.exceptions.InvalidTag if the ciphertext or info changed.
plaintext = suite.decrypt(sealed, recipient, info=b"billing-v1")
```

## Take every secret value from the OS CSPRNG

Keys, nonces, salts, session IDs, reset tokens and API keys must come from the operating system's CSPRNG through the language APIs listed below. ASVS 5.0 requires at least 128 bits of entropy for values meant to be unguessable; 32 bytes (256 bits) costs nothing extra. Never seed a CSPRNG yourself, and never use `Math.random`, Python's random module, `java.util.Random` or `math/rand` for security values: their future output can be predicted from past output.

A version 4 UUID carries 122 random bits and RFC 9562 only recommends a CSPRNG for it, so use UUIDs as identifiers, not secrets; ASVS 5.0 says they do not meet its 128-bit rule. For a number in a range, use the unbiased helper (`randomInt`, `secrets.randbelow`, `RandomNumberGenerator.GetInt32`) instead of a modulo.

```js
import { randomBytes, randomInt, randomUUID } from 'node:crypto';

const resetToken = randomBytes(32).toString('base64url');         // 256 bits, URL-safe
const otp = randomInt(0, 1_000_000).toString().padStart(6, '0');  // unbiased 6-digit code
const requestId = randomUUID();                                  // v4 UUID: an ID, not a secret
```

## The same jobs in Node.js, Python, Java, Go and .NET

Every API below was exercised on Node.js 26.3, Python 3.14 with cryptography 50, JDK 21 and 27, Go 1.27 and .NET 10; the .NET post-quantum classes were checked for availability only, because the test machine's OS library lacks them. Version notes mark where a feature arrived; entries without one work on current long-term-support releases.

.NET has no built-in Ed25519, X25519 or Argon2, so use a maintained library such as NSec (built on libsodium) for those; its MLKem and MLDsa classes report `IsSupported` only where the operating system library provides them (OpenSSL 3.5+ on Linux, or Windows CNG with post-quantum support). Java has no built-in Argon2; Bouncy Castle provides it. Go keeps Argon2, bcrypt, scrypt and ChaCha20-Poly1305 in `golang.org/x/crypto`.

| Job | Node.js (node:crypto) | Python | Java (JCA) | Go | .NET |
| --- | --- | --- | --- | --- | --- |
| Random bytes | `randomBytes(32)`; `randomInt`; `randomUUID` | `secrets.token_bytes(32)`; `token_urlsafe`; `randbelow` | `new SecureRandom().nextBytes(buf)` | `rand.Read(buf)`; `rand.Text()` (1.24+) | `RandomNumberGenerator.GetBytes(32)`; `GetInt32`; `GetString` |
| AEAD | `createCipheriv('aes-256-gcm' or 'chacha20-poly1305', key, iv, { authTagLength: 16 })` | cryptography: `AESGCM`, `ChaCha20Poly1305` | `Cipher` `"AES/GCM/NoPadding"` with `GCMParameterSpec(128, iv)`; `"ChaCha20-Poly1305"` | `cipher.NewGCM`; `NewGCMWithRandomNonce` (1.24+); `x/crypto/chacha20poly1305` `New`, `NewX` | `new AesGcm(key, tagSizeInBytes: 16)`; `ChaCha20Poly1305` |
| Hash and HMAC | `hash('sha256', data)`; `createHmac('sha256', key)` | `hashlib.sha256`; `hmac.new(key, msg, 'sha256')` | `MessageDigest` `"SHA-256"`; `Mac` `"HmacSHA256"` | `sha256.Sum256`; `hmac.New(sha256.New, key)` | `SHA256.HashData`; `HMACSHA256.HashData` |
| Constant-time compare | `timingSafeEqual(a, b)` | `hmac.compare_digest(a, b)` | `MessageDigest.isEqual(a, b)` | `hmac.Equal`; `subtle.ConstantTimeCompare` | `CryptographicOperations.FixedTimeEquals` |
| Password hashing | `argon2('argon2id', ...)` (24.7+); `scrypt` with `maxmem`; `pbkdf2` | argon2-cffi `PasswordHasher`; `hashlib.scrypt` with `maxmem`; `hashlib.pbkdf2_hmac` | `SecretKeyFactory` `"PBKDF2WithHmacSHA256"`; Argon2 via Bouncy Castle | `x/crypto/argon2` `IDKey`; `pbkdf2.Key` (1.24+) | `Rfc2898DeriveBytes.Pbkdf2`; Argon2 via a library |
| Signatures | `generateKeyPairSync('ed25519')`; `sign(null, data, key)`; `verify` | `Ed25519PrivateKey`; `ec.ECDSA(hashes.SHA256())`; `padding.PSS` | `Signature` `"Ed25519"`, `"SHA256withECDSA"`, `"RSASSA-PSS"` | `ed25519.Sign`; `ecdsa.SignASN1`; `rsa.SignPSS` | `ECDsa`; RSA with `RSASignaturePadding.Pss`; Ed25519 via a library |
| Key agreement and KDF | `generateKeyPairSync('x25519')` with `diffieHellman()`; `hkdfSync` | `X25519PrivateKey.exchange`; `HKDF` | `KeyAgreement` `"X25519"`; `KDF` `"HKDF-SHA256"` (25+) | `ecdh.X25519()`; `hkdf.Key` (1.24+) | `ECDiffieHellman` (P-256); `HKDF.DeriveKey`; X25519 via a library |
| ML-KEM and ML-DSA | `'ml-kem-768'` with `encapsulate` (24.7+); `'ml-dsa-65'` (24.6+); needs OpenSSL 3.5+ | cryptography 48+: `mlkem`, `mldsa` | `KEM` `"ML-KEM"`; `Signature` `"ML-DSA"` (24+) | `crypto/mlkem` (1.24+); `crypto/mldsa` (1.27+) | `MLKem`, `MLDsa` (where `IsSupported`) |

## Use TLS 1.3; keep TLS 1.2 only where you must

TLS 1.3 is now specified by RFC 9846 (July 2026), which replaced RFC 8446 without changing the protocol version. It allows only AEAD cipher suites, dropped static RSA and custom Diffie-Hellman groups, and gives every public-key exchange forward secrecy. RFC 9852 tells new protocols to require TLS 1.3, and RFC 9851 freezes TLS 1.2, so post-quantum key exchange will arrive only in TLS 1.3.

Keep TLS 1.2 only for clients that cannot do 1.3, and then only with ECDHE key exchange, AEAD suites (RFC 9325 recommends the ECDHE AES-GCM suites) and the extended master secret extension; RFC 10015 (July 2026) forbids RSA and finite-field DH key exchange in TLS 1.2. TLS 1.0 and 1.1 must not be negotiated (RFC 8996), and neither may SSL. Most TLS failures in practice are configuration: certificate or hostname checks switched off in a client, or a silent fallback to plaintext. Keep validation on for internal calls too.

```js
import tls from 'node:tls';

const host = 'example.com';
const socket = tls.connect({
  host, port: 443, servername: host,
  minVersion: 'TLSv1.2', // never negotiate TLS 1.0 or 1.1
  // rejectUnauthorized defaults to true: never set it to false
}, () => {
  console.log(socket.getProtocol(), socket.getCipher().name); // e.g. TLSv1.3 TLS_AES_256_GCM_SHA384
  socket.end();
});
socket.on('error', (err) => console.error('TLS failed:', err.code ?? err.message));
```

## Post-quantum status in October 2026

NIST published FIPS 203 (ML-KEM, key encapsulation), FIPS 204 (ML-DSA, signatures) and FIPS 205 (SLH-DSA, hash-based signatures) on 13 August 2024, and SP 800-227, its guidance on using KEMs, in September 2025. HQC was selected in March 2025 as a backup KEM, and FN-DSA (Falcon) is still being standardized. NIST IR 8547, still an initial public draft from November 2024, would deprecate 112-bit RSA and elliptic-curve algorithms after 2030 and disallow quantum-vulnerable public-key algorithms after 2035. Symmetric algorithms with keys of 128 bits or more and SHA-2 or SHA-3 hashes do not need replacing.

Key exchange is ready. RFC 10024 (August 2026) defines X25519MLKEM768, SecP256r1MLKEM768 and SecP384r1MLKEM1024 for TLS 1.3 using the hybrid design of RFC 9954. X25519MLKEM768 is offered by default in Chrome 131+, OpenSSL 3.5+, Go 1.24+ and JDK 27, and many servers already negotiate it, as the check below shows. A hybrid exchange stays secure as long as either X25519 or ML-KEM-768 holds, which protects recorded traffic against a future quantum computer.

Signatures are not ready for most uses. ML-DSA certificates are defined (RFC 9881), but publicly trusted TLS certificates still allow only RSA and ECDSA, and ML-DSA in TLS 1.3 and composite signatures are still drafts. Inventory every RSA and elliptic-curve use now, prioritize data that must stay confidential for years and verifiers that are slow to update, and adopt ML-KEM and ML-DSA through protocols and services (TLS, HPKE, your KMS) rather than combining them yourself.

```bash
# OpenSSL 3.5+ offers X25519MLKEM768 by default; -brief shows what the server chose.
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null 2>&1 \
  | grep -E 'Protocol version|Ciphersuite|Negotiated TLS1.3 group'
# Protocol version: TLSv1.3
# Ciphersuite: TLS_AES_256_GCM_SHA384
# Negotiated TLS1.3 group: X25519MLKEM768
```

## Retire these on sight

Each item below has a published practical attack or a standards body telling you to stop. Treat a finding as a defect with an owner and a date.

- MD5 and SHA-1 in signatures, certificates, password storage or any integrity check an attacker can influence.
- DES, 3DES and other 64-bit block ciphers (Sweet32), RC4 and Blowfish.
- ECB mode, and CBC without a MAC, which invites padding oracles.
- RSA PKCS#1 v1.5 encryption, raw RSA, and RSA keys under 2048 bits.
- Static, reset or key-derived nonces and IVs, and truncated authentication tags.
- Fast hashes or reversible encryption for passwords, and passwords used directly as keys.
- Home-made constructions: custom modes, hash(key || message) MACs and hand-built hybrid encryption.
- SSL, TLS 1.0 and TLS 1.1, export or anonymous suites, and disabled certificate validation.

## Check code and endpoints

Automated checks find the names of primitives; a reviewer still has to confirm what each one protects before ranking the finding.

- Code: Semgrep or CodeQL cryptography rules in CI, plus a reviewer search for the weak names in the verification list.
- Libraries: Project Wycheproof test vectors (`github.com/C2SP/wycheproof`) for wrappers and less common implementations.
- Endpoints: `testssl.sh` or SSLyze for a protocol and cipher suite inventory; `openssl s_client -brief` for a quick look.
- Data at rest: sample stored hashes and ciphertext headers to confirm which algorithms and parameters are actually in use.

## Guided practice

A pull request adds encryption, reset tokens, password storage and webhook checks to a Node.js service. Review it against this reference, then specify replacements, a migration plan and tests.

### Supplied material

```js
// crypto-utils.js, proposed in a pull request
const crypto = require('node:crypto');
const KEY = crypto.createHash('md5').update(process.env.APP_SECRET).digest(); // 16 bytes
const IV = Buffer.alloc(16, 0);

exports.encrypt = (text) => {
  const c = crypto.createCipheriv('aes-128-cbc', KEY, IV);
  return Buffer.concat([c.update(text), c.final()]).toString('base64');
};
exports.resetToken = () => Math.random().toString(36).slice(2, 12);
exports.hashPassword = (pw) => crypto.createHash('sha256').update('s4lt' + pw).digest('hex');
exports.webhookOk = (body, sig) =>
  crypto.createHmac('sha256', process.env.WEBHOOK_KEY).update(body).digest('hex') === sig;

// server.js
https.createServer({ key, cert, minVersion: 'TLSv1' }, app);

// Observed: encrypt('amount=100;to=alice') returns the same base64 string every time.
```

### 1. List the defects

Name each problem in `crypto-utils.js` and `server.js` and the attack it enables.

Worked answer: MD5 over an environment string is not a key derivation function and yields only an AES-128 key; a guessable `APP_SECRET` falls to offline guessing. A fixed all-zero IV with CBC makes encryption deterministic and unauthenticated, so equal plaintexts are visible, ciphertexts can be altered and a padding oracle may appear. `Math.random` gives short, predictable tokens. One SHA-256 with a constant salt lets an attacker test billions of guesses per second against every account at once. The `===` comparison leaks timing, and `minVersion` `TLSv1` permits TLS 1.0 and 1.1.

### 2. Choose replacements with parameters

Write the replacement for each defect with concrete parameters.

Worked answer: A 32-byte random key from the secret manager, or HKDF from a random master key with a purpose label; AES-256-GCM with a fresh 12-byte nonce, a 16-byte tag and the record context as associated data; `randomBytes(32).toString('base64url')` for tokens; Argon2id with at least 19 MiB, 2 iterations and 1 lane and a per-user salt; a length check plus `timingSafeEqual` for the webhook tag; `minVersion` `'TLSv1.2'`, preferring TLS 1.3.

### 3. Plan the migration

Decide what happens to data, tokens and hashes the old code already produced.

Worked answer: Re-encrypt stored ciphertexts into a versioned AEAD format with a resumable job; invalidate outstanding reset tokens; on each successful login verify the old SHA-256 hash and store an Argon2id hash, and force a reset for accounts that do not return; rotate `APP_SECRET`, because it was used directly as key material.

### 4. Write tests that fail on the old code

List the negative tests that prove the fix.

Worked answer: Encrypting the same plaintext twice gives different output; changing any byte of ciphertext, tag or nonce, or the associated data, makes decryption throw; tokens are 43 base64url characters; two hashes of one password differ and record their parameters; a wrong webhook tag of the right length is rejected; a TLS 1.1-only client cannot connect.

Deliverable: Review comments with a severity for each finding, replacement code, and tests that pass on the new code and fail on the old.

Scope: The code is a synthetic sample; no live service, data or keys are involved. Parameter floors follow OWASP and NIST as of October 2026; benchmark before adopting them.

## Executable practice

- [Insecure randomness lab: Generate unguessable API keys](https://softwaresecurity.com/labs/insecure-randomness/)

Articles and teaching material: CC BY 4.0. Original executable code: MIT. Attribution: Software Security.

## Sources

- [NIST SP 800-38D: GCM and GMAC (nonce construction and invocation limits)](https://csrc.nist.gov/pubs/sp/800/38/d/final). November 2007.
- [RFC 8439: ChaCha20 and Poly1305 for IETF Protocols](https://www.rfc-editor.org/rfc/rfc8439.html). June 2018.
- [OWASP Password Storage Cheat Sheet: Argon2id, scrypt, bcrypt and PBKDF2 parameters](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id)
- [OWASP Cryptographic Storage Cheat Sheet: Algorithms](https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html#algorithms)
- [OWASP ASVS 5.0.0 Appendix C: Cryptography Standards](https://github.com/OWASP/ASVS/blob/v5.0.0_release/5.0/en/0x92-Appendix-C_Cryptography.md). Version 5.0.0 (May 2025).
- [NIST FIPS 203: Module-Lattice-Based Key-Encapsulation Mechanism Standard](https://csrc.nist.gov/pubs/fips/203/final). August 2024.
- [NIST IR 8547 (Initial Public Draft): Transition to Post-Quantum Cryptography Standards](https://csrc.nist.gov/pubs/ir/8547/ipd). November 2024.
- [RFC 10024: Post-Quantum Traditional Hybrid Key Agreement Mechanisms for TLS 1.3](https://www.rfc-editor.org/rfc/rfc10024.html). August 2026.
- [RFC 9325: Recommendations for Secure Use of TLS and DTLS (BCP 195)](https://www.rfc-editor.org/rfc/rfc9325.html). November 2022 (BCP 195); updated by RFC 9852 and RFC 10015.
- [RFC 9846: The Transport Layer Security (TLS) Protocol Version 1.3](https://www.rfc-editor.org/rfc/rfc9846.html). July 2026; obsoletes RFC 8446.