Files
suaveplan/openspec/archive/2026-05-02-bitpacker-crypto-port/proposal.md
2026-05-27 23:29:53 +01:00

13 KiB

Bitpacker crypto port — close the 1,906-line gap

Package: @suaveplan/bitpacker-crypto Status: SUPERSEDED by bitpacker-crypto-rewrite (2026-05-02). Both folders archive together so the decision history reads cleanly. Type: spec-conformance + refactor (deprecate-and-route) OR full port

Why this change was superseded — A deeper consumer scan run after this proposal was authored found 8+ consumers across genesis and upstream manually re-implementing the same crypto-with-bitpacker patterns inline (sign-then-pack, encrypt-pack-frame, schema-versioned envelopes, schema fingerprints, stateless verifiers). The "Option A: deprecate-and-route" framing this change recommended is correct for the upstream provider abstraction but does not address the inline duplication. bitpacker-crypto-rewrite (2026-05-02) replaces this change with a consumer-driven design that consolidates the patterns into five bit-packer-aware primitives: SignedBitSchema, EncryptedBitSchema, EnvelopeSchema, SchemaFingerprint, ChunkVerifier. The SUPERSEDED.md file and audit test created by this change are PRESERVED under the rewrite (revised framing only); the bitpacker-modular-restructure parity-table flips this change introduced are PRESERVED (the upstream provider abstraction is still intentionally not ported).

Why

The 2026-05-02 audit of bitpacker-modular-restructure confirmed five files of upstream's bitpacker/src/crypto/ are absent from genesis: crypto/provider/provider.ts (296 L), crypto/registry/registry.ts (370 L), and three adapters under crypto/adapters/{noop,libsodium,elliptic}/ (337 + 331 + 572 L). Total: ~1,906 L of provider abstraction. The parity table in bitpacker-modular-restructure/proposal.md flags every one of them 🚫. That gap has to be closed before this restructure can archive cleanly.

But before authoring a 1,906-line port, this proposal documents three findings that change what "closing the gap" should mean:

  1. No upstream consumer depends on BaseCryptoProvider / CryptoRegistry outside the bitpacker package itself. Searched grep -rlE "BaseCryptoProvider|CryptoRegistry|LibsodiumCryptoProvider|EllipticCryptoProvider" /mnt/s/web/suaveplan/packages | grep -v /bitpacker/ → only one hit, and that hit is a documentation file (packages/universal/overview/bitpacker.md), not a code import. Upstream's crypto provider abstraction was speculative API surface; nothing actually called it.
  2. Genesis already ships the underlying primitives in five focused packages: @suaveplan/crypto (multi-algorithm façade), @suaveplan/crypto-sodium (libsodium engine), @suaveplan/crypto-browser (WebCrypto engine), @suaveplan/crypto-keys (keypair generation + Keychain), @suaveplan/crypto-vault (BIP39 + Argon2id HD vault + ChainSigner). Upstream's LibsodiumCryptoProvider (line 1) literally does import from "@suaveplan/crypto" — it is not duplicate primitives, it is a provider-pattern wrapper around them.
  3. Genesis already ships chunked AEAD at a higher level in @suaveplan/packer (chunked aead/verifier/unpacker/packer/manifest, 99 tests at 100% coverage). Any caller wanting "encrypt my bitpacked bytes" should use packer, not invent a parallel provider/registry.

A literal port would re-add a provider abstraction that no consumer asked for, while shadowing the crypto family that already exists. That is the opposite of the genesis directive.

Decision required

Two options. The proposal recommends Option A; Option B is fully scoped here so a reviewer can flip the call at review without restarting the change.

Mark the upstream bitpacker-crypto provider/registry/adapters as superseded and route any future bitpacker-internal crypto need through the existing @suaveplan/crypto family. Specifically:

  • DO NOT port BaseCryptoProvider, CryptoRegistry, LibsodiumCryptoProvider, EllipticCryptoProvider, NoOpCryptoProvider, or their type companions.
  • DO NOT port generateNonce / concatBytes / validateKeyLength / constantTimeEqual / secureWipe from upstream provider.ts — every one of them already exists in @suaveplan/crypto-utils (bytes.concat, bytes.equalsConstantTime, random.bytes, bytes.zeroize).
  • DO add a SUPERSEDED.md file at packages/bitpacker/bitpacker-crypto/SUPERSEDED.md recording the decision, the upstream surface that's intentionally not ported, and the genesis replacement for each one.
  • DO close the parity-table 🚫 rows in bitpacker-modular-restructure/proposal.md by flipping their status to ⛔ "intentionally not ported" with a pointer to this change.
  • KEEP the existing bitpacker-crypto checksum surface (crc32, adler32, xxhash32, appendCrc32, verifyAndStripCrc32) — that's not the gap; it's a separate, non-controversial concern.

Why recommended: closes the parity gap honestly, avoids 1,906 L of duplicate abstraction, lets future bitpacker callers reach for the crypto family that's already documented in genesis, and matches the genesis directive of "no duplicate libraries."

Cost: zero implementation lines. The change is a decision document plus an audit-table flip.

Option B — Full port

Port the upstream crypto subsystem verbatim into bitpacker-crypto, with the genesis-required hardening:

  • ADDED: BaseCryptoProvider extends EventEmitter abstract class — encrypt / decrypt / sign / verify / deriveKey with event emission and capability advertisement. Replace upstream's plain Error throws with BitpackerError extending @suaveplan/error.
  • ADDED: LibsodiumCryptoProvider — wraps @suaveplan/crypto-sodium (genesis package, not the upstream @suaveplan/crypto import); supports XChaCha20-Poly1305, AES-GCM, ed25519 sign / verify, X25519 agree, Argon2id key derivation.
  • ADDED: EllipticCryptoProvider — wraps @suaveplan/crypto-curve (genesis package); supports secp256k1 / ed25519 / p256 sign / verify; key generation; ECDH agree.
  • ADDED: NoOpCryptoProvider — for tests and "encryption disabled" code paths; passes data through unchanged with provenance markers; createSlowProvider, createUnreliableProvider factories for fault-injection tests.
  • ADDED: CryptoRegistry extends EventEmitter — registers and resolves providers by name; emits provider:registered, provider:unregistered, provider:replaced events; supports a global registry singleton.
  • ADDED: CryptoCapabilities interface and EncryptedPayload, SignatureResult, KeyDerivationOptions, EncryptConfig, DecryptConfig, SignConfig, VerifyConfig types.
  • ADDED: BITPACKER.CRYPTO_* codes (CRYPTO_KEY_LENGTH_INVALID, CRYPTO_PROVIDER_NOT_INITIALIZED, CRYPTO_PROVIDER_NOT_REGISTERED, CRYPTO_CAPABILITY_UNSUPPORTED, CRYPTO_DECRYPT_FAILED, CRYPTO_VERIFY_FAILED).
  • REMOVED (vs upstream): the Math.random() fallback in generateNonce (line 244 upstream) — genesis routes through @suaveplan/crypto-utils::random.bytes which uses crypto.getRandomValues only and throws on environments without it.
  • REMOVED (vs upstream): the duplicate concatBytes / constantTimeEqual / secureWipe — re-export from @suaveplan/crypto-utils instead.

Why this might still be the right call: if genesis intends to ship a swappable crypto provider abstraction at the bitpacker layer specifically (e.g. so CompressedBitPacker can advertise "encryption enabled / disabled" without coupling to @suaveplan/crypto-vault), the port encodes that decision durably. It would also un-block any future feature where bitpacker's wire format wants to declare "this payload is signed by provider X with capabilities Y."

Cost: roughly 8 implementation files at ~200 L each, ~12 spec-conformance test files, plus the hardening and documentation; estimate ~3,500 L net new (port + hardening + tests + docs). Coverage gate is 100/100/100/100 per the genesis quality bar.


The remainder of this proposal is the spec delta if Option A is chosen. If Option B is chosen at review, the delta will be re-authored to match the ADDED/MODIFIED/REMOVED lists above and the source-mapping table will cite the upstream files line-by-line.

What changes (Option A)

  • ADDED: packages/bitpacker/bitpacker-crypto/SUPERSEDED.md — decision record listing every upstream surface intentionally not ported and naming its genesis replacement.
  • MODIFIED: openspec/changes/bitpacker-modular-restructure/proposal.md parity table — flip the five 🚫 rows for crypto-provider, crypto-registry, crypto-adapter-noop, crypto-adapter-libsodium, crypto-adapter-elliptic from "🚫 not ported" to "⛔ intentionally superseded by @suaveplan/crypto*; see bitpacker-crypto-port."
  • MODIFIED: openspec/changes/bitpacker-modular-restructure/tasks.md — tick §14.6 with a pointer to this change.
  • MODIFIED: packages/bitpacker/bitpacker-crypto/README.md — add a "Crypto provider needs" section pointing at @suaveplan/crypto, crypto-sodium, crypto-browser, crypto-vault, packer.
  • ADDED: packages/bitpacker/bitpacker-crypto/SUPERSEDED.test.ts — pins the decision (SUPERSEDED.md exists; the upstream symbol names listed in it do NOT exist as exports of @suaveplan/bitpacker-crypto).

Out of scope

  • The crc32 / adler32 / xxhash32 checksum helpers already in bitpacker-crypto — those are not part of the upstream provider abstraction and are not affected by this decision.
  • appendAdler32 / verifyAdler32 helper restoration (tracked in bitpacker-modular-restructure §14.7).
  • Adding event-emitting wrappers around @suaveplan/crypto in any other package — if a downstream consumer genuinely needs event emission on every encrypt / decrypt, that is a separate proposal scoped to that consumer.
  • Authoring openspec/specs/bitpacker/spec.md (the canonical core spec) — covered by a follow-up change.

Source mapping

Every requirement in the spec delta below cites upstream-file evidence. Because Option A is a deliberate scope-down, the mapping is "what is intentionally NOT ported" rather than "what we ported".

Upstream surface (intentionally not ported) File Line range Genesis replacement
BaseCryptoProvider extends EventEmitter crypto/provider/provider.ts L26-L232 None — direct calls to @suaveplan/crypto façade
generateNonce(length) crypto/provider/provider.ts L237-L248 @suaveplan/crypto-utils::random.bytes(length)
concatBytes(...arrays) crypto/provider/provider.ts L253-L262 @suaveplan/crypto-utils::bytes.concat(...arrays)
validateKeyLength(key, expected, type) crypto/provider/provider.ts L267-L277 Caller-side guard; no central helper needed (genesis throws coded errors at the boundary)
constantTimeEqual(a, b) crypto/provider/provider.ts L282-L289 @suaveplan/crypto-utils::bytes.equalsConstantTime(a, b)
secureWipe(buffer) crypto/provider/provider.ts L294-L296 @suaveplan/crypto-utils::bytes.zeroize(buffer)
LibsodiumCryptoProvider + createXChaCha20Provider + createAesGcmProvider crypto/adapters/libsodium/libsodium.ts L94-L335 @suaveplan/crypto-sodium (libsodium engine) + @suaveplan/crypto::aead
EllipticCryptoProvider + 3 factory helpers crypto/adapters/elliptic/elliptic.ts L36-L572 @suaveplan/crypto-curve (secp256k1 / ed25519 / p256)
NoOpCryptoProvider + createTestProvider + createSlowProvider + createUnreliableProvider crypto/adapters/noop/noop.ts L35-L335 Test-only; consumers can stub @suaveplan/crypto calls directly with @suaveplan/testing fakes
CryptoRegistry extends EventEmitter + createCryptoRegistry + getGlobalRegistry crypto/registry/registry.ts L21-L370 None — no upstream consumer relied on it; the registry was self-referential

Requirements with no source line

Requirement Justification
SUPERSEDED.md audit trail Genesis policy: every removed surface needs a documented reason. Recording the decision durably in-repo is cheaper than re-litigating it at every future audit.
Test that pins the absence of upstream symbols Defensive: prevents a future contributor from accidentally re-introducing the abstraction without re-running the decision.

Verification approach

  • packages/bitpacker/bitpacker-crypto/SUPERSEDED.test.ts — asserts BaseCryptoProvider, CryptoRegistry, LibsodiumCryptoProvider, EllipticCryptoProvider, NoOpCryptoProvider, generateNonce, concatBytes, validateKeyLength, constantTimeEqual, secureWipe are NOT exported from @suaveplan/bitpacker-crypto. Each missing symbol is one it(...).
  • A grep-based check (run in tasks.md §3.3) confirming nothing in packages/ imports the names above from @suaveplan/bitpacker-crypto.

Migration impact

  • Internal: zero. The bitpacker-crypto package today only exports checksum helpers; no genesis package depends on the provider abstraction.
  • Future consumers: anyone wanting to encrypt or sign packed bytes should reach for @suaveplan/crypto (façade), @suaveplan/crypto-sodium / crypto-browser (engines), @suaveplan/crypto-vault (HD vault + ChainSigner), or @suaveplan/packer (chunked AEAD). The decision record makes that routing visible.
  • Upstream port aspirants — anyone reading bitpacker-modular-restructure/proposal.md and seeing the 🚫 rows will be redirected to this decision instead of attempting a literal port.