Files
suaveplan/openspec/changes/archive/2026-05-03-crypto-completion/proposal.md
2026-05-27 23:29:53 +01:00

170 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Crypto completion — streaming AEAD, recovery, device keys, scrypt, blake2b, password helpers, hash-password CLI
**Packages**: `@suaveplan/crypto`, `@suaveplan/crypto-cipher`, `@suaveplan/crypto-curve`, `@suaveplan/crypto-hash`, `@suaveplan/crypto-keys`
**Status**: PROPOSED
**Type**: feature + spec-conformance
## Why
The 2026-05-02 audit of the genesis crypto stack against `../suaveplan/packages/universal/crypto/src` identified seven confirmed feature gaps that the upstream package shipped but the genesis sub-packages do not. None of these are MAJOR architecture changes — they are concrete capabilities (streaming AEAD, ECDSA recovery, device keys, scrypt KDF, blake2b hashing + password helpers, password-driven AEAD wrappers, and the `hash-password` CLI) that downstream consumers (drive, network, audit-logging, slsa) currently work around or block on. Genesis policy (CLAUDE.md §Workflow rule 5) says "preserve the full public surface during rewrites — trimming is OK ONLY when code is duplicated elsewhere in-repo or can be replaced by an existing `@suaveplan/*` library", and none of these seven features are duplicated by anything that already landed.
Closing all seven in one coordinated pass is preferable to seven small changes because (a) they share validation utilities and `crypto-utils` helpers that have to land at the same revision to avoid mid-port circular pins, (b) the streaming AEAD façade in `crypto/aead` and the engine in `crypto-cipher/primitives` are linked — adding either alone leaves the public surface incoherent, and (c) the password helpers (`encryptWithPassword`, `deriveKeyFromPassword`) sit on top of the new scrypt + blake2b primitives, so they cannot ship until those primitives exist.
The seven gaps:
1. **Streaming AEAD** (`crypto-cipher` + `crypto/aead`): `encryptStreamAead` / `decryptStreamAead` async generators (255 L), `aesGcmStreamingEngine` and `xchacha20StreamingEngine` engines (35 L each), `createEncryptionStream` / `createDecryptionStream` Web-Streams adapters, `streamEncrypt` / `streamDecrypt` legacy aliases, `collectStream`, `createStreamKey`, `createStreamEncryption` (430 L of `streaming.ts`).
2. **secp256k1 ECDSA recovery** (`crypto-curve/primitives/secp256k1`): `signWithRecovery`, `recoverPublicKey`, `verifyWithRecovery` — all three present in upstream (lines 74, 143, 243 of `signing.ts`) and absent from the genesis 5-method secp256k1 noble engine.
3. **Device-key suite** (`crypto-keys/keypair/device`): `createDeviceKey`, `deriveDeviceKeyFromMaster`, `generateDeviceUUID`, `extractDeviceMetadata`, `compareDeviceKeys`, `isValidDeviceKey`, plus `generateDerivedDeviceId` for one-release back-compat (261 L of `device.ts`).
4. **scrypt KDF** (`crypto-hash/primitives/scrypt`): N/r/p parameter set, salt + outputLength, async via `@noble/hashes/scrypt` (53 L of `hashing.ts` lines L273-L325).
5. **blake2b hashing** (`crypto-hash/primitives/blake2b`): variable output length, optional key, plus `hashPasswordBlake2b` / `verifyPasswordBlake2b` constant-time wrappers with the parameterised `$blake2b$v=1$cf=N$salt$hash` format.
6. **encryptWithPassword + deriveKeyFromPassword** (`crypto/password`): the high-level "password → encrypted blob" pattern (Argon2id-derived key + AEAD), 344 L of `password.ts`.
7. **`hash-password` CLI bin** (new `packages/crypto/crypto-bin` or `crypto/bin/hash-password.ts`): a one-shot CLI that hashes a plaintext password to a verifier and exits 0/1 on verify (98 L of `bin/hash-password.ts`).
## What changes
### `@suaveplan/crypto-cipher`
- ADDED: `streaming/streaming-aead.ts` — `encryptStreamAead` and `decryptStreamAead` async generators with chunk-level header / footer tags, `EncryptStreamAeadOptions` and `DecryptStreamAeadOptions` (chunk size, AAD, AbortSignal, optional `header` callback for chunk-tag attestation).
- ADDED: `streaming/streaming-aead-aes-gcm.ts` — `aesGcmStreamingEngine: StreamingAeadEngine` adapter built on the existing `aes-256-gcm` primitive; nonce per chunk derived from a 12-byte base nonce + 32-bit counter (RFC 5116-compatible).
- ADDED: `streaming/streaming-aead-xchacha20.ts` — `xchacha20StreamingEngine: StreamingAeadEngine` adapter built on the existing `xchacha20-poly1305` primitive; nonce per chunk derived from a 24-byte base nonce + 32-bit counter.
- ADDED: `streaming/streaming-aead.types.ts` — `StreamingAeadEngine` interface, `StreamingChunkTag` interface, `StreamingHeader` interface.
- ADDED: error codes `CRYPTO_CIPHER.STREAM_TAG_MISMATCH`, `CRYPTO_CIPHER.STREAM_OUT_OF_ORDER`, `CRYPTO_CIPHER.STREAM_ABORTED`, `CRYPTO_CIPHER.STREAM_INVALID_NONCE_BASE`.
### `@suaveplan/crypto-curve`
- ADDED: `primitives/secp256k1/impl/secp256k1.noble/secp256k1.noble.ts` exports `signWithRecovery(digest, privateKey)`, `recoverPublicKey(signatureWithRecovery, digest)`, and `verifyWithRecovery(signatureWithRecovery, digest, expectedPublicKey)`. The signature-with-recovery byte layout is the canonical 65-byte form (64-byte compact signature + 1 recovery byte).
- ADDED: `primitives/secp256k1/impl/secp256k1.native/secp256k1.native.ts` matching surface (libsodium does not ship secp256k1 — native engine throws `CRYPTO_CURVE.PRIMITIVE_NOT_IMPLEMENTED` per genesis convention).
- MODIFIED: `secp256k1.select` re-exports the three new functions.
- ADDED: error codes `CRYPTO_CURVE.RECOVERY_BYTE_MISSING`, `CRYPTO_CURVE.RECOVERY_BYTE_INVALID`, `CRYPTO_CURVE.RECOVERED_KEY_MISMATCH`.
### `@suaveplan/crypto-hash`
- ADDED: `primitives/scrypt/scrypt.ts` — `scrypt(password, salt, options)` with `N`, `r`, `p`, `dkLen` parameters; uses `@noble/hashes/scrypt`'s `scryptAsync`.
- ADDED: `primitives/scrypt/scrypt.select` and `primitives/scrypt/impl/scrypt.noble/scrypt.noble.ts` matching the existing per-primitive selector layout.
- ADDED: `primitives/blake2b/blake2b.ts` — `blake2b(message, options)` with `dkLen` (1–64 bytes), optional `key`, optional `salt`, optional `personalization`.
- ADDED: `primitives/blake2b/blake2b.select` and `primitives/blake2b/impl/blake2b.noble/blake2b.noble.ts`.
- ADDED: `mac/blake2b-password/blake2b-password.ts` — `hashPasswordBlake2b(password, options)` and `verifyPasswordBlake2b(storedHash, password)`, using the `$blake2b$v=1$cf=<costFactor>$<saltHex>$<hashHex>` MCF-style format and constant-time comparison.
- ADDED: error codes `CRYPTO_HASH.SCRYPT_INVALID_N`, `CRYPTO_HASH.BLAKE2B_OUTPUT_TOO_LONG`, `CRYPTO_HASH.BLAKE2B_KEY_TOO_LONG`, `CRYPTO_HASH.BLAKE2B_PASSWORD_FORMAT_INVALID`.
### `@suaveplan/crypto-keys`
- ADDED: `keypair/device/device.ts` — `createDeviceKey`, `deriveDeviceKeyFromMaster`, `generateDeviceUUID`, `extractDeviceMetadata`, `compareDeviceKeys`, `isValidDeviceKey`, plus the `generateDerivedDeviceId` back-compat alias.
- ADDED: `keypair/device/device.types.ts` — `DeviceKey`, `MasterKey`, `DeviceKeyOptions`, `DeviceKeyDerivationOptions`, `DeviceIdOptions`, `DeviceMetadata` interfaces.
- ADDED: error codes `CRYPTO_KEYS.DEVICE_INVALID_MASTER`, `CRYPTO_KEYS.DEVICE_INVALID_KEY`, `CRYPTO_KEYS.DEVICE_DERIVATION_FAILED`.
### `@suaveplan/crypto`
- ADDED: `aead/streaming/streaming-aead.ts` — high-level façade `encryptStreamAead` / `decryptStreamAead` that selects the engine (`xchacha20-poly1305` or `aes-256-gcm`) from the opts argument and delegates to `crypto-cipher`. Re-exports `aesGcmStreamingEngine` and `xchacha20StreamingEngine`.
- ADDED: `aead/streaming/web-streams.ts` — `createEncryptionStream(opts)` and `createDecryptionStream(opts)` returning `TransformStream<Uint8Array, Uint8Array>` Web-Streams adapters; `collectStream(stream)`, `createStreamKey(rng?)`, `createStreamEncryption(opts)` helpers; `streamEncrypt` / `streamDecrypt` aliases preserved for back-compat.
- ADDED: `password/encrypt-with-password.ts` — `encryptWithPassword(password, plaintext, options?)` and `decryptWithPassword(password, ciphertext, options?)`. Default KDF is Argon2id; opt into scrypt via `kdf: "scrypt"` in options. Default cipher is `xchacha20-poly1305`. Output is a self-describing binary header (`magic`, `kdf`, `kdfParams`, `cipher`, `salt`, `nonce`) followed by the ciphertext.
- ADDED: `password/derive-key-from-password.ts` — `deriveKeyFromPassword(password, salt, options?)` returning a 32-byte `Uint8Array` suitable for use as a symmetric key. KDF selection (`argon2id` / `scrypt` / `pbkdf2`) is via the options bundle.
- ADDED: error codes `CRYPTO.STREAM_AEAD_HEADER_INVALID`, `CRYPTO.PASSWORD_KDF_UNKNOWN`, `CRYPTO.PASSWORD_BLOB_HEADER_INVALID`, `CRYPTO.PASSWORD_BLOB_VERSION_UNSUPPORTED`.
### `@suaveplan/crypto-bin` (new package)
- ADDED: a thin CLI package `packages/crypto/crypto-bin` whose only purpose is to ship the `hash-password` bin. Per-Suaveplan policy CLI surfaces live in their own package so they do not leak `@suaveplan/cli` runtime deps into the universal-tier `@suaveplan/crypto`. Depends on `@suaveplan/cli`, `@suaveplan/crypto-hash`, `@suaveplan/error`. Bin: `suaveplan-hash-password`. Reads a password from stdin or `--password`, writes a verifier hash (Argon2id by default, `--algo blake2b` opt-in) to stdout; with `--verify <stored>` reads `--password` and exits `0` on match, `1` on mismatch.
## Out of scope
- **Post-quantum signature primitives** (Dilithium, SPHINCS+, ML-KEM key-encapsulation) — separate `crypto-pqc-*` family of sub-packages already discussed in `STATUS.md`. Adding PQC primitives requires a separate engine adapter substrate that is larger than this entire change.
- **FIDO2 / WebAuthn helpers** — already covered by browser-tier deps in the genesis app shells; no upstream parity gap here.
- **HSM / KMS adapters** (CloudHSM, AWS KMS, GCP KMS) — operational concern, not a library gap; tracked separately under the audit-logging adapter family.
- **Argon2id parameter migration helper** — upstream ships `migrateArgon2idParams(oldHash, newOptions)` for re-hashing on login. Genesis already routes Argon2id verification through `crypto-hash/primitives/argon2id` and migration belongs in a future `auth` change, not in `crypto-hash`.
- **Modifying the existing Argon2id primitive** — the new `encryptWithPassword` defers to the existing `argon2id` primitive without changing its signature.
- **`crypto-vault` integration** — `crypto-vault` already wraps Argon2id-hardened HD derivation; once `scrypt` lands in `crypto-hash`, a follow-up change can add a `kdf: "scrypt"` option to the vault. Out of scope here to keep this change focused on the seven enumerated gaps.
## Source mapping
Genesis `enterprise/crypto.md` is a generic enterprise-quality template (no per-feature requirements), so source mapping cites the upstream package source directly.
| Requirement | Source file | Source lines | Notes |
|---|---|---|---|
| `encryptStreamAead` / `decryptStreamAead` async generators | `packages/universal/crypto/src/symmetric/streaming/streaming-aead.ts` | L11-L255 | Verbatim port; thread `AbortSignal` through |
| `aesGcmStreamingEngine` adapter | `packages/universal/crypto/src/symmetric/streaming/streaming-aead-aes-gcm.ts` | L9-L35 | Verbatim |
| `xchacha20StreamingEngine` adapter | `packages/universal/crypto/src/symmetric/streaming/streaming-aead-xchacha20.ts` | L9-L35 | Verbatim |
| `createEncryptionStream` / `createDecryptionStream` / `streamEncrypt` / `streamDecrypt` / `collectStream` / `createStreamKey` / `createStreamEncryption` | `packages/universal/crypto/src/symmetric/streaming/streaming.ts` | L23-L430 | Verbatim port; preserve `ChaCha20Stream` / `AESStream` named exports |
| `signWithRecovery` (secp256k1) | `packages/universal/crypto/src/asymmetric/signing/signing.ts` | L74-L120 | Verbatim |
| `recoverPublicKey` (secp256k1) | `packages/universal/crypto/src/asymmetric/signing/signing.ts` | L143-L235 | Verbatim |
| `verifyWithRecovery` (secp256k1) | `packages/universal/crypto/src/asymmetric/signing/signing.ts` | L243-L307 | Verbatim |
| `createDeviceKey` | `packages/universal/crypto/src/keys/device/device.ts` | L32-L49 | Verbatim |
| `deriveDeviceKeyFromMaster` | `packages/universal/crypto/src/keys/device/device.ts` | L56-L89 | Verbatim |
| `generateDeviceUUID` | `packages/universal/crypto/src/keys/device/device.ts` | L96-L155 | Verbatim |
| `extractDeviceMetadata` | `packages/universal/crypto/src/keys/device/device.ts` | L157-L181 | Verbatim |
| `isValidDeviceKey`, `compareDeviceKeys` | `packages/universal/crypto/src/keys/device/device.ts` | L182-L245 | Verbatim |
| `generateDerivedDeviceId` back-compat alias | `packages/universal/crypto/src/keys/device/device.ts` | L249-L260 | Replace upstream `@deprecated` JSDoc with prose ("Backwards-compatibility alias for `generateDeviceUUID`.") per CLAUDE.md §2 |
| `scrypt` async KDF | `packages/universal/crypto/src/core/hashing/hashing.ts` | L273-L325 | Verbatim (delegates to `@noble/hashes/scrypt::scryptAsync`) |
| `blake2b` hash | `packages/universal/crypto/src/core/hashing/hashing.ts` | L1-L60 (imports + helpers used) | Re-host as a top-level primitive in `crypto-hash` |
| `hashPasswordBlake2b` | `packages/universal/crypto/src/core/hashing/hashing.ts` | L617-L666 | Verbatim |
| `verifyPasswordBlake2b` | `packages/universal/crypto/src/core/hashing/hashing.ts` | L672-L745 | Verbatim |
| `encryptWithPassword` | `packages/universal/crypto/src/symmetric/password/password.ts` | L54-L113 | Verbatim port; replace `PasswordError` plain class with `SuaveplanError` code `CRYPTO.PASSWORD_*` |
| `deriveKeyFromPassword` | `packages/universal/crypto/src/symmetric/password/password.ts` | L172-L209 | Verbatim |
| `hash-password` CLI | `packages/universal/crypto/src/bin/hash-password.ts` | L1-L98 | Verbatim port; replace direct `process.exit` with `cli.exit(code)` from `@suaveplan/cli` |
### Requirements with no source line
| Requirement | Justification |
|---|---|
| `StreamingAeadEngine` interface (formal) | Upstream uses a duck-typed object; genesis convention is a named interface in a `*.types.ts` file (see CLAUDE.md §11 "No nested object literals in interfaces"). |
| Error codes mapped to `@suaveplan/error` (`CRYPTO_CIPHER.STREAM_*`, `CRYPTO_CURVE.RECOVERY_*`, `CRYPTO_KEYS.DEVICE_*`, `CRYPTO_HASH.SCRYPT_*` / `BLAKE2B_*`, `CRYPTO.PASSWORD_*`) | Genesis-wide policy: every thrown error routes through `@suaveplan/error` with a stable code. Upstream throws plain `Error` / `PasswordError`. |
| `crypto-bin` as its own package | Universal-tier `@suaveplan/crypto` cannot depend on `@suaveplan/cli` (CLI is browser/server-tier and pulls in `@suaveplan/readline`). Splitting the CLI out keeps `crypto` zero-side-effect and tree-shakeable. |
| `encryptWithPassword` self-describing binary header | Upstream stores `salt + ciphertext` only; consumers had to know the iteration count externally. Genesis adds a versioned 16-byte header (`magic[4] \| version[1] \| kdf[1] \| cipher[1] \| reserved[1] \| saltLen[2] \| nonceLen[2] \| kdfParamsLen[4]`) so a blob is self-descriptive and rotation-safe. |
| `decryptWithPassword` companion to `encryptWithPassword` | Upstream's `decryptWithPassword` (L115-L160 of `password.ts`) is included. Listed here only because the proposal text earlier groups gap 6 as one verb; this row exists so reviewers can see both halves are covered. |
| `signWithRecovery` / `recoverPublicKey` / `verifyWithRecovery` exposed at `secp256k1.noble.ts` rather than the higher-level `signing.ts` | Upstream's `signing.ts` is a multi-key façade that pulls `BaseKeySchema` plumbing genesis hasn't ported. Putting the three functions on the curve primitive (where they belong cryptographically) avoids a circular dep with `crypto-keys`. |
## Verification approach
Each spec delta requirement carries a `**Verified by:**` line pointing at concrete `it(...)` titles. The test files that will exist after this change ships:
- `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.test.ts` — chunk round-trip per engine; tag mismatch detection; abort propagation
- `packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.spec-conformance.test.ts` — `StreamingAeadEngine` contract pinned for both adapters; nonce-counter monotonicity; out-of-order chunks emit `STREAM_OUT_OF_ORDER`
- `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts` — extended with sign/recover/verify-with-recovery cases; recovery byte 0/1 round-trip
- `packages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.spec-conformance.test.ts` — invalid recovery byte emits `RECOVERY_BYTE_INVALID`; missing recovery byte emits `RECOVERY_BYTE_MISSING`; recovered key mismatch emits `RECOVERED_KEY_MISMATCH`
- `packages/crypto/crypto-hash/src/primitives/scrypt/scrypt.test.ts` — KAT round-trip vs upstream RFC 7914 vectors; `dkLen` 32 / 64 / 128
- `packages/crypto/crypto-hash/src/primitives/scrypt/scrypt.spec-conformance.test.ts` — invalid `N` (non-power-of-two, < 2) emits `SCRYPT_INVALID_N`
- `packages/crypto/crypto-hash/src/primitives/blake2b/blake2b.test.ts` — RFC 7693 KAT round-trip; output length 1..64; keyed mode
- `packages/crypto/crypto-hash/src/primitives/blake2b/blake2b.spec-conformance.test.ts` — output > 64 emits `BLAKE2B_OUTPUT_TOO_LONG`; key > 64 emits `BLAKE2B_KEY_TOO_LONG`
- `packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.test.ts` — round-trip hash → verify; salt freshness across calls; constant-time comparison observable via timing-side-channel test
- `packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.spec-conformance.test.ts` — malformed stored hash emits `BLAKE2B_PASSWORD_FORMAT_INVALID`
- `packages/crypto/crypto-keys/src/keypair/device/device.test.ts` — `createDeviceKey` round-trip; `deriveDeviceKeyFromMaster` deterministic; `generateDeviceUUID` valid v4; `extractDeviceMetadata` round-trip; `compareDeviceKeys` constant-time
- `packages/crypto/crypto-keys/src/keypair/device/device.spec-conformance.test.ts` — invalid master emits `DEVICE_INVALID_MASTER`; corrupt key emits `DEVICE_INVALID_KEY`
- `packages/crypto/crypto/src/aead/streaming/streaming-aead.test.ts` — façade selects correct engine; round-trip per `xchacha20-poly1305` and `aes-256-gcm`
- `packages/crypto/crypto/src/aead/streaming/web-streams.test.ts` — Web-Streams round-trip; `collectStream` aggregates correctly; `streamEncrypt` / `streamDecrypt` aliases match the canonical functions
- `packages/crypto/crypto/src/password/encrypt-with-password.test.ts` — round-trip `encrypt → decrypt`; KDF selection (`argon2id` vs `scrypt`); blob header self-descriptive (different KDF params produce different headers; reading a v1 blob with a v2 reader emits `PASSWORD_BLOB_VERSION_UNSUPPORTED`)
- `packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts` — `kdf: "unknown"` emits `PASSWORD_KDF_UNKNOWN`; truncated blob emits `PASSWORD_BLOB_HEADER_INVALID`
- `packages/crypto/crypto/src/password/derive-key-from-password.test.ts` — KDF selection; deterministic given fixed salt + params; output length is exactly 32 bytes
- `packages/crypto/crypto-bin/src/hash-password/hash-password.test.ts` — Argon2id mode end-to-end (hash, then verify, exit 0); blake2b mode end-to-end; bad password verify exits 1; missing `--password` arg emits non-zero exit
- `packages/crypto/crypto-bin/src/hash-password/hash-password.spec-conformance.test.ts` — error path emits `CRYPTO.HASH_PASSWORD_CLI_*` codes (one per known failure mode)
## Migration impact
This change is **additive** — it does not modify any existing public surface. Verification:
- Existing `@suaveplan/crypto` consumers (`crypto-vault`, `slsa`, `audit-logging`, `auth`, `streams`, `bitpacker-crypto`, `drive-plugin-chunks`, `data/packer`) — none use `signWithRecovery` / streaming AEAD / device keys / scrypt / blake2b / `encryptWithPassword` / hash-password CLI today (the symbols don't exist on genesis). New exports therefore have zero callers at land time.
- Existing `crypto-cipher` callers (`crypto`, `crypto-sodium`) — only consume the three primitive selectors (`aes-256-gcm`, `chacha20-poly1305`, `xchacha20-poly1305`). The new `streaming/*` subpath is an isolated subtree.
- Existing `crypto-curve` callers (`crypto`) — only consume the four primitive selectors. The three new `signWithRecovery` / `recoverPublicKey` / `verifyWithRecovery` exports do not collide with the existing `sign` / `verify`.
- Existing `crypto-hash` callers (`crypto`, `crypto-vault`, `crypto-curve`) — consume `sha256`, `sha384`, `sha512`, `hmac`, `hkdf`, `pbkdf2`, `argon2id`. The new `scrypt` and `blake2b` primitives are isolated.
- Existing `crypto-keys` callers (`crypto-vault`, `auth`) — consume `Keychain` and `keypair/{generate,validate}`. The new `keypair/device` subtree is isolated.
A `bunx turbo build typecheck test --filter=...@suaveplan/crypto*` sweep runs in §9 of `tasks.md` to verify all dependents compile and test green before archive.
## Impact
### Affected dependents
Run `bunx turbo ls --filter=...@suaveplan/crypto` to enumerate. For each dependent, check one box:
- `@suaveplan/crypto-sodium` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes only the existing AEAD / sign / agree façade; new streaming / password helpers are additive subpaths)
- `@suaveplan/crypto-browser` — [ ] tests run / [ ] docs reviewed / [x] no action needed (same — consumes existing AEAD / sign / agree façade)
- `@suaveplan/crypto-vault` — [x] tests run / [x] docs reviewed / [ ] no action needed (vault may want to opt into `scrypt` KDF in a follow-up; verifying it still typechecks is required because it imports `crypto-hash` directly)
- `@suaveplan/crypto-keys` — [x] tests run / [x] docs reviewed / [ ] no action needed (we are *adding* `keypair/device/*` to it in this change, so its own tests must run)
- `@suaveplan/auth` — [x] tests run / [ ] docs reviewed / [ ] no action needed (consumes `crypto-keys`, depends on us not breaking the existing `keypair/{generate,validate}` exports)
- `@suaveplan/audit-logging` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes only the AEAD façade for log encryption — new streaming subpath does not collide)
- `@suaveplan/slsa` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes only `crypto/sign` for in-toto attestation signing — `signWithRecovery` is a new export, not a modification)
- `@suaveplan/streams` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes only the existing primitive selectors)
- `@suaveplan/bitpacker-crypto` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes the AEAD façade for packed-bit encryption — new exports do not collide)
- `@suaveplan/drive-plugin-chunks` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes the AEAD façade for at-rest chunk encryption — new streaming subpath is a candidate for a follow-up port, not a regression risk here)
- `@suaveplan/data/packer` — [ ] tests run / [ ] docs reviewed / [x] no action needed (consumes the AEAD façade only)
- `@suaveplan/crypto-utils` — [x] tests run / [ ] docs reviewed / [ ] no action needed (foundation — its own coverage gate must pass after this change; `crypto-utils` is unchanged but `crypto-hash` and `crypto-cipher` consume it, so its tests must still pass)
For the four [x] dependents above (`crypto-vault`, `crypto-keys`, `auth`, `crypto-utils`), running their full test suite is part of §9 of `tasks.md`.