agentic/code/frameworks/security-engineering/skills/crypto-primitive-selection/SKILL.md
Decision aid for choosing AEAD, KDF, MAC, and signature primitives — flags anti-patterns (CBC-without-MAC, ad-hoc KDF, key reuse, PBKDF2-on-high-entropy).
npx skillsauth add jmagly/aiwg crypto-primitive-selectionInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Decision aid for cryptographic primitive choices. Use this skill when designing or reviewing systems that encrypt, authenticate, derive, or sign — and especially before writing any line of code that calls a low-level crypto API.
The skill does not pick a single product or library for you. It gives you a suggested default for the common case, a vetted alternatives menu with selection criteria, and a research path for when neither default nor menu fits your constraints.
Primary phrases are matched automatically from the description. Alternate expressions:
sdlc-complete/skills/security-assessment)Every primitive section follows the same three-part pattern:
The skill never hard-picks a vendor product. It describes properties; you choose against your constraints.
Use when: Encrypting data at rest or in flight where you also need to detect tampering. This is the default for ~95% of "encrypt this" tasks.
XChaCha20-Poly1305 via libsodium (crypto_secretstream_xchacha20poly1305_* for streams, crypto_aead_xchacha20poly1305_ietf_* for one-shot).
Rationale:
secretstream handles multi-chunk encryption with replay/reorder/truncation protection| Primitive | Pick when |
|---|---|
| AES-256-GCM | Hardware AES-NI dominates and you need top throughput on x86_64/ARM with crypto extensions; OR you need FIPS 140-3 validation and your validated module exposes GCM but not ChaCha. Watch the 96-bit nonce — never reuse with the same key, use a counter not a random. |
| AES-256-GCM-SIV (RFC 8452) | You can't guarantee non-reuse of nonces (e.g., distributed system without a counter source). SIV is misuse-resistant against accidental reuse — degrades to revealing equality of plaintexts, not catastrophic loss. |
| ChaCha20-Poly1305 IETF (RFC 8439) | Same algorithm as XChaCha but 96-bit nonce. Pick when interoperating with TLS 1.3, WireGuard, or other IETF-spec stacks that mandate this exact construction. |
| AES-256-CCM | Constrained embedded environments where code size matters and only AES is in ROM. Use only in profile (CCM has tight nonce/length tradeoffs). |
| age (file format) | Encrypting files for asymmetric recipients (X25519 public keys or SSH keys). Don't reimplement; use the age reference tool or a library binding. |
When the default and menu don't fit:
datatracker.ietf.org/wg/cfrg/documents/ — the IRTF Crypto Forum vets new constructions before IETF/NIST adoptioncsrc.nist.gov/projects/cryptographic-algorithm-validation-program) — required if you need FIPS-validated software/hardwareno-unauthenticated-encryptionopenssl enc without -pbkdf2 -iter <N> (or similar explicit KDF flags). Defaults to a single-MD5-iteration KDF. → Rule: crypto-flag-verificationUse when: You have an Input Keying Material (IKM) and need to turn it into one or more cryptographic keys. The right KDF depends entirely on the entropy class of the IKM.
What is the entropy class of your input?
│
├── HIGH ENTROGY (≥128 bits effective)
│ - Output of another KDF
│ - Hardware-derived secret (TPM, YubiKey HMAC, FIDO2 PRF)
│ - DH/ECDH shared secret
│ - Random key from CSPRNG
│ ↓
│ USE: HKDF-Extract + HKDF-Expand (RFC 5869)
│
└── LOW ENTROPY (passwords, passphrases, PINs, recovery codes)
↓
USE: Argon2id (preferred) OR PBKDF2-HMAC-SHA-256 ≥600k iter (legacy/FIPS)
The most common mistake (review finding H1 in the motivating gap analysis): applying PBKDF2 with 100k iterations to a 256-bit hardware-derived secret. This slows the defender without slowing the attacker, because the attacker is not doing brute force on a 256-bit key — that's already infeasible. PBKDF2 only buys you anything when the input is brute-forceable. With a high-entropy input, use HKDF.
| IKM entropy | Default | Rationale |
|---|---|---|
| High | HKDF-SHA-256 (libsodium crypto_kdf_*) | RFC 5869, universally supported, fast, parametric in salt and info string |
| Low (interactive password) | Argon2id with m=65536, t=3, p=4 (libsodium crypto_pwhash_* with OPSLIMIT_INTERACTIVE) | RFC 9106; memory-hard against GPU/ASIC attackers |
| Low (FIPS or legacy constraint) | PBKDF2-HMAC-SHA-256 with ≥600,000 iterations (OWASP 2023) | RFC 8018; available everywhere; CPU-bound only |
| Primitive | Pick when |
|---|---|
| scrypt | You need memory-hardness but Argon2 isn't available in your runtime. Parameters: N=2^17, r=8, p=1 for interactive, scale up for offline. |
| bcrypt | Legacy systems already using bcrypt. Don't introduce in greenfield. Cost factor ≥12; truncates passwords at 72 bytes (pre-hash if longer). |
| HKDF-SHA-512 | Output keys longer than 8160 bits OR you specifically want SHA-512's domain. Default to SHA-256 otherwise. |
| NIST SP 800-108 KBKDF (KDF in counter mode) | FIPS-mandated environments where HKDF isn't approved. Functionally equivalent to HKDF-Expand for typical uses. |
When one IKM produces multiple output keys (e.g., enc_key AND mac_key), use distinct info strings via HKDF-Expand. Never reuse a single derived key for two purposes. This is the fix for review finding B2:
prk = HKDF-Extract(salt, ikm)
enc_key = HKDF-Expand(prk, info="app-aead-v1", length=32)
mac_key = HKDF-Expand(prk, info="app-mac-v1", length=32)
sub_key = HKDF-Expand(prk, info="app-token-v1", length=32)
The info string MUST include a version suffix (-v1) so you can rotate without changing the IKM.
SHA-256(secret_a || secret_b) as a KDF. Concat-and-hash is not a KDF. → Rule: no-adhoc-kdfMD5(password || salt) anywhere. → Rule: no-adhoc-kdfenc_key and mac_key (review B2). → Rule: no-key-reuse-across-purposesUse when: You need to detect tampering of data you control end-to-end (sender and receiver share a key). If parties don't share a key, you need a signature, not a MAC — see Section 4.
HMAC-SHA-256 (libsodium crypto_auth_*, OpenSSL EVP_MAC family).
Rationale:
| Primitive | Pick when | |---|---| | Poly1305 (paired with XChaCha or AES) | You're already using AEAD — the AEAD's MAC (Poly1305 or GCM tag) IS your MAC. Don't add a second layer. | | KMAC-256 (SHA3 family) | You're already using SHA3 in the stack and don't want two hash families. | | BLAKE2b/BLAKE3 keyed mode | Performance is critical, you're not constrained by FIPS, and you're hashing large quantities. | | CMAC (AES-based) | Hardware AES is available, SHA-256 isn't, and you're in a constrained device. |
Authentication keys MUST NOT be the same as encryption keys. See KDF Section's domain separation pattern. If you find yourself writing HMAC(key, data) where key is also an encryption key, stop and derive a separate mac_key.
SHA-256(key || data) is not a MAC. Use HMAC. (Length-extension makes this a real attack on Merkle-Damgård hashes; SHA-256 is technically vulnerable, SHA3 isn't, but use HMAC anyway for hygiene.)SHA-256(data || shared_key) (review finding B2) — that's a custom keyed hash with the encryption key reused as the MAC key. Two violations: ad-hoc construction AND key reuse. Always two distinct primitives, two distinct keys.Use when: You need a third party to verify data integrity using only a public key. If both parties share a secret, use a MAC (Section 3) — signatures are slower, larger, and more dangerous when misused.
Ed25519 (libsodium crypto_sign_*).
Rationale:
| Primitive | Pick when |
|---|---|
| ECDSA P-256 (SECP256R1) | Interop with TLS, JWT (ES256), Web Crypto API, FIPS-validated stacks. Beware nonce-reuse → key recovery; use deterministic ECDSA (RFC 6979) when possible. |
| RSA-PSS-SHA-256, 3072-bit modulus | Interop with very old systems or strict FIPS profiles. Not for new design without justification. |
| Ed448 | Higher security level (~224-bit) than Ed25519 (~128-bit). Almost never needed; pick if your threat model includes very long-term archival. |
| Hybrid PQ (Ed25519 + ML-DSA) | Long-term archival signatures (>10 years) where post-quantum threat is in scope. Track NIST FIPS 204/205 finalization. |
SHA-256(manifest || derived_key) and labeled sign_dual — both wrong.)Use when: One-way fingerprint, content-addressing, deduplication, indexing.
| Use | Default | Notes | |---|---|---| | Content addressing, integrity | SHA-256 | Universal. Fast enough. | | Performance-critical, no FIPS | BLAKE3 | 5-10x faster than SHA-256, parallelizable, tree-mode for huge files | | FIPS or interop with newer stacks | SHA-512/256 or SHA3-256 | SHA-512/256 is faster on 64-bit CPUs than plain SHA-256. SHA3 sidesteps Merkle-Damgård length-extension. | | Anywhere | NOT MD5, NOT SHA-1 | Use only when interop demands (e.g., legacy file format). Document the constraint. |
HMAC-SHA-1 is acceptable as a PRF even in 2026. The known weaknesses of SHA-1 (collision resistance) do not apply when SHA-1 is used inside HMAC's construction (PRF security is what matters). Examples: YubiKey 5 OTP slot's challenge-response is hard-locked to HMAC-SHA-1 — using it is fine, but document the rationale.
That said: do not introduce HMAC-SHA-1 in greenfield code. Auditors flag it, and explaining why it's actually OK costs you reviewer time. Plan migration to a FIDO2 PRF (HMAC-SHA-256 underlying) when hardware permits.
| Use | Default | Don't |
|---|---|---|
| Cryptographic randomness | /dev/urandom (Linux), getrandom(2), libsodium randombytes_buf, OpenSSL RAND_bytes | Don't use rand(), Math.random(), srand(time(NULL)). |
| Secret generation, nonces, keys | Same as above | Don't seed a deterministic PRNG from the OS RNG and then use the deterministic stream — just use the OS RNG directly. |
| Reproducible "random" (tests) | A documented deterministic PRNG (xoshiro, ChaCha8) seeded explicitly | Mark clearly as not for security use. |
/dev/urandom and /dev/random are both fine for cryptographic use on modern Linux; the historical "random for keys, urandom for the rest" advice is obsolete. See man 7 random.
openssl enc and other command-line crypto toolsopenssl enc is a footgun-rich tool. It's also ubiquitous, so it shows up. Apply these rules:
openssl enc-pbkdf2 -iter <N> (N ≥ 600,000 for SHA-256). Without these flags, OpenSSL uses a single-MD5-iteration KDF (EVP_BytesToKey), which is essentially zero work to brute-force. → Rule: crypto-flag-verification-aes-256-gcm or another AEAD mode, not -aes-256-cbc. enc does support GCM as of OpenSSL 1.1.0+.-pass fd:N) or env var (-pass env:VAR), never via -k password (visible in ps).openssl enc for new codeFor a security product, use a small program that calls libsodium's crypto_secretstream_xchacha20poly1305_* directly, or use the age tool. openssl enc is a Swiss Army knife with many rusty blades; the failure modes (silent default to weak KDF, CBC default, no AEAD by default until you know to ask) are not what you want at the bottom of your security stack.
| Tool | Notes |
|---|---|
| gpg --symmetric | Use --s2k-mode 3 --s2k-count <high> and --s2k-cipher-algo AES256 --s2k-digest-algo SHA512. Default S2K count is too low. |
| age | Generally safe defaults. Pin a specific version and verify the binary signature. |
| 7z -p<pwd> | AES-256, but the password derivation is SHA-256 524288 times — equivalent to PBKDF2 with ~500k iterations, marginal. Acceptable for casual; not a security product. |
| zip --encrypt | Legacy ZIP encryption is broken. AE-2 (WinZip) is OK but rare. Use 7z or age instead. |
If you're reading this skill, you almost certainly should not be designing a new primitive. Use existing libraries that wrap reviewed implementations. The bar for "new primitive" is:
If you can't check all four boxes, use the suggested default.
This skill exists because a 2026-05-03 pre-production review caught real problems no STRIDE/OWASP pass would have surfaced. Walking through what the skill would flag:
openssl enc -aes-256-cbc without MACopenssl enc without explicit AEAD mode → BLOCK-aes-256-gcm OR wrap CBC ciphertext in HMAC-SHA-256(mac_key, iv || ct) with a separate mac_key from HKDFsign_dual produces SHA-256(manifest || derived_key)enc_key AND mac_key AND (if you genuinely need third-party verification) a real Ed25519 signing key. Three primitives, three keys, three purposes.openssl enc -aes-256-cbc -pass fd:0 without -pbkdf2 -iter-pbkdf2 -iter-pbkdf2 -iter 600000, then either migrate to -aes-256-gcm or layer HMAC. Better: replace with a libsodium-based small program.When this skill is invoked as part of a review, produce findings in this format:
### Finding: <SHORT-NAME>
**Severity**: BLOCK | HIGH | MEDIUM | LOW
**Location**: <file:line> or <component>
**Section**: <skill section, e.g., "Section 1: AEAD anti-pattern">
**Rule**: <enforcement rule, e.g., `no-unauthenticated-encryption`>
**Issue**: <one-paragraph explanation of what's wrong>
**Remediation**:
<concrete fix, with code snippet if helpful>
**References**:
- <RFC, NIST publication, or IACR paper>
When a cryptographic-decisions.md (template — see templates/cryptographic-decisions.md) is being created, the skill drives the Decision and Rationale sections.
aiwg-utils if installed):
no-unauthenticated-encryptionno-key-reuse-across-purposesno-adhoc-kdfcrypto-flag-verificationcryptographic-decisions.md — record-of-decision per primitiveapplied-cryptographer — narrow-scope reviewer that loads this skill on activationchain-of-trust-design — the other half of "is your security design sound"data-ai
Report which research-corpus radar sidecars are overdue for refresh. Computes staleness (days since last refresh vs the cadence window) for every radar, sorted most-overdue-first. Runs via `aiwg corpus radar-status`.
data-ai
Aggregate research-corpus radar sidecars into a corpus or per-cluster freshness report — totals, overdue count, per-cluster / per-GRADE / per-trajectory breakdowns, an overdue table, and per-radar rationale snippets. Runs via `aiwg corpus radar-report`.
testing
Scaffold radar/freshness sidecars for research-corpus REFs. Pulls title/authors from the citation sidecar and GRADE from the analysis doc, defaults the refresh cadence from GRADE and the cluster from a corpus-local map, and stamps documentation/radar/REF-XXX-radar.md. Runs via `aiwg corpus radar-init`.
data-ai
Compute an entity's publication trajectory — per-year paper counts, topic drift, hot-streak detection (≥3 consecutive A-grade years), and career phase. Runs via `aiwg corpus profile-temporal`.