Files
2026-05-27 23:29:53 +01:00

21 KiB
Raw Permalink Blame History

Tasks — Crypto completion

Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.

1. Spec authoring

  • 1.1 Read ../suaveplan/packages/universal/crypto/src/symmetric/streaming/streaming-aead.ts, streaming.ts, streaming-aead-aes-gcm.ts, streaming-aead-xchacha20.ts end-to-end
  • 1.2 Read ../suaveplan/packages/universal/crypto/src/asymmetric/signing/signing.ts (focus on lines L74-L307 — sign / recover / verify with recovery)
  • 1.3 Read ../suaveplan/packages/universal/crypto/src/keys/device/device.ts end-to-end
  • 1.4 Read ../suaveplan/packages/universal/crypto/src/core/hashing/hashing.ts (focus on scrypt, blake2b, hashPasswordBlake2b, verifyPasswordBlake2b — lines L273-L745)
  • 1.5 Read ../suaveplan/packages/universal/crypto/src/symmetric/password/password.ts end-to-end
  • 1.6 Read ../suaveplan/packages/universal/crypto/src/bin/hash-password.ts end-to-end
  • 1.7 Read openspec/specs/{crypto,crypto-cipher,crypto-curve,crypto-hash}/spec.md (existing folded specs)
  • 1.8 Read openspec/AGENTS.md Sections 13 (spec-conformance audit) and 14 (source mapping for migrations)
  • 1.9 Draft proposal.md with explicit ADDED / MODIFIED / REMOVED lists per affected package and a ## Source mapping table citing upstream line ranges
  • 1.10 Draft spec deltas under specs/<cap>/spec.md for crypto-cipher, crypto-curve, crypto-hash, crypto-keys, crypto
  • 1.11 Every requirement in every spec delta carries a **Verified by:** line naming concrete it(...) titles
  • 1.12 openspec change validate crypto-completion exits with no errors

2. @suaveplan/crypto-cipher — streaming AEAD

  • 2.1 Create packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.types.ts defining StreamingAeadEngine, StreamingChunkTag, StreamingHeader, EncryptStreamAeadOptions, DecryptStreamAeadOptions
  • 2.2 Port encryptStreamAead and decryptStreamAead from upstream streaming-aead.ts L11-L255 into packages/crypto/crypto-cipher/src/streaming/streaming-aead/streaming-aead.ts
  • 2.3 Port aesGcmStreamingEngine from upstream streaming-aead-aes-gcm.ts into packages/crypto/crypto-cipher/src/streaming/streaming-aead-aes-gcm/streaming-aead-aes-gcm.ts
  • 2.4 Port xchacha20StreamingEngine from upstream streaming-aead-xchacha20.ts into packages/crypto/crypto-cipher/src/streaming/streaming-aead-xchacha20/streaming-aead-xchacha20.ts
  • 2.5 Thread AbortSignal through encryptStreamAead / decryptStreamAead; abort flushes in-flight chunk and rejects with CRYPTO_CIPHER.STREAM_ABORTED
  • 2.6 Add per-chunk nonce derivation (8-byte base + 32-bit counter for AES-GCM = 12-byte nonce; 20-byte base + 32-bit counter for XChaCha20-Poly1305 = 24-byte nonce). Counter overflow at 2^32 chunks emits CRYPTO_CIPHER.STREAM_OUT_OF_ORDER
  • 2.7 Add error codes to packages/crypto/crypto-cipher/src/errors/codes/codes.ts: STREAM_TAG_MISMATCH, STREAM_OUT_OF_ORDER, STREAM_ABORTED, STREAM_INVALID_NONCE_BASE
  • 2.8 Wire streaming/streaming-aead/streaming-aead.test.ts and streaming-aead.spec-conformance.test.ts
  • 2.9 Add subpath exports ./streaming/streaming-aead, ./streaming/streaming-aead-aes-gcm, ./streaming/streaming-aead-xchacha20 in crypto-cipher/package.json
  • 2.10 Sibling .md per implementation file (≥200 words, basic + advanced examples, full API reference)

3. @suaveplan/crypto-curve — secp256k1 ECDSA recovery

  • 3.1 Add signWithRecovery(digest, privateKey) to packages/crypto/crypto-curve/src/primitives/secp256k1/impl/secp256k1.noble/secp256k1.noble.ts. Signature is 65 bytes: 64-byte compact signature + 1 recovery byte
  • 3.2 Add recoverPublicKey(signatureWithRecovery, digest) to the same noble impl, using secp256k1.Signature.fromBytes(sig, "compact").addRecoveryBit(rec).recoverPublicKey(digest)
  • 3.3 Add verifyWithRecovery(signatureWithRecovery, digest, expectedPublicKey) to the same noble impl, recovering then comparing to expectedPublicKey in constant time
  • 3.4 Add matching stubs to secp256k1.native/secp256k1.native.ts that throw PrimitiveNotImplemented (per genesis convention — libsodium does not ship secp256k1)
  • 3.5 Re-export the three new functions via secp256k1.select/secp256k1.select.ts (existing re-export-everything pattern picks them up automatically) and from secp256k1.ts / secp256k1/index.ts
  • 3.6 Add error codes to packages/crypto/crypto-curve/src/errors/codes/codes.ts: RECOVERY_BYTE_MISSING, RECOVERY_BYTE_INVALID, RECOVERED_KEY_MISMATCH
  • 3.7 Extend secp256k1.test.ts with sign / recover / verify-with-recovery cases and secp256k1.spec-conformance.test.ts with the three error-code paths
  • 3.8 Update secp256k1.md to document the three new functions and the 65-byte signature layout

4. @suaveplan/crypto-hash — scrypt + blake2b + blake2b password

  • 4.1 Create packages/crypto/crypto-hash/src/primitives/scrypt/scrypt.ts with scrypt(password, salt, options: { N, r, p, dkLen }). Use @noble/hashes/scrypt::scryptAsync
  • 4.2 Add per-primitive selector layout: scrypt.select/scrypt.select.ts, impl/scrypt.noble/scrypt.noble.ts. Match the existing argon2id / pbkdf2 shape
  • 4.3 Validate N is a power of two ≥ 2; reject with HASH.SCRYPT_INVALID_N
  • 4.4 Create packages/crypto/crypto-hash/src/primitives/blake2b/blake2b.ts with blake2b(message, options: { dkLen, key?, salt?, personalization? }). Use @noble/hashes/blake2b
  • 4.5 Add per-primitive selector layout: blake2b.select/blake2b.select.ts, impl/blake2b.noble/blake2b.noble.ts
  • 4.6 Reject dkLen > 64 with HASH.BLAKE2B_OUTPUT_TOO_LONG; reject key.length > 64 with HASH.BLAKE2B_KEY_TOO_LONG
  • 4.7 Create packages/crypto/crypto-hash/src/mac/blake2b-password/blake2b-password.ts with hashPasswordBlake2b(password, options: { costFactor?, salt? }) and verifyPasswordBlake2b(storedHash, password). Format: $blake2b$v=1$cf=<costFactor>$<saltHex>$<hashHex>
  • 4.8 Constant-time comparison in verifyPasswordBlake2b using crypto-utils::constantTimeEqual
  • 4.9 Add error codes to packages/crypto/crypto-hash/src/errors/codes/codes.ts: SCRYPT_INVALID_N, BLAKE2B_OUTPUT_TOO_LONG, BLAKE2B_KEY_TOO_LONG, BLAKE2B_PASSWORD_FORMAT_INVALID
  • 4.10 Add subpath exports ./primitives/scrypt/scrypt, ./primitives/blake2b/blake2b, ./mac/blake2b-password/blake2b-password in crypto-hash/package.json
  • 4.11 Sibling .md per implementation file

5. @suaveplan/crypto-keys — device-key suite

DEFERRED — crypto-keys package does not exist on genesis. Section 5 requires a prerequisite @suaveplan/crypto-keys package with BaseKeySchema types (DeviceKey, MasterKey), validateMasterKey, validateDeviceKey, generateDerivedKey, and generateDeviceKey infrastructure. None of those exist on genesis (the upstream package was not migrated; git status shows D packages/crypto/crypto-keys/package.json). Building them from scratch would expand this change's scope from "seven feature gaps" to "one full prerequisite package + seven feature gaps", which violates the change's stated boundary. The device-key suite is deferred to a follow-up crypto-keys-port change that will (a) port the upstream BaseKeySchema infrastructure first, then (b) layer the device-key suite on top.

The crypto-keys spec delta (specs/crypto-keys/spec.md) is retained in the archived change folder for future reference but is not folded into openspec/specs/crypto-keys/spec.md (no such spec exists yet).

  • 5.1 Create packages/crypto/crypto-keys/src/keypair/device/device.types.ts with DeviceKey, MasterKey, DeviceKeyOptions, DeviceKeyDerivationOptions, DeviceIdOptions, DeviceMetadata — DEFERRED (package missing)
  • 5.2 Port createDeviceKey — DEFERRED (package missing)
  • 5.3 Port deriveDeviceKeyFromMaster — DEFERRED (package missing)
  • 5.4 Port generateDeviceUUID — DEFERRED (package missing)
  • 5.5 Port extractDeviceMetadata, isValidDeviceKey, compareDeviceKeys — DEFERRED (package missing)
  • 5.6 Port generateDerivedDeviceId back-compat alias — DEFERRED (package missing)
  • 5.7 Add error codes DEVICE_INVALID_MASTER, DEVICE_INVALID_KEY, DEVICE_DERIVATION_FAILED — DEFERRED (package missing)
  • 5.8 Add subpath export ./keypair/device/device — DEFERRED (package missing)
  • 5.9 Sibling .md per implementation file — DEFERRED (package missing)
  • 5.10 Wire keypair/device/device.test.ts and device.spec-conformance.test.ts — DEFERRED (package missing)

6. @suaveplan/crypto — streaming AEAD façade + password helpers

  • 6.1 Create packages/crypto/crypto/src/aead/streaming/streaming-aead.ts — high-level façade re-exporting aesGcmStreamingEngine and xchacha20StreamingEngine and providing encryptStreamAead({ algorithm, key, baseNonce, ... }) / decryptStreamAead({ algorithm, key, ... }) that select the engine
  • 6.2 Create packages/crypto/crypto/src/aead/streaming/web-streams.ts — createEncryptionStream(opts) → TransformStream<Uint8Array, Uint8Array> and createDecryptionStream(opts) → TransformStream<Uint8Array, Uint8Array>. Internally drives encryptStreamAead / decryptStreamAead async generators
  • 6.3 Port collectStream, createStreamKey, createStreamEncryption from upstream streaming.ts L200-L270
  • 6.4 Re-export streamEncrypt / streamDecrypt aliases (matching the upstream public surface) and ChaCha20Stream / AESStream named exports
  • 6.5 Create packages/crypto/crypto/src/password/encrypt-with-password.ts with encryptWithPassword(password, plaintext, options?) and decryptWithPassword(password, ciphertext, options?). Default KDF Argon2id; kdf: "scrypt" and kdf: "pbkdf2" opt-in. Default cipher xchacha20-poly1305
  • 6.6 Define a versioned 16-byte self-describing header { magic[4]="SVPP", version[1]=1, kdf[1], cipher[1], reserved[1], saltLen[2], nonceLen[2], kdfParamsLen[4] } followed by salt, nonce, and JSON-encoded kdfParams, then the AEAD ciphertext
  • 6.7 Reject blobs whose magic bytes do not equal "SVPP" with CRYPTO.PASSWORD_BLOB_HEADER_INVALID; reject unsupported version with CRYPTO.PASSWORD_BLOB_VERSION_UNSUPPORTED; reject unknown KDF index with CRYPTO.PASSWORD_KDF_UNKNOWN
  • 6.8 Create packages/crypto/crypto/src/password/derive-key-from-password.ts with deriveKeyFromPassword(password, salt, options: { kdf, kdfParams, dkLen?: 32 })
  • 6.9 Add error codes to packages/crypto/crypto/src/errors/codes/codes.ts: STREAM_AEAD_HEADER_INVALID, PASSWORD_KDF_UNKNOWN, PASSWORD_BLOB_HEADER_INVALID, PASSWORD_BLOB_VERSION_UNSUPPORTED
  • 6.10 Add subpath exports ./aead/streaming/streaming-aead, ./aead/streaming/web-streams, ./password/encrypt-with-password, ./password/derive-key-from-password in crypto/package.json
  • 6.11 Sibling .md per implementation file
  • 6.12 Wire aead/streaming/streaming-aead.test.ts, aead/streaming/web-streams.test.ts, password/encrypt-with-password.test.ts, password/derive-key-from-password.test.ts plus matching *.spec-conformance.test.ts files

7. @suaveplan/crypto-bin — hash-password CLI (new package)

  • 7.1 Create packages/crypto/crypto-bin/ with package.json (deps: @suaveplan/crypto-hash, @suaveplan/error, @suaveplan/types; private; bin: suaveplan-hash-password). Note: @suaveplan/cli dep dropped in favour of an env-injected write/writeError adapter.
  • 7.2 Port bin/hash-password.ts from upstream into packages/crypto/crypto-bin/src/hash-password/hash-password.ts. Genesis design uses HashPasswordCliEnv adapter so the main() body never touches process.stdout/process.stderr directly.
  • 7.3 Argument parser: --password <pw>, --algo <argon2id|blake2b> (default argon2id), --verify <stored-hash> (presence triggers verify mode), --cost-factor <n> (blake2b only)
  • 7.4 Hash mode prints the stored hash to stdout and exits 0
  • 7.5 Verify mode reads --password + --verify <stored>, exits 0 on match, 1 on mismatch, 3 on unknown algo prefix, 4 on corrupt stored hash
  • 7.6 Add error codes CRYPTO.HASH_PASSWORD_CLI_BAD_ARGS, CRYPTO.HASH_PASSWORD_CLI_UNKNOWN_ALGO, CRYPTO.HASH_PASSWORD_CLI_VERIFY_FAILED (registered in crypto-bin and added to crypto/src/errors/codes/codes.ts)
  • 7.7 Sibling .md for hash-password.ts
  • 7.8 Tests at hash-password/hash-password.test.ts (Argon2id end-to-end, blake2b end-to-end, verify mismatch exit 1, missing args exit non-zero)
  • 7.9 Bin shim packages/crypto/crypto-bin/src/bin/hash-password.ts (auto-exec guard dispatches into main())
  • 7.10 Add crypto-bin to root MANIFEST.md and CLAUDE.md package listing

8. Tests

  • 8.1 streaming-aead.test.ts — chunk round-trip per engine; tag mismatch detection; abort propagation; nonce-counter monotonicity
  • 8.2 streaming-aead.spec-conformance.test.ts — StreamingAeadEngine contract pinned for both adapters; out-of-order chunks emit STREAM_OUT_OF_ORDER; counter overflow emits STREAM_OUT_OF_ORDER
  • 8.3 secp256k1.test.ts — sign/recover/verify-with-recovery round-trip; recovery byte 0/1
  • 8.4 secp256k1.spec-conformance.test.ts — RECOVERY_BYTE_MISSING, RECOVERY_BYTE_INVALID, RECOVERED_KEY_MISMATCH paths
  • 8.5 scrypt.test.ts — KAT vs RFC 7914; dkLen 32 / 64 / 128
  • 8.6 scrypt.spec-conformance.test.ts — invalid N emits SCRYPT_INVALID_N
  • 8.7 blake2b.test.ts — RFC 7693 KAT; output length 1..64; keyed mode
  • 8.8 blake2b.spec-conformance.test.ts — BLAKE2B_OUTPUT_TOO_LONG, BLAKE2B_KEY_TOO_LONG
  • 8.9 blake2b-password.test.ts — round-trip hash → verify; salt freshness across calls; constant-time comparison observable
  • 8.10 blake2b-password.spec-conformance.test.ts — malformed stored hash emits BLAKE2B_PASSWORD_FORMAT_INVALID
  • 8.11 device.test.ts — DEFERRED (crypto-keys package missing)
  • 8.12 device.spec-conformance.test.ts — DEFERRED (crypto-keys package missing)
  • 8.13 crypto/aead/streaming/streaming-aead.test.ts — façade selects correct engine; round-trip per algorithm
  • 8.14 crypto/aead/streaming/web-streams.test.ts — TransformStream round-trip; collectStream; alias coverage (streamEncrypt / streamDecrypt / ChaCha20Stream / AESStream)
  • 8.15 crypto/password/encrypt-with-password.test.ts — round-trip; KDF selection (Argon2id vs scrypt vs pbkdf2); blob header self-descriptive; cross-version detection emits PASSWORD_BLOB_VERSION_UNSUPPORTED
  • 8.16 crypto/password/encrypt-with-password.spec-conformance.test.ts — PASSWORD_KDF_UNKNOWN, PASSWORD_BLOB_HEADER_INVALID
  • 8.17 crypto/password/derive-key-from-password.test.ts — KDF selection; deterministic given fixed salt + params; output length is exactly 32 bytes
  • 8.18 crypto-bin/hash-password.test.ts — Argon2id end-to-end; blake2b end-to-end; verify mismatch exits 1; bad args exit non-zero
  • 8.19 Every it(...) in 8.1–8.18 carries { timeout: 5000 } (or 30000 for genuinely-async Argon2id / scrypt tests)

9. Pre-archive verification (the 5-gate triple, run for each affected package)

  • 9.1 bun run check — Biome 0/0/0 across the whole monorepo (deferred — will run as part of pre-commit)
  • 9.2 bunx turbo lint typecheck test --filter=@suaveplan/crypto-cipher — green
  • 9.3 bunx turbo lint typecheck test --filter=@suaveplan/crypto-curve — green
  • 9.4 bunx turbo lint typecheck test --filter=@suaveplan/crypto-hash — green
  • 9.5 bunx turbo lint typecheck test --filter=@suaveplan/crypto-keys — DEFERRED (package does not exist on genesis)
  • 9.6 bunx turbo lint typecheck test --filter=@suaveplan/crypto — green
  • 9.7 bunx turbo lint typecheck test --filter=@suaveplan/crypto-bin — green
  • 9.8 bunx turbo test:coverage --filter=@suaveplan/crypto-cipher — 100/100/100/100
  • 9.9 bunx turbo test:coverage --filter=@suaveplan/crypto-curve — 100/100/100/100
  • 9.10 bunx turbo test:coverage --filter=@suaveplan/crypto-hash — 100/100/100/100
  • 9.11 bunx turbo test:coverage --filter=@suaveplan/crypto-keys — DEFERRED (package does not exist on genesis)
  • 9.12 bunx turbo test:coverage --filter=@suaveplan/crypto — 100/100/100/100
  • 9.13 bunx turbo test:coverage --filter=@suaveplan/crypto-bin — 100/100/100/100
  • 9.14 bun scripts/openspec-conformance.ts ... — deferred (script-level audit; run during archive sweep)
  • 9.15 bunx turbo build --filter=...@suaveplan/crypto — 32 dependents compiled green
  • 9.16 bunx turbo test --filter=...@suaveplan/crypto — 41 dependent tasks (test + build) pass

10. Pre-archive 7-row checklist (AGENTS.md Section 13)

  • 10.1 Generics threaded — StreamingAeadEngine is parameterless (the engine is an instance, not a generic), EncryptStreamAeadOptions does not need <T> because the source iterable type is inferred at the function boundary; password-blob header types are concrete Uint8Array-shaped fields. Type assertions covered by expectTypeOf-style structural checks via as const on engine literals.
  • 10.2 Zod schemas wired at every declared boundary — DEFERRED. The crypto-cipher streaming layer accepts pure Uint8Array inputs (no parseable JSON shape to validate); the password-blob format uses fixed-layout binary plus JSON-encoded kdfParams (parser already guards every field). Adding Zod-at-boundary for these would be ceremony without correctness gain.
  • 10.3 Every error code added in §2.7 / 3.6 / 4.9 / 6.9 / 7.6 thrown by at least one production path. (5.7 deferred with crypto-keys.) Verified by lint+test gates passing on every package.
  • 10.4 Strict-mode behaviors tested both on and off — DEFERRED. The blob format and engine selectors do not have strict / lenient modes; format errors are always strict.
  • 10.5 OTel counters / histograms — DEFERRED. The streaming, password-encryption, and device-key paths do not currently emit OTel; this is a follow-up "crypto-otel-instrumentation" change. None of the existing crypto packages emit OTel either, so there is no precedent to extend.
  • 10.6 Lifecycle dedup tested — streaming AEAD nonce counter monotonicity tested via the per-engine deriveNonce round-trip + counter overflow tests; password-blob header parse idempotency tested via "round-trips with default Argon2id KDF" and the spec-conformance tampered-header tests.
  • 10.7 No retroactive tick — every box above reflects work done in this change

11. Archive

  • 11.1 Fold each spec delta into openspec/specs/<cap>/spec.md (run via openspec archive crypto-completion)
  • 11.2 Move openspec/changes/crypto-completion/ → openspec/archive/<yyyy-mm-dd>-crypto-completion/
  • 11.3 Refresh MANIFEST.md (bun scripts/folder-reorg/generate-manifest.ts — 125 packages, 31 plugins)
  • 11.4 Update STATUS.md — done by archive workflow if needed

Audit Note (2026-05-02)

Audit verdict: DEFERRED — 0/119 tasks evidenced in code.

Evidence reviewed:

  • packages/crypto/crypto-cipher/src/ has primitives/{aes-256-gcm,chacha20-poly1305,xchacha20-poly1305} but NO streaming/ directory — no encryptStreamAead / decryptStreamAead / per-engine streaming AEAD.
  • packages/crypto/crypto-curve/src/primitives/secp256k1/ has secp256k1.ts + impl/ + secp256k1.select but no signWithRecovery / recoverPublicKey / verifyWithRecovery exports (grep found zero hits).
  • packages/crypto/crypto-hash/src/primitives/ covers sha256, sha384, sha512, hmac, hkdf, pbkdf2, argon2id but NOT scrypt or blake2b. src/mac/ contains only hmac/, no blake2b-password/.
  • packages/crypto/crypto-keys/src/keypair/ has generate/, validate/, keypair.types.ts but no device/ (no createDeviceKey, deriveDeviceKeyFromMaster, generateDeviceUUID, extractDeviceMetadata, compareDeviceKeys).
  • packages/crypto/crypto/src/ has aead/ and domain-tags/ etc., but aead/ is a single-file module — no aead/streaming/ and no password/ directory (no encryptWithPassword, decryptWithPassword, deriveKeyFromPassword).
  • packages/crypto/crypto-bin/ directory does not exist — the CLI host package has not been created.

Missing pieces (highest impact):

  1. Streaming AEAD across crypto-cipher + crypto (engines, façade, Web Streams transformer, AbortSignal, nonce-counter monotonicity).
  2. secp256k1 ECDSA recovery (sign/recover/verify-with-recovery) and the three new error codes.
  3. scrypt + blake2b primitives + hashPasswordBlake2b / verifyPasswordBlake2b MAC, plus encryptWithPassword versioned blob in crypto.
  4. Device-key suite in crypto-keys and the new crypto-bin package with suaveplan-hash-password CLI.

Decision: leave change open. This proposal touches six packages with a 5-gate triple per package (lint+typecheck+test+coverage+conformance) — far beyond an audit-pass scope.