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.

Algorithmdigestsign / verifyencrypt / decryptderivewrap / unwrapgenerateKey
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-384 and its signature verifies). One export limit remains: moving an EC key out as spki or pkcs8 is wired for P-256 only; a P-384 or P-521 key exports throws. If you need to serialize a wider curve, use jwk or raw.
  • derive covers both deriveBits and deriveKey. HKDF, PBKDF2, ECDH, and X25519 all produce raw bits and can derive a CryptoKey (deriveKey into an AES-GCM key 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:

FormatWorks forLimit
rawAES keys, HMAC, EC/OKP public points, HKDF/PBKDF2 seedBroad
jwksymmetric and EC keysAn EC public key exports to a proper {kty:"EC", crv, x, y}
spkiEC public, RSA publicEC spki export is wired for P-256
pkcs8EC private, RSA privateEC 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:

SurfaceStatusNotes
createHashsha1/sha256/sha384/sha512
createHmacSame hash set
randomBytes, randomFillSync, randomIntCSPRNG-backed
randomUUIDv4
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
hkdfSyncSynchronous HKDF; returns a plain object (not Node's ArrayBuffer). Async crypto.hkdf callback form also exists
generateKeyPairSyncrsa / ec / ed25519, SPKI/PKCS8 PEM export
sign / verifyEC P-256
publicEncrypt / privateDecryptRSA-OAEP
createECDHprime256v1 shared-secret agreement
createDiffieHellmanPartialNamed groups / supplied prime work; createDiffieHellman(primeLength) throws prime generation not implemented
timingSafeEqualConstant-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.
  • rejectUnauthorized is 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.