Initial Commit
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# Capability Spec Delta: crypto (crypto-completion)
|
||||
|
||||
**Package**: `@suaveplan/crypto`
|
||||
**Status**: PROPOSED
|
||||
|
||||
> Every requirement below carries a `**Verified by:**` line at authoring time.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: High-level streaming AEAD façade
|
||||
|
||||
The package SHALL expose `encryptStreamAead(opts)` and `decryptStreamAead(opts)` from `aead/streaming/streaming-aead.ts` that select the engine (`xchacha20-poly1305` or `aes-256-gcm`) from `opts.algorithm` and delegate to the corresponding `StreamingAeadEngine` in `@suaveplan/crypto-cipher`. The package SHALL re-export the named engines `aesGcmStreamingEngine` and `xchacha20StreamingEngine` so callers wanting the engine directly do not need to depend on `crypto-cipher`.
|
||||
|
||||
#### Scenario: facade routes to AES-GCM engine
|
||||
|
||||
- **WHEN** `encryptStreamAead({ algorithm: "aes-256-gcm", key, baseNonce: 8-byte, plaintext })` is called
|
||||
- **THEN** the produced ciphertext decrypts via the standalone `aesGcmStreamingEngine` exported from this package, byte-for-byte
|
||||
|
||||
#### Scenario: facade routes to XChaCha20-Poly1305 engine
|
||||
|
||||
- **WHEN** `encryptStreamAead({ algorithm: "xchacha20-poly1305", key, baseNonce: 20-byte, plaintext })` is called
|
||||
- **THEN** the produced ciphertext decrypts via the standalone `xchacha20StreamingEngine` exported from this package, byte-for-byte
|
||||
|
||||
#### Scenario: unknown algorithm rejected
|
||||
|
||||
- **WHEN** `encryptStreamAead({ algorithm: "rot13" as never, ... })` is called
|
||||
- **THEN** it throws `SuaveplanError` with code `CRYPTO.STREAM_AEAD_HEADER_INVALID` and `context.algorithm === "rot13"`
|
||||
|
||||
**Verified by:** `packages/crypto/crypto/src/aead/streaming/streaming-aead.test.ts::"facade routes aes-256-gcm to aesGcmStreamingEngine"` AND `packages/crypto/crypto/src/aead/streaming/streaming-aead.test.ts::"facade routes xchacha20-poly1305 to xchacha20StreamingEngine"` AND `packages/crypto/crypto/src/aead/streaming/streaming-aead.spec-conformance.test.ts::"throws STREAM_AEAD_HEADER_INVALID on unknown algorithm"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Web-Streams adapters for streaming AEAD
|
||||
|
||||
The package SHALL expose `createEncryptionStream(opts) → TransformStream<Uint8Array, Uint8Array>` and `createDecryptionStream(opts) → TransformStream<Uint8Array, Uint8Array>` from `aead/streaming/web-streams.ts`. These SHALL be drop-in `pipeThrough` targets for any Web-Streams `ReadableStream<Uint8Array>` source (e.g. `Blob.stream()`, `Response.body`, `fetch().body`). The package SHALL also expose `collectStream(stream) → Promise<Uint8Array>` (concatenates all chunks), `createStreamKey(rng?) → Uint8Array` (returns 32 random bytes), and `createStreamEncryption(opts)` (combined key + base-nonce + initialised stream factory).
|
||||
|
||||
#### Scenario: pipeThrough round-trips a Blob
|
||||
|
||||
- **WHEN** `blob.stream().pipeThrough(createEncryptionStream(opts)).pipeThrough(createDecryptionStream(opts))` is collected via `collectStream`
|
||||
- **THEN** the collected bytes equal the original blob bytes
|
||||
|
||||
#### Scenario: legacy aliases preserved
|
||||
|
||||
- **WHEN** `streamEncrypt`, `streamDecrypt`, `ChaCha20Stream`, `AESStream` are imported
|
||||
- **THEN** all four resolve to defined values that satisfy the documented upstream contract
|
||||
|
||||
**Verified by:** `packages/crypto/crypto/src/aead/streaming/web-streams.test.ts::"pipeThrough round-trips a multi-chunk source through encrypt then decrypt"` AND `packages/crypto/crypto/src/aead/streaming/web-streams.test.ts::"streamEncrypt / streamDecrypt / ChaCha20Stream / AESStream aliases resolve and round-trip"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: Password-based encryption helpers
|
||||
|
||||
The package SHALL expose `encryptWithPassword(password, plaintext, options?)` and `decryptWithPassword(password, ciphertext, options?)` from `password/encrypt-with-password.ts`. The `options.kdf` field SHALL accept `"argon2id"` (default), `"scrypt"`, or `"pbkdf2"`. The output blob SHALL begin with a versioned 16-byte self-describing header `{ magic: "SVPP", version: 1, kdf, cipher, reserved, saltLen, nonceLen, kdfParamsLen }` followed by salt, nonce, CBOR-encoded `kdfParams`, then AEAD ciphertext. `decryptWithPassword` SHALL parse the header to recover all parameters and SHALL NOT require any out-of-band metadata from the caller.
|
||||
|
||||
#### Scenario: round-trip with default Argon2id
|
||||
|
||||
- **WHEN** `encryptWithPassword("correct horse battery staple", plaintext)` produces `blob` and `decryptWithPassword("correct horse battery staple", blob)` is called
|
||||
- **THEN** the result equals `plaintext` byte-for-byte
|
||||
|
||||
#### Scenario: round-trip with scrypt KDF
|
||||
|
||||
- **WHEN** `encryptWithPassword(pw, plaintext, { kdf: "scrypt" })` produces `blob` and `decryptWithPassword(pw, blob)` is called
|
||||
- **THEN** the result equals `plaintext` byte-for-byte AND the blob's KDF byte signals scrypt
|
||||
|
||||
#### Scenario: blob header self-describes
|
||||
|
||||
- **WHEN** the same `(password, plaintext)` is encrypted twice with different `kdfParams`
|
||||
- **THEN** the two blobs have different KDF-params bytes in their headers AND both decrypt successfully without the caller passing `kdfParams` to `decryptWithPassword`
|
||||
|
||||
#### Scenario: unknown KDF rejected
|
||||
|
||||
- **WHEN** `encryptWithPassword(pw, plaintext, { kdf: "bcrypt" as never })` is called
|
||||
- **THEN** it throws `SuaveplanError` with code `CRYPTO.PASSWORD_KDF_UNKNOWN` and `context.kdf === "bcrypt"`
|
||||
|
||||
#### Scenario: corrupt header rejected
|
||||
|
||||
- **WHEN** `decryptWithPassword(pw, blob)` is called with a `blob` whose first four bytes are not `"SVPP"`
|
||||
- **THEN** it throws `SuaveplanError` with code `CRYPTO.PASSWORD_BLOB_HEADER_INVALID`
|
||||
|
||||
#### Scenario: unsupported version rejected
|
||||
|
||||
- **WHEN** `decryptWithPassword(pw, blob)` is called with a `blob` whose version byte is `99`
|
||||
- **THEN** it throws `SuaveplanError` with code `CRYPTO.PASSWORD_BLOB_VERSION_UNSUPPORTED` and `context.version === 99`
|
||||
|
||||
**Verified by:** `packages/crypto/crypto/src/password/encrypt-with-password.test.ts::"round-trips with default Argon2id KDF"` AND `packages/crypto/crypto/src/password/encrypt-with-password.test.ts::"round-trips with scrypt KDF"` AND `packages/crypto/crypto/src/password/encrypt-with-password.test.ts::"blob header is self-describing across kdfParams variants"` AND `packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts::"throws PASSWORD_KDF_UNKNOWN on unknown KDF name"` AND `packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts::"throws PASSWORD_BLOB_HEADER_INVALID on corrupt magic bytes"` AND `packages/crypto/crypto/src/password/encrypt-with-password.spec-conformance.test.ts::"throws PASSWORD_BLOB_VERSION_UNSUPPORTED on unknown version byte"`
|
||||
|
||||
---
|
||||
|
||||
### Requirement: deriveKeyFromPassword
|
||||
|
||||
The package SHALL expose `deriveKeyFromPassword(password, salt, options?)` from `password/derive-key-from-password.ts`. The `options` SHALL accept `kdf: "argon2id" | "scrypt" | "pbkdf2"` (default `"argon2id"`), `kdfParams: Record<string, number>` (algorithm-specific), and `dkLen: number` (default 32). The function SHALL return a `Uint8Array` of length `dkLen` suitable for direct use as a symmetric AEAD key. Identical `(password, salt, kdf, kdfParams)` SHALL produce byte-identical output.
|
||||
|
||||
#### Scenario: deterministic given fixed inputs
|
||||
|
||||
- **WHEN** `deriveKeyFromPassword(pw, salt, { kdf: "scrypt", kdfParams: { N: 1024, r: 8, p: 1 } })` is called twice
|
||||
- **THEN** both calls return byte-identical 32-byte Uint8Arrays
|
||||
|
||||
#### Scenario: dkLen honored
|
||||
|
||||
- **WHEN** `deriveKeyFromPassword(pw, salt, { kdf: "argon2id", dkLen: 64 })` is called
|
||||
- **THEN** the result is a 64-byte Uint8Array
|
||||
|
||||
**Verified by:** `packages/crypto/crypto/src/password/derive-key-from-password.test.ts::"deterministic for fixed password / salt / params"` AND `packages/crypto/crypto/src/password/derive-key-from-password.test.ts::"honors dkLen option"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user