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

110 lines
13 KiB
Markdown

# 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.
### Option A — Deprecate-and-route (recommended)
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.