7.6 KiB
Change: crypto-primitive-replaceability
Why
enterprise-crypto establishes @suaveplan/hash + @suaveplan/crypto at enterprise quality but keeps a direct runtime dependency on @noble/{hashes,curves,ciphers}. That delivers audited primitives today but leaves two properties on the table: supply-chain ownership (every byte of crypto code lives under @suaveplan/* with no external runtime dep) and per-primitive replaceability (ability to swap individual primitives from the vendored noble impl to an owned native impl incrementally, under a rigorous CI gate).
Those properties matter because primitive-level crypto is the one place where a single compromised upstream release — or a single npm-registry takeover — silently owns every downstream service we ship. Vendoring noble source into our tree and adding a per-primitive selector lets us: (a) freeze the exact bytes we trust; (b) review upstream diffs on our cadence instead of theirs; (c) replace individual primitives with owned implementations one at a time, without ever going through a big-bang rewrite; (d) leave cryptographically dangerous primitives (curves, AES, MACs) on the audited noble impl indefinitely if we choose — that is a legitimate terminal state.
This change builds on top of enterprise-crypto's primitives, it does not replace them. The public surface of @suaveplan/crypto does not change. What changes is where the underlying bytes come from and how we swap them.
What Changes
Package family (new)
- NEW
@suaveplan/crypto-curve— ECC primitives (secp256k1, Ed25519, X25519, P-256) as vendor-fork of@noble/curves, exposed behind per-primitive selector modules. Dangerous-tier CI gate. - NEW
@suaveplan/crypto-cipher— AEAD primitives (AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305) as vendor-fork of@noble/ciphers, same selector architecture. Dangerous-tier CI gate. - NEW
@suaveplan/crypto-utils— pure-composition utilities (hex, base64, base64url, base58, concatBytes, xor, I2OSP / OS2IP, Merkle helpers, constant-time-equal, zeroize, KAT harness, differential-fuzz harness, constant-time lint). No vendored code. Depends only on@suaveplan/error,@suaveplan/types,tslib,zod.
Package rename (no external consumers — genesis only)
- RENAME
@suaveplan/hash→@suaveplan/crypto-hashso the family shares thecrypto-*prefix. The one in-tree consumer (@suaveplan/crypto) is updated in the same change. No other workspace imports@suaveplan/hash.
Replaceability architecture (new, applied to crypto-hash / crypto-curve / crypto-cipher)
Every hash, curve, and AEAD primitive is refactored to the following layout:
src/primitives/<id>/
<id>.types.ts # Interface
<id>.ts # Public API — re-exports from <id>.select.ts
<id>.vectors.ts # RFC/NIST/Wycheproof KATs, used by tests
<id>.test.ts # Runs vectors × every impl + cross-impl equivalence
<id>.md
<id>.select.ts # GENERATED — do not edit
impl/
<id>.noble.ts # Vendored-noble impl (Phase 0: via @noble/* dep; Phase 0.5: vendored source)
<id>.native.ts # Owned impl (starts: throw PrimitiveNotImplemented)
A root crypto.config.ts maps primitiveId → 'noble' | 'native'. A pre-build codegen script (scripts/gen-crypto-selects.ts) emits each <id>.select.ts as a one-line re-export of the chosen impl, so Vite tree-shakes the unused impl out. Flipping a primitive is a one-line config change.
Modifications
- MODIFIED
@suaveplan/crypto—@noble/curvesand@noble/ciphersimports are rewired to@suaveplan/crypto-curveand@suaveplan/crypto-cipher.@noble/*removed fromdependencies. Public API surface unchanged. - MODIFIED
@suaveplan/crypto-hash(formerly@suaveplan/hash) — each primitive (sha256 / sha384 / sha512 / hmac / hkdf / pbkdf2 / argon2id / encoding) refactored into the replaceability layout. Public API surface unchanged.
CI gate for flipping a primitive to native
A primitive default SHALL NOT change from 'noble' to 'native' in crypto.config.ts unless the flip PR demonstrates:
- Full KAT suite (RFC / NIST / Wycheproof) passes on the native impl.
- Nightly differential fuzz (≥ 1M inputs over 7 days) shows zero mismatches vs noble.
- Constant-time lint: automated scan for secret-dependent branches / secret-indexed lookups + reviewer checklist +
dudect-style statistical timing test on Bun. - Dangerous tier (every primitive in
crypto-curveandcrypto-cipher, plus HMAC and any MAC-like primitive incrypto-hash): third-party cryptographic audit sign-off attached to the flip PR, referencing the exact commit SHA of the native impl. Hashes, HKDF, and PBKDF2 can flip at gate 3 without audit.
Vendoring hygiene (Phase 0.5 — after initial rollout)
@noble/{hashes,curves,ciphers}source copied intopackages/universal/crypto-{hash,curve,cipher}/src/vendored/at a pinned upstream commit, with LICENSE preserved.NOBLE_UPSTREAM.mdper package records upstream repo, commit SHA, sync date, reviewer, and files copied.scripts/sync-noble.tsfetches upstream, diffs, and opens a PR for human review — never auto-merges.- After vendoring:
@noble/*removed from every package'sdependencies;impl/<id>.noble.tsimports from../vendored/...relative paths.
Capabilities
New Capabilities
crypto-curve: ECC primitive family (secp256k1, Ed25519, X25519, P-256) with per-primitive noble/native selector and dangerous-tier audit gate.crypto-cipher: AEAD primitive family (AES-256-GCM, ChaCha20-Poly1305, XChaCha20-Poly1305) with per-primitive selector and dangerous-tier audit gate.crypto-utils: encoding + byte + test-harness utilities. Zero vendored code, pure-composition only.
Modified Capabilities
crypto-hash(renamed fromhash): primitives refactored into the per-primitive selector layout. Public API unchanged.crypto: curve and cipher primitive sources migrate from@noble/*to@suaveplan/crypto-{curve,cipher}.@noble/*removed fromdependencies. Public API unchanged.
Impact
- Consumers of
@suaveplan/crypto: no source changes required. Public API stable. - Consumers of
@suaveplan/hash: one-line import rewrite@suaveplan/hash→@suaveplan/crypto-hash. Codemod provided. - Bundle size: neutral in Phase 0. Tree-shakes per-primitive as natives land.
- CI: four new tasks —
crypto:kat(PR-blocking),crypto:fuzz(nightly),crypto:timing(nightly),crypto:noble-sync(weekly). - Phase 0 exit criteria: four packages publish at 0.0.0 with 95% coverage; every primitive selector defaults to
'noble'; every.native.tsstub is present;@suaveplan/cryptohas zero@noble/*imports in source and zero@noble/*entries in its directdependencies;crypto-hash/crypto-curve/crypto-cipherstill declare@noble/*as catalog deps (vendoring deferred to Phase 0.5). - Supersession: updates the
@noble/*dep decision documented inenterprise-cryptowithout reverting any of its other work.enterprise-cryptoremains the enterprise-quality baseline; this change layers replaceability on top.
Relationship to enterprise-crypto
enterprise-crypto is the enterprise-quality baseline for @suaveplan/crypto + @suaveplan/hash: OTel, structured errors, Zod schemas, 100% coverage, benchmark suite. This change preserves every one of those properties and adds the replaceability architecture. The two changes are composable — whichever ships first, the other rebases cleanly by updating import paths (for rename) and adding the impl/ and select.ts files per primitive (for architecture).