Web Crypto

web-crypto implements the cryptographic primitives behind webcrypto.subtle and node:crypto: AES modes, SHA and HMAC, PBKDF2/HKDF/scrypt/Argon2, RSA, ECDSA, ECDH, Ed25519, X25519, and the CSPRNG. The bignum and elliptic-curve math are its own. Correctness is checked against published vectors and Node; the asymmetric paths are not yet constant-time.

require('node:crypto'), a call to webcrypto.subtle, and the bytes come back. Whose AES ran, whose big-integer math signed the ECDSA blob, and where the random nonce came from rarely enter the picture. On Node the answer is OpenSSL (and historically ring / RustCrypto in the Rust ecosystem you may be porting from), decades of audits, FIPS validation, and CAVP/Wycheproof vectors, inherited without asking.

Cruft's web-crypto crate answers the same calls and replaces ring / RustCrypto with its own implementation. So the question that never comes up on Node is the one that matters here: who wrote this AES, and what has it been checked against?

Alpha, unaudited. web-crypto (Cruft 0.0.10) stands in for ring / RustCrypto. It has had no external audit. Do not rely on it in production, and do not feed it adversarial input expecting the hardening a mature library gives you.

Correctness and hardening are separate properties

A crypto library must do two distinct things. It must be correct (AES-GCM produces the ciphertext the spec says) and it must be hardened (it does not leak the key through timing, does not leave secrets in freed memory, does not hand you predictable randomness). OpenSSL gives both in one require. In this crate they come apart, and they are tracked separately throughout this page.

What it implements

web-crypto is a single crate covering the WebCrypto and node:crypto primitive floor:

  • Symmetric: AES-GCM, AES-CBC, AES-CTR, AES-KW.
  • Hash / MAC: SHA-1, SHA-256, SHA-384, SHA-512, HMAC.
  • KDF: PBKDF2, HKDF, scrypt, Argon2.
  • Asymmetric: RSA (OAEP, PSS, PKCS1-v1_5), ECDSA and ECDH over P-256 / P-384 / P-521, Ed25519, X25519.
  • Randomness: a single get_random_values entry backing getRandomValues, randomUUID, keygen nonces, and IVs.

The big integers, the prime field, and the elliptic-curve math are all implemented in-crate (a BigUInt type), not delegated. That is the point of the crate, and also the reason the Limitations section matters.

Correctness: what has a test behind it

Correctness is the half that is well-covered. The primitives are checked against published Known-Answer Tests, not only self-round-trips:

  • Symmetric, hash, HMAC, and KDF primitives are checked against published vectors (FIPS 180 for SHA, SP 800-38A/38D for AES modes, RFC 5869 for HKDF, plus PBKDF2, scrypt, and Argon2 vectors).
  • Ed25519 and X25519 are checked against their RFC 8032 / RFC 7748 vectors.
  • The asymmetric schemes have a differential test against Node's own WebCrypto: it checks Cruft RSA-PSS, RSASSA-PKCS1-v1_5, and ECDSA P-256/384/521 signatures with Node-imported keys, compares ECDH P-256 against Node deriveBits, and decrypts a Node-produced RSA-OAEP ciphertext.

That is a real correctness floor: interoperability with Node, not only internal self-consistency.

Hardening: the half not to assume

Side-channel resistance does not come free here, and the crate says so plainly.

  • The asymmetric private-key operations (RSA rsadp, RSA-PKCS1/PSS signing, RSA-OAEP decrypt, ECDSA, ECDH, X25519, Ed25519) run over variable-time in-crate big-integer, field, and EC arithmetic. They are correctness-oriented, not constant-time with respect to private exponents, scalars, or nonces. Under a co-resident or remote-timing threat model this is a real exposure. ring / OpenSSL harden these paths; this crate does not yet.
  • The one genuinely constant-time comparison, timing_safe_equal, exists and is used for tag/MAC checks.

So correctness has vectors behind it; timing hardening on the asymmetric side is a documented gap, not a delivered property.

What has been addressed, and what "addressed" means

Several security-relevant issues have been worked through. What each one delivered varies, because "fixed" does not always mean "hardened":

  • Windows CSPRNG. getRandomValues once read /dev/urandom on Unix and had no Windows entropy source, risking silent weak randomness on Windows (the worst class of bug: every key and nonce flows from here). All production random bytes now route through get_random_values, with a cfg(windows) BCryptGenRandom binding using BCRYPT_USE_SYSTEM_PREFERRED_RNG. Unix still uses /dev/urandom; unsupported targets fail loudly with ErrorKind::Unsupported rather than returning non-CSPRNG bytes; EC keygen no longer opens /dev/urandom directly. One residual evidence gap: the fix was validated on a macOS host, so a Windows run is still the outstanding platform witness.
  • ECDSA nonce. Signing once took a caller-supplied nonce with no RFC 6979 fallback, and the inversion was variable-time on the secret. Deterministic ECDSA signing (HMAC-DRBG-shaped nonce derived from the private scalar plus message hash, with masking/rejection instead of modulo bias) is now the default for P-256/384/521; TLS CertificateVerify, node:crypto EC signing, and the WebCrypto helper all route through it. Caller-supplied nonce APIs survive only as low-level/vector surfaces.
  • AES table-timing. The public AES modes no longer default to the T-table encryptor (GCM's counter stream uses the byte-oriented aes_encrypt_block path; the T-table function survives only as a test-guarded parity artifact). The variable-time asymmetric exposure is now documented at each private-operation entry point rather than silently inherited. A constant-time arithmetic rewrite is future work.
  • RSA-OAEP padding oracle. rsa_oaep_decrypt once had data-dependent early returns while its comment claimed branchlessness (a Manger-style signal). It now scans the entire DB suffix, accumulates the checks, and emits one collapsed verdict.
  • Zeroization. A volatile-wipe primitive (zeroize_bytes / zeroize_words) wipes HMAC pads and transient buffers, AES round-key schedules zero on drop, and BigUInt owns a Drop that wipes its limbs (covering RSA private exponents/CRT factors, EC / X25519 / Ed25519 scalars, ECDSA nonces).
  • External asymmetric vectors. Once self-round-trip only; now the Node WebCrypto differential above provides interop evidence. Broader Wycheproof/CAVP ingestion remains a quality expansion, not a gating absence.

Limitations

  • Timing hardening on the asymmetric side is documented, not delivered. RSA, ECDSA, ECDH, X25519, and Ed25519 private-key math is variable-time. The crate stopped implying otherwise; it did not make the arithmetic constant-time. ring / OpenSSL lead decisively here.
  • The correctness floor is interop-tested, not exhaustively fuzzed. The Node differential and published KATs are real, but there is no Wycheproof edge-case corpus and no CAVP response-file harness. Systematic edge-case behavior (malformed encodings, boundary lengths, invalid curve points beyond the on-curve check) is far less proven than in the library it replaces.
  • The Windows CSPRNG fix lacks a Windows execution witness. The binding and test gate are in place; they were not yet run on a Windows target. The Unix path and the loud-failure fallback are the parts to trust today.
  • This is one 0.0.10 crate versus decades of ring / RustCrypto / OpenSSL. No external audit, no formal verification, its own bignum and EC math.