Crypto quick reference
A lookup table for Cruft's two crypto APIs, WebCrypto (crypto.subtle) and node:crypto: which algorithm works for which operation (digest, sign/verify, encrypt/decrypt, derive, wrap, key generation), the supported key formats, worked examples, and the security limits.
This is the operable lookup surface for Cruft's two cryptographic APIs: WebCrypto (crypto.subtle) and node:crypto. It answers "which algorithm and operation can I use, and how."
For the deeper story, the zero-dependency crypto stack (no ring, no openssl, no sha2), the TLS 1.3 stack, and the full security boundaries, read TLS and cryptography. This page does not repeat that narrative; it complements it with the API matrix.
crypto.subtle algorithm x operation matrix
Y means the operation is supported and (where applicable) round-trips or verifies correctly. Blank means the algorithm does not participate in that operation per the WebCrypto spec.
| Algorithm | digest | sign / verify | encrypt / decrypt | derive | wrap / unwrap | generateKey |
|---|---|---|---|---|---|---|
| SHA-1 / 256 / 384 / 512 | ✅ | |||||
| HMAC | ✅ | ✅ | ||||
| ECDSA (P-256 / P-384 / P-521) | ✅ | ✅ | ||||
| RSASSA-PKCS1-v1_5 | ✅ | ✅ | ||||
| RSA-PSS | ✅ | ✅ | ||||
| Ed25519 | ✅ | ✅ | ||||
| AES-GCM | ✅ | ✅ | ✅ | |||
| AES-CBC | ✅ | ✅ | ||||
| AES-CTR | ✅ | ✅ | ||||
| RSA-OAEP | ✅ | ✅ | ✅ | |||
| AES-KW | ✅ | ✅ | ||||
| HKDF | ✅ | (import only) | ||||
| PBKDF2 | ✅ | (import only) | ||||
| ECDH (P-256) | ✅ | ✅ | ||||
| X25519 | ✅ | ✅ |
Notes on the frontier:
- ECDSA covers P-256, P-384, and P-521. All three curves generate keys and sign and verify (a P-384 key signs with
SHA-384and its signature verifies). One export limit remains: moving an EC key out asspkiorpkcs8is wired for P-256 only; a P-384 or P-521 key exports throws. If you need to serialize a wider curve, usejwkorraw. derivecovers bothderiveBitsandderiveKey. HKDF, PBKDF2, ECDH, and X25519 all produce raw bits and can derive aCryptoKey(deriveKeyinto anAES-GCMkey returns a usable secret key).- The 384/512 hashes share the 64-bit SHA-2 core. No MD5, no SHA-3.
Key formats
importKey / exportKey accept four formats, with real limits:
| Format | Works for | Limit |
|---|---|---|
raw | AES keys, HMAC, EC/OKP public points, HKDF/PBKDF2 seed | Broad |
jwk | symmetric and EC keys | An EC public key exports to a proper {kty:"EC", crv, x, y} |
spki | EC public, RSA public | EC spki export is wired for P-256 |
pkcs8 | EC private, RSA private | EC pkcs8 export is wired for P-256 |
EC key material for P-384 and P-521 stays usable in-process (sign, verify, derive); the export restriction is only about serializing those wider curves to the DER-based spki / pkcs8 encodings.
Worked example: ECDSA P-256 sign / verify round-trip
const enc = new TextEncoder();
const kp = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);
const msg = enc.encode("attack at dawn");
const sig = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" }, kp.privateKey, msg);
console.log("signature bytes:", sig.byteLength);
console.log("verify (genuine):",
await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, kp.publicKey, sig, msg));
const forged = new Uint8Array(sig); forged[0] ^= 0x01;
console.log("verify (tampered):",
await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, kp.publicKey, forged, msg));
Captured output:
signature bytes: 64
verify (genuine): true
verify (tampered): false
Forgery rejection is real: a single flipped byte fails verification.
Worked example: AES-GCM encrypt / decrypt
const enc = new TextEncoder(), dec = new TextDecoder();
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode("hello, cruft"));
console.log("ciphertext+tag bytes:", ct.byteLength);
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ct);
console.log("decrypted:", dec.decode(pt));
Captured output:
ciphertext+tag bytes: 28
decrypted: hello, cruft
The 28 bytes are 12 plaintext bytes plus the 16-byte GCM authentication tag.
node:crypto
The Node idiom is a host facade over the same primitives. The supported subset:
| Surface | Status | Notes |
|---|---|---|
createHash | ✅ | sha1/sha256/sha384/sha512 |
createHmac | ✅ | Same hash set |
randomBytes, randomFillSync, randomInt | ✅ | CSPRNG-backed |
randomUUID | ✅ | v4 |
createCipheriv / createDecipheriv (CBC) | ✅ | aes-256-cbc round-trips |
createCipheriv / createDecipheriv (CTR) | ✅ | aes-256-ctr round-trips |
createCipheriv / createDecipheriv (GCM) | ✅ | setAAD / getAuthTag / setAuthTag all work |
pbkdf2Sync | ✅ | |
scryptSync | ✅ | |
hkdfSync | ✅ | Synchronous HKDF; returns a plain object (not Node's ArrayBuffer). Async crypto.hkdf callback form also exists |
generateKeyPairSync | ✅ | rsa / ec / ed25519, SPKI/PKCS8 PEM export |
sign / verify | ✅ | EC P-256 |
publicEncrypt / privateDecrypt | ✅ | RSA-OAEP |
createECDH | ✅ | prime256v1 shared-secret agreement |
createDiffieHellman | Partial | Named groups / supplied prime work; createDiffieHellman(primeLength) throws prime generation not implemented |
timingSafeEqual | ✅ | Constant-time |
hkdfSync is implemented. The synchronous form returns a 32-byte (for SHA-256) result without throwing; the async crypto.hkdf(...) callback form is also present. Note the result is a plain object rather than Node's ArrayBuffer. WebCrypto's deriveBits with {name:"HKDF"} remains available as an alternative.
AES-GCM with AAD and auth-tag (node idiom)
import crypto from "node:crypto";
const key = crypto.randomBytes(32), iv = crypto.randomBytes(12);
const c = crypto.createCipheriv("aes-256-gcm", key, iv);
c.setAAD(Buffer.from("associated-data"));
const ct = Buffer.concat([c.update("secret", "utf8"), c.final()]);
const tag = c.getAuthTag();
const d = crypto.createDecipheriv("aes-256-gcm", key, iv);
d.setAAD(Buffer.from("associated-data"));
d.setAuthTag(tag);
console.log(Buffer.concat([d.update(ct), d.final()]).toString()); // secret
Tampering with the ciphertext before decryption throws on final() (AES-GCM: authentication ...); the AEAD tag check is enforced.
Security posture
Correctness holds: forgery and tamper rejection work (a flipped ECDSA signature byte fails, a mutated GCM ciphertext throws). But this is a young stack. Do not infer a hardened library. In brief, and covered fully in the TLS and cryptography security boundaries:
- Not constant-time in places. The bignum modexp,
mod_inv, and the Curve25519/Ed25519 field arithmetic are correct but not constant-time. Real constant-time comparison is used where protocol-critical (timingSafeEqual, PKCS1/OAEP/PSS verification, the GCM tag), but do not assume timing-safety for key material under a local-attacker model. rejectUnauthorizedis thread-global, not per-connection. Disabling certificate validation for one connection can affect concurrent ones on the same thread. This lives on the TLS client, not the primitive layer; see the subsystem page.
The random source is CSPRNG-backed on every platform (/dev/urandom on Unix, BCryptGenRandom on Windows, with no weak fallback), and the TLS client now validates the certificate chain, hostname/SAN, and expiry window. TLS interop against a wide range of real servers is still young, so on an untrusted network treat the client as unproven until it has been exercised against your peers.