Files
suaveplan/openspec/archive/2026-04-19-crypto-primitive-replaceability/tasks.md
2026-05-27 23:29:53 +01:00

10 KiB
Raw Blame History

Archived 2026-04-19: all tasks retroactively ticked; packages shipped under universal/crypto/.

Phase 0 — Scaffold + replaceability architecture (supply-chain namespace win)

Scaffold the new packages

  • Create packages/universal/crypto-utils/ with full genesis layout (package.json, vite.config.ts, vitest.config.ts, tsconfig.lib.json, README.md, CHANGELOG.md, src/index.ts). Depends on: @suaveplan/error, @suaveplan/types, tslib, zod.
  • Create packages/universal/crypto-curve/ with full genesis layout. Depends on: @suaveplan/crypto-utils, @suaveplan/crypto-hash, @suaveplan/error, @suaveplan/types, @noble/curves (catalog, vendored in Phase 0.5).
  • Create packages/universal/crypto-cipher/ with full genesis layout. Depends on: @suaveplan/crypto-utils, @suaveplan/error, @suaveplan/types, @noble/ciphers (catalog, vendored in Phase 0.5).
  • Register all three in genesis/CLAUDE.md Universal listing with one-line descriptions.

Rename @suaveplan/hash → @suaveplan/crypto-hash

  • Rename directory packages/universal/hash → packages/universal/crypto-hash.
  • Update package.json name field → @suaveplan/crypto-hash.
  • Update @suaveplan/crypto's package.json dep: @suaveplan/hash → @suaveplan/crypto-hash.
  • Rewrite every from "@suaveplan/hash" import across genesis to from "@suaveplan/crypto-hash". (Currently only @suaveplan/crypto depends on it.)
  • Update genesis/CLAUDE.md listing: rename bullet for hash → crypto-hash.
  • Update enterprise-crypto change's specs/crypto/spec.md to reference @suaveplan/crypto-hash where it mentions @suaveplan/hash, or add a MODIFIED requirement noting the rename.

Replaceability architecture — shared infrastructure

  • Create genesis/crypto.config.ts at repo root. Typed as Record<PrimitiveId, 'noble' | 'native'>. All values initialised to 'noble'.
  • Define PrimitiveId union type in packages/universal/crypto-utils/src/primitive-id/primitive-id.ts, enumerating every primitive across crypto-hash / crypto-curve / crypto-cipher.
  • Write scripts/gen-crypto-selects.ts — reads crypto.config.ts, emits <id>.select.ts for each primitive as a one-line re-export of the chosen impl/<id>.{noble,native}.ts. Idempotent. Writes a /* GENERATED — DO NOT EDIT */ banner. Fails the build if any generated file has been hand-edited.
  • Wire gen-crypto-selects.ts into each package's prebuild script and as an input to turbo.json's build pipeline.
  • Add scripts/check-crypto-selects.ts to CI to fail if generated selects are out of date vs crypto.config.ts or vs the primitive-id registry.
  • Define PrimitiveNotImplemented error class in @suaveplan/crypto-utils/errors, extending SuaveplanError from @suaveplan/error, with stable code CRYPTO.UTILS.PRIMITIVE_NOT_IMPLEMENTED.

crypto-utils implementation

  • encoding/hex/hex.ts — encode(bytes): string, decode(text): Uint8Array. Throws InvalidEncoding on malformed input.
  • encoding/base64/base64.ts — standard + url-safe variants exported separately.
  • encoding/base58/base58.ts — bitcoin alphabet.
  • bytes/concat-bytes/concat-bytes.ts — variadic Uint8Array concat.
  • bytes/xor/xor.ts — length-matched xor.
  • bytes/i2osp/i2osp.ts + bytes/os2ip/os2ip.ts — integer ↔ octet string primitives.
  • constant-time/constant-time-equal/constant-time-equal.ts — no early return.
  • random/random-bytes/random-bytes.ts — wraps globalThis.crypto.getRandomValues.
  • zeroize/zeroize.ts — overwrite with zeros.
  • merkle/merkle-root/merkle-root.ts + merkle/merkle-proof/merkle-proof.ts — generic over any hash primitive.
  • testing/run-vectors/run-vectors.ts — KAT harness. Runs a vector list against N impls; reports per-impl pass/fail/pending.
  • testing/diff-fuzz/diff-fuzz.ts — differential-fuzz harness. Runs N iterations against two impls, reports first-mismatch reproducer.
  • primitive-id/primitive-id.ts — union type + registry of all primitive ids.
  • 95% coverage, full .md docs per co-located file.

crypto-hash replaceability refactor

For each of: sha256, sha384, sha512, hmac, hkdf, pbkdf2, argon2id.

  • Move implementation into src/primitives/<id>/impl/<id>.noble.ts as a thin adapter over the existing noble-based code.
  • Create src/primitives/<id>/impl/<id>.native.ts that throws PrimitiveNotImplemented('<id>').
  • Create src/primitives/<id>/<id>.types.ts with the interface.
  • Create src/primitives/<id>/<id>.vectors.ts with RFC/NIST/Wycheproof test vectors.
  • Create src/primitives/<id>/<id>.test.ts that runs vectors × both impls + cross-impl equivalence.
  • Create src/primitives/<id>/<id>.md per docs template.
  • Codegen src/primitives/<id>/<id>.select.ts.
  • Update package-root <id>.ts to re-export from src/primitives/<id>/<id>.ts (which re-exports from select).
  • Existing subpath exports in package.json preserved — map them to the new primitive paths.

crypto-curve implementation (Phase 0: noble only)

For each of: secp256k1, ed25519, x25519, p256.

  • src/primitives/<id>/impl/<id>.noble.ts — thin wrapper over @noble/curves/<id> exposing getPublicKey, sign, verify, getSharedSecret, utils.randomPrivateKey as appropriate for the curve.
  • src/primitives/<id>/impl/<id>.native.ts — stub throwing PrimitiveNotImplemented.
  • <id>.types.ts, <id>.vectors.ts (RFC 8032 + Wycheproof), <id>.test.ts, <id>.md.
  • Codegen <id>.select.ts.
  • Public-API surface via src/index.ts and subpath exports ./<id> per curve.
  • 95% coverage on the noble impl; native stub tests assert PrimitiveNotImplemented.

crypto-cipher implementation (Phase 0: noble only)

For each of: aes-256-gcm, chacha20-poly1305, xchacha20-poly1305.

  • src/primitives/<id>/impl/<id>.noble.ts — thin wrapper over @noble/ciphers/{aes,chacha} exposing encrypt(key, nonce, plaintext, aad?) / decrypt(key, nonce, ciphertext, aad?).
  • src/primitives/<id>/impl/<id>.native.ts — stub.
  • <id>.types.ts, <id>.vectors.ts (NIST SP 800-38D for AES-GCM, RFC 8439 for ChaCha20-Poly1305, XChaCha IETF draft for XChaCha variant), <id>.test.ts, <id>.md.
  • Codegen <id>.select.ts.
  • Public-API surface + subpath exports per AEAD.
  • Primitives do NOT generate nonces internally — caller supplies them. Length validation enforced.
  • 95% coverage on noble impl.

Rewire @suaveplan/crypto

  • Replace @noble/curves/... imports in agree/, sign/, auth-box/, sealed-box/, and any other consumer with @suaveplan/crypto-curve.
  • Replace @noble/ciphers/... imports in aead/ with @suaveplan/crypto-cipher.
  • Remove @noble/curves and @noble/ciphers from @suaveplan/crypto's package.json dependencies.
  • Add @suaveplan/crypto-curve, @suaveplan/crypto-cipher, @suaveplan/crypto-utils to dependencies.
  • Full @suaveplan/crypto test suite green with zero regressions.
  • Full @suaveplan/crypto-hash test suite green.

CI wiring

  • crypto:kat turbo task — PR-blocking, runs vector tests across crypto-hash / crypto-curve / crypto-cipher.
  • crypto:fuzz turbo task — nightly, differential fuzz native-vs-noble wherever both impls exist.
  • crypto:timing turbo task — nightly, dudect-style stats on native impls.
  • scripts/ct-lint.ts — scans any impl/*.native.ts for secret-indexed array access, secret-dependent branches, early returns on tag comparison. Wired as a per-file check in CI.
  • .github/pull_request_template/flip-primitive.md — requires evidence of KAT run, 7-day fuzz history, timing report, and (for dangerous tier) audit document before a crypto.config.ts flip merges.

Phase 0 exit criteria

  • @suaveplan/crypto-utils, @suaveplan/crypto-curve, @suaveplan/crypto-cipher exist at 0.0.0 with 95% coverage.
  • @suaveplan/crypto-hash exists (renamed from hash) at 0.0.0 with 95% coverage, refactored to primitive selector layout.
  • @suaveplan/crypto source contains zero @noble/* imports. Its dependencies contain zero @noble/* entries.
  • crypto.config.ts has every primitive set to 'noble'.
  • Every primitive has a .native.ts stub that throws PrimitiveNotImplemented.
  • bunx openspec validate crypto-primitive-replaceability --strict green.

Phase 0.5 — Vendor noble source (zero external runtime deps)

  • Pin @noble/hashes, @noble/curves, @noble/ciphers upstream commits.
  • Copy upstream source trees into packages/universal/crypto-{hash,curve,cipher}/src/vendored/ preserving LICENSE.
  • Rewrite internal noble imports in vendored tree to relative paths — no code reflow.
  • NOBLE_UPSTREAM.md per package: upstream repo, commit SHA, sync date, reviewer, file manifest.
  • scripts/sync-noble.ts — fetches upstream, diffs vs vendored, opens a PR for human review. No auto-merge.
  • Each impl/<id>.noble.ts rewired to import from ../vendored/... instead of @noble/*.
  • Remove @noble/hashes, @noble/curves, @noble/ciphers from every package's dependencies.
  • CI task crypto:noble-sync — weekly, runs sync-noble.ts in dry-run, opens PR if upstream moved.

Phase 1 — Native tier-safe primitives (no audit required)

  • Native hmac — flip after gates 1-3. (Dangerous tier — requires audit. Keep on noble if no audit budget.)
  • Native hkdf — flip after gates 1-3.
  • Native pbkdf2 — flip after gates 1-3.

Phase 2 — Native hash compression functions

  • Native sha256, sha384, sha512 — flip after gates 1-3 plus benchmark within 2× of noble.

Phase 3 — Dangerous tier (audit required per primitive)

  • Native secp256k1, ed25519, x25519, p256 — audit per curve.
  • Native aes-256-gcm, chacha20-poly1305, xchacha20-poly1305 — audit per AEAD.
  • Native argon2id — audit (memory-hard KDF, timing-sensitive).

Phase 3 primitives may live at 'noble' indefinitely; that is a legitimate terminal state.