23 KiB
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:
- Streaming AEAD (
crypto-cipher+crypto/aead):encryptStreamAead/decryptStreamAeadasync generators (255 L),aesGcmStreamingEngineandxchacha20StreamingEngineengines (35 L each),createEncryptionStream/createDecryptionStreamWeb-Streams adapters,streamEncrypt/streamDecryptlegacy aliases,collectStream,createStreamKey,createStreamEncryption(430 L ofstreaming.ts). - secp256k1 ECDSA recovery (
crypto-curve/primitives/secp256k1):signWithRecovery,recoverPublicKey,verifyWithRecovery— all three present in upstream (lines 74, 143, 243 ofsigning.ts) and absent from the genesis 5-method secp256k1 noble engine. - Device-key suite (
crypto-keys/keypair/device):createDeviceKey,deriveDeviceKeyFromMaster,generateDeviceUUID,extractDeviceMetadata,compareDeviceKeys,isValidDeviceKey, plusgenerateDerivedDeviceIdfor one-release back-compat (261 L ofdevice.ts). - scrypt KDF (
crypto-hash/primitives/scrypt): N/r/p parameter set, salt + outputLength, async via@noble/hashes/scrypt(53 L ofhashing.tslines L273-L325). - blake2b hashing (
crypto-hash/primitives/blake2b): variable output length, optional key, plushashPasswordBlake2b/verifyPasswordBlake2bconstant-time wrappers with the parameterised$blake2b$v=1$cf=N$salt$hashformat. - encryptWithPassword + deriveKeyFromPassword (
crypto/password): the high-level "password → encrypted blob" pattern (Argon2id-derived key + AEAD), 344 L ofpassword.ts. hash-passwordCLI bin (newpackages/crypto/crypto-binorcrypto/bin/hash-password.ts): a one-shot CLI that hashes a plaintext password to a verifier and exits 0/1 on verify (98 L ofbin/hash-password.ts).
What changes
@suaveplan/crypto-cipher
- ADDED:
streaming/streaming-aead.ts—encryptStreamAeadanddecryptStreamAeadasync generators with chunk-level header / footer tags,EncryptStreamAeadOptionsandDecryptStreamAeadOptions(chunk size, AAD, AbortSignal, optionalheadercallback for chunk-tag attestation). - ADDED:
streaming/streaming-aead-aes-gcm.ts—aesGcmStreamingEngine: StreamingAeadEngineadapter built on the existingaes-256-gcmprimitive; nonce per chunk derived from a 12-byte base nonce + 32-bit counter (RFC 5116-compatible). - ADDED:
streaming/streaming-aead-xchacha20.ts—xchacha20StreamingEngine: StreamingAeadEngineadapter built on the existingxchacha20-poly1305primitive; nonce per chunk derived from a 24-byte base nonce + 32-bit counter. - ADDED:
streaming/streaming-aead.types.ts—StreamingAeadEngineinterface,StreamingChunkTaginterface,StreamingHeaderinterface. - 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.tsexportssignWithRecovery(digest, privateKey),recoverPublicKey(signatureWithRecovery, digest), andverifyWithRecovery(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.tsmatching surface (libsodium does not ship secp256k1 — native engine throwsCRYPTO_CURVE.PRIMITIVE_NOT_IMPLEMENTEDper genesis convention). - MODIFIED:
secp256k1.selectre-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)withN,r,p,dkLenparameters; uses@noble/hashes/scrypt'sscryptAsync. - ADDED:
primitives/scrypt/scrypt.selectandprimitives/scrypt/impl/scrypt.noble/scrypt.noble.tsmatching the existing per-primitive selector layout. - ADDED:
primitives/blake2b/blake2b.ts—blake2b(message, options)withdkLen(1–64 bytes), optionalkey, optionalsalt, optionalpersonalization. - ADDED:
primitives/blake2b/blake2b.selectandprimitives/blake2b/impl/blake2b.noble/blake2b.noble.ts. - ADDED:
mac/blake2b-password/blake2b-password.ts—hashPasswordBlake2b(password, options)andverifyPasswordBlake2b(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 thegenerateDerivedDeviceIdback-compat alias. - ADDED:
keypair/device/device.types.ts—DeviceKey,MasterKey,DeviceKeyOptions,DeviceKeyDerivationOptions,DeviceIdOptions,DeviceMetadatainterfaces. - 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çadeencryptStreamAead/decryptStreamAeadthat selects the engine (xchacha20-poly1305oraes-256-gcm) from the opts argument and delegates tocrypto-cipher. Re-exportsaesGcmStreamingEngineandxchacha20StreamingEngine. - ADDED:
aead/streaming/web-streams.ts—createEncryptionStream(opts)andcreateDecryptionStream(opts)returningTransformStream<Uint8Array, Uint8Array>Web-Streams adapters;collectStream(stream),createStreamKey(rng?),createStreamEncryption(opts)helpers;streamEncrypt/streamDecryptaliases preserved for back-compat. - ADDED:
password/encrypt-with-password.ts—encryptWithPassword(password, plaintext, options?)anddecryptWithPassword(password, ciphertext, options?). Default KDF is Argon2id; opt into scrypt viakdf: "scrypt"in options. Default cipher isxchacha20-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-byteUint8Arraysuitable 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-binwhose only purpose is to ship thehash-passwordbin. Per-Suaveplan policy CLI surfaces live in their own package so they do not leak@suaveplan/cliruntime 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 blake2bopt-in) to stdout; with--verify <stored>reads--passwordand exits0on match,1on mismatch.
Out of scope
- Post-quantum signature primitives (Dilithium, SPHINCS+, ML-KEM key-encapsulation) — separate
crypto-pqc-*family of sub-packages already discussed inSTATUS.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 throughcrypto-hash/primitives/argon2idand migration belongs in a futureauthchange, not incrypto-hash. - Modifying the existing Argon2id primitive — the new
encryptWithPassworddefers to the existingargon2idprimitive without changing its signature. crypto-vaultintegration —crypto-vaultalready wraps Argon2id-hardened HD derivation; oncescryptlands incrypto-hash, a follow-up change can add akdf: "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 propagationpackages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.spec-conformance.test.ts—StreamingAeadEnginecontract pinned for both adapters; nonce-counter monotonicity; out-of-order chunks emitSTREAM_OUT_OF_ORDERpackages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.test.ts— extended with sign/recover/verify-with-recovery cases; recovery byte 0/1 round-trippackages/crypto/crypto-curve/src/primitives/secp256k1/secp256k1.spec-conformance.test.ts— invalid recovery byte emitsRECOVERY_BYTE_INVALID; missing recovery byte emitsRECOVERY_BYTE_MISSING; recovered key mismatch emitsRECOVERED_KEY_MISMATCHpackages/crypto/crypto-hash/src/primitives/scrypt/scrypt.test.ts— KAT round-trip vs upstream RFC 7914 vectors;dkLen32 / 64 / 128packages/crypto/crypto-hash/src/primitives/scrypt/scrypt.spec-conformance.test.ts— invalidN(non-power-of-two, < 2) emitsSCRYPT_INVALID_Npackages/crypto/crypto-hash/src/primitives/blake2b/blake2b.test.ts— RFC 7693 KAT round-trip; output length 1..64; keyed modepackages/crypto/crypto-hash/src/primitives/blake2b/blake2b.spec-conformance.test.ts— output > 64 emitsBLAKE2B_OUTPUT_TOO_LONG; key > 64 emitsBLAKE2B_KEY_TOO_LONGpackages/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 testpackages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.spec-conformance.test.ts— malformed stored hash emitsBLAKE2B_PASSWORD_FORMAT_INVALIDpackages/crypto/crypto-keys/src/keypair/device/device.test.ts—createDeviceKeyround-trip;deriveDeviceKeyFromMasterdeterministic;generateDeviceUUIDvalid v4;extractDeviceMetadataround-trip;compareDeviceKeysconstant-timepackages/crypto/crypto-keys/src/keypair/device/device.spec-conformance.test.ts— invalid master emitsDEVICE_INVALID_MASTER; corrupt key emitsDEVICE_INVALID_KEYpackages/crypto/crypto/src/aead/streaming/streaming-aead.test.ts— façade selects correct engine; round-trip perxchacha20-poly1305andaes-256-gcmpackages/crypto/crypto/src/aead/streaming/web-streams.test.ts— Web-Streams round-trip;collectStreamaggregates correctly;streamEncrypt/streamDecryptaliases match the canonical functionspackages/crypto/crypto/src/password/encrypt-with-password.test.ts— round-tripencrypt → decrypt; KDF selection (argon2idvsscrypt); blob header self-descriptive (different KDF params produce different headers; reading a v1 blob with a v2 reader emitsPASSWORD_BLOB_VERSION_UNSUPPORTED)packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts—kdf: "unknown"emitsPASSWORD_KDF_UNKNOWN; truncated blob emitsPASSWORD_BLOB_HEADER_INVALIDpackages/crypto/crypto/src/password/derive-key-from-password.test.ts— KDF selection; deterministic given fixed salt + params; output length is exactly 32 bytespackages/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--passwordarg emits non-zero exitpackages/crypto/crypto-bin/src/hash-password/hash-password.spec-conformance.test.ts— error path emitsCRYPTO.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/cryptoconsumers (crypto-vault,slsa,audit-logging,auth,streams,bitpacker-crypto,drive-plugin-chunks,data/packer) — none usesignWithRecovery/ 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-ciphercallers (crypto,crypto-sodium) — only consume the three primitive selectors (aes-256-gcm,chacha20-poly1305,xchacha20-poly1305). The newstreaming/*subpath is an isolated subtree. - Existing
crypto-curvecallers (crypto) — only consume the four primitive selectors. The three newsignWithRecovery/recoverPublicKey/verifyWithRecoveryexports do not collide with the existingsign/verify. - Existing
crypto-hashcallers (crypto,crypto-vault,crypto-curve) — consumesha256,sha384,sha512,hmac,hkdf,pbkdf2,argon2id. The newscryptandblake2bprimitives are isolated. - Existing
crypto-keyscallers (crypto-vault,auth) — consumeKeychainandkeypair/{generate,validate}. The newkeypair/devicesubtree 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 intoscryptKDF in a follow-up; verifying it still typechecks is required because it importscrypto-hashdirectly)@suaveplan/crypto-keys— [x] tests run / [x] docs reviewed / [ ] no action needed (we are addingkeypair/device/*to it in this change, so its own tests must run)@suaveplan/auth— [x] tests run / [ ] docs reviewed / [ ] no action needed (consumescrypto-keys, depends on us not breaking the existingkeypair/{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 onlycrypto/signfor in-toto attestation signing —signWithRecoveryis 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-utilsis unchanged butcrypto-hashandcrypto-cipherconsume 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.