From b7d0d5b740a5486cd5eaa95911bb7b35238498a5 Mon Sep 17 00:00:00 2001 From: Kauan Guesser Date: Sun, 16 Aug 2026 17:17:53 -0300 Subject: [PATCH] feat: zero-access key-hierarchy primitives (password KDF, key-wrap records, nmcs1 stream) Add three capabilities that the artifact-studio zero-access encryption program builds on: - @nestm/crypto/password: registry-based scrypt KDF with absolute parameter ceilings and a hard maxmem cap, canonical versioned params codec, and recovery codes (Crockford base32, checksum-padded groups). - @nestm/crypto/keys: KeyWrapRecord (A256GCMKW secret wraps + X25519 sealed boxes) with recipient-identity-bound AAD and length-checked unwrap, plus hmacSha256 / timingSafeEqualBytes. - @nestm/crypto/stream: the nmcs1 chunked AEAD container (fixed 512B header, per-chunk HKDF-derived keys/nonces from a per-object random fileId, truncation/ reorder/splice/header-tamper resistant, buffered + Web-stream APIs). fileId is a testing-only seam, never a public option, so nonce reuse cannot be caused by a caller. Also export isCipherEnvelope from ./core. No new runtime deps; ESM-only. --- .../zero-access-key-hierarchy-primitives.md | 41 ++ README.md | 147 ++++ SECURITY.md | 81 ++- package.json | 10 + scripts/check-package.mjs | 2 + scripts/test-packed-consumers.mjs | 29 +- src/core/cipher-engine.ts | 20 +- src/core/encoding.ts | 26 + src/core/envelope.ts | 9 + src/core/index.ts | 2 +- src/keys/hmac.ts | 52 ++ src/keys/index.ts | 22 + src/keys/key-wrap-record.ts | 565 ++++++++++++++++ src/password/index.ts | 24 + src/password/password-kdf.ts | 389 +++++++++++ src/password/recovery-code.ts | 183 +++++ src/stream/index.ts | 19 + src/stream/nmcs-chunked.ts | 640 ++++++++++++++++++ src/stream/nmcs-format.ts | 439 ++++++++++++ src/testing/index.ts | 24 + tests/unit/format-vectors.test.ts | 173 +++++ tests/unit/key-wrap-record.test.ts | 356 ++++++++++ tests/unit/password.test.ts | 373 ++++++++++ tests/unit/stream.test.ts | 633 +++++++++++++++++ tests/unit/vectors/format-vectors.json | 23 + tsdown.config.ts | 2 + 26 files changed, 4257 insertions(+), 27 deletions(-) create mode 100644 .changeset/zero-access-key-hierarchy-primitives.md create mode 100644 src/keys/hmac.ts create mode 100644 src/keys/key-wrap-record.ts create mode 100644 src/password/index.ts create mode 100644 src/password/password-kdf.ts create mode 100644 src/password/recovery-code.ts create mode 100644 src/stream/index.ts create mode 100644 src/stream/nmcs-chunked.ts create mode 100644 src/stream/nmcs-format.ts create mode 100644 tests/unit/format-vectors.test.ts create mode 100644 tests/unit/key-wrap-record.test.ts create mode 100644 tests/unit/password.test.ts create mode 100644 tests/unit/stream.test.ts create mode 100644 tests/unit/vectors/format-vectors.json diff --git a/.changeset/zero-access-key-hierarchy-primitives.md b/.changeset/zero-access-key-hierarchy-primitives.md new file mode 100644 index 0000000..06d9246 --- /dev/null +++ b/.changeset/zero-access-key-hierarchy-primitives.md @@ -0,0 +1,41 @@ +--- +"@nestm/crypto": minor +--- + +Add the primitives a zero-access key hierarchy needs: password-derived key-encryption keys, +recipient-addressed key-wrap records, and a chunked streaming file format. + +- `@nestm/crypto/password`: a registry-based `PasswordKdf` (`createPasswordKdf`, + `scryptPasswordKdf`) deriving a 32-byte `KeyObject` from a login password via scrypt over + `node:crypto` — no native dependency, versioned parameters stored through a canonical + `encodePasswordKdfParams`/`decodePasswordKdfParams` codec, an explicit `maxmem`, and a FIFO + semaphore bounding concurrent memory-hard derivations. Also ships display-once recovery + codes (`generateRecoveryCode`, `parseRecoveryCode`, `deriveRecoveryKey`) as Crockford + base32 in uniform eight-character groups — `ASR1-XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX` for a + 128-bit secret — carrying a checksum wide enough to reject every single-character + corruption, parsed fail-closed. +- `@nestm/crypto/keys`: `KeyWrapRecord` — one versioned record type covering both + `wrapKeyWithSecret` (AES-256-GCM under a shared KEK) and `wrapKeyToRecipient` (sealed to an + X25519 public key), with encode/decode for single records and lists, `selectKeyWrapRecord`, + and `keyWrapRecordLength`. Both flavours authenticate the algorithm, recipient type, and + recipient identifier, so a record cannot be replayed against a different recipient. Adds + `hmacSha256` and a length-tolerant `timingSafeEqualBytes`. +- `@nestm/crypto/stream`: the `nmcs1` container — a fixed 512-byte authenticated header + (magic, suite, chunk size, per-object random file identifier, key reference, context + associated data, and optional inline wrap records) followed by AES-256-GCM chunks framed as + `u32BE length ‖ ciphertext ‖ tag`. Per-chunk keys and nonce prefixes derive from + HKDF-SHA256 over the data key, the file identifier, and a hash of the whole header, so no + nonce counter is ever persisted and a rolled-back database cannot cause nonce reuse. The + final chunk is flagged in both its nonce and its associated data, which makes truncation, + reordering, and cross-object splicing fail authentication. Buffered `sealChunked`/ + `openChunked` and Web-Streams `createChunkedSealStream`/`createChunkedOpenStream` produce + identical bytes when the plaintext length is declared (a stream that cannot know its length + clears the declared-length flag instead), and the fixed header makes plaintext size exactly + recoverable from ciphertext size via `chunkedPlaintextLength`. Sealing and opening share one + default chunk-size ceiling of `2^24`, so the library never writes an object its own default + reader would refuse; `2^25` and `2^26` require `maxChunkSizeLog2` on both sides. The + per-object file identifier is never caller-supplied — pinning it is possible only through + `@nestm/crypto/testing`, because reusing one under the same data key would repeat a + keystream. +- `@nestm/crypto/core` now exports `isCipherEnvelope` for routing legacy plaintext during a + migration. The envelope parser stays internal. diff --git a/README.md b/README.md index 4c24825..f32b891 100644 --- a/README.md +++ b/README.md @@ -567,6 +567,150 @@ function createTenantPrismaFieldEncryption( ): TenantPrismaWriteProcessor; ``` +## Key hierarchies + +`@nestm/crypto/keys`, `@nestm/crypto/password`, and `@nestm/crypto/stream` are framework-neutral +building blocks for key hierarchies where the server holds no long-lived master key: a password +unlocks a user key, user keys hold grants to shared keys, and shared keys protect per-object data +keys. They are independent of `CipherEngine` and `DataKeyProvider`. + +### Asymmetric keys and sealed boxes — `@nestm/crypto/keys` + +```ts +import { generateX25519KeyPair, sealTo, openKeyFrom, hkdfSha256 } from "@nestm/crypto/keys"; + +const recipient = generateX25519KeyPair(); +// Sealing needs only the public key, so a writer never needs the recipient online. +const sealed = sealTo(recipient.publicKey, dataKey, { info: "wsk.seal", aad: "epoch:1" }); +const recovered = openKeyFrom(recipient.privateKey, sealed, { info: "wsk.seal", aad: "epoch:1" }); +``` + +`sealTo` uses ephemeral-static X25519 → HKDF-SHA256 → AES-256-GCM, binds the recipient public key +into the key schedule, derives the nonce (never transmitting it), and caps payloads at 64 KiB — it +is a key-wrapping primitive, not a data cipher. + +### Key-wrap records + +A single versioned record covers both wrapping flavours, so a grant table or a file header can +carry a heterogeneous list of them: + +```ts +import { + wrapKeyWithSecret, + wrapKeyToRecipient, + encodeKeyWrapRecords, + decodeKeyWrapRecords, + selectKeyWrapRecord, + unwrapKeyFromRecipient, +} from "@nestm/crypto/keys"; + +const grants = [ + wrapKeyToRecipient(alicePublicKey, workspaceKey, { recipientId: "user:alice" }), + wrapKeyWithSecret(orgKek, workspaceKey, { recipientId: "org:acme" }), +]; +const stored = encodeKeyWrapRecords(grants); + +const mine = selectKeyWrapRecord(decodeKeyWrapRecords(stored), { + recipientType: "x25519", + recipientId: "user:alice", +}); +const workspaceKeyAgain = unwrapKeyFromRecipient(alicePrivateKey, mine!); +``` + +The algorithm, recipient type, and recipient identifier are authenticated, so a record cannot be +retargeted at a different recipient even under the same wrapping key. + +### Password-derived keys — `@nestm/crypto/password` + +```ts +import { + createPasswordKdf, + generatePasswordSalt, + encodePasswordKdfParams, + PASSWORD_KDF_SCRYPT_DEFAULT, +} from "@nestm/crypto/password"; + +const kdf = createPasswordKdf(); // scrypt N=2^16, r=8, p=1; at most 4 concurrent derivations +const salt = generatePasswordSalt(); +const kek = await kdf.derive({ + password, + salt, + kdf: PASSWORD_KDF_SCRYPT_DEFAULT, + info: "umk.password", // domain separation; the same password yields unrelated keys per purpose +}); +// Persist the salt and encodePasswordKdfParams(PASSWORD_KDF_SCRYPT_DEFAULT) beside the wrap. +``` + +Parameters are versioned per record, so a stored key can be re-wrapped under stronger parameters +later without a format change. Registering an Argon2id implementation of `PasswordKdfAlgorithm` +requires no format change either; the default stays dependency-free. + +Recovery codes are full-entropy secrets, so they derive through HKDF rather than a memory-hard KDF: + +```ts +import { generateRecoveryCode, parseRecoveryCode, deriveRecoveryKey } from "@nestm/crypto/password"; + +// ASR1-XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX — display once, never store the code itself +const { code, secret } = generateRecoveryCode(); +const recoveryKek = deriveRecoveryKey(secret, recoverySalt, "umk.recovery"); +// later +const recoveryKekAgain = deriveRecoveryKey( + parseRecoveryCode(userInput), + recoverySalt, + "umk.recovery", +); +``` + +Store only the wrap the code produces. Persisting a verifier hash would hand an offline oracle to +anyone who reads the database. + +### Chunked files — `@nestm/crypto/stream` + +`nmcs1` is a self-contained container: a fixed 512-byte authenticated header followed by +AES-256-GCM chunks (1 MiB by default, 20 bytes of framing overhead each). + +```ts +import { sealChunked, openChunked, inspectChunked } from "@nestm/crypto/stream"; + +const sealed = sealChunked(dataKey, plaintext, { + keyReference: "ws:1f9e:e1", + aad: "org:acme|ws:1f9e", // binds the object to its location; a relocated copy fails to open + wrapRecords: [wrapKeyToRecipient(workspacePublicKey, dataKey, { recipientId: "ws:1f9e" })], +}); + +const header = inspectChunked(sealed); // framing metadata only — nothing is authenticated yet +const plaintextAgain = openChunked(dataKey, sealed, { aad: "org:acme|ws:1f9e" }); +``` + +Large payloads use the Web-Streams form, which is byte-identical to the buffered form when +`plaintextLength` is declared (`sealChunked` always knows the length; a stream does not, so an +undeclared stream records the length as absent): + +```ts +import { createChunkedSealStream, createChunkedOpenStream } from "@nestm/crypto/stream"; + +await source.pipeThrough(createChunkedSealStream(dataKey, { keyReference })).pipeTo(destination); +await ciphertext.pipeThrough(createChunkedOpenStream(dataKey)).pipeTo(sink); +``` + +`createChunkedOpenStream` emits an **authenticated prefix**: every chunk is verified before it is +emitted, but truncation is only detected at end of stream. Consumers that must not act on a partial +payload should use `openChunked`. + +Because the header is fixed-size and every chunk but the last is full, sizes convert exactly in both +directions without reading the object — useful for reporting plaintext sizes from a storage listing: + +```ts +import { chunkedCiphertextLength, chunkedPlaintextLength } from "@nestm/crypto/stream"; + +chunkedCiphertextLength(1_234); // 1_766 +chunkedPlaintextLength(1_766); // 1_234 (undefined when the size is not one the format can produce) +``` + +Nonces are derived per chunk from the data key, a per-object random file identifier, and a hash of +the header; no counter is persisted anywhere, so restoring a database to an earlier point in time +cannot cause nonce reuse. + ## `nestjs-field-encryption` compatibility map This package now covers the integration surfaces demonstrated by @@ -595,6 +739,9 @@ plaintext with `@nestm/crypto`; relabeling or importing the old ciphertext is no | Entry point | Purpose | | ---------------------------- | ----------------------------------------------------------------------------- | | `@nestm/crypto/core` | AES-256-GCM engine, envelope codec, local AES KEK ring, contracts, and errors | +| `@nestm/crypto/keys` | X25519 keys, HKDF-SHA256, sealed boxes, key-wrap records, HMAC helpers | +| `@nestm/crypto/password` | password-derived key-encryption keys and display-once recovery codes | +| `@nestm/crypto/stream` | `nmcs1` chunked AES-256-GCM container for buffered and streaming payloads | | `@nestm/crypto/fields` | purpose-decorated class traversal | | `@nestm/crypto/tenant` | tenant-bound cipher and field services | | `@nestm/crypto/http` | request-encryption pipe and opt-in response-decryption interceptor | diff --git a/SECURITY.md b/SECURITY.md index 39518dd..046dc08 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -141,6 +141,83 @@ different row that uses the same purpose in the same tenant. Applications that r must use an explicit service boundary with stable row-specific AAD; a future Prisma AAD-resolver API would need to define update/upsert identity and migration behavior before offering that guarantee. +## Chunked stream boundary + +`@nestm/crypto/stream` (`nmcs1`) protects whole objects, buffered or streamed. + +- The 512-byte header is authenticated by its own tag and hashed into the chunk key schedule, so a + change to the key reference, chunk size, context associated data, or inline wrap records makes + every chunk fail authentication. Header padding must be zero and reserved flag bits must be clear. +- Each chunk's nonce is `noncePrefix ‖ chunkIndex ‖ finalFlag`, where the prefix derives from the + data key and a per-object random 16-byte file identifier. **No nonce input is ever persisted or + read back from storage.** A counter written to a database and later restored to an earlier point + in time — by point-in-time recovery, a replica promotion, or a restored backup — would reissue + nonces under the same key and break AES-GCM catastrophically. Any future change to this format + must preserve that property: nonces come from a CSPRNG draw or from an HKDF over per-object + random material, never from durable mutable state. +- The final chunk is flagged in both its nonce and its associated data, so truncation, chunk + reordering, and splicing chunks between objects all fail authentication rather than yielding a + short or wrong plaintext. +- `createChunkedOpenStream` emits an **authenticated prefix**. Each chunk is verified before it is + emitted, but truncation is only detectable at end of stream, so a consumer acting on partial + output may act on a prefix of the plaintext. Use `openChunked` where all-or-nothing semantics are + required, and treat a stream that errors mid-flight as having produced nothing. +- `inspectChunked` reports framing only and marks itself `authenticated: false`. Never make a trust + decision on its output. +- The context associated data in the header is **not encrypted**. Bind location and ownership there + (organization, workspace, object key); never put secrets in it. +- Bound `maxPlaintextBytes` and `maxChunkSizeLog2` when opening untrusted ciphertext; a hostile + header can otherwise declare a chunk size far larger than the reader intends to buffer. +- The chunk-size ceiling is symmetric: sealing and opening both default to `2^24`, so the + library never writes an object that a default reader would refuse. Exponents of 25 and 26 + exist in the format but require `maxChunkSizeLog2` on the seal side _and_ the open side; + an object written above the default is unreadable to a caller that has not opted in. +- The per-object file identifier is drawn from the CSPRNG on every seal and is deliberately + not settable through `ChunkedSealOptions`. It is the only per-object input to the key + schedule, so reusing one under the same data key would repeat a chunk key and nonce + prefix — exposing the XOR of two plaintexts and leaking the header GMAC subkey. The + `@nestm/crypto/testing` seam that pins it exists solely to freeze format vectors and must + never be reachable from production code. +- Never persist a nonce, a nonce counter, or a file identifier and replay it. Nonces here are + either drawn fresh or derived from a fresh per-object identifier, which is what makes a + database rollback (point-in-time restore, replica promotion) unable to cause nonce reuse. +- `scrypt` parameters are bounded absolutely, not just per field: a derivation may not + reserve more than 1 GiB, and `maxmem` is capped independently of the caller. Combined with + the concurrency semaphore, this keeps a password-hardening parameter from becoming a + denial-of-service lever. + +## Password and recovery-code boundary + +`@nestm/crypto/password` derives key-encryption keys from user-supplied secrets. + +- scrypt is memory-hard by design: each derivation holds roughly `128 * N * r` bytes, which is + 64 MiB at the shipped defaults. `createPasswordKdf` bounds concurrent derivations (default 4) to + keep a burst of sign-ins from exhausting memory; raise the limit only against measured headroom. +- Parameters and salt are per-record and versioned. Re-derive and re-wrap on the next successful + authentication when policy strengthens; never reuse a salt across records. +- Always pass `info` to separate purposes. Without it, the same password yields the same key for + every use. +- Recovery secrets are full-entropy, so `deriveRecoveryKey` uses HKDF rather than a memory-hard KDF. + Store only the wrap the recovery key produces — persisting a verifier hash of the code creates an + offline oracle for anyone who reads the database. +- Recovery codes are display-once. The library never retains one, and `parseRecoveryCode` fails + closed on any prefix, character, length, canonicality, or checksum deviation. +- A derived key is a `KeyObject`. Node keeps that material outside the JavaScript heap, but it + cannot be wiped on demand; see the custody note below. + +## Key custody + +Unwrapped keys live only in process memory for as long as the application holds them. + +- The library zeroes its own intermediate buffers, but a resident `KeyObject` is released to the + runtime, not erased. Anything that can read process memory — a core dump, a heap snapshot, an + attached debugger, `--inspect` — is equivalent to holding the keys. +- Disable core dumps and inspector ports in production, and keep key material out of logs, error + causes, and crash reporters. +- Keys held for a session should have an explicit lifetime and be dropped on sign-out, session + revocation, and password change. Replicating them to a shared cache re-introduces a decryptable + copy at rest and weakens the model that per-user key derivation is there to provide. + ## Operational guidance - Put stable, domain-specific data classification in `purpose`; never use request IDs or mutable labels. @@ -149,6 +226,6 @@ would need to define update/upsert identity and migration behavior before offeri bound a batch to 256 items and 10 MiB of aggregate plaintext/ciphertext. - Monitor authentication failures and provider failures without logging values or native SDK payloads. - Test key rotation and disaster recovery with representative ciphertext before retiring a key. -- Bound input sizes before accepting untrusted ciphertext; the library is buffered and not a streaming - encryption format. +- Bound input sizes before accepting untrusted ciphertext. The `nmc1` envelope is buffered and not a + streaming format; use `@nestm/crypto/stream` for payloads that must not be held in memory whole. - Run credential-gated live tests only against disposable provider resources. diff --git a/package.json b/package.json index af7204f..1710389 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,16 @@ "import": "./dist/keys/index.mjs", "default": "./dist/keys/index.mjs" }, + "./password": { + "types": "./dist/password/index.d.mts", + "import": "./dist/password/index.mjs", + "default": "./dist/password/index.mjs" + }, + "./stream": { + "types": "./dist/stream/index.d.mts", + "import": "./dist/stream/index.mjs", + "default": "./dist/stream/index.mjs" + }, "./key-wrap/rsa": { "types": "./dist/key-wrap/rsa/index.d.mts", "import": "./dist/key-wrap/rsa/index.mjs", diff --git a/scripts/check-package.mjs b/scripts/check-package.mjs index 2491b0f..9df1acf 100644 --- a/scripts/check-package.mjs +++ b/scripts/check-package.mjs @@ -12,6 +12,8 @@ const expectedExports = [ "./http", "./prisma", "./keys", + "./password", + "./stream", "./key-wrap/rsa", "./kms/aws", "./kms/gcp", diff --git a/scripts/test-packed-consumers.mjs b/scripts/test-packed-consumers.mjs index ae74ab3..d4bb86c 100644 --- a/scripts/test-packed-consumers.mjs +++ b/scripts/test-packed-consumers.mjs @@ -142,6 +142,10 @@ function testIntegrationConsumer(tarball) { const subpaths = [ "@nestm/crypto", + "@nestm/crypto/core", + "@nestm/crypto/keys", + "@nestm/crypto/password", + "@nestm/crypto/stream", "@nestm/crypto/fields", "@nestm/crypto/tenant", "@nestm/crypto/http", @@ -168,6 +172,21 @@ function testIntegrationConsumer(tarball) { `const entries = [${subpaths.map((_, index) => `entry${index}`).join(", ")}];`, 'if (entries.some((entry) => Object.keys(entry).length === 0)) throw new Error("empty entry");', 'assert.equal(manifest.name, "@nestm/crypto");', + // The new subpaths must work from a packed tarball, not just from source. + "const packedKeyPair = entry2.generateX25519KeyPair();", + 'const packedDek = generateKeySync("aes", { length: 256 });', + 'const packedRecord = entry2.wrapKeyToRecipient(packedKeyPair.publicKey, packedDek, { recipientId: "user:packed" });', + "assert.equal(entry2.unwrapKeyFromRecipient(packedKeyPair.privateKey, packedRecord).symmetricKeySize, 32);", + 'const packedSealed = entry4.sealChunked(packedDek, Buffer.from("packed stream"), { keyReference: "ws:packed", wrapRecords: [packedRecord] });', + 'assert.equal(Buffer.from(entry4.openChunked(packedDek, packedSealed)).toString(), "packed stream");', + "assert.equal(entry4.inspectChunked(packedSealed).wrapRecords.length, 1);", + "const packedKdf = entry3.createPasswordKdf();", + "const packedSalt = entry3.generatePasswordSalt();", + 'const packedKek = await packedKdf.derive({ password: "packed pw", salt: packedSalt, kdf: entry3.PASSWORD_KDF_SCRYPT_DEFAULT });', + "assert.equal(packedKek.symmetricKeySize, 32);", + "const packedRecovery = entry3.generateRecoveryCode();", + "assert.deepEqual(entry3.parseRecoveryCode(packedRecovery.code), packedRecovery.secret);", + 'assert.equal(entry1.isCipherEnvelope("nmc1.a.b.c.d.e"), true);', 'const moduleRef = await Test.createTestingModule({ imports: [entry0.CryptoModule.forRoot({ defaultProvider: "packed", providers: [{ name: "packed", provider: new entry0.AesKeyRingProvider({ activeKeyId: "packed-key", keys: { "packed-key": generateKeySync("aes", { length: 256 }) } }) }] })] }).compile();', "const cipher = moduleRef.get(entry0.CipherService);", "const fieldCipher = moduleRef.get(entry0.FieldCipherService);", @@ -186,23 +205,23 @@ function testIntegrationConsumer(tarball) { 'const tenantReader: TenantContextReader = { current: () => ({ mode: "tenant", tenant: snapshot() }), require: () => ({ mode: "tenant", tenant: snapshot() }), requireTenant: snapshot, requireTenantId: () => activeTenantId, requireTenantTarget: () => ({ authority: "tenant", tenant: snapshot() }) };', "class PackedContextModule {}", "Module({ providers: [{ provide: TENANT_CONTEXT_READER, useValue: tenantReader }], exports: [TENANT_CONTEXT_READER] })(PackedContextModule);", - 'const tenantModuleRef = await Test.createTestingModule({ imports: [entry2.TenantCryptoModule.forRoot({ namespace: "packed-app", imports: [PackedContextModule, entry0.CryptoModule.forRoot({ defaultProvider: "packed-tenant", providers: [{ name: "packed-tenant", provider: new entry0.AesKeyRingProvider({ activeKeyId: "packed-tenant-key", keys: { "packed-tenant-key": generateKeySync("aes", { length: 256 }) } }) }] })] })] }).compile();', - "const tenantCipher = tenantModuleRef.get(entry2.TenantCipherService);", + 'const tenantModuleRef = await Test.createTestingModule({ imports: [entry6.TenantCryptoModule.forRoot({ namespace: "packed-app", imports: [PackedContextModule, entry0.CryptoModule.forRoot({ defaultProvider: "packed-tenant", providers: [{ name: "packed-tenant", provider: new entry0.AesKeyRingProvider({ activeKeyId: "packed-tenant-key", keys: { "packed-tenant-key": generateKeySync("aes", { length: 256 }) } }) }] })] })] }).compile();', + "const tenantCipher = tenantModuleRef.get(entry6.TenantCipherService);", 'const tenantEnvelope = await tenantCipher.encryptText("tenant secret", { purpose: "packed.tenant" });', 'activeTenantId = "tenant-b";', 'await assert.rejects(() => tenantCipher.decryptText(tenantEnvelope, { purpose: "packed.tenant" }), (error: unknown) => entry0.isCryptoError(error, "AUTHENTICATION_FAILED"));', 'activeTenantId = "tenant-a";', 'assert.equal(await tenantCipher.decryptText(tenantEnvelope, { purpose: "packed.tenant" }), "tenant secret");', - "const tenantFields = tenantModuleRef.get(entry2.TenantFieldCipherService);", + "const tenantFields = tenantModuleRef.get(entry6.TenantFieldCipherService);", 'class PackedHttpSecret { secret = "packed HTTP"; }', 'entry0.EncryptedField("packed.http")(PackedHttpSecret.prototype, "secret");', "const packedHttpSecret = new PackedHttpSecret();", - "const httpPipe = new entry3.TenantEncryptFieldsPipe(tenantFields);", + "const httpPipe = new entry7.TenantEncryptFieldsPipe(tenantFields);", 'await httpPipe.transform(packedHttpSecret, { type: "body", metatype: PackedHttpSecret });', "assert.match(packedHttpSecret.secret, /^nmc1\\./u);", "await tenantFields.decryptFieldsInPlace(packedHttpSecret);", 'assert.equal(packedHttpSecret.secret, "packed HTTP");', - 'const prismaEncryption = entry4.createTenantPrismaFieldEncryption(tenantCipher, { registry: { PackedRecord: { secret: { purpose: "packed.prisma" } } } });', + 'const prismaEncryption = entry8.createTenantPrismaFieldEncryption(tenantCipher, { registry: { PackedRecord: { secret: { purpose: "packed.prisma" } } } });', 'const prismaArgs = { data: { secret: "packed Prisma" } };', 'await prismaEncryption.encryptWriteArgs({ model: "PackedRecord", operation: "create", args: prismaArgs });', "assert.match(prismaArgs.data.secret, /^nmc1\\./u);", diff --git a/src/core/cipher-engine.ts b/src/core/cipher-engine.ts index d90c525..20c1080 100644 --- a/src/core/cipher-engine.ts +++ b/src/core/cipher-engine.ts @@ -6,7 +6,7 @@ import { encodeCipherValue, type CipherCodec, } from "./cipher-codec.js"; -import { aadBytes, frame, utf8, decodeUtf8 } from "./encoding.js"; +import { aadBytes, frame, isStableBytes, utf8, decodeUtf8 } from "./encoding.js"; import { encodeProtectedHeader, envelopeInfo, @@ -77,24 +77,6 @@ function validIdentifier(value: unknown, maxLength: number): value is string { ); } -function isStableBytes(value: unknown): value is Uint8Array { - try { - if (!(value instanceof Uint8Array) || !ArrayBuffer.isView(value)) return false; - const prototype = Object.getPrototypeOf(value) as object | null; - if (prototype !== Uint8Array.prototype && prototype !== Buffer.prototype) return false; - if ( - Reflect.ownKeys(value).some( - (key) => typeof key !== "string" || !/^(?:0|[1-9]\d*)$/u.test(key), - ) - ) { - return false; - } - return !(value.buffer instanceof SharedArrayBuffer) && Number.isSafeInteger(value.byteLength); - } catch { - return false; - } -} - function captureAad(value: unknown, message: string): Uint8Array { if (value === undefined || typeof value === "string" || isStableBytes(value)) { return aadBytes(value); diff --git a/src/core/encoding.ts b/src/core/encoding.ts index 9abd3a7..318cd92 100644 --- a/src/core/encoding.ts +++ b/src/core/encoding.ts @@ -18,6 +18,32 @@ export function decodeUtf8(value: Uint8Array): string { } } +/** + * Byte views whose contents cannot change under us and whose length is trustworthy. + * Exotic subclasses, index-shadowing own properties, and `SharedArrayBuffer` backings are + * all rejected: each of them can turn a validated length into a lie before the bytes are + * consumed. + * + * @internal Shared by every boundary that accepts bulk caller data. + */ +export function isStableBytes(value: unknown): value is Uint8Array { + try { + if (!(value instanceof Uint8Array) || !ArrayBuffer.isView(value)) return false; + const prototype = Object.getPrototypeOf(value) as object | null; + if (prototype !== Uint8Array.prototype && prototype !== Buffer.prototype) return false; + if ( + Reflect.ownKeys(value).some( + (key) => typeof key !== "string" || !/^(?:0|[1-9]\d*)$/u.test(key), + ) + ) { + return false; + } + return !(value.buffer instanceof SharedArrayBuffer) && Number.isSafeInteger(value.byteLength); + } catch { + return false; + } +} + export function aadBytes(value?: CipherAad): Uint8Array { try { if (value === undefined) return new Uint8Array(); diff --git a/src/core/envelope.ts b/src/core/envelope.ts index d4c838b..8b4b69b 100644 --- a/src/core/envelope.ts +++ b/src/core/envelope.ts @@ -9,6 +9,15 @@ const MAX_WRAPPED_KEY_BYTES = 65_536; const MAX_NONCE_BYTES = 64; const MAX_TAG_BYTES = 64; +/** + * Cheap shape test: does `value` look like a `nmc1` envelope? Framing only — nothing is + * parsed or authenticated. Use it to route legacy plaintext during a migration, never as + * a validity check. + */ +export function isCipherEnvelope(value: string): boolean { + return typeof value === "string" && value.startsWith(`${ENVELOPE_PREFIX}.`); +} + export interface ProtectedHeader { readonly v: 1; readonly cipher: string; diff --git a/src/core/index.ts b/src/core/index.ts index e307e4f..da6543a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -16,7 +16,7 @@ export { isCryptoError, type CryptoErrorCode, } from "./errors.js"; -export { ENVELOPE_PREFIX } from "./envelope.js"; +export { ENVELOPE_PREFIX, isCipherEnvelope } from "./envelope.js"; export { aadBytes, base64url, decodeUtf8, frame, parseBase64url, utf8 } from "./encoding.js"; export type { BatchDecryptItem, diff --git a/src/keys/hmac.ts b/src/keys/hmac.ts new file mode 100644 index 0000000..77a488a --- /dev/null +++ b/src/keys/hmac.ts @@ -0,0 +1,52 @@ +import { createHmac, timingSafeEqual, KeyObject } from "node:crypto"; +import { CryptoError } from "../core/errors.js"; + +/** HMAC-SHA256 over the concatenation of `parts`. Callers that need unambiguous + * separation between parts must frame them first — this helper does not. */ +export function hmacSha256( + key: KeyObject | Uint8Array, + ...parts: readonly Uint8Array[] +): Uint8Array { + if (key instanceof KeyObject) { + if (key.type !== "secret") { + throw new CryptoError("INVALID_KEY", "An HMAC key must be a secret key."); + } + } else if (!(key instanceof Uint8Array)) { + throw new CryptoError("INVALID_KEY", "An HMAC key must be bytes or a secret key."); + } + try { + const mac = createHmac("sha256", key); + for (const part of parts) { + if (!(part instanceof Uint8Array)) { + throw new TypeError("Every HMAC input must be bytes."); + } + mac.update(part); + } + return new Uint8Array(mac.digest()); + } catch (error: unknown) { + if (error instanceof CryptoError) throw error; + throw new CryptoError("CIPHER_FAILURE", "HMAC computation failed.", { cause: error }); + } +} + +/** + * Constant-time byte comparison that tolerates unequal lengths. + * + * Equal-length inputs go straight to `timingSafeEqual`. Node's `timingSafeEqual` throws + * on a size mismatch, so unequal lengths are instead compared as fixed-width SHA-256 + * HMACs. The HMAC key is a fixed all-zero constant and is not a secret: it is only there + * to reach a fixed-width comparison, and the length difference that triggers this branch + * is already public through `byteLength`. + */ +export function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean { + if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array)) { + throw new CryptoError("INVALID_ARGUMENT", "Both comparison operands must be bytes."); + } + if (a.byteLength === b.byteLength) { + return timingSafeEqual(a, b); + } + // Different lengths already leak through `byteLength`; hash both sides anyway so the + // comparison itself stays a fixed-width operation rather than an early return. + const key = new Uint8Array(32); + return timingSafeEqual(hmacSha256(key, a), hmacSha256(key, b)); +} diff --git a/src/keys/index.ts b/src/keys/index.ts index 0788e38..d23fb9a 100644 --- a/src/keys/index.ts +++ b/src/keys/index.ts @@ -9,6 +9,28 @@ export { type X25519PrivateKeyInput, } from "./x25519.js"; export { hkdfSha256 } from "./hkdf.js"; +export { hmacSha256, timingSafeEqualBytes } from "./hmac.js"; +export { + decodeKeyWrapRecord, + decodeKeyWrapRecords, + encodeKeyWrapRecord, + encodeKeyWrapRecords, + keyWrapRecordLength, + selectKeyWrapRecord, + unwrapKeyFromRecipient, + unwrapKeyWithSecret, + wrapKeyToRecipient, + wrapKeyWithSecret, + KEY_WRAP_A256GCMKW, + KEY_WRAP_SEAL_X25519, + type KeyWrapRecipientOptions, + type KeyWrapRecipientType, + type KeyWrapRecord, + type KeyWrapRecordMatch, + type KeyWrapSecretOptions, + type KeyWrapUnwrapRecipientOptions, + type KeyWrapUnwrapSecretOptions, +} from "./key-wrap-record.js"; export { sealTo, openFrom, diff --git a/src/keys/key-wrap-record.ts b/src/keys/key-wrap-record.ts new file mode 100644 index 0000000..dc670e0 --- /dev/null +++ b/src/keys/key-wrap-record.ts @@ -0,0 +1,565 @@ +import { + createCipheriv, + createDecipheriv, + createSecretKey, + randomBytes, + KeyObject, +} from "node:crypto"; +import { aadBytes, decodeUtf8, frame, utf8 } from "../core/encoding.js"; +import { authenticationFailed, CryptoError } from "../core/errors.js"; +import type { CipherAad } from "../core/types.js"; +import { openKeyFrom, sealTo, SEAL_X25519_HKDF_SHA256_A256GCM, type SealOptions } from "./seal.js"; +import type { X25519PrivateKeyInput, X25519PublicKeyInput } from "./x25519.js"; + +/** AES-256-GCM key wrapping under a shared secret KEK. */ +export const KEY_WRAP_A256GCMKW = "A256GCMKW"; +/** Recipient-addressed key wrapping to an X25519 public key. */ +export const KEY_WRAP_SEAL_X25519 = SEAL_X25519_HKDF_SHA256_A256GCM; + +/** Matches a lone surrogate: a valid pair encodes as one non-surrogate code point. */ +const LONE_SURROGATE = /\p{Surrogate}/u; + +const RECORD_VERSION = 0x01; +const RECIPIENT_TYPE_SECRET = 0x01; +const RECIPIENT_TYPE_X25519 = 0x02; +const RECORD_HEADER_BYTES = 6; +const MAX_ALGORITHM_BYTES = 64; +const MAX_RECIPIENT_ID_BYTES = 255; +const MAX_WRAPPED_BYTES = 65_535; +/** One ceiling for both directions: the encoder never writes a list the decoder rejects. */ +const MAX_RECORDS = 64; +const AAD_LABEL = "nmc/keywrap/v1"; + +const NONCE_BYTES = 12; +const TAG_BYTES = 16; +const WRAP_VERSION = 0x01; +const MIN_KEY_BYTES = 16; +const MAX_KEY_BYTES = 512; + +export type KeyWrapRecipientType = "secret" | "x25519"; + +export interface KeyWrapRecord { + readonly version: 1; + readonly algorithm: string; + readonly recipientType: KeyWrapRecipientType; + /** Authenticated recipient identity. A record cannot be moved between recipients. */ + readonly recipientId: string; + readonly wrapped: Uint8Array; +} + +export interface KeyWrapSecretOptions { + readonly recipientId: string; + readonly aad?: CipherAad; +} + +export interface KeyWrapRecipientOptions { + readonly recipientId: string; + /** Bound into the seal key schedule. Must match at unwrap. */ + readonly info?: CipherAad; + /** Bound as additional associated data. Must match at unwrap. */ + readonly aad?: CipherAad; +} + +export interface KeyWrapUnwrapSecretOptions { + readonly aad?: CipherAad; + /** Require the recovered key to be exactly this many bytes. */ + readonly expectedKeyBytes?: number; +} + +export interface KeyWrapUnwrapRecipientOptions { + readonly info?: CipherAad; + readonly aad?: CipherAad; + /** Require the recovered key to be exactly this many bytes. */ + readonly expectedKeyBytes?: number; +} + +export interface KeyWrapRecordMatch { + readonly recipientType: KeyWrapRecipientType; + readonly recipientId: string; +} + +function typeByte(recipientType: KeyWrapRecipientType): number { + return recipientType === "secret" ? RECIPIENT_TYPE_SECRET : RECIPIENT_TYPE_X25519; +} + +/** + * Recipient identifiers are the binding between a wrap and the principal it is for, so + * they must be present and canonical. An empty identifier would bind nothing, and a lone + * surrogate would be folded onto U+FFFD by UTF-8 encoding — collapsing distinct + * recipients onto one identifier and breaking the encode/decode round trip. + */ +function assertRecipientId(recipientId: string): Uint8Array { + if ( + typeof recipientId !== "string" || + recipientId.length === 0 || + recipientId.trim() !== recipientId || + // A lone surrogate would be folded onto U+FFFD by UTF-8 encoding. + LONE_SURROGATE.test(recipientId) + ) { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap recipient identifier is invalid."); + } + if (/\p{Cc}/u.test(recipientId)) { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap recipient identifier is invalid."); + } + const bytes = utf8(recipientId); + if (bytes.byteLength > MAX_RECIPIENT_ID_BYTES) { + throw new CryptoError("LIMIT_EXCEEDED", "The key-wrap recipient identifier is too long."); + } + return bytes; +} + +/** Recipient options are mandatory: there is no unbound wrap. */ +function assertRecipientOptions(options: { readonly recipientId: string } | undefined): string { + if (typeof options !== "object" || options === null) { + throw new CryptoError("INVALID_ARGUMENT", "Key-wrap options are required."); + } + assertRecipientId(options.recipientId); + return options.recipientId; +} + +function assertAlgorithm(algorithm: string): Uint8Array { + if ( + typeof algorithm !== "string" || + algorithm.length === 0 || + algorithm.trim() !== algorithm || + // eslint-disable-next-line no-control-regex + !/^[\x20-\x7e]+$/u.test(algorithm) + ) { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap algorithm is invalid."); + } + const bytes = utf8(algorithm); + if (bytes.byteLength > MAX_ALGORITHM_BYTES) { + throw new CryptoError("LIMIT_EXCEEDED", "The key-wrap algorithm is too long."); + } + return bytes; +} + +/** + * Identity-binding associated data. Both wrap flavours authenticate the algorithm, + * recipient type, and recipient identifier, so a record produced for one recipient + * cannot be replayed as a record for another even under the same wrapping key. + */ +function recordAad( + algorithm: string, + recipientType: KeyWrapRecipientType, + recipientId: string, + callerAad?: CipherAad, +): Uint8Array { + return frame( + utf8(AAD_LABEL), + utf8(algorithm), + Uint8Array.of(typeByte(recipientType)), + utf8(recipientId), + aadBytes(callerAad), + ); +} + +function keyBytes(key: KeyObject | Uint8Array): { bytes: Uint8Array; owned: boolean } { + if (key instanceof KeyObject) { + if (key.type !== "secret") { + throw new CryptoError("INVALID_ARGUMENT", "Only a secret key can be wrapped."); + } + return { bytes: new Uint8Array(key.export()), owned: true }; + } + if (key instanceof Uint8Array) { + return { bytes: key, owned: false }; + } + throw new CryptoError("INVALID_ARGUMENT", "The wrapped key must be bytes or a secret key."); +} + +function assertKeyLength(bytes: Uint8Array): void { + if (bytes.byteLength < MIN_KEY_BYTES || bytes.byteLength > MAX_KEY_BYTES) { + throw new CryptoError("INVALID_KEY", "The wrapped key length is out of range."); + } +} + +/** + * Length-check a *recovered* key. An attacker who mints a record cannot be trusted to + * have wrapped a key of a sane size, so the range is always enforced and callers that + * know the exact size they expect can pin it. + */ +function assertRecoveredKeyLength(byteLength: number, expected: number | undefined): void { + if (byteLength < MIN_KEY_BYTES || byteLength > MAX_KEY_BYTES) { + throw new CryptoError("INVALID_KEY", "The unwrapped key length is out of range."); + } + if (expected === undefined) return; + if (!Number.isInteger(expected) || expected < MIN_KEY_BYTES || expected > MAX_KEY_BYTES) { + throw new CryptoError("INVALID_ARGUMENT", "The expected key length is out of range."); + } + if (byteLength !== expected) { + throw new CryptoError("INVALID_KEY", "The unwrapped key is not the expected length."); + } +} + +function normalizeKek(kek: KeyObject | Uint8Array): KeyObject { + if (kek instanceof KeyObject) { + if (kek.type !== "secret" || kek.symmetricKeySize !== 32) { + throw new CryptoError("INVALID_KEY", "A key-wrap KEK must contain 32 bytes."); + } + return kek; + } + if (!(kek instanceof Uint8Array) || kek.byteLength !== 32) { + throw new CryptoError("INVALID_KEY", "A key-wrap KEK must contain 32 bytes."); + } + const copy = Buffer.from(kek); + try { + return createSecretKey(copy); + } finally { + copy.fill(0); + } +} + +/** Wrap a key under a shared 32-byte KEK. Wire: `0x01 ‖ nonce(12) ‖ ciphertext ‖ tag(16)`. */ +export function wrapKeyWithSecret( + kek: KeyObject | Uint8Array, + key: KeyObject | Uint8Array, + options: KeyWrapSecretOptions, +): KeyWrapRecord { + const wrappingKey = normalizeKek(kek); + const recipientId = assertRecipientOptions(options); + const { bytes: raw, owned } = keyBytes(key); + try { + assertKeyLength(raw); + const nonce = randomBytes(NONCE_BYTES); + const aad = recordAad(KEY_WRAP_A256GCMKW, "secret", recipientId, options.aad); + const cipher = createCipheriv("aes-256-gcm", wrappingKey, nonce, { + authTagLength: TAG_BYTES, + }); + cipher.setAAD(aad, { plaintextLength: raw.byteLength }); + const ciphertext = Buffer.concat([cipher.update(raw), cipher.final()]); + try { + const wrapped = new Uint8Array(1 + NONCE_BYTES + ciphertext.byteLength + TAG_BYTES); + wrapped[0] = WRAP_VERSION; + wrapped.set(nonce, 1); + wrapped.set(ciphertext, 1 + NONCE_BYTES); + wrapped.set(cipher.getAuthTag(), 1 + NONCE_BYTES + ciphertext.byteLength); + return Object.freeze({ + version: 1, + algorithm: KEY_WRAP_A256GCMKW, + recipientType: "secret", + recipientId, + wrapped, + }) satisfies KeyWrapRecord; + } finally { + ciphertext.fill(0); + } + } catch (error: unknown) { + if (error instanceof CryptoError) throw error; + throw new CryptoError("CIPHER_FAILURE", "Key wrapping failed.", { cause: error }); + } finally { + if (owned) raw.fill(0); + } +} + +/** Unwrap a `secret` record produced by {@link wrapKeyWithSecret}. */ +export function unwrapKeyWithSecret( + kek: KeyObject | Uint8Array, + record: KeyWrapRecord, + options?: KeyWrapUnwrapSecretOptions, +): KeyObject { + const wrappingKey = normalizeKek(kek); + assertRecordShape(record); + if (record.recipientType !== "secret") { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap record is not a secret-KEK record."); + } + if (record.algorithm !== KEY_WRAP_A256GCMKW) { + throw new CryptoError("INVALID_KEY", "The key-wrap algorithm is unsupported."); + } + const wrapped = record.wrapped; + if (wrapped.byteLength < 1 + NONCE_BYTES + MIN_KEY_BYTES + TAG_BYTES) { + throw authenticationFailed(); + } + if (wrapped[0] !== WRAP_VERSION) { + throw new CryptoError("UNSUPPORTED_VERSION", "The key-wrap record version is unsupported."); + } + const nonce = wrapped.subarray(1, 1 + NONCE_BYTES); + const ciphertext = wrapped.subarray(1 + NONCE_BYTES, wrapped.byteLength - TAG_BYTES); + const tag = wrapped.subarray(wrapped.byteLength - TAG_BYTES); + let first: Buffer | undefined; + let last: Buffer | undefined; + let raw: Buffer | undefined; + try { + const aad = recordAad(record.algorithm, "secret", record.recipientId, options?.aad); + const decipher = createDecipheriv("aes-256-gcm", wrappingKey, nonce, { + authTagLength: TAG_BYTES, + }); + decipher.setAAD(aad, { plaintextLength: ciphertext.byteLength }); + decipher.setAuthTag(tag); + first = decipher.update(ciphertext); + last = decipher.final(); + raw = Buffer.concat([first, last]); + assertRecoveredKeyLength(raw.byteLength, options?.expectedKeyBytes); + return createSecretKey(raw); + } catch (error: unknown) { + if ( + error instanceof CryptoError && + (error.code === "INVALID_KEY" || error.code === "INVALID_ARGUMENT") + ) { + throw error; + } + throw authenticationFailed({ cause: error }); + } finally { + first?.fill(0); + last?.fill(0); + raw?.fill(0); + } +} + +/** Seal a key to an X25519 public key. Wire: the `sealTo` blob, unchanged. */ +export function wrapKeyToRecipient( + publicKey: X25519PublicKeyInput, + key: KeyObject | Uint8Array, + options: KeyWrapRecipientOptions, +): KeyWrapRecord { + const recipientId = assertRecipientOptions(options); + const { bytes: raw, owned } = keyBytes(key); + try { + assertKeyLength(raw); + const sealOptions: SealOptions = { + ...(options.info === undefined ? {} : { info: options.info }), + aad: recordAad(KEY_WRAP_SEAL_X25519, "x25519", recipientId, options.aad), + }; + return Object.freeze({ + version: 1, + algorithm: KEY_WRAP_SEAL_X25519, + recipientType: "x25519", + recipientId, + wrapped: sealTo(publicKey, raw, sealOptions), + }) satisfies KeyWrapRecord; + } finally { + if (owned) raw.fill(0); + } +} + +/** Open an `x25519` record produced by {@link wrapKeyToRecipient}. */ +export function unwrapKeyFromRecipient( + privateKey: X25519PrivateKeyInput, + record: KeyWrapRecord, + options?: KeyWrapUnwrapRecipientOptions, +): KeyObject { + assertRecordShape(record); + if (record.recipientType !== "x25519") { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap record is not a recipient record."); + } + if (record.algorithm !== KEY_WRAP_SEAL_X25519) { + throw new CryptoError("INVALID_KEY", "The key-wrap algorithm is unsupported."); + } + const key = openKeyFrom(privateKey, record.wrapped, { + ...(options?.info === undefined ? {} : { info: options.info }), + aad: recordAad(record.algorithm, "x25519", record.recipientId, options?.aad), + }); + assertRecoveredKeyLength(key.symmetricKeySize ?? 0, options?.expectedKeyBytes); + return key; +} + +function assertRecordShape(record: KeyWrapRecord): void { + if (typeof record !== "object" || record === null) { + throw new CryptoError("INVALID_ARGUMENT", "A key-wrap record is required."); + } + if (record.version !== 1) { + throw new CryptoError("UNSUPPORTED_VERSION", "The key-wrap record version is unsupported."); + } + if (record.recipientType !== "secret" && record.recipientType !== "x25519") { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap recipient type is invalid."); + } + assertAlgorithm(record.algorithm); + assertRecipientId(record.recipientId); + if (!(record.wrapped instanceof Uint8Array) || record.wrapped.byteLength === 0) { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap record payload is invalid."); + } + if (record.wrapped.byteLength > MAX_WRAPPED_BYTES) { + throw new CryptoError("LIMIT_EXCEEDED", "The key-wrap record payload is too large."); + } +} + +/** Serialized size of a record, without encoding it. */ +export function keyWrapRecordLength(record: KeyWrapRecord): number { + assertRecordShape(record); + return ( + RECORD_HEADER_BYTES + + utf8(record.algorithm).byteLength + + utf8(record.recipientId).byteLength + + record.wrapped.byteLength + ); +} + +export function encodeKeyWrapRecord(record: KeyWrapRecord): Uint8Array { + assertRecordShape(record); + const algorithm = assertAlgorithm(record.algorithm); + const recipientId = assertRecipientId(record.recipientId); + const output = new Uint8Array( + RECORD_HEADER_BYTES + algorithm.byteLength + recipientId.byteLength + record.wrapped.byteLength, + ); + const view = new DataView(output.buffer); + output[0] = RECORD_VERSION; + output[1] = typeByte(record.recipientType); + output[2] = algorithm.byteLength; + output[3] = recipientId.byteLength; + view.setUint16(4, record.wrapped.byteLength, false); + let offset = RECORD_HEADER_BYTES; + output.set(algorithm, offset); + offset += algorithm.byteLength; + output.set(recipientId, offset); + offset += recipientId.byteLength; + output.set(record.wrapped, offset); + return output; +} + +/** Decode record text, mapping any UTF-8 failure onto a parsing error. */ +function decodeRecordText(bytes: Uint8Array): string { + try { + return decodeUtf8(bytes); + } catch (error: unknown) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record is malformed.", { + cause: error, + }); + } +} + +function decodeRecordAt(bytes: Uint8Array, start: number): { record: KeyWrapRecord; end: number } { + if (bytes.byteLength - start < RECORD_HEADER_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record is truncated."); + } + if (bytes[start] !== RECORD_VERSION) { + throw new CryptoError("UNSUPPORTED_VERSION", "The key-wrap record version is unsupported."); + } + const rawType = bytes[start + 1]; + if (rawType !== RECIPIENT_TYPE_SECRET && rawType !== RECIPIENT_TYPE_X25519) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap recipient type is unknown."); + } + const algorithmLength = bytes[start + 2] ?? 0; + const recipientIdLength = bytes[start + 3] ?? 0; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const wrappedLength = view.getUint16(start + 4, false); + if (algorithmLength === 0 || wrappedLength === 0) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record is malformed."); + } + if (recipientIdLength === 0) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap recipient identifier is missing."); + } + const end = start + RECORD_HEADER_BYTES + algorithmLength + recipientIdLength + wrappedLength; + if (end > bytes.byteLength) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record is truncated."); + } + let offset = start + RECORD_HEADER_BYTES; + // Hostile wire bytes are a parsing failure, not a decryption failure. + const algorithm = decodeRecordText(bytes.subarray(offset, offset + algorithmLength)); + offset += algorithmLength; + const recipientId = decodeRecordText(bytes.subarray(offset, offset + recipientIdLength)); + offset += recipientIdLength; + const wrapped = new Uint8Array(bytes.subarray(offset, offset + wrappedLength)); + const record: KeyWrapRecord = Object.freeze({ + version: 1, + algorithm, + recipientType: rawType === RECIPIENT_TYPE_SECRET ? "secret" : "x25519", + recipientId, + wrapped, + }); + try { + assertRecordShape(record); + } catch (error: unknown) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record is malformed.", { + cause: error, + }); + } + return { record, end }; +} + +/** + * Decode exactly `count` records starting at `start`, returning the end offset. + * + * @internal Used by container formats that carry records inline and already know the + * count from their own header. Not part of the published surface. + */ +export function decodeKeyWrapRecordSequence( + bytes: Uint8Array, + start: number, + count: number, +): { records: readonly KeyWrapRecord[]; end: number } { + if (count > MAX_RECORDS) { + throw new CryptoError("MALFORMED_ENVELOPE", "Too many key-wrap records."); + } + const records: KeyWrapRecord[] = []; + let offset = start; + for (let index = 0; index < count; index += 1) { + const decoded = decodeRecordAt(bytes, offset); + records.push(decoded.record); + offset = decoded.end; + } + return { records: Object.freeze(records), end: offset }; +} + +export function decodeKeyWrapRecord(bytes: Uint8Array): KeyWrapRecord { + if (!(bytes instanceof Uint8Array)) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record must be bytes."); + } + const { record, end } = decodeRecordAt(bytes, 0); + if (end !== bytes.byteLength) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record has trailing bytes."); + } + return record; +} + +export function encodeKeyWrapRecords(records: readonly KeyWrapRecord[]): Uint8Array { + if (!Array.isArray(records)) { + throw new CryptoError("INVALID_ARGUMENT", "A key-wrap record list is required."); + } + if (records.length > MAX_RECORDS) { + throw new CryptoError("LIMIT_EXCEEDED", "Too many key-wrap records."); + } + const encoded = records.map((record) => encodeKeyWrapRecord(record)); + let total = 2; + for (const record of encoded) total += record.byteLength; + const output = new Uint8Array(total); + new DataView(output.buffer).setUint16(0, records.length, false); + let offset = 2; + for (const record of encoded) { + output.set(record, offset); + offset += record.byteLength; + } + return output; +} + +export function decodeKeyWrapRecords( + bytes: Uint8Array, + maxRecords = MAX_RECORDS, +): readonly KeyWrapRecord[] { + if (!(bytes instanceof Uint8Array)) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record list must be bytes."); + } + if (!Number.isInteger(maxRecords) || maxRecords < 0) { + throw new CryptoError("INVALID_ARGUMENT", "The key-wrap record limit is invalid."); + } + if (bytes.byteLength < 2) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record list is truncated."); + } + const count = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint16(0, false); + if (count > maxRecords) { + throw new CryptoError("LIMIT_EXCEEDED", "Too many key-wrap records."); + } + const records: KeyWrapRecord[] = []; + let offset = 2; + for (let index = 0; index < count; index += 1) { + const { record, end } = decodeRecordAt(bytes, offset); + records.push(record); + offset = end; + } + if (offset !== bytes.byteLength) { + throw new CryptoError("MALFORMED_ENVELOPE", "The key-wrap record list has trailing bytes."); + } + return Object.freeze(records); +} + +/** First record addressed to `match`, or `undefined`. Nothing is authenticated here. */ +export function selectKeyWrapRecord( + records: readonly KeyWrapRecord[], + match: KeyWrapRecordMatch, +): KeyWrapRecord | undefined { + if (!Array.isArray(records)) { + throw new CryptoError("INVALID_ARGUMENT", "A key-wrap record list is required."); + } + return records.find( + (record) => + record.recipientType === match.recipientType && record.recipientId === match.recipientId, + ); +} diff --git a/src/password/index.ts b/src/password/index.ts new file mode 100644 index 0000000..8c4c77d --- /dev/null +++ b/src/password/index.ts @@ -0,0 +1,24 @@ +export { + createPasswordKdf, + decodePasswordKdfParams, + encodePasswordKdfParams, + generatePasswordSalt, + PASSWORD_KDF_SCRYPT_DEFAULT, + scryptPasswordKdf, + type CreatePasswordKdfOptions, + type DerivePasswordKeyInput, + type PasswordKdf, + type PasswordKdfAlgorithm, + type PasswordKdfDeriveInput, + type PasswordKdfId, + type PasswordKdfParams, + type ScryptParams, +} from "./password-kdf.js"; +export { + deriveRecoveryKey, + generateRecoveryCode, + parseRecoveryCode, + RECOVERY_CODE_PREFIX, + type GeneratedRecoveryCode, + type GenerateRecoveryCodeOptions, +} from "./recovery-code.js"; diff --git a/src/password/password-kdf.ts b/src/password/password-kdf.ts new file mode 100644 index 0000000..b7d2f0e --- /dev/null +++ b/src/password/password-kdf.ts @@ -0,0 +1,389 @@ +import { createSecretKey, randomBytes, scrypt, type KeyObject } from "node:crypto"; +import { aadBytes, frame, utf8 } from "../core/encoding.js"; +import { CryptoError, throwIfAborted } from "../core/errors.js"; +import type { CipherAad } from "../core/types.js"; +import { hkdfSha256 } from "../keys/hkdf.js"; + +export type PasswordKdfId = "scrypt-1"; + +export interface ScryptParams { + readonly N: number; + readonly r: number; + readonly p: number; +} + +export interface PasswordKdfParams { + readonly id: PasswordKdfId | (string & {}); + readonly params: unknown; +} + +/** Interactive-login defaults: 64 MiB, roughly 100-150 ms on current server hardware. */ +export const PASSWORD_KDF_SCRYPT_DEFAULT: PasswordKdfParams = Object.freeze({ + id: "scrypt-1", + params: Object.freeze({ N: 65_536, r: 8, p: 1 }), +}); + +const DERIVED_KEY_BYTES = 32; +const MIN_SALT_BYTES = 16; +const DEFAULT_SALT_BYTES = 32; +const MAX_SALT_BYTES = 1024; +const DEFAULT_MAX_CONCURRENCY = 4; +const INFO_LABEL = "nmc/password/v1"; +const EMPTY_SALT = new Uint8Array(); + +/** + * Absolute parameter ceilings, independent of anything a caller passes. Without them a + * single derivation could reserve tens of gigabytes and run for minutes, which is a + * denial of service wearing a password-hardening costume. + */ +const MAX_SCRYPT_COST = 2 ** 20; +const MAX_SCRYPT_BLOCK_SIZE = 32; +const MAX_SCRYPT_PARALLELISM = 16; +/** Hard per-derivation footprint ceiling: 1 GiB. */ +const MAX_SCRYPT_MEMORY_BYTES = 1024 * 1024 * 1024; +/** Ceiling on what is ever handed to Node as `maxmem`, headroom included. */ +const MAX_SCRYPT_MAXMEM_BYTES = Math.ceil(MAX_SCRYPT_MEMORY_BYTES * 1.25); + +/** RFC 7914 working-memory footprint of a single derivation. */ +function scryptMemoryBytes(N: number, r: number): number { + return 128 * N * r; +} + +export interface DerivePasswordKeyInput { + readonly password: string | Uint8Array; + readonly salt: Uint8Array; + readonly kdf: PasswordKdfParams; + /** Domain separation applied to the KDF output. Must match to reproduce the key. */ + readonly info?: CipherAad; + readonly signal?: AbortSignal; +} + +export interface PasswordKdfDeriveInput { + readonly password: Uint8Array; + readonly salt: Uint8Array; + readonly params: unknown; + readonly keyLength: number; + readonly signal?: AbortSignal; +} + +export interface PasswordKdfAlgorithm { + readonly id: string; + validateParams(params: unknown): void; + derive(input: PasswordKdfDeriveInput): Promise; +} + +export interface PasswordKdf { + readonly defaultParams: PasswordKdfParams; + derive(input: DerivePasswordKeyInput): Promise; + supports(id: string): boolean; +} + +export interface CreatePasswordKdfOptions { + readonly algorithms?: readonly PasswordKdfAlgorithm[]; + readonly defaultParams?: PasswordKdfParams; + /** Bounds concurrent derivations. Each scrypt call holds `128 * N * r` bytes. */ + readonly maxConcurrency?: number; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function isPowerOfTwo(value: number): boolean { + return Number.isInteger(value) && value > 1 && (value & (value - 1)) === 0; +} + +function assertScryptParams(params: unknown): asserts params is ScryptParams { + if (!isRecord(params)) { + throw new CryptoError("INVALID_ARGUMENT", "The scrypt parameters are invalid."); + } + if (Object.keys(params).toSorted().join(",") !== "N,p,r") { + throw new CryptoError("INVALID_ARGUMENT", "The scrypt parameters have an invalid shape."); + } + const { N, r, p } = params as Record<"N" | "r" | "p", unknown>; + if (typeof N !== "number" || !isPowerOfTwo(N) || N < 1024 || N > MAX_SCRYPT_COST) { + throw new CryptoError("INVALID_ARGUMENT", "The scrypt cost parameter is out of range."); + } + if (typeof r !== "number" || !Number.isInteger(r) || r < 1 || r > MAX_SCRYPT_BLOCK_SIZE) { + throw new CryptoError("INVALID_ARGUMENT", "The scrypt block size is out of range."); + } + if (typeof p !== "number" || !Number.isInteger(p) || p < 1 || p > MAX_SCRYPT_PARALLELISM) { + throw new CryptoError("INVALID_ARGUMENT", "The scrypt parallelism is out of range."); + } + // The individual ranges still admit combinations that would reserve many gigabytes per + // derivation, so the footprint itself is what has to be bounded. Rejecting outright + // rather than clamping keeps `maxmem` a true ceiling instead of a value the caller + // dictates. + if (scryptMemoryBytes(N, r) > MAX_SCRYPT_MEMORY_BYTES) { + throw new CryptoError("INVALID_ARGUMENT", "The scrypt parameters require too much memory."); + } +} + +/** RFC 7914 scrypt over `node:crypto`. No native dependency; core stays dependency-free. */ +export function scryptPasswordKdf(): PasswordKdfAlgorithm { + return Object.freeze({ + id: "scrypt-1", + validateParams(params: unknown): void { + assertScryptParams(params); + }, + async derive(input: PasswordKdfDeriveInput): Promise { + assertScryptParams(input.params); + const { N, r, p } = input.params; + // Node rejects the derivation outright unless maxmem covers 128 * N * r. The + // parameters are already bounded above, so this stays a ceiling rather than + // something the caller can inflate. + const maxmem = Math.min(Math.ceil(scryptMemoryBytes(N, r) * 1.25), MAX_SCRYPT_MAXMEM_BYTES); + return await new Promise((resolve, reject) => { + scrypt( + input.password, + input.salt, + input.keyLength, + { N, r, p, maxmem }, + (error, derived) => { + if (error) { + reject( + new CryptoError("CIPHER_FAILURE", "Password derivation failed.", { cause: error }), + ); + return; + } + resolve(new Uint8Array(derived)); + }, + ); + }); + }, + }) satisfies PasswordKdfAlgorithm; +} + +/** Random salt for a new password record. */ +export function generatePasswordSalt(byteLength: number = DEFAULT_SALT_BYTES): Uint8Array { + if (!Number.isInteger(byteLength) || byteLength < MIN_SALT_BYTES || byteLength > MAX_SALT_BYTES) { + throw new CryptoError("INVALID_ARGUMENT", "The password salt length is out of range."); + } + return new Uint8Array(randomBytes(byteLength)); +} + +function canonicalize(value: unknown): unknown { + if (Array.isArray(value)) return value.map((entry) => canonicalize(entry)); + if (isRecord(value)) { + const result: Record = {}; + for (const key of Object.keys(value).toSorted()) { + result[key] = canonicalize(value[key]); + } + return result; + } + return value; +} + +function assertParamsShape(value: unknown): asserts value is PasswordKdfParams { + if (!isRecord(value)) { + throw new CryptoError("INVALID_ARGUMENT", "The password KDF parameters are invalid."); + } + if (Object.keys(value).toSorted().join(",") !== "id,params") { + throw new CryptoError("INVALID_ARGUMENT", "The password KDF parameters have an invalid shape."); + } + if ( + typeof value.id !== "string" || + value.id.length === 0 || + value.id.length > 64 || + value.id.trim() !== value.id || + /\p{Cc}/u.test(value.id) + ) { + throw new CryptoError("INVALID_ARGUMENT", "The password KDF identifier is invalid."); + } + if (!isRecord(value.params)) { + throw new CryptoError("INVALID_ARGUMENT", "The password KDF parameters are invalid."); + } +} + +/** Canonical JSON encoding, stable across releases and safe for a database column. */ +export function encodePasswordKdfParams(params: PasswordKdfParams): string { + assertParamsShape(params); + return JSON.stringify({ id: params.id, params: canonicalize(params.params) }); +} + +export function decodePasswordKdfParams(value: string): PasswordKdfParams { + if (typeof value !== "string" || value.length === 0 || value.length > 4096) { + throw new CryptoError("INVALID_ARGUMENT", "The encoded password KDF parameters are invalid."); + } + let parsed: unknown; + try { + parsed = JSON.parse(value) as unknown; + } catch (error: unknown) { + throw new CryptoError( + "INVALID_ARGUMENT", + "The encoded password KDF parameters are malformed.", + { + cause: error, + }, + ); + } + assertParamsShape(parsed); + const normalized: PasswordKdfParams = Object.freeze({ + id: parsed.id, + params: Object.freeze(canonicalize(parsed.params)), + }); + if (encodePasswordKdfParams(normalized) !== value) { + throw new CryptoError( + "INVALID_ARGUMENT", + "The encoded password KDF parameters are not canonical.", + ); + } + return normalized; +} + +/** FIFO semaphore. Memory-hard derivations must never run unbounded in parallel. */ +function createSemaphore(limit: number): (signal?: AbortSignal) => Promise<() => void> { + let active = 0; + const waiting: (() => void)[] = []; + const release = (): void => { + active -= 1; + const next = waiting.shift(); + if (next) next(); + }; + return async (signal?: AbortSignal): Promise<() => void> => { + throwIfAborted(signal); + if (active < limit) { + active += 1; + return release; + } + await new Promise((resolve, reject) => { + const onAbort = (): void => { + const index = waiting.indexOf(admit); + if (index !== -1) waiting.splice(index, 1); + reject(new CryptoError("ABORTED", "The cryptographic operation was aborted.")); + }; + const admit = (): void => { + signal?.removeEventListener("abort", onAbort); + active += 1; + resolve(); + }; + waiting.push(admit); + signal?.addEventListener("abort", onAbort, { once: true }); + }); + return release; + }; +} + +function passwordBytes(password: string | Uint8Array): { bytes: Uint8Array; owned: boolean } { + if (typeof password === "string") { + if (password.length === 0) { + throw new CryptoError("INVALID_ARGUMENT", "The password must be non-empty."); + } + return { bytes: utf8(password), owned: true }; + } + if (password instanceof Uint8Array) { + if (password.byteLength === 0) { + throw new CryptoError("INVALID_ARGUMENT", "The password must be non-empty."); + } + return { bytes: password, owned: false }; + } + throw new CryptoError("INVALID_ARGUMENT", "The password must be text or bytes."); +} + +export function createPasswordKdf(options?: CreatePasswordKdfOptions): PasswordKdf { + const algorithms = options?.algorithms ?? [scryptPasswordKdf()]; + if (!Array.isArray(algorithms) || algorithms.length === 0) { + throw new CryptoError("CONFIGURATION", "A password KDF registry is required."); + } + const registry = new Map(); + for (const algorithm of algorithms) { + try { + if ( + typeof algorithm !== "object" || + algorithm === null || + typeof algorithm.id !== "string" || + algorithm.id.length === 0 || + algorithm.id.length > 64 || + algorithm.id.trim() !== algorithm.id || + typeof algorithm.derive !== "function" || + typeof algorithm.validateParams !== "function" + ) { + throw new TypeError("Invalid password KDF registration."); + } + if (registry.has(algorithm.id)) throw new TypeError("Duplicate password KDF identifier."); + registry.set(algorithm.id, algorithm); + } catch (error: unknown) { + throw new CryptoError("CONFIGURATION", "A password KDF registration is invalid.", { + cause: error, + }); + } + } + + const defaultParams = options?.defaultParams ?? PASSWORD_KDF_SCRYPT_DEFAULT; + assertParamsShape(defaultParams); + const defaultAlgorithm = registry.get(defaultParams.id); + if (defaultAlgorithm === undefined) { + throw new CryptoError("CONFIGURATION", "The default password KDF is not registered."); + } + try { + defaultAlgorithm.validateParams(defaultParams.params); + } catch (error: unknown) { + throw new CryptoError("CONFIGURATION", "The default password KDF parameters are invalid.", { + cause: error, + }); + } + + const maxConcurrency = options?.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY; + if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1 || maxConcurrency > 1024) { + throw new CryptoError("CONFIGURATION", "The password KDF concurrency limit is invalid."); + } + const acquire = createSemaphore(maxConcurrency); + + return Object.freeze({ + defaultParams: Object.freeze({ + id: defaultParams.id, + params: Object.freeze(canonicalize(defaultParams.params)), + }), + supports(id: string): boolean { + return registry.has(id); + }, + async derive(input: DerivePasswordKeyInput): Promise { + assertParamsShape(input.kdf); + const algorithm = registry.get(input.kdf.id); + if (algorithm === undefined) { + throw new CryptoError("CONFIGURATION", "The password KDF is not registered."); + } + algorithm.validateParams(input.kdf.params); + if ( + !(input.salt instanceof Uint8Array) || + input.salt.byteLength < MIN_SALT_BYTES || + input.salt.byteLength > MAX_SALT_BYTES + ) { + throw new CryptoError("INVALID_ARGUMENT", "The password salt length is out of range."); + } + const { bytes: password, owned } = passwordBytes(input.password); + // Acquisition can reject (abort, or an aborted wait in the queue). Keeping it inside + // the same try is what guarantees the plaintext copy is wiped on that path too. + let release: (() => void) | undefined; + let raw: Uint8Array | undefined; + let okm: Uint8Array | undefined; + try { + release = await acquire(input.signal); + throwIfAborted(input.signal); + raw = await algorithm.derive({ + password, + salt: input.salt, + params: input.kdf.params, + keyLength: DERIVED_KEY_BYTES, + ...(input.signal === undefined ? {} : { signal: input.signal }), + }); + throwIfAborted(input.signal); + if (!(raw instanceof Uint8Array) || raw.byteLength !== DERIVED_KEY_BYTES) { + throw new CryptoError("INVALID_KEY", "The password KDF returned invalid key material."); + } + okm = hkdfSha256( + raw, + EMPTY_SALT, + frame(utf8(INFO_LABEL), aadBytes(input.info)), + DERIVED_KEY_BYTES, + ); + return createSecretKey(okm); + } finally { + release?.(); + if (owned) password.fill(0); + raw?.fill(0); + okm?.fill(0); + } + }, + }) satisfies PasswordKdf; +} diff --git a/src/password/recovery-code.ts b/src/password/recovery-code.ts new file mode 100644 index 0000000..c6d5590 --- /dev/null +++ b/src/password/recovery-code.ts @@ -0,0 +1,183 @@ +import { createHash, createSecretKey, randomBytes, type KeyObject } from "node:crypto"; +import { aadBytes, frame, utf8 } from "../core/encoding.js"; +import { CryptoError } from "../core/errors.js"; +import type { CipherAad } from "../core/types.js"; +import { hkdfSha256 } from "../keys/hkdf.js"; +import { timingSafeEqualBytes } from "../keys/hmac.js"; + +/** Prefix identifying a v1 account-recovery code. */ +export const RECOVERY_CODE_PREFIX = "ASR1"; + +const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; +/** + * Five bytes encode to exactly eight base32 characters, so padding the payload to a + * multiple of five bytes yields uniform groups with no ragged tail and no partial-byte + * padding for the parser to second-guess. + */ +const GROUP_SIZE = 8; +const GROUP_BYTES = 5; +const DERIVED_KEY_BYTES = 32; +const MIN_SALT_BYTES = 16; +const MAX_SALT_BYTES = 1024; +const INFO_LABEL = "nmc/recovery/v1"; + +export interface GenerateRecoveryCodeOptions { + readonly entropyBits?: 128 | 160; +} + +export interface GeneratedRecoveryCode { + /** Display-once value. Never persist this — store only a wrap derived from `secret`. */ + readonly code: string; + readonly secret: Uint8Array; +} + +/** + * Checksum width is whatever it takes to pad the secret to a whole number of groups: + * four bytes for a 128-bit secret, five for a 160-bit one. Both are far stronger than a + * single byte, so a mistyped code is rejected on sight rather than sent to the KDF. + */ +function checksumBytesFor(secretBytes: number): number { + const remainder = secretBytes % GROUP_BYTES; + return remainder === 0 ? GROUP_BYTES : GROUP_BYTES - remainder; +} + +function checksum(secret: Uint8Array, length: number): Uint8Array { + return new Uint8Array(createHash("sha256").update(secret).digest().subarray(0, length)); +} + +function encodeBase32(bytes: Uint8Array): string { + let output = ""; + let buffer = 0; + let bits = 0; + for (const byte of bytes) { + buffer = (buffer << 8) | byte; + bits += 8; + while (bits >= 5) { + output += ALPHABET[(buffer >>> (bits - 5)) & 0x1f]; + bits -= 5; + } + } + if (bits > 0) { + output += ALPHABET[(buffer << (5 - bits)) & 0x1f]; + } + return output; +} + +function decodeBase32(value: string, expectedBytes: number): Uint8Array { + const output = new Uint8Array(expectedBytes); + let buffer = 0; + let bits = 0; + let index = 0; + for (const character of value) { + const digit = ALPHABET.indexOf(character); + if (digit === -1) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code contains invalid characters."); + } + buffer = (buffer << 5) | digit; + bits += 5; + if (bits >= 8) { + if (index >= expectedBytes) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code is malformed."); + } + output[index] = (buffer >>> (bits - 8)) & 0xff; + index += 1; + bits -= 8; + } + } + if (index !== expectedBytes) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code is malformed."); + } + // Trailing bits exist only as zero padding; a non-zero remainder is not canonical. + if (bits > 0 && (buffer & ((1 << bits) - 1)) !== 0) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code is not canonical."); + } + return output; +} + +/** + * Generate a display-once recovery code. The secret is full-entropy, so a memory-hard + * KDF buys nothing over HKDF at {@link deriveRecoveryKey}. + */ +export function generateRecoveryCode(options?: GenerateRecoveryCodeOptions): GeneratedRecoveryCode { + const entropyBits = options?.entropyBits ?? 128; + if (entropyBits !== 128 && entropyBits !== 160) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code entropy is unsupported."); + } + const secret = new Uint8Array(randomBytes(entropyBits / 8)); + const checksumLength = checksumBytesFor(secret.byteLength); + const payload = new Uint8Array(secret.byteLength + checksumLength); + payload.set(secret, 0); + payload.set(checksum(secret, checksumLength), secret.byteLength); + try { + const encoded = encodeBase32(payload); + const groups: string[] = []; + for (let index = 0; index < encoded.length; index += GROUP_SIZE) { + groups.push(encoded.slice(index, index + GROUP_SIZE)); + } + return Object.freeze({ code: `${RECOVERY_CODE_PREFIX}-${groups.join("-")}`, secret }); + } finally { + payload.fill(0); + } +} + +/** Parse and checksum-verify a user-entered recovery code. Fail-closed on any deviation. */ +export function parseRecoveryCode(code: string): Uint8Array { + if (typeof code !== "string" || code.length === 0 || code.length > 128) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code is invalid."); + } + const normalized = code + .trim() + .toUpperCase() + .replace(/[\s-]+/gu, "") + // Crockford aliases: visually ambiguous characters map onto their canonical digit. + .replace(/[IL]/gu, "1") + .replace(/O/gu, "0"); + if (!normalized.startsWith(RECOVERY_CODE_PREFIX)) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code prefix is unrecognized."); + } + const body = normalized.slice(RECOVERY_CODE_PREFIX.length); + // 128-bit → 16 secret + 4 checksum = 20 bytes → 32 characters; + // 160-bit → 20 secret + 5 checksum = 25 bytes → 40 characters. + const secretBytes = body.length === 32 ? 16 : body.length === 40 ? 20 : undefined; + if (secretBytes === undefined) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery code length is invalid."); + } + const checksumLength = checksumBytesFor(secretBytes); + const payload = decodeBase32(body, secretBytes + checksumLength); + try { + const secret = new Uint8Array(payload.subarray(0, secretBytes)); + const actual = new Uint8Array(payload.subarray(secretBytes)); + if (!timingSafeEqualBytes(checksum(secret, checksumLength), actual)) { + secret.fill(0); + throw new CryptoError("INVALID_ARGUMENT", "The recovery code checksum does not match."); + } + return secret; + } finally { + payload.fill(0); + } +} + +/** Derive the recovery key-encryption key from a parsed recovery secret. */ +export function deriveRecoveryKey( + secret: Uint8Array, + salt: Uint8Array, + info?: CipherAad, +): KeyObject { + if (!(secret instanceof Uint8Array) || secret.byteLength < 16) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery secret is invalid."); + } + if ( + !(salt instanceof Uint8Array) || + salt.byteLength < MIN_SALT_BYTES || + salt.byteLength > MAX_SALT_BYTES + ) { + throw new CryptoError("INVALID_ARGUMENT", "The recovery salt length is out of range."); + } + let okm: Uint8Array | undefined; + try { + okm = hkdfSha256(secret, salt, frame(utf8(INFO_LABEL), aadBytes(info)), DERIVED_KEY_BYTES); + return createSecretKey(okm); + } finally { + okm?.fill(0); + } +} diff --git a/src/stream/index.ts b/src/stream/index.ts new file mode 100644 index 0000000..ae2da2a --- /dev/null +++ b/src/stream/index.ts @@ -0,0 +1,19 @@ +export { + chunkedChunkRange, + chunkedCiphertextLength, + chunkedPlaintextLength, + createChunkedOpenStream, + createChunkedSealStream, + inspectChunked, + openChunked, + sealChunked, + type ChunkedOpenOptions, + type ChunkedSealOptions, +} from "./nmcs-chunked.js"; +export { + NMCS_DEFAULT_CHUNK_SIZE_LOG2, + NMCS_HEADER_BYTES, + NMCS_MAGIC, + NMCS_SUITE_AES256GCM_HKDF_SHA256, + type ChunkedHeaderInfo, +} from "./nmcs-format.js"; diff --git a/src/stream/nmcs-chunked.ts b/src/stream/nmcs-chunked.ts new file mode 100644 index 0000000..0627846 --- /dev/null +++ b/src/stream/nmcs-chunked.ts @@ -0,0 +1,640 @@ +import { KeyObject } from "node:crypto"; +import { Aes256GcmCipher } from "../core/aes-256-gcm.js"; +import { isStableBytes } from "../core/encoding.js"; +import { authenticationFailed, CryptoError, throwIfAborted } from "../core/errors.js"; +import type { CipherAad } from "../core/types.js"; +import type { KeyWrapRecord } from "../keys/key-wrap-record.js"; +import { + assertByteLimit, + assertChunkSizeCeiling, + assertChunkSizeLog2, + CHUNK_LENGTH_PREFIX_BYTES, + CHUNK_OVERHEAD_BYTES, + chunkAad, + chunkNonce, + contextAadBytes, + decodeHeaderContent, + deriveChunkSchedule, + deriveHeaderSchedule, + encodeHeaderContent, + generateFileId, + HEADER_CONTENT_BYTES, + headerInfo, + MAX_CHUNK_INDEX, + NMCS_DEFAULT_CHUNK_SIZE_LOG2, + NMCS_HEADER_BYTES, + normalizeStreamKey, + readFileIdOverride, + TAG_BYTES, + type ChunkedHeaderInfo, + type HeaderFields, +} from "./nmcs-format.js"; + +export interface ChunkedSealOptions { + /** Opaque key selector recorded in the authenticated header. */ + readonly keyReference: string; + /** Application context bound into the header (never encrypted — it is associated data). */ + readonly aad?: CipherAad; + readonly chunkSizeLog2?: number; + /** + * Raises the chunk-size ceiling above the default of 24. Readers must opt in to the same + * ceiling, so anything written above the default is unreadable by a default reader. + */ + readonly maxChunkSizeLog2?: number; + /** Declares the plaintext length in the header. Must match the payload exactly. */ + readonly plaintextLength?: number; + /** Wrapped data keys carried inline so the object is self-contained. */ + readonly wrapRecords?: readonly KeyWrapRecord[]; + readonly maxPlaintextBytes?: number; + readonly signal?: AbortSignal; +} + +export interface ChunkedOpenOptions { + readonly aad?: CipherAad; + /** Enforced against the authenticated header when present. */ + readonly expectedKeyReference?: string; + readonly maxPlaintextBytes?: number; + readonly maxChunkSizeLog2?: number; + readonly signal?: AbortSignal; +} + +export interface InspectChunkedOptions { + readonly maxChunkSizeLog2?: number; +} + +/** + * Byte accumulator for the streaming paths. + * + * Source writes are retained by reference and only copied when a frame is actually + * consumed, so a chunk assembled from many small writes costs one copy of its own bytes + * rather than one copy of the whole backlog per write. + */ +class ByteQueue { + #parts: Uint8Array[] = []; + #offset = 0; + #byteLength = 0; + + get byteLength(): number { + return this.#byteLength; + } + + push(chunk: Uint8Array): void { + if (chunk.byteLength === 0) return; + this.#parts.push(chunk); + this.#byteLength += chunk.byteLength; + } + + /** Big-endian u32 at `position`, without consuming. Callers must check `byteLength`. */ + readUint32(position: number): number { + let value = 0; + for (let index = 0; index < 4; index += 1) { + value = value * 0x100 + this.#byteAt(position + index); + } + return value; + } + + /** Copy out and consume exactly `length` bytes. */ + take(length: number): Uint8Array { + if (length > this.#byteLength) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + const output = new Uint8Array(length); + let written = 0; + while (written < length) { + const part = this.#parts[0] as Uint8Array; + const available = part.byteLength - this.#offset; + const wanted = Math.min(available, length - written); + output.set(part.subarray(this.#offset, this.#offset + wanted), written); + written += wanted; + this.#offset += wanted; + if (this.#offset === part.byteLength) { + this.#parts.shift(); + this.#offset = 0; + } + } + this.#byteLength -= length; + return output; + } + + #byteAt(position: number): number { + let remaining = position + this.#offset; + for (const part of this.#parts) { + if (remaining < part.byteLength) return part[remaining] as number; + remaining -= part.byteLength; + } + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } +} + +/** Sealed byte length for a plaintext of `plaintextLength` bytes. */ +export function chunkedCiphertextLength( + plaintextLength: number, + chunkSizeLog2: number = NMCS_DEFAULT_CHUNK_SIZE_LOG2, +): number { + assertChunkSizeLog2(chunkSizeLog2); + if (!Number.isInteger(plaintextLength) || plaintextLength < 0) { + throw new CryptoError( + "INVALID_ARGUMENT", + "The plaintext length must be a non-negative integer.", + ); + } + const chunkSize = 2 ** chunkSizeLog2; + const chunks = plaintextLength === 0 ? 1 : Math.ceil(plaintextLength / chunkSize); + return NMCS_HEADER_BYTES + plaintextLength + chunks * CHUNK_OVERHEAD_BYTES; +} + +/** + * Exact inverse of {@link chunkedCiphertextLength}. Returns `undefined` when + * `ciphertextLength` is not a value the format can produce, which lets callers + * translate stored sizes back to plaintext sizes without reading the object. + */ +export function chunkedPlaintextLength( + ciphertextLength: number, + chunkSizeLog2: number = NMCS_DEFAULT_CHUNK_SIZE_LOG2, +): number | undefined { + assertChunkSizeLog2(chunkSizeLog2); + if (!Number.isInteger(ciphertextLength) || ciphertextLength < 0) return undefined; + const body = ciphertextLength - NMCS_HEADER_BYTES; + if (body < CHUNK_OVERHEAD_BYTES) return undefined; + const chunkSize = 2 ** chunkSizeLog2; + const perChunk = chunkSize + CHUNK_OVERHEAD_BYTES; + // Full chunks first, then whatever remains must form exactly one final chunk. + const fullChunks = Math.floor(body / perChunk); + for (const chunks of [fullChunks, fullChunks + 1]) { + if (chunks < 1) continue; + const plaintext = body - chunks * CHUNK_OVERHEAD_BYTES; + if (plaintext < 0) continue; + if (chunkedCiphertextLength(plaintext, chunkSizeLog2) === ciphertextLength) return plaintext; + } + return undefined; +} + +/** Ciphertext byte range of chunk `chunkIndex`, for range-read planning. */ +export function chunkedChunkRange( + chunkIndex: number, + chunkSizeLog2: number = NMCS_DEFAULT_CHUNK_SIZE_LOG2, +): { start: number; end: number } { + assertChunkSizeLog2(chunkSizeLog2); + if (!Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex >= MAX_CHUNK_INDEX) { + throw new CryptoError("INVALID_ARGUMENT", "The chunk index is out of range."); + } + const stride = 2 ** chunkSizeLog2 + CHUNK_OVERHEAD_BYTES; + const start = NMCS_HEADER_BYTES + chunkIndex * stride; + return { start, end: start + stride }; +} + +/** Untrusted framing metadata. Nothing is authenticated until an open succeeds. */ +export function inspectChunked( + headerPrefix: Uint8Array, + options?: InspectChunkedOptions, +): ChunkedHeaderInfo { + // A complete header includes its tag: accepting 496 bytes would report on a prefix that + // no reader could ever authenticate. + if (!isStableBytes(headerPrefix) || headerPrefix.byteLength < NMCS_HEADER_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream header is truncated."); + } + const ceiling = assertChunkSizeCeiling(options?.maxChunkSizeLog2); + return headerInfo(decodeHeaderContent(headerPrefix.subarray(0, HEADER_CONTENT_BYTES), ceiling)); +} + +interface SealContext { + readonly header: Uint8Array; + readonly chunkKey: KeyObject; + readonly noncePrefix: Uint8Array; + readonly chunkSize: number; +} + +/** Build the full 512-byte header and the chunk key schedule it commits to. */ +function beginSeal(key: KeyObject, options: ChunkedSealOptions): SealContext { + const chunkSizeLog2 = assertChunkSizeLog2( + options.chunkSizeLog2 ?? NMCS_DEFAULT_CHUNK_SIZE_LOG2, + assertChunkSizeCeiling(options.maxChunkSizeLog2), + ); + assertByteLimit(options.maxPlaintextBytes); + if (options.plaintextLength !== undefined) { + if (!Number.isSafeInteger(options.plaintextLength) || options.plaintextLength < 0) { + throw new CryptoError("INVALID_ARGUMENT", "The declared plaintext length is invalid."); + } + } + const override = readFileIdOverride(options); + const fields: HeaderFields = { + chunkSizeLog2, + fileId: override ?? generateFileId(), + keyReference: options.keyReference, + contextAad: contextAadBytes(options.aad), + wrapRecords: options.wrapRecords ?? [], + ...(options.plaintextLength === undefined ? {} : { plaintextLength: options.plaintextLength }), + }; + const content = encodeHeaderContent(fields); + const { headerKey, headerNonce } = deriveHeaderSchedule(key, fields.fileId); + const { tag } = new Aes256GcmCipher().encrypt({ + plaintext: new Uint8Array(), + key: headerKey, + nonce: headerNonce, + aad: content, + }); + const header = new Uint8Array(NMCS_HEADER_BYTES); + header.set(content, 0); + header.set(tag, HEADER_CONTENT_BYTES); + const { chunkKey, noncePrefix } = deriveChunkSchedule(key, content); + return { header, chunkKey, noncePrefix, chunkSize: 2 ** chunkSizeLog2 }; +} + +/** Encode one chunk: `u32BE length ‖ ciphertext ‖ tag`. */ +function sealChunk( + context: SealContext, + index: number, + plaintext: Uint8Array, + final: boolean, +): Uint8Array { + if (index >= MAX_CHUNK_INDEX) { + throw new CryptoError("LIMIT_EXCEEDED", "The chunked stream has too many chunks."); + } + const { ciphertext, tag } = new Aes256GcmCipher().encrypt({ + plaintext, + key: context.chunkKey, + nonce: chunkNonce(context.noncePrefix, index, final), + aad: chunkAad(index, final, plaintext.byteLength), + }); + const framed = new Uint8Array(CHUNK_LENGTH_PREFIX_BYTES + ciphertext.byteLength + TAG_BYTES); + new DataView(framed.buffer).setUint32(0, ciphertext.byteLength, false); + framed.set(ciphertext, CHUNK_LENGTH_PREFIX_BYTES); + framed.set(tag, CHUNK_LENGTH_PREFIX_BYTES + ciphertext.byteLength); + return framed; +} + +export function sealChunked( + key: KeyObject | Uint8Array, + plaintext: Uint8Array, + options: ChunkedSealOptions, +): Uint8Array { + if (!isStableBytes(plaintext)) { + throw new CryptoError("INVALID_ARGUMENT", "The chunked stream plaintext must be bytes."); + } + throwIfAborted(options.signal); + const maxPlaintextBytes = assertByteLimit(options.maxPlaintextBytes); + if (maxPlaintextBytes !== undefined && plaintext.byteLength > maxPlaintextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + // A declared length that contradicts the payload would produce an object whose own + // header makes it permanently unopenable, so refuse it rather than emit it. + if (options.plaintextLength !== undefined && options.plaintextLength !== plaintext.byteLength) { + throw new CryptoError( + "INVALID_ARGUMENT", + "The declared plaintext length does not match the payload.", + ); + } + const streamKey = normalizeStreamKey(key); + const context = beginSeal(streamKey, { ...options, plaintextLength: plaintext.byteLength }); + const parts: Uint8Array[] = [context.header]; + let index = 0; + let offset = 0; + do { + throwIfAborted(options.signal); + const end = Math.min(offset + context.chunkSize, plaintext.byteLength); + const final = end >= plaintext.byteLength; + parts.push(sealChunk(context, index, plaintext.subarray(offset, end), final)); + offset = end; + index += 1; + } while (offset < plaintext.byteLength); + + let total = 0; + for (const part of parts) total += part.byteLength; + const sealed = new Uint8Array(total); + let cursor = 0; + for (const part of parts) { + sealed.set(part, cursor); + cursor += part.byteLength; + } + return sealed; +} + +interface OpenContext { + readonly fields: HeaderFields; + readonly chunkKey: KeyObject; + readonly noncePrefix: Uint8Array; + readonly chunkSize: number; +} + +/** Verify the header tag and derive the chunk schedule it commits to. */ +function beginOpen( + key: KeyObject, + header: Uint8Array, + options: ChunkedOpenOptions | undefined, +): OpenContext { + const content = header.subarray(0, HEADER_CONTENT_BYTES); + // The chunk-size ceiling is enforced here, before a single chunk is buffered, so a + // hostile header cannot make the reader hold an oversized frame in memory. + const fields = decodeHeaderContent(content, assertChunkSizeCeiling(options?.maxChunkSizeLog2)); + const { headerKey, headerNonce } = deriveHeaderSchedule(key, fields.fileId); + new Aes256GcmCipher().decrypt({ + ciphertext: new Uint8Array(), + key: headerKey, + nonce: headerNonce, + tag: header.subarray(HEADER_CONTENT_BYTES, NMCS_HEADER_BYTES), + aad: content, + }); + + // Only compare caller expectations after the header is authenticated. + const expectedAad = contextAadBytes(options?.aad); + if ( + expectedAad.byteLength !== fields.contextAad.byteLength || + !expectedAad.every((byte, index) => byte === fields.contextAad[index]) + ) { + throw authenticationFailed(); + } + if ( + options?.expectedKeyReference !== undefined && + options.expectedKeyReference !== fields.keyReference + ) { + throw authenticationFailed(); + } + const maxPlaintextBytes = assertByteLimit(options?.maxPlaintextBytes); + if ( + maxPlaintextBytes !== undefined && + fields.plaintextLength !== undefined && + fields.plaintextLength > maxPlaintextBytes + ) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + const { chunkKey, noncePrefix } = deriveChunkSchedule(key, content); + return { fields, chunkKey, noncePrefix, chunkSize: 2 ** fields.chunkSizeLog2 }; +} + +function openChunk( + context: OpenContext, + index: number, + ciphertext: Uint8Array, + tag: Uint8Array, + final: boolean, +): Uint8Array { + if (index >= MAX_CHUNK_INDEX) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream has too many chunks."); + } + return new Aes256GcmCipher().decrypt({ + ciphertext, + key: context.chunkKey, + nonce: chunkNonce(context.noncePrefix, index, final), + tag, + aad: chunkAad(index, final, ciphertext.byteLength), + }); +} + +export function openChunked( + key: KeyObject | Uint8Array, + sealed: Uint8Array, + options?: ChunkedOpenOptions, +): Uint8Array { + if (!isStableBytes(sealed)) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream must be bytes."); + } + if (sealed.byteLength < NMCS_HEADER_BYTES + CHUNK_OVERHEAD_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + throwIfAborted(options?.signal); + const streamKey = normalizeStreamKey(key); + const context = beginOpen(streamKey, sealed.subarray(0, NMCS_HEADER_BYTES), options); + + const parts: Uint8Array[] = []; + let offset = NMCS_HEADER_BYTES; + let index = 0; + let total = 0; + let terminated = false; + const view = new DataView(sealed.buffer, sealed.byteOffset, sealed.byteLength); + + while (offset < sealed.byteLength) { + throwIfAborted(options?.signal); + if (terminated) { + throw new CryptoError( + "MALFORMED_ENVELOPE", + "The chunked stream continues past its final chunk.", + ); + } + if (sealed.byteLength - offset < CHUNK_OVERHEAD_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + const length = view.getUint32(offset, false); + if (length > context.chunkSize) { + throw new CryptoError("MALFORMED_ENVELOPE", "A chunk exceeds the declared chunk size."); + } + const bodyStart = offset + CHUNK_LENGTH_PREFIX_BYTES; + const tagStart = bodyStart + length; + if (tagStart + TAG_BYTES > sealed.byteLength) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + // The final flag is authenticated, so it is resolved by trial: a chunk that is not the + // last one in the buffer must be a full non-final chunk. + const isLast = tagStart + TAG_BYTES === sealed.byteLength; + if (!isLast && length !== context.chunkSize) { + throw new CryptoError("MALFORMED_ENVELOPE", "A non-final chunk is not full."); + } + const plaintext = openChunk( + context, + index, + sealed.subarray(bodyStart, tagStart), + sealed.subarray(tagStart, tagStart + TAG_BYTES), + isLast, + ); + parts.push(plaintext); + total += plaintext.byteLength; + if (options?.maxPlaintextBytes !== undefined && total > options.maxPlaintextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + terminated = isLast; + offset = tagStart + TAG_BYTES; + index += 1; + } + + if (!terminated) { + // Unreachable for well-formed input; a truncated stream fails the final-flag check above. + throw authenticationFailed(); + } + if (context.fields.plaintextLength !== undefined && context.fields.plaintextLength !== total) { + throw authenticationFailed(); + } + + const output = new Uint8Array(total); + let cursor = 0; + for (const part of parts) { + output.set(part, cursor); + cursor += part.byteLength; + } + return output; +} + +/** + * Streaming seal. + * + * Byte-identical to {@link sealChunked} for the same inputs **only when + * `plaintextLength` is declared**: `sealChunked` always knows the size and records it, + * whereas a stream does not, so an undeclared stream sets the header's declared-length + * flag to zero and produces a different (equally valid, same-length) header. Pass + * `plaintextLength` when the size is known so readers can validate it and so the two + * paths agree byte for byte. + */ +export function createChunkedSealStream( + key: KeyObject | Uint8Array, + options: ChunkedSealOptions, +): TransformStream { + const streamKey = normalizeStreamKey(key); + const maxPlaintextBytes = assertByteLimit(options.maxPlaintextBytes); + let context: SealContext | undefined; + const pending = new ByteQueue(); + let index = 0; + let total = 0; + + return new TransformStream({ + start(controller) { + context = beginSeal(streamKey, options); + controller.enqueue(context.header); + }, + transform(chunk, controller) { + throwIfAborted(options.signal); + if (!isStableBytes(chunk)) { + throw new CryptoError("INVALID_ARGUMENT", "The chunked stream plaintext must be bytes."); + } + const active = context!; + total += chunk.byteLength; + if (maxPlaintextBytes !== undefined && total > maxPlaintextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + pending.push(chunk); + // Hold back one full chunk: the final chunk is flagged, so it can only be emitted + // once the stream is known to be finished. + while (pending.byteLength > active.chunkSize) { + controller.enqueue(sealChunk(active, index, pending.take(active.chunkSize), false)); + index += 1; + } + }, + flush(controller) { + throwIfAborted(options.signal); + const active = context!; + if (options.plaintextLength !== undefined && options.plaintextLength !== total) { + throw new CryptoError( + "INVALID_ARGUMENT", + "The declared plaintext length does not match the stream.", + ); + } + controller.enqueue(sealChunk(active, index, pending.take(pending.byteLength), true)); + }, + }); +} + +/** + * Streaming open. + * + * IMPORTANT: this emits an *authenticated prefix*. Every chunk is tag-verified before it + * is enqueued, but truncation of the stream is only detected at the end — a consumer that + * acts on partial output before the stream completes successfully may act on a prefix of + * the plaintext. Callers needing all-or-nothing semantics must use {@link openChunked}. + */ +export function createChunkedOpenStream( + key: KeyObject | Uint8Array, + options?: ChunkedOpenOptions, +): TransformStream { + const streamKey = normalizeStreamKey(key); + const maxPlaintextBytes = assertByteLimit(options?.maxPlaintextBytes); + assertChunkSizeCeiling(options?.maxChunkSizeLog2); + let context: OpenContext | undefined; + const pending = new ByteQueue(); + let index = 0; + let total = 0; + let received = 0; + /** Largest ciphertext a compliant object could have under `maxPlaintextBytes`. */ + let maxCiphertextBytes = Number.POSITIVE_INFINITY; + + /** Emit every chunk that is provably complete and provably not the final one. */ + const drain = (controller: TransformStreamDefaultController): void => { + const active = context!; + const stride = active.chunkSize + CHUNK_OVERHEAD_BYTES; + // A chunk is only known to be non-final once bytes beyond it have arrived. + while (pending.byteLength > stride) { + const length = pending.readUint32(0); + if (length !== active.chunkSize) { + throw new CryptoError("MALFORMED_ENVELOPE", "A non-final chunk is not full."); + } + const frame = pending.take(stride); + const bodyStart = CHUNK_LENGTH_PREFIX_BYTES; + const tagStart = bodyStart + length; + const plaintext = openChunk( + active, + index, + frame.subarray(bodyStart, tagStart), + frame.subarray(tagStart, tagStart + TAG_BYTES), + false, + ); + total += plaintext.byteLength; + if (maxPlaintextBytes !== undefined && total > maxPlaintextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + controller.enqueue(plaintext); + index += 1; + } + }; + + return new TransformStream({ + transform(chunk, controller) { + throwIfAborted(options?.signal); + if (!isStableBytes(chunk)) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream must be bytes."); + } + received += chunk.byteLength; + // Trip the plaintext budget on arrival rather than after a full chunk decrypts, so a + // hostile object cannot force the reader to buffer one whole chunk per limit check. + if (received > maxCiphertextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + pending.push(chunk); + if (context === undefined) { + if (pending.byteLength < NMCS_HEADER_BYTES) return; + context = beginOpen(streamKey, pending.take(NMCS_HEADER_BYTES), options); + if (maxPlaintextBytes !== undefined) { + maxCiphertextBytes = chunkedCiphertextLength( + maxPlaintextBytes, + context.fields.chunkSizeLog2, + ); + if (received > maxCiphertextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + } + } + drain(controller); + }, + flush(controller) { + throwIfAborted(options?.signal); + if (context === undefined) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + const active = context; + drain(controller); + if (pending.byteLength < CHUNK_OVERHEAD_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + const length = pending.readUint32(0); + if (length > active.chunkSize) { + throw new CryptoError("MALFORMED_ENVELOPE", "A chunk exceeds the declared chunk size."); + } + const bodyStart = CHUNK_LENGTH_PREFIX_BYTES; + const tagStart = bodyStart + length; + if (tagStart + TAG_BYTES !== pending.byteLength) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream is truncated."); + } + const frame = pending.take(pending.byteLength); + const plaintext = openChunk( + active, + index, + frame.subarray(bodyStart, tagStart), + frame.subarray(tagStart, tagStart + TAG_BYTES), + true, + ); + total += plaintext.byteLength; + if (maxPlaintextBytes !== undefined && total > maxPlaintextBytes) { + throw new CryptoError("LIMIT_EXCEEDED", "The plaintext exceeds the configured limit."); + } + if (active.fields.plaintextLength !== undefined && active.fields.plaintextLength !== total) { + throw authenticationFailed(); + } + controller.enqueue(plaintext); + }, + }); +} diff --git a/src/stream/nmcs-format.ts b/src/stream/nmcs-format.ts new file mode 100644 index 0000000..fd7a687 --- /dev/null +++ b/src/stream/nmcs-format.ts @@ -0,0 +1,439 @@ +import { createSecretKey, createHash, randomBytes, KeyObject } from "node:crypto"; +import { aadBytes, decodeUtf8, frame, utf8 } from "../core/encoding.js"; +import { CryptoError } from "../core/errors.js"; +import type { CipherAad } from "../core/types.js"; +import { hkdfSha256 } from "../keys/hkdf.js"; +import { + decodeKeyWrapRecordSequence, + encodeKeyWrapRecords, + type KeyWrapRecord, +} from "../keys/key-wrap-record.js"; + +/** Magic prefix of the chunked stream container. */ +export const NMCS_MAGIC = "nmcs"; +/** Suite identifier: AES-256-GCM chunks with an HKDF-SHA256 key schedule. */ +export const NMCS_SUITE_AES256GCM_HKDF_SHA256 = 0x01; +/** Default plaintext chunk size exponent (2^20 = 1 MiB). */ +export const NMCS_DEFAULT_CHUNK_SIZE_LOG2 = 20; +/** Fixed on-the-wire header size, including its authentication tag. */ +export const NMCS_HEADER_BYTES = 512; + +/** Authenticated header content: everything the header tag covers. */ +export const HEADER_CONTENT_BYTES = 496; +export const TAG_BYTES = 16; +export const CHUNK_LENGTH_PREFIX_BYTES = 4; +/** Per-chunk framing overhead: the length prefix plus the AEAD tag. */ +export const CHUNK_OVERHEAD_BYTES = CHUNK_LENGTH_PREFIX_BYTES + TAG_BYTES; + +export const NMCS_VERSION = 0x01; +export const MIN_CHUNK_SIZE_LOG2 = 16; +/** Largest exponent the wire format can express. Requires an explicit opt-in on both sides. */ +export const MAX_CHUNK_SIZE_LOG2 = 26; +/** + * Default policy ceiling, applied symmetrically to sealing and opening so the library + * never writes an object it would refuse to read back. + */ +export const DEFAULT_MAX_CHUNK_SIZE_LOG2 = 24; +/** + * Exclusive upper bound on the chunk index: the policy caps an object at 2^20 + * chunks, so valid indices are `0 .. MAX_CHUNK_INDEX - 1`. The u32 index field + * bounds it at 2^32 regardless. + */ +export const MAX_CHUNK_INDEX = 0x10_0000; + +const FILE_ID_BYTES = 16; +const FLAG_WRAP_RECORDS = 0b0000_0001; +const FLAG_PLAINTEXT_LENGTH = 0b0000_0010; +const KNOWN_FLAGS = FLAG_WRAP_RECORDS | FLAG_PLAINTEXT_LENGTH; +const UNDECLARED_LENGTH = 0xffff_ffff_ffff_ffffn; + +/** Matches a lone surrogate: a valid pair encodes as one non-surrogate code point. */ +const LONE_SURROGATE = /\p{Surrogate}/u; + +const MAGIC_BYTES = utf8(NMCS_MAGIC); +const HDR_INFO_LABEL = "nmcs/v1/hdr"; +const CHUNK_INFO_LABEL = "nmcs/v1/chunk"; +const AES_KEY_BYTES = 32; +const NONCE_BYTES = 12; +const NONCE_PREFIX_BYTES = 7; +const HDR_OKM_BYTES = AES_KEY_BYTES + NONCE_BYTES; +const CHUNK_OKM_BYTES = AES_KEY_BYTES + NONCE_PREFIX_BYTES; + +// Fixed field offsets inside the header content. +const OFF_VERSION = 4; +const OFF_SUITE = 5; +const OFF_FLAGS = 6; +const OFF_CHUNK_LOG2 = 7; +const OFF_FILE_ID = 8; +const OFF_PLAINTEXT_LENGTH = 24; +const OFF_KEY_REF_LENGTH = 32; +const OFF_CONTEXT_AAD_LENGTH = 34; +const OFF_WRAP_COUNT = 36; +const OFF_VARIABLE = 38; + +export interface ChunkedHeaderInfo { + readonly version: 1; + readonly suite: number; + readonly chunkSize: number; + readonly fileId: Uint8Array; + readonly keyReference: string; + readonly contextAad: Uint8Array; + readonly wrapRecords: readonly KeyWrapRecord[]; + readonly plaintextLength?: number; + readonly headerBytes: number; + readonly authenticated: false; +} + +export interface HeaderFields { + readonly chunkSizeLog2: number; + readonly fileId: Uint8Array; + readonly keyReference: string; + readonly contextAad: Uint8Array; + readonly wrapRecords: readonly KeyWrapRecord[]; + readonly plaintextLength?: number; +} + +/** + * Validate a chunk-size exponent against the format range and a policy ceiling. + * + * `ceiling` defaults to the widest the format can express because the pure size helpers + * allocate nothing; the seal and open paths pass {@link DEFAULT_MAX_CHUNK_SIZE_LOG2} + * unless the caller opts in to a larger one on *both* sides. + */ +export function assertChunkSizeLog2(value: number, ceiling: number = MAX_CHUNK_SIZE_LOG2): number { + if (!Number.isInteger(value) || value < MIN_CHUNK_SIZE_LOG2 || value > MAX_CHUNK_SIZE_LOG2) { + throw new CryptoError("INVALID_ARGUMENT", "The chunk size exponent is out of range."); + } + if (value > ceiling) { + throw new CryptoError("LIMIT_EXCEEDED", "The chunk size exponent exceeds the limit."); + } + return value; +} + +/** Validate an opt-in chunk-size ceiling supplied by a caller. */ +export function assertChunkSizeCeiling(value: number | undefined): number { + if (value === undefined) return DEFAULT_MAX_CHUNK_SIZE_LOG2; + if (!Number.isInteger(value) || value < MIN_CHUNK_SIZE_LOG2 || value > MAX_CHUNK_SIZE_LOG2) { + throw new CryptoError("INVALID_ARGUMENT", "The chunk size limit is out of range."); + } + return value; +} + +/** Validate an optional byte budget. `NaN` and friends must never disable a bound. */ +export function assertByteLimit(value: number | undefined): number | undefined { + if (value === undefined) return undefined; + if (!Number.isSafeInteger(value) || value < 0) { + throw new CryptoError("INVALID_ARGUMENT", "The byte limit must be a non-negative integer."); + } + return value; +} + +export function normalizeStreamKey(key: KeyObject | Uint8Array): KeyObject { + if (key instanceof KeyObject) { + if (key.type !== "secret" || key.symmetricKeySize !== AES_KEY_BYTES) { + throw new CryptoError("INVALID_KEY", "A chunked stream key must contain 32 bytes."); + } + return key; + } + if (!(key instanceof Uint8Array) || key.byteLength !== AES_KEY_BYTES) { + throw new CryptoError("INVALID_KEY", "A chunked stream key must contain 32 bytes."); + } + const copy = Buffer.from(key); + try { + return createSecretKey(copy); + } finally { + copy.fill(0); + } +} + +/** + * Canonical form of a header key reference. Lone surrogates are rejected because UTF-8 + * encoding would silently fold them onto U+FFFD, collapsing distinct references onto one + * and breaking the encode/decode round trip. + */ +function isCanonicalKeyReference(keyReference: string): boolean { + return ( + typeof keyReference === "string" && + keyReference.length > 0 && + keyReference.trim() === keyReference && + // A lone surrogate would be folded onto U+FFFD by UTF-8 encoding. + !LONE_SURROGATE.test(keyReference) && + !/\p{Cc}/u.test(keyReference) + ); +} + +function assertKeyReference(keyReference: string): Uint8Array { + if (!isCanonicalKeyReference(keyReference)) { + throw new CryptoError("INVALID_ARGUMENT", "The key reference is invalid."); + } + const bytes = utf8(keyReference); + if (bytes.byteLength > 0xffff) { + throw new CryptoError("LIMIT_EXCEEDED", "The key reference is too long."); + } + return bytes; +} + +/** Decode header text, mapping any UTF-8 failure onto a parsing error. */ +function decodeHeaderText(bytes: Uint8Array): string { + try { + return decodeUtf8(bytes); + } catch (error: unknown) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream header is malformed.", { + cause: error, + }); + } +} + +/** Build the 496-byte authenticated header content. Padding is verified on parse. */ +export function encodeHeaderContent(fields: HeaderFields): Uint8Array { + const keyReference = assertKeyReference(fields.keyReference); + const contextAad = fields.contextAad; + const wrapRecords = fields.wrapRecords; + const encodedRecords = wrapRecords.length > 0 ? encodeKeyWrapRecords(wrapRecords) : undefined; + if (contextAad.byteLength > 0xffff) { + throw new CryptoError("LIMIT_EXCEEDED", "The context associated data is too long."); + } + + const content = new Uint8Array(HEADER_CONTENT_BYTES); + const view = new DataView(content.buffer); + content.set(MAGIC_BYTES, 0); + content[OFF_VERSION] = NMCS_VERSION; + content[OFF_SUITE] = NMCS_SUITE_AES256GCM_HKDF_SHA256; + content[OFF_FLAGS] = + (encodedRecords === undefined ? 0 : FLAG_WRAP_RECORDS) | + (fields.plaintextLength === undefined ? 0 : FLAG_PLAINTEXT_LENGTH); + content[OFF_CHUNK_LOG2] = fields.chunkSizeLog2; + content.set(fields.fileId, OFF_FILE_ID); + view.setBigUint64( + OFF_PLAINTEXT_LENGTH, + fields.plaintextLength === undefined ? UNDECLARED_LENGTH : BigInt(fields.plaintextLength), + false, + ); + view.setUint16(OFF_KEY_REF_LENGTH, keyReference.byteLength, false); + view.setUint16(OFF_CONTEXT_AAD_LENGTH, contextAad.byteLength, false); + view.setUint16(OFF_WRAP_COUNT, wrapRecords.length, false); + + // The record list carries its own u16 count; only the records themselves are appended + // so the header stays a single canonical encoding of the same information. + const recordBody = encodedRecords?.subarray(2) ?? new Uint8Array(); + const variable = + OFF_VARIABLE + keyReference.byteLength + contextAad.byteLength + recordBody.byteLength; + if (variable > HEADER_CONTENT_BYTES) { + throw new CryptoError("LIMIT_EXCEEDED", "The chunked stream header is too large."); + } + let offset = OFF_VARIABLE; + content.set(keyReference, offset); + offset += keyReference.byteLength; + content.set(contextAad, offset); + offset += contextAad.byteLength; + content.set(recordBody, offset); + return content; +} + +export function decodeHeaderContent(content: Uint8Array, maxChunkSizeLog2: number): HeaderFields { + if (content.byteLength !== HEADER_CONTENT_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream header is malformed."); + } + for (let index = 0; index < MAGIC_BYTES.byteLength; index += 1) { + if (content[index] !== MAGIC_BYTES[index]) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream magic is invalid."); + } + } + if (content[OFF_VERSION] !== NMCS_VERSION) { + throw new CryptoError("UNSUPPORTED_VERSION", "The chunked stream version is unsupported."); + } + if (content[OFF_SUITE] !== NMCS_SUITE_AES256GCM_HKDF_SHA256) { + throw new CryptoError("UNSUPPORTED_CIPHER", "The chunked stream suite is unsupported."); + } + const flags = content[OFF_FLAGS] ?? 0; + if ((flags & ~KNOWN_FLAGS) !== 0) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream header sets reserved flags."); + } + const chunkSizeLog2 = content[OFF_CHUNK_LOG2] ?? 0; + if (chunkSizeLog2 < MIN_CHUNK_SIZE_LOG2 || chunkSizeLog2 > MAX_CHUNK_SIZE_LOG2) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream chunk size is invalid."); + } + if (chunkSizeLog2 > maxChunkSizeLog2) { + throw new CryptoError("LIMIT_EXCEEDED", "The chunked stream chunk size exceeds the limit."); + } + + const view = new DataView(content.buffer, content.byteOffset, content.byteLength); + const fileId = new Uint8Array(content.subarray(OFF_FILE_ID, OFF_FILE_ID + FILE_ID_BYTES)); + const rawLength = view.getBigUint64(OFF_PLAINTEXT_LENGTH, false); + const declared = (flags & FLAG_PLAINTEXT_LENGTH) !== 0; + if (!declared && rawLength !== UNDECLARED_LENGTH) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream length flag is inconsistent."); + } + if (declared && rawLength > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new CryptoError("LIMIT_EXCEEDED", "The chunked stream declares an unusable length."); + } + const keyReferenceLength = view.getUint16(OFF_KEY_REF_LENGTH, false); + const contextAadLength = view.getUint16(OFF_CONTEXT_AAD_LENGTH, false); + const wrapCount = view.getUint16(OFF_WRAP_COUNT, false); + if (wrapCount > 0 !== ((flags & FLAG_WRAP_RECORDS) !== 0)) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream wrap flag is inconsistent."); + } + if (keyReferenceLength === 0) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream key reference is missing."); + } + + let offset = OFF_VARIABLE; + if (offset + keyReferenceLength + contextAadLength > HEADER_CONTENT_BYTES) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream header is malformed."); + } + const keyReference = decodeHeaderText(content.subarray(offset, offset + keyReferenceLength)); + if (!isCanonicalKeyReference(keyReference)) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream key reference is invalid."); + } + offset += keyReferenceLength; + const contextAad = new Uint8Array(content.subarray(offset, offset + contextAadLength)); + offset += contextAadLength; + + let wrapRecords: readonly KeyWrapRecord[] = []; + if (wrapCount > 0) { + // Records are self-delimiting and the count is already authenticated by the header tag. + const decoded = decodeKeyWrapRecordSequence(content, offset, wrapCount); + wrapRecords = decoded.records; + offset = decoded.end; + } + + for (let index = offset; index < HEADER_CONTENT_BYTES; index += 1) { + if (content[index] !== 0) { + throw new CryptoError("MALFORMED_ENVELOPE", "The chunked stream header padding is not zero."); + } + } + + return { + chunkSizeLog2, + fileId, + keyReference, + contextAad, + wrapRecords, + ...(declared ? { plaintextLength: Number(rawLength) } : {}), + }; +} + +export interface StreamKeySchedule { + readonly headerKey: KeyObject; + readonly headerNonce: Uint8Array; +} + +/** Header-tag key schedule: bound to the data key and the per-object file id. */ +export function deriveHeaderSchedule(key: KeyObject, fileId: Uint8Array): StreamKeySchedule { + // `export()` already hands back a private copy; do not clone it again — every extra + // copy of the data key is one more buffer that has to be wiped. + const ikm = key.export(); + let okm: Uint8Array | undefined; + try { + okm = hkdfSha256(ikm, fileId, frame(utf8(HDR_INFO_LABEL)), HDR_OKM_BYTES); + return { + headerKey: createSecretKey(okm.subarray(0, AES_KEY_BYTES)), + headerNonce: new Uint8Array(okm.subarray(AES_KEY_BYTES, HDR_OKM_BYTES)), + }; + } finally { + ikm.fill(0); + okm?.fill(0); + } +} + +export interface ChunkKeySchedule { + readonly chunkKey: KeyObject; + readonly noncePrefix: Uint8Array; +} + +/** + * Chunk key schedule. The whole authenticated header is hashed into the HKDF info, so + * any header change — key reference, chunk size, context, wrap records — yields a + * different chunk key and every chunk fails to authenticate. + */ +export function deriveChunkSchedule(key: KeyObject, headerContent: Uint8Array): ChunkKeySchedule { + const fileId = headerContent.subarray(OFF_FILE_ID, OFF_FILE_ID + FILE_ID_BYTES); + const headerHash = new Uint8Array(createHash("sha256").update(headerContent).digest()); + const ikm = key.export(); + let okm: Uint8Array | undefined; + try { + okm = hkdfSha256( + ikm, + fileId, + frame(utf8(CHUNK_INFO_LABEL), Uint8Array.of(NMCS_SUITE_AES256GCM_HKDF_SHA256), headerHash), + CHUNK_OKM_BYTES, + ); + return { + chunkKey: createSecretKey(okm.subarray(0, AES_KEY_BYTES)), + noncePrefix: new Uint8Array(okm.subarray(AES_KEY_BYTES, CHUNK_OKM_BYTES)), + }; + } finally { + ikm.fill(0); + okm?.fill(0); + } +} + +/** `noncePrefix(7) ‖ u32BE(index) ‖ final(1)` — no counter is ever persisted. */ +export function chunkNonce(noncePrefix: Uint8Array, index: number, final: boolean): Uint8Array { + const nonce = new Uint8Array(NONCE_BYTES); + nonce.set(noncePrefix, 0); + new DataView(nonce.buffer).setUint32(NONCE_PREFIX_BYTES, index, false); + nonce[NONCE_BYTES - 1] = final ? 0x01 : 0x00; + return nonce; +} + +function u32(value: number): Uint8Array { + const bytes = new Uint8Array(4); + new DataView(bytes.buffer).setUint32(0, value, false); + return bytes; +} + +/** Per-chunk AAD authenticates the framing the parser has not yet trusted. */ +export function chunkAad(index: number, final: boolean, ciphertextLength: number): Uint8Array { + return frame(u32(index), Uint8Array.of(final ? 0x01 : 0x00), u32(ciphertextLength)); +} + +export function generateFileId(): Uint8Array { + return new Uint8Array(randomBytes(FILE_ID_BYTES)); +} + +export function assertFileId(fileId: Uint8Array): Uint8Array { + if (!(fileId instanceof Uint8Array) || fileId.byteLength !== FILE_ID_BYTES) { + throw new CryptoError("INVALID_ARGUMENT", "The file identifier must be 16 bytes."); + } + return new Uint8Array(fileId); +} + +/** + * Test-only override for the per-object file identifier. + * + * The file identifier is the *sole* source of per-object randomness in the key schedule: + * reusing one under the same data key repeats the chunk key and nonce prefix, which + * reveals the XOR of two plaintexts and leaks the header GMAC subkey. It is therefore + * deliberately unreachable from {@link ChunkedSealOptions}; only `@nestm/crypto/testing` + * can attach it, and only to freeze format vectors. + * + * @internal + */ +export const FILE_ID_OVERRIDE: unique symbol = Symbol("@nestm/crypto/nmcs.fileId"); + +/** Read a test-seam file identifier, if one was attached by `@nestm/crypto/testing`. */ +export function readFileIdOverride(options: object): Uint8Array | undefined { + const value = (options as Record)[FILE_ID_OVERRIDE]; + if (value === undefined) return undefined; + return assertFileId(value as Uint8Array); +} + +export function contextAadBytes(aad?: CipherAad): Uint8Array { + return aadBytes(aad); +} + +export function headerInfo(fields: HeaderFields): ChunkedHeaderInfo { + return Object.freeze({ + version: 1, + suite: NMCS_SUITE_AES256GCM_HKDF_SHA256, + chunkSize: 2 ** fields.chunkSizeLog2, + fileId: fields.fileId, + keyReference: fields.keyReference, + contextAad: fields.contextAad, + wrapRecords: fields.wrapRecords, + ...(fields.plaintextLength === undefined ? {} : { plaintextLength: fields.plaintextLength }), + headerBytes: NMCS_HEADER_BYTES, + authenticated: false, + }) satisfies ChunkedHeaderInfo; +} diff --git a/src/testing/index.ts b/src/testing/index.ts index 5adf09f..fa70d0a 100644 --- a/src/testing/index.ts +++ b/src/testing/index.ts @@ -1,3 +1,5 @@ +import { FILE_ID_OVERRIDE } from "../stream/nmcs-format.js"; + export function fixedNonceSource(noncePrefix: Uint8Array): (length: number) => Uint8Array { const copy = new Uint8Array(noncePrefix); return (length: number): Uint8Array => { @@ -7,3 +9,25 @@ export function fixedNonceSource(noncePrefix: Uint8Array): (length: number) => U return new Uint8Array(copy); }; } + +/** + * Pin the per-object file identifier of a chunked seal. + * + * **Never use this outside tests.** The file identifier is the only per-object randomness + * in the `nmcs1` key schedule: sealing twice with the same identifier under the same data + * key repeats the chunk key and nonce prefix, which exposes the XOR of the two plaintexts + * and leaks the header GMAC subkey. It exists so format vectors can be frozen, and it is + * reachable only from this entry point — `ChunkedSealOptions` has no such field. + * + * @example + * sealChunked(key, plaintext, withFixedFileId({ keyReference: "ws:1" }, fileId)); + */ +export function withFixedFileId( + options: Options, + fileId: Uint8Array, +): Options { + if (!(fileId instanceof Uint8Array) || fileId.byteLength !== 16) { + throw new TypeError("A fixed file identifier must contain 16 bytes."); + } + return { ...options, [FILE_ID_OVERRIDE]: new Uint8Array(fileId) }; +} diff --git a/tests/unit/format-vectors.test.ts b/tests/unit/format-vectors.test.ts new file mode 100644 index 0000000..47a8cf1 --- /dev/null +++ b/tests/unit/format-vectors.test.ts @@ -0,0 +1,173 @@ +import { createSecretKey } from "node:crypto"; +import vectors from "./vectors/format-vectors.json" with { type: "json" }; +import { + decodeKeyWrapRecord, + unwrapKeyFromRecipient, + unwrapKeyWithSecret, + wrapKeyWithSecret, + encodeKeyWrapRecord, + x25519PrivateKeyFromRaw, +} from "../../src/keys/index.js"; +import { + createChunkedSealStream, + inspectChunked, + openChunked, + sealChunked, +} from "../../src/stream/index.js"; +import { withFixedFileId } from "../../src/testing/index.js"; + +/** + * Format-stability gate. These vectors were produced once with fixed keys and a fixed + * file identifier; a change here means previously written objects can no longer be read. + * If a test in this file fails, the wire format changed — bump the version, do not + * regenerate the fixture. + */ + +const b64 = (value: Uint8Array): string => Buffer.from(value).toString("base64"); +const fromB64 = (value: string): Uint8Array => new Uint8Array(Buffer.from(value, "base64")); +const hex = (value: Uint8Array): string => Buffer.from(value).toString("hex"); + +const dek = createSecretKey(Buffer.from(vectors.nmcs1.key, "hex")); +const fileId = new Uint8Array(Buffer.from(vectors.nmcs1.fileId, "hex")); +const { keyReference, chunkSizeLog2 } = vectors.nmcs1; +const CHUNK = 2 ** chunkSizeLog2; + +const fill = (length: number): Uint8Array => { + const bytes = new Uint8Array(length); + for (let index = 0; index < length; index += 1) bytes[index] = index % 251; + return bytes; +}; + +describe("nmcs1 frozen vectors", () => { + const cases: readonly [name: keyof typeof vectors.nmcs1, plaintext: Uint8Array, aad?: string][] = + [ + ["empty", new Uint8Array(0)], + ["oneByte", Uint8Array.of(0x41)], + ["exactChunk", fill(CHUNK)], + ["multiChunk", fill(CHUNK + 17)], + ["withAad", fill(64), "org:acme|ws:1"], + ]; + + it.each(cases)("re-seals %s to the frozen bytes", (name, plaintext, aad) => { + const sealed = sealChunked( + dek, + plaintext, + withFixedFileId( + { keyReference, chunkSizeLog2, ...(aad === undefined ? {} : { aad }) }, + fileId, + ), + ); + expect(b64(sealed)).toBe(vectors.nmcs1[name]); + }); + + it.each(cases)("opens the frozen %s vector", (name, plaintext, aad) => { + const opened = openChunked( + dek, + fromB64(vectors.nmcs1[name] as string), + aad === undefined ? undefined : { aad }, + ); + expect(hex(opened)).toBe(hex(plaintext)); + }); +}); + +describe("key-wrap record frozen vectors", () => { + it("decodes and unwraps the frozen secret record", () => { + const kek = new Uint8Array(Buffer.from(vectors.keyWrapRecord.kek, "hex")); + const record = decodeKeyWrapRecord(fromB64(vectors.keyWrapRecord.secret)); + + expect(record).toMatchObject({ + version: 1, + algorithm: "A256GCMKW", + recipientType: "secret", + recipientId: "user:vector", + }); + expect(hex(unwrapKeyWithSecret(kek, record).export())).toBe(hex(dek.export())); + }); + + it("re-encodes a record to a byte-stable layout", () => { + // The wrap itself is randomized by its nonce, so the framing is what is frozen: + // header + algorithm + recipient id + a 61-byte AES-GCM wrap. + const kek = new Uint8Array(Buffer.from(vectors.keyWrapRecord.kek, "hex")); + const encoded = encodeKeyWrapRecord( + wrapKeyWithSecret(kek, dek, { recipientId: "user:vector" }), + ); + const frozen = fromB64(vectors.keyWrapRecord.secret); + expect(encoded.byteLength).toBe(frozen.byteLength); + // Everything up to the wrapped payload is deterministic. + const prefix = 6 + "A256GCMKW".length + "user:vector".length; + expect(hex(encoded.subarray(0, prefix))).toBe(hex(frozen.subarray(0, prefix))); + }); +}); + +describe("nmcs1 frozen flag coverage", () => { + // The flag byte selects the header layout, so every combination the writers can emit + // needs a frozen witness: 0b00 from a stream, 0b10 from a buffered seal (above), and + // 0b11 when wrap records ride inline. + const inlineRecord = { + version: 1, + algorithm: "A256GCMKW", + recipientType: "secret", + recipientId: "ws:vector", + wrapped: new Uint8Array(61).map((_, index) => (index * 7 + 1) % 256), + } as const; + + it("freezes the undeclared-length header a stream writes", async () => { + const frozen = fromB64(vectors.nmcs1.undeclaredLength); + expect(frozen[6]).toBe(0b0000_0000); + expect(inspectChunked(frozen).plaintextLength).toBeUndefined(); + expect(Buffer.from(openChunked(dek, frozen)).toString()).toBe("streamed payload"); + + const source = new ReadableStream({ + start(controller) { + controller.enqueue(Buffer.from("streamed payload")); + controller.close(); + }, + }); + const parts: Uint8Array[] = []; + const reader = source + .pipeThrough( + createChunkedSealStream(dek, withFixedFileId({ keyReference, chunkSizeLog2 }, fileId)), + ) + .getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + parts.push(value); + } + expect(b64(new Uint8Array(Buffer.concat(parts.map((part) => Buffer.from(part)))))).toBe( + vectors.nmcs1.undeclaredLength, + ); + }); + + it("freezes a header carrying an inline wrap record", () => { + const frozen = fromB64(vectors.nmcs1.withWrapRecord); + expect(frozen[6]).toBe(0b0000_0011); + const info = inspectChunked(frozen); + expect(info.wrapRecords).toHaveLength(1); + expect(info.wrapRecords[0]?.recipientId).toBe("ws:vector"); + expect(Buffer.from(openChunked(dek, frozen)).toString()).toBe("wrapped"); + + const resealed = sealChunked( + dek, + Buffer.from("wrapped"), + withFixedFileId({ keyReference, chunkSizeLog2, wrapRecords: [inlineRecord] }, fileId), + ); + expect(b64(resealed)).toBe(vectors.nmcs1.withWrapRecord); + }); + + it("freezes a record of each recipient type", () => { + expect(b64(encodeKeyWrapRecord(inlineRecord))).toBe(vectors.keyWrapRecord.inlineSecret); + + const privateKey = x25519PrivateKeyFromRaw( + new Uint8Array(Buffer.from(vectors.keyWrapRecord.x25519RecipientPrivateKey, "hex")), + ); + const record = decodeKeyWrapRecord(fromB64(vectors.keyWrapRecord.x25519)); + expect(record).toMatchObject({ + version: 1, + algorithm: "X25519-HKDF-SHA256-A256GCM", + recipientType: "x25519", + recipientId: "user:vector", + }); + expect(hex(unwrapKeyFromRecipient(privateKey, record).export())).toBe(hex(dek.export())); + }); +}); diff --git a/tests/unit/key-wrap-record.test.ts b/tests/unit/key-wrap-record.test.ts new file mode 100644 index 0000000..c6cd150 --- /dev/null +++ b/tests/unit/key-wrap-record.test.ts @@ -0,0 +1,356 @@ +import { createSecretKey, randomBytes } from "node:crypto"; +import { + decodeKeyWrapRecord, + decodeKeyWrapRecords, + encodeKeyWrapRecord, + encodeKeyWrapRecords, + generateX25519KeyPair, + hmacSha256, + keyWrapRecordLength, + selectKeyWrapRecord, + timingSafeEqualBytes, + unwrapKeyFromRecipient, + unwrapKeyWithSecret, + wrapKeyToRecipient, + wrapKeyWithSecret, + KEY_WRAP_A256GCMKW, + KEY_WRAP_SEAL_X25519, + type KeyWrapRecord, +} from "../../src/keys/index.js"; +import { decodeKeyWrapRecordSequence } from "../../src/keys/key-wrap-record.js"; + +const hex = (value: Uint8Array): string => Buffer.from(value).toString("hex"); + +describe("wrapKeyWithSecret / unwrapKeyWithSecret", () => { + it("round-trips a secret key under a shared KEK", () => { + const kek = randomBytes(32); + const key = createSecretKey(randomBytes(32)); + const record = wrapKeyWithSecret(kek, key, { recipientId: "user:42" }); + + expect(record).toMatchObject({ + version: 1, + algorithm: KEY_WRAP_A256GCMKW, + recipientType: "secret", + recipientId: "user:42", + }); + // version + nonce(12) + ciphertext(32) + tag(16) + expect(record.wrapped.byteLength).toBe(61); + expect(hex(unwrapKeyWithSecret(kek, record).export())).toBe(hex(key.export())); + }); + + it("binds caller aad", () => { + const kek = randomBytes(32); + const key = createSecretKey(randomBytes(32)); + const record = wrapKeyWithSecret(kek, key, { recipientId: "user:42", aad: "epoch:1" }); + + expect(hex(unwrapKeyWithSecret(kek, record, { aad: "epoch:1" }).export())).toBe( + hex(key.export()), + ); + expect(() => unwrapKeyWithSecret(kek, record, { aad: "epoch:2" })).toThrowError( + /authentication failed/i, + ); + expect(() => unwrapKeyWithSecret(kek, record)).toThrowError(/authentication failed/i); + }); + + it("refuses a record retargeted at another recipient", () => { + const kek = randomBytes(32); + const record = wrapKeyWithSecret(kek, createSecretKey(randomBytes(32)), { + recipientId: "user:42", + }); + const swapped: KeyWrapRecord = { ...record, recipientId: "user:43" }; + expect(() => unwrapKeyWithSecret(kek, swapped)).toThrowError(/authentication failed/i); + }); + + it("cannot be unwrapped by a different KEK", () => { + const record = wrapKeyWithSecret(randomBytes(32), createSecretKey(randomBytes(32)), { + recipientId: "user:42", + }); + expect(() => unwrapKeyWithSecret(randomBytes(32), record)).toThrowError( + /authentication failed/i, + ); + }); + + it("rejects invalid KEKs, keys, and recipient identifiers", () => { + const key = createSecretKey(randomBytes(32)); + expect(() => wrapKeyWithSecret(randomBytes(16), key, { recipientId: "a" })).toThrowError( + /32 bytes/i, + ); + expect(() => + wrapKeyWithSecret(randomBytes(32), new Uint8Array(8), { recipientId: "a" }), + ).toThrowError(/out of range/i); + expect(() => wrapKeyWithSecret(randomBytes(32), key, { recipientId: " a" })).toThrowError( + /recipient identifier is invalid/i, + ); + expect(() => + wrapKeyWithSecret(randomBytes(32), key, { recipientId: "a".repeat(256) }), + ).toThrowError(/too long/i); + }); +}); + +describe("wrapKeyToRecipient / unwrapKeyFromRecipient", () => { + it("round-trips a data key sealed to a public key", () => { + const pair = generateX25519KeyPair(); + const dek = createSecretKey(randomBytes(32)); + const record = wrapKeyToRecipient(pair.publicKey, dek, { recipientId: "ws:7" }); + + expect(record).toMatchObject({ + version: 1, + algorithm: KEY_WRAP_SEAL_X25519, + recipientType: "x25519", + recipientId: "ws:7", + }); + expect(hex(unwrapKeyFromRecipient(pair.privateKey, record).export())).toBe(hex(dek.export())); + }); + + it("binds info, aad, and the recipient identifier", () => { + const pair = generateX25519KeyPair(); + const dek = createSecretKey(randomBytes(32)); + const record = wrapKeyToRecipient(pair.publicKey, dek, { + recipientId: "ws:7", + info: "wsk.seal", + aad: "epoch:3", + }); + + expect( + hex( + unwrapKeyFromRecipient(pair.privateKey, record, { + info: "wsk.seal", + aad: "epoch:3", + }).export(), + ), + ).toBe(hex(dek.export())); + expect(() => + unwrapKeyFromRecipient(pair.privateKey, record, { info: "other", aad: "epoch:3" }), + ).toThrowError(/authentication failed/i); + expect(() => + unwrapKeyFromRecipient(pair.privateKey, record, { info: "wsk.seal", aad: "epoch:4" }), + ).toThrowError(/authentication failed/i); + + const swapped: KeyWrapRecord = { ...record, recipientId: "ws:8" }; + expect(() => + unwrapKeyFromRecipient(pair.privateKey, swapped, { info: "wsk.seal", aad: "epoch:3" }), + ).toThrowError(/authentication failed/i); + }); + + it("cannot be opened by another recipient", () => { + const recipient = generateX25519KeyPair(); + const stranger = generateX25519KeyPair(); + const record = wrapKeyToRecipient(recipient.publicKey, createSecretKey(randomBytes(32)), { + recipientId: "ws:7", + }); + expect(() => unwrapKeyFromRecipient(stranger.privateKey, record)).toThrowError( + /authentication failed/i, + ); + }); + + it("refuses to cross record flavours", () => { + const pair = generateX25519KeyPair(); + const sealed = wrapKeyToRecipient(pair.publicKey, createSecretKey(randomBytes(32)), { + recipientId: "ws:7", + }); + const secret = wrapKeyWithSecret(randomBytes(32), createSecretKey(randomBytes(32)), { + recipientId: "user:1", + }); + expect(() => unwrapKeyWithSecret(randomBytes(32), sealed)).toThrowError( + /not a secret-KEK record/i, + ); + expect(() => unwrapKeyFromRecipient(pair.privateKey, secret)).toThrowError( + /not a recipient record/i, + ); + }); +}); + +describe("key-wrap record encoding", () => { + it("round-trips a single record of each type", () => { + const pair = generateX25519KeyPair(); + const records = [ + wrapKeyWithSecret(randomBytes(32), createSecretKey(randomBytes(32)), { + recipientId: "user:1", + }), + wrapKeyToRecipient(pair.publicKey, createSecretKey(randomBytes(32)), { + recipientId: "ws:2", + }), + ]; + for (const record of records) { + const encoded = encodeKeyWrapRecord(record); + expect(encoded.byteLength).toBe(keyWrapRecordLength(record)); + const decoded = decodeKeyWrapRecord(encoded); + expect(decoded.algorithm).toBe(record.algorithm); + expect(decoded.recipientType).toBe(record.recipientType); + expect(decoded.recipientId).toBe(record.recipientId); + expect(hex(decoded.wrapped)).toBe(hex(record.wrapped)); + } + }); + + it("round-trips a record list and preserves order", () => { + const kek = randomBytes(32); + const records = ["user:1", "user:2", "user:3"].map((recipientId) => + wrapKeyWithSecret(kek, createSecretKey(randomBytes(32)), { recipientId }), + ); + const decoded = decodeKeyWrapRecords(encodeKeyWrapRecords(records)); + expect(decoded.map((record) => record.recipientId)).toEqual(["user:1", "user:2", "user:3"]); + }); + + it("round-trips an empty list", () => { + expect(decodeKeyWrapRecords(encodeKeyWrapRecords([]))).toHaveLength(0); + }); + + it("rejects truncated, trailing, and over-count encodings", () => { + const kek = randomBytes(32); + const record = wrapKeyWithSecret(kek, createSecretKey(randomBytes(32)), { + recipientId: "user:1", + }); + const encoded = encodeKeyWrapRecord(record); + + expect(() => decodeKeyWrapRecord(encoded.subarray(0, encoded.byteLength - 1))).toThrowError( + /truncated/i, + ); + const trailing = new Uint8Array(encoded.byteLength + 1); + trailing.set(encoded, 0); + expect(() => decodeKeyWrapRecord(trailing)).toThrowError(/trailing bytes/i); + + const list = encodeKeyWrapRecords([record, record]); + expect(() => decodeKeyWrapRecords(list, 1)).toThrowError(/too many/i); + expect(() => decodeKeyWrapRecords(new Uint8Array(1))).toThrowError(/truncated/i); + }); + + it("rejects unknown versions and recipient types", () => { + const record = wrapKeyWithSecret(randomBytes(32), createSecretKey(randomBytes(32)), { + recipientId: "user:1", + }); + const encoded = encodeKeyWrapRecord(record); + + const badVersion = Uint8Array.from(encoded); + badVersion[0] = 0x02; + expect(() => decodeKeyWrapRecord(badVersion)).toThrowError(/version is unsupported/i); + + const badType = Uint8Array.from(encoded); + badType[1] = 0x09; + expect(() => decodeKeyWrapRecord(badType)).toThrowError(/recipient type is unknown/i); + }); + + it("selects records by recipient", () => { + const kek = randomBytes(32); + const records = ["user:1", "user:2"].map((recipientId) => + wrapKeyWithSecret(kek, createSecretKey(randomBytes(32)), { recipientId }), + ); + expect( + selectKeyWrapRecord(records, { recipientType: "secret", recipientId: "user:2" })?.recipientId, + ).toBe("user:2"); + expect( + selectKeyWrapRecord(records, { recipientType: "x25519", recipientId: "user:2" }), + ).toBeUndefined(); + expect( + selectKeyWrapRecord(records, { recipientType: "secret", recipientId: "user:9" }), + ).toBeUndefined(); + }); +}); + +describe("hmacSha256 / timingSafeEqualBytes", () => { + it("matches RFC 4231 test case 1", () => { + const mac = hmacSha256(Buffer.from("0b".repeat(20), "hex"), Buffer.from("Hi There")); + expect(hex(mac)).toBe("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"); + }); + + it("concatenates parts without separation", () => { + const key = randomBytes(32); + expect(hex(hmacSha256(key, Buffer.from("ab"), Buffer.from("c")))).toBe( + hex(hmacSha256(key, Buffer.from("abc"))), + ); + }); + + it("compares equal and unequal lengths without throwing", () => { + expect(timingSafeEqualBytes(Uint8Array.of(1, 2, 3), Uint8Array.of(1, 2, 3))).toBe(true); + expect(timingSafeEqualBytes(Uint8Array.of(1, 2, 3), Uint8Array.of(1, 2, 4))).toBe(false); + expect(timingSafeEqualBytes(Uint8Array.of(1, 2, 3), Uint8Array.of(1, 2))).toBe(false); + expect(timingSafeEqualBytes(new Uint8Array(), new Uint8Array())).toBe(true); + }); +}); + +describe("key-wrap record hardening regressions", () => { + it("refuses to mint a record with no recipient binding", () => { + const kek = randomBytes(32); + const dek = randomBytes(32); + const { publicKey } = generateX25519KeyPair(); + // An empty identifier binds nothing, so the wrap would be replayable to anyone. + expect(() => wrapKeyWithSecret(kek, dek, { recipientId: "" })).toThrowError( + /recipient identifier is invalid/i, + ); + expect(() => wrapKeyToRecipient(publicKey, dek, { recipientId: "" })).toThrowError( + /recipient identifier is invalid/i, + ); + // Omitting it entirely must throw rather than silently default. + expect(() => + wrapKeyWithSecret(kek, dek, undefined as unknown as { recipientId: string }), + ).toThrowError(/options are required|recipient identifier is invalid/i); + expect(() => + wrapKeyWithSecret(kek, dek, {} as unknown as { recipientId: string }), + ).toThrowError(/recipient identifier is invalid/i); + }); + + it("caps the header record-sequence count before decoding, matching the encoder limit", () => { + // The nmcs1 header path decodes a caller-declared record count; without a cap a + // hostile wrapCount could drive an unbounded decode loop. The encoder rejects >64, + // so the sequence decoder must too — before it reads any record bytes. + expect(() => decodeKeyWrapRecordSequence(new Uint8Array(0), 0, 65)).toThrowError( + /too many key-wrap records/i, + ); + }); + + it("rejects a recipient identifier that would not survive encoding", () => { + const kek = randomBytes(32); + // A lone surrogate is folded onto U+FFFD by UTF-8, which would collapse distinct + // recipients onto one identifier and break the wire round trip. + expect(() => + wrapKeyWithSecret(kek, randomBytes(32), { recipientId: "user:\uD800" }), + ).toThrowError(/recipient identifier is invalid/i); + }); + + it("length-checks a key recovered from an attacker-minted record", () => { + const { publicKey, privateKey } = generateX25519KeyPair(); + const wrapped = wrapKeyToRecipient(publicKey, randomBytes(32), { recipientId: "user:1" }); + expect(unwrapKeyFromRecipient(privateKey, wrapped).symmetricKeySize).toBe(32); + // Pinning the expected size rejects a record that carries a different-sized key. + expect(() => + unwrapKeyFromRecipient(privateKey, wrapped, { expectedKeyBytes: 16 }), + ).toThrowError(/not the expected length/i); + + const secretWrap = wrapKeyWithSecret(randomBytes(32), randomBytes(16), { + recipientId: "user:1", + }); + expect(() => + unwrapKeyWithSecret(randomBytes(32), secretWrap, { expectedKeyBytes: 32 }), + ).toThrowError(/authentication failed/i); + }); + + it("reports hostile wire bytes as malformed rather than as a cipher failure", () => { + const record = wrapKeyWithSecret(randomBytes(32), randomBytes(32), { recipientId: "user:1" }); + const encoded = encodeKeyWrapRecord(record); + + const badText = Uint8Array.from(encoded); + badText[6] = 0x80; // first algorithm byte: a bare UTF-8 continuation byte + expect(() => decodeKeyWrapRecord(badText)).toThrowError(/malformed/i); + + const noRecipient = Uint8Array.from(encoded); + noRecipient[3] = 0; // recipientIdLength + expect(() => decodeKeyWrapRecord(noRecipient)).toThrowError(/malformed|missing/i); + }); + + it("never encodes a list its own decoder would refuse", () => { + const kek = randomBytes(32); + const record = wrapKeyWithSecret(kek, randomBytes(32), { recipientId: "user:1" }); + const tooMany = Array.from({ length: 65 }, () => record); + expect(() => encodeKeyWrapRecords(tooMany)).toThrowError(/too many/i); + + const atLimit = Array.from({ length: 64 }, () => record); + expect(decodeKeyWrapRecords(encodeKeyWrapRecords(atLimit))).toHaveLength(64); + }); + + it("rejects a non-secret KeyObject as an HMAC key", () => { + const { publicKey } = generateX25519KeyPair(); + expect(() => hmacSha256(publicKey, Buffer.from("x"))).toThrowError(/must be a secret key/i); + expect(() => hmacSha256(undefined as unknown as Uint8Array, Buffer.from("x"))).toThrowError( + /must be bytes or a secret key/i, + ); + }); +}); diff --git a/tests/unit/password.test.ts b/tests/unit/password.test.ts new file mode 100644 index 0000000..bb49c28 --- /dev/null +++ b/tests/unit/password.test.ts @@ -0,0 +1,373 @@ +import { + createPasswordKdf, + decodePasswordKdfParams, + deriveRecoveryKey, + encodePasswordKdfParams, + generatePasswordSalt, + generateRecoveryCode, + parseRecoveryCode, + PASSWORD_KDF_SCRYPT_DEFAULT, + RECOVERY_CODE_PREFIX, + scryptPasswordKdf, + type PasswordKdfAlgorithm, + type PasswordKdfParams, +} from "../../src/password/index.js"; + +const hex = (value: Uint8Array): string => Buffer.from(value).toString("hex"); + +// Interactive scrypt defaults cost ~100ms each; tests use a cheap profile except where +// the default itself is under test. +const FAST: PasswordKdfParams = { id: "scrypt-1", params: { N: 1024, r: 8, p: 1 } }; +const SALT = new Uint8Array(32).fill(7); + +describe("createPasswordKdf", () => { + it("derives a deterministic 32-byte key", async () => { + const kdf = createPasswordKdf(); + const a = await kdf.derive({ password: "correct horse", salt: SALT, kdf: FAST }); + const b = await kdf.derive({ password: "correct horse", salt: SALT, kdf: FAST }); + + expect(a.type).toBe("secret"); + expect(a.symmetricKeySize).toBe(32); + expect(hex(a.export())).toBe(hex(b.export())); + }); + + it("separates by password, salt, params, and info", async () => { + const kdf = createPasswordKdf(); + const base = await kdf.derive({ password: "pw", salt: SALT, kdf: FAST }); + const variants = await Promise.all([ + kdf.derive({ password: "pw2", salt: SALT, kdf: FAST }), + kdf.derive({ password: "pw", salt: new Uint8Array(32).fill(8), kdf: FAST }), + kdf.derive({ + password: "pw", + salt: SALT, + kdf: { id: "scrypt-1", params: { N: 2048, r: 8, p: 1 } }, + }), + kdf.derive({ password: "pw", salt: SALT, kdf: FAST, info: "umk.password" }), + ]); + for (const variant of variants) { + expect(hex(variant.export())).not.toBe(hex(base.export())); + } + }); + + it("accepts byte passwords equivalently to text", async () => { + const kdf = createPasswordKdf(); + const fromText = await kdf.derive({ password: "héllo", salt: SALT, kdf: FAST }); + const fromBytes = await kdf.derive({ + password: new TextEncoder().encode("héllo"), + salt: SALT, + kdf: FAST, + }); + expect(hex(fromBytes.export())).toBe(hex(fromText.export())); + }); + + it("uses the interactive scrypt profile by default", async () => { + const kdf = createPasswordKdf(); + expect(kdf.defaultParams).toEqual(PASSWORD_KDF_SCRYPT_DEFAULT); + expect(kdf.supports("scrypt-1")).toBe(true); + expect(kdf.supports("argon2id")).toBe(false); + + const key = await kdf.derive({ + password: "pw", + salt: SALT, + kdf: PASSWORD_KDF_SCRYPT_DEFAULT, + }); + expect(key.symmetricKeySize).toBe(32); + }); + + it("rejects invalid salts, passwords, and unknown algorithms", async () => { + const kdf = createPasswordKdf(); + await expect( + kdf.derive({ password: "pw", salt: new Uint8Array(8), kdf: FAST }), + ).rejects.toThrowError(/salt length is out of range/i); + await expect(kdf.derive({ password: "", salt: SALT, kdf: FAST })).rejects.toThrowError( + /non-empty/i, + ); + await expect( + kdf.derive({ password: "pw", salt: SALT, kdf: { id: "argon2id", params: {} } }), + ).rejects.toThrowError(/not registered/i); + }); + + it("validates scrypt parameters", async () => { + const kdf = createPasswordKdf(); + const bad: readonly unknown[] = [ + { N: 1000, r: 8, p: 1 }, + { N: 512, r: 8, p: 1 }, + { N: 1024, r: 0, p: 1 }, + { N: 1024, r: 8, p: 0 }, + { N: 1024, r: 8 }, + { N: 1024, r: 8, p: 1, extra: 1 }, + ]; + for (const params of bad) { + await expect( + kdf.derive({ password: "pw", salt: SALT, kdf: { id: "scrypt-1", params } }), + ).rejects.toThrowError(/scrypt|invalid/i); + } + }); + + it("rejects invalid registries and concurrency limits", () => { + expect(() => createPasswordKdf({ algorithms: [] })).toThrowError(/registry is required/i); + expect(() => + createPasswordKdf({ algorithms: [scryptPasswordKdf(), scryptPasswordKdf()] }), + ).toThrowError(/registration is invalid/i); + expect(() => createPasswordKdf({ maxConcurrency: 0 })).toThrowError(/concurrency limit/i); + expect(() => createPasswordKdf({ defaultParams: { id: "nope", params: {} } })).toThrowError( + /not registered/i, + ); + expect(() => + createPasswordKdf({ defaultParams: { id: "scrypt-1", params: { N: 3, r: 8, p: 1 } } }), + ).toThrowError(/default password KDF parameters are invalid/i); + }); + + it("bounds concurrent derivations", async () => { + let active = 0; + let peak = 0; + const slow: PasswordKdfAlgorithm = { + id: "slow", + validateParams: () => undefined, + async derive() { + active += 1; + peak = Math.max(peak, active); + await new Promise((resolve) => setTimeout(resolve, 15)); + active -= 1; + return new Uint8Array(32).fill(1); + }, + }; + const kdf = createPasswordKdf({ + algorithms: [slow], + defaultParams: { id: "slow", params: {} }, + maxConcurrency: 2, + }); + await Promise.all( + Array.from({ length: 8 }, () => + kdf.derive({ password: "pw", salt: SALT, kdf: { id: "slow", params: {} } }), + ), + ); + expect(peak).toBe(2); + }); + + it("honours an abort signal", async () => { + const kdf = createPasswordKdf(); + const controller = new AbortController(); + controller.abort(); + await expect( + kdf.derive({ password: "pw", salt: SALT, kdf: FAST, signal: controller.signal }), + ).rejects.toThrowError(/aborted/i); + }); + + it("rejects key material of the wrong length from a custom algorithm", async () => { + const broken: PasswordKdfAlgorithm = { + id: "broken", + validateParams: () => undefined, + async derive() { + return new Uint8Array(16); + }, + }; + const kdf = createPasswordKdf({ + algorithms: [broken], + defaultParams: { id: "broken", params: {} }, + }); + await expect( + kdf.derive({ password: "pw", salt: SALT, kdf: { id: "broken", params: {} } }), + ).rejects.toThrowError(/invalid key material/i); + }); +}); + +describe("password KDF parameter codec", () => { + it("round-trips canonically", () => { + const encoded = encodePasswordKdfParams(PASSWORD_KDF_SCRYPT_DEFAULT); + expect(encoded).toBe('{"id":"scrypt-1","params":{"N":65536,"p":1,"r":8}}'); + expect(decodePasswordKdfParams(encoded)).toEqual(PASSWORD_KDF_SCRYPT_DEFAULT); + }); + + it("sorts keys so the encoding is stable", () => { + expect(encodePasswordKdfParams({ id: "scrypt-1", params: { r: 8, p: 1, N: 1024 } })).toBe( + '{"id":"scrypt-1","params":{"N":1024,"p":1,"r":8}}', + ); + }); + + it("rejects non-canonical and malformed encodings", () => { + expect(() => + decodePasswordKdfParams('{"params":{"N":1024,"p":1,"r":8},"id":"scrypt-1"}'), + ).toThrowError(/not canonical/i); + expect(() => + decodePasswordKdfParams('{"id":"scrypt-1","params":{"r":8,"p":1,"N":1024}}'), + ).toThrowError(/not canonical/i); + expect(() => decodePasswordKdfParams("{")).toThrowError(/malformed/i); + expect(() => decodePasswordKdfParams('{"id":"scrypt-1"}')).toThrowError(/invalid shape/i); + expect(() => decodePasswordKdfParams('{"id":"","params":{}}')).toThrowError(/identifier/i); + expect(() => decodePasswordKdfParams('{"id":"x","params":1}')).toThrowError(/invalid/i); + expect(() => decodePasswordKdfParams("")).toThrowError(/invalid/i); + }); +}); + +describe("generatePasswordSalt", () => { + it("returns fresh salts of the requested size", () => { + expect(generatePasswordSalt().byteLength).toBe(32); + expect(generatePasswordSalt(16).byteLength).toBe(16); + expect(hex(generatePasswordSalt())).not.toBe(hex(generatePasswordSalt())); + }); + + it("rejects lengths below the minimum", () => { + expect(() => generatePasswordSalt(15)).toThrowError(/out of range/i); + expect(() => generatePasswordSalt(2048)).toThrowError(/out of range/i); + }); +}); + +describe("recovery codes", () => { + it("generates a grouped, prefixed code and parses it back", () => { + const { code, secret } = generateRecoveryCode(); + expect(secret.byteLength).toBe(16); + expect(code.startsWith(`${RECOVERY_CODE_PREFIX}-`)).toBe(true); + // 16 secret bytes + a 4-byte checksum encode to exactly four 8-character groups. + expect(code).toMatch(/^ASR1(?:-[0-9A-HJKMNP-TV-Z]{8}){4}$/u); + expect(hex(parseRecoveryCode(code))).toBe(hex(secret)); + }); + + it("supports 160-bit codes", () => { + const { code, secret } = generateRecoveryCode({ entropyBits: 160 }); + expect(secret.byteLength).toBe(20); + // 20 secret bytes + a 5-byte checksum encode to exactly five groups. + expect(code).toMatch(/^ASR1(?:-[0-9A-HJKMNP-TV-Z]{8}){5}$/u); + expect(hex(parseRecoveryCode(code))).toBe(hex(secret)); + }); + + it("accepts lowercase, spacing, and Crockford aliases", () => { + const { code, secret } = generateRecoveryCode(); + const mangled = code.toLowerCase().replace(/-/gu, " "); + expect(hex(parseRecoveryCode(mangled))).toBe(hex(secret)); + + // O/I/L are aliases of 0/1/1 and never appear in the alphabet, so substituting them + // into a valid code must still parse to the same secret. + const aliased = code.replace(/0/gu, "O").replace(/1/gu, "I"); + expect(hex(parseRecoveryCode(aliased))).toBe(hex(secret)); + }); + + it("rejects every single-character corruption via the checksum", () => { + const ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; + let attempted = 0; + // The checksum is 32 bits wide, so a mistyped character is caught with probability + // 1 - 2^-32: asserting "every corruption is rejected" is the honest bar here, and a + // weakened checksum would fail this immediately rather than hide behind a ratio. + for (let trial = 0; trial < 8; trial += 1) { + const { code } = generateRecoveryCode(); + for (let index = RECOVERY_CODE_PREFIX.length + 1; index < code.length; index += 1) { + if (code[index] === "-") continue; + for (const replacement of ALPHABET) { + if (replacement === code[index]) continue; + const tampered = `${code.slice(0, index)}${replacement}${code.slice(index + 1)}`; + attempted += 1; + expect(() => parseRecoveryCode(tampered)).toThrowError(/checksum|canonical/i); + } + } + } + expect(attempted).toBeGreaterThan(7000); + }); + + it("rejects a truncated or lengthened code", () => { + const { code } = generateRecoveryCode(); + expect(() => parseRecoveryCode(code.slice(0, -1))).toThrowError(/length/i); + expect(() => parseRecoveryCode(`${code}A`)).toThrowError(/length/i); + }); + + it("rejects malformed codes fail-closed", () => { + expect(() => parseRecoveryCode("")).toThrowError(/invalid/i); + expect(() => parseRecoveryCode("NOPE-ABCDE")).toThrowError(/prefix/i); + expect(() => parseRecoveryCode("ASR1-ABC")).toThrowError(/length/i); + expect(() => parseRecoveryCode(`ASR1-${"U".repeat(32)}`)).toThrowError(/invalid characters/i); + expect(() => parseRecoveryCode(`ASR1-${"A".repeat(32)}`)).toThrowError(/checksum|canonical/i); + expect(() => generateRecoveryCode({ entropyBits: 64 as 128 })).toThrowError(/entropy/i); + }); + + it("derives a stable 32-byte key bound to salt and info", () => { + const { secret } = generateRecoveryCode(); + const salt = generatePasswordSalt(); + const key = deriveRecoveryKey(secret, salt); + + expect(key.symmetricKeySize).toBe(32); + expect(hex(deriveRecoveryKey(secret, salt).export())).toBe(hex(key.export())); + expect(hex(deriveRecoveryKey(secret, generatePasswordSalt()).export())).not.toBe( + hex(key.export()), + ); + expect(hex(deriveRecoveryKey(secret, salt, "umk.recovery").export())).not.toBe( + hex(key.export()), + ); + }); + + it("rejects weak recovery secrets and salts", () => { + expect(() => deriveRecoveryKey(new Uint8Array(8), SALT)).toThrowError(/secret is invalid/i); + expect(() => deriveRecoveryKey(new Uint8Array(16), new Uint8Array(8))).toThrowError( + /salt length is out of range/i, + ); + }); +}); + +describe("password KDF hardening regressions", () => { + it("rejects parameters that would reserve gigabytes per derivation", async () => { + const kdf = createPasswordKdf(); + // Each of these is inside the individual ranges but far outside a sane footprint; + // 128 * N * r is what actually has to be bounded. + for (const params of [ + { N: 2 ** 21, r: 8, p: 1 }, + { N: 2 ** 20, r: 32, p: 1 }, + { N: 2 ** 22, r: 32, p: 16 }, + ]) { + await expect( + kdf.derive({ password: "pw", salt: SALT, kdf: { id: "scrypt-1", params } }), + ).rejects.toThrowError(/out of range|too much memory/i); + } + // The largest still-accepted footprint sits exactly on the 1 GiB ceiling. + expect(() => scryptPasswordKdf().validateParams({ N: 2 ** 20, r: 8, p: 1 })).not.toThrow(); + }); + + it("frees the concurrency slot and rejects when a queued derivation is aborted", async () => { + const kdf = createPasswordKdf({ maxConcurrency: 1 }); + const controller = new AbortController(); + const running = kdf.derive({ + password: "first", + salt: SALT, + kdf: PASSWORD_KDF_SCRYPT_DEFAULT, + }); + const queued = kdf.derive({ + password: "second", + salt: SALT, + kdf: PASSWORD_KDF_SCRYPT_DEFAULT, + signal: controller.signal, + }); + controller.abort(); + + await expect(queued).rejects.toThrowError(/aborted/i); + expect((await running).symmetricKeySize).toBe(32); + // The slot the aborted waiter never took must still be usable. + const after = await kdf.derive({ + password: "third", + salt: SALT, + kdf: PASSWORD_KDF_SCRYPT_DEFAULT, + }); + expect(after.symmetricKeySize).toBe(32); + }); + + it("wipes the password copy even when the derivation fails", async () => { + const captured: Uint8Array[] = []; + const failing = createPasswordKdf({ + algorithms: [ + { + id: "capture", + validateParams(): void {}, + async derive(input): Promise { + captured.push(input.password); + await Promise.resolve(); + throw new Error("derivation exploded"); + }, + }, + ], + defaultParams: { id: "capture", params: {} }, + }); + + await expect( + failing.derive({ password: "hunter2", salt: SALT, kdf: { id: "capture", params: {} } }), + ).rejects.toThrow(); + expect(captured).toHaveLength(1); + // The UTF-8 copy the library made must not outlive the call with plaintext in it. + expect(captured[0]!.every((byte) => byte === 0)).toBe(true); + }); +}); diff --git a/tests/unit/stream.test.ts b/tests/unit/stream.test.ts new file mode 100644 index 0000000..e55c5a1 --- /dev/null +++ b/tests/unit/stream.test.ts @@ -0,0 +1,633 @@ +import { createSecretKey, randomBytes } from "node:crypto"; +import { + generateX25519KeyPair, + selectKeyWrapRecord, + unwrapKeyFromRecipient, + wrapKeyToRecipient, + wrapKeyWithSecret, +} from "../../src/keys/index.js"; +import { + chunkedChunkRange, + chunkedCiphertextLength, + chunkedPlaintextLength, + createChunkedOpenStream, + createChunkedSealStream, + inspectChunked, + openChunked, + sealChunked, + NMCS_DEFAULT_CHUNK_SIZE_LOG2, + NMCS_HEADER_BYTES, + NMCS_SUITE_AES256GCM_HKDF_SHA256, + type ChunkedSealOptions, +} from "../../src/stream/index.js"; +import { withFixedFileId } from "../../src/testing/index.js"; + +const hex = (value: Uint8Array): string => Buffer.from(value).toString("hex"); + +const KEY_REFERENCE = "ws:1f9e:e1"; +// 64 KiB chunks keep multi-chunk fixtures small enough to run quickly. +const SMALL_LOG2 = 16; +const SMALL_CHUNK = 2 ** SMALL_LOG2; + +const key = (): ReturnType => createSecretKey(randomBytes(32)); + +async function pipe( + input: Uint8Array, + transform: TransformStream, + sliceSize = 7_000, +): Promise { + const source = new ReadableStream({ + start(controller) { + for (let offset = 0; offset < input.byteLength; offset += sliceSize) { + controller.enqueue(input.subarray(offset, Math.min(offset + sliceSize, input.byteLength))); + } + controller.close(); + }, + }); + const parts: Uint8Array[] = []; + const reader = source.pipeThrough(transform).getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + parts.push(value); + } + let total = 0; + for (const part of parts) total += part.byteLength; + const output = new Uint8Array(total); + let cursor = 0; + for (const part of parts) { + output.set(part, cursor); + cursor += part.byteLength; + } + return output; +} + +describe("sealChunked / openChunked", () => { + it("round-trips payloads across chunk boundaries", () => { + const dek = key(); + const sizes = [0, 1, 1024, SMALL_CHUNK - 1, SMALL_CHUNK, SMALL_CHUNK + 1, SMALL_CHUNK * 3]; + for (const size of sizes) { + const plaintext = randomBytes(size); + const sealed = sealChunked(dek, plaintext, { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }); + expect(sealed.byteLength).toBe(chunkedCiphertextLength(size, SMALL_LOG2)); + expect(hex(openChunked(dek, sealed))).toBe(hex(plaintext)); + } + }); + + it("uses a 1 MiB chunk and a 512-byte header by default", () => { + const dek = key(); + const sealed = sealChunked(dek, randomBytes(10), { keyReference: KEY_REFERENCE }); + expect(sealed.byteLength).toBe(NMCS_HEADER_BYTES + 10 + 20); + expect(inspectChunked(sealed).chunkSize).toBe(2 ** NMCS_DEFAULT_CHUNK_SIZE_LOG2); + }); + + it("keeps ciphertext indistinguishable and unique per seal", () => { + const dek = key(); + const plaintext = Buffer.from("# Q3 Launch Plan\n- pricing draft"); + const a = sealChunked(dek, plaintext, { keyReference: KEY_REFERENCE }); + const b = sealChunked(dek, plaintext, { keyReference: KEY_REFERENCE }); + + expect(hex(a)).not.toBe(hex(b)); + expect(a.subarray(NMCS_HEADER_BYTES).includes(plaintext[0]!)).toBeDefined(); + expect(Buffer.from(a).includes("Q3 Launch")).toBe(false); + expect(Buffer.from(a.subarray(0, 4)).toString()).toBe("nmcs"); + }); + + it("binds aad and the key reference", () => { + const dek = key(); + const plaintext = randomBytes(64); + const sealed = sealChunked(dek, plaintext, { keyReference: KEY_REFERENCE, aad: "org:1|ws:2" }); + + expect(hex(openChunked(dek, sealed, { aad: "org:1|ws:2" }))).toBe(hex(plaintext)); + expect(() => openChunked(dek, sealed, { aad: "org:1|ws:3" })).toThrowError( + /authentication failed/i, + ); + expect(() => openChunked(dek, sealed)).toThrowError(/authentication failed/i); + expect(() => + openChunked(dek, sealed, { aad: "org:1|ws:2", expectedKeyReference: "ws:other" }), + ).toThrowError(/authentication failed/i); + expect( + hex(openChunked(dek, sealed, { aad: "org:1|ws:2", expectedKeyReference: KEY_REFERENCE })), + ).toBe(hex(plaintext)); + }); + + it("cannot be opened with the wrong key", () => { + const sealed = sealChunked(key(), randomBytes(64), { keyReference: KEY_REFERENCE }); + expect(() => openChunked(key(), sealed)).toThrowError(/authentication failed/i); + }); + + it("carries inline wrap records", () => { + const dek = key(); + const record = wrapKeyWithSecret(randomBytes(32), dek, { recipientId: "ws:1" }); + const sealed = sealChunked(dek, randomBytes(32), { + keyReference: KEY_REFERENCE, + wrapRecords: [record], + }); + const info = inspectChunked(sealed); + expect(info.wrapRecords).toHaveLength(1); + expect(info.wrapRecords[0]?.recipientId).toBe("ws:1"); + expect(hex(info.wrapRecords[0]!.wrapped)).toBe(hex(record.wrapped)); + }); + + it("reports untrusted header metadata", () => { + const dek = key(); + const sealed = sealChunked(dek, randomBytes(200), { + keyReference: KEY_REFERENCE, + aad: "ctx", + chunkSizeLog2: SMALL_LOG2, + }); + expect(inspectChunked(sealed)).toMatchObject({ + version: 1, + suite: NMCS_SUITE_AES256GCM_HKDF_SHA256, + chunkSize: SMALL_CHUNK, + keyReference: KEY_REFERENCE, + plaintextLength: 200, + headerBytes: NMCS_HEADER_BYTES, + authenticated: false, + }); + }); + + it("rejects oversized payloads and out-of-range chunk sizes", () => { + const dek = key(); + expect(() => + sealChunked(dek, randomBytes(64), { keyReference: KEY_REFERENCE, maxPlaintextBytes: 32 }), + ).toThrowError(/exceeds the configured limit/i); + expect(() => + sealChunked(dek, randomBytes(8), { keyReference: KEY_REFERENCE, chunkSizeLog2: 15 }), + ).toThrowError(/out of range/i); + expect(() => + sealChunked(dek, randomBytes(8), { keyReference: KEY_REFERENCE, chunkSizeLog2: 27 }), + ).toThrowError(/out of range/i); + expect(() => sealChunked(dek, randomBytes(8), { keyReference: "" })).toThrowError( + /key reference is invalid/i, + ); + }); + + it("rejects a header whose content does not fit", () => { + expect(() => + sealChunked(key(), randomBytes(8), { keyReference: "k".repeat(500) }), + ).toThrowError(/too large/i); + }); +}); + +describe("nmcs1 tamper resistance", () => { + const dek = key(); + const plaintext = randomBytes(SMALL_CHUNK * 2 + 100); + const options: ChunkedSealOptions = { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }; + const sealed = sealChunked(dek, plaintext, options); + + it("rejects a bit flip anywhere in the header", () => { + for (let offset = 0; offset < NMCS_HEADER_BYTES; offset += 1) { + const tampered = Uint8Array.from(sealed); + tampered[offset] = (tampered[offset] ?? 0) ^ 0x01; + expect(() => openChunked(dek, tampered)).toThrow(); + } + }); + + it("rejects a bit flip in the chunk body", () => { + const tampered = Uint8Array.from(sealed); + const target = NMCS_HEADER_BYTES + 8; + tampered[target] = (tampered[target] ?? 0) ^ 0x01; + expect(() => openChunked(dek, tampered)).toThrowError(/authentication failed/i); + }); + + it("rejects truncation of the final chunk and of trailing bytes", () => { + const stride = SMALL_CHUNK + 20; + // Dropping whole chunks leaves a full non-final chunk carrying the final flag: the + // reader requires a final-flagged chunk to terminate, so the tag check fails. + expect(() => openChunked(dek, sealed.subarray(0, NMCS_HEADER_BYTES + stride * 2))).toThrowError( + /authentication failed/i, + ); + // Dropping bytes mid-chunk is caught by framing before any tag is checked. + expect(() => openChunked(dek, sealed.subarray(0, sealed.byteLength - 1))).toThrowError( + /truncated/i, + ); + expect(() => openChunked(dek, sealed.subarray(0, NMCS_HEADER_BYTES))).toThrowError( + /truncated/i, + ); + }); + + it("rejects reordered chunks", () => { + const stride = SMALL_CHUNK + 20; + const reordered = Uint8Array.from(sealed); + const first = sealed.subarray(NMCS_HEADER_BYTES, NMCS_HEADER_BYTES + stride); + const second = sealed.subarray(NMCS_HEADER_BYTES + stride, NMCS_HEADER_BYTES + stride * 2); + reordered.set(second, NMCS_HEADER_BYTES); + reordered.set(first, NMCS_HEADER_BYTES + stride); + expect(() => openChunked(dek, reordered)).toThrowError(/authentication failed/i); + }); + + it("rejects a chunk spliced in from another object", () => { + const other = sealChunked(dek, randomBytes(SMALL_CHUNK * 2 + 100), options); + const stride = SMALL_CHUNK + 20; + const spliced = Uint8Array.from(sealed); + spliced.set(other.subarray(NMCS_HEADER_BYTES, NMCS_HEADER_BYTES + stride), NMCS_HEADER_BYTES); + expect(() => openChunked(dek, spliced)).toThrowError(/authentication failed/i); + }); + + it("rejects a header transplanted from another object", () => { + const other = sealChunked(dek, plaintext, options); + const swapped = Uint8Array.from(sealed); + swapped.set(other.subarray(0, NMCS_HEADER_BYTES), 0); + expect(() => openChunked(dek, swapped)).toThrowError(/authentication failed/i); + }); + + it("rejects reserved flag bits, bad padding, and hostile chunk sizes", () => { + const withFlags = Uint8Array.from(sealed); + withFlags[6] = (withFlags[6] ?? 0) | 0b1000_0000; + expect(() => inspectChunked(withFlags)).toThrowError(/reserved flags/i); + + const withPadding = Uint8Array.from(sealed); + withPadding[480] = 0x01; + expect(() => inspectChunked(withPadding)).toThrowError(/padding is not zero/i); + + const badChunk = Uint8Array.from(sealed); + badChunk[7] = 40; + expect(() => inspectChunked(badChunk)).toThrowError(/chunk size is invalid/i); + + const bigChunk = sealChunked(key(), randomBytes(4), { + keyReference: KEY_REFERENCE, + chunkSizeLog2: 26, + maxChunkSizeLog2: 26, + }); + expect(() => openChunked(key(), bigChunk, { maxChunkSizeLog2: 20 })).toThrowError( + /exceeds the limit/i, + ); + }); + + it("never writes a chunk size its own default reader would refuse", () => { + const dekLocal = key(); + // The seal ceiling and the open ceiling are the same number, so a default writer + // cannot produce an object that a default reader (or inspector) rejects. + for (const chunkSizeLog2 of [25, 26]) { + expect(() => + sealChunked(dekLocal, randomBytes(4), { keyReference: KEY_REFERENCE, chunkSizeLog2 }), + ).toThrowError(/exceeds the limit/i); + } + + const optedIn = sealChunked(dekLocal, randomBytes(4), { + keyReference: KEY_REFERENCE, + chunkSizeLog2: 25, + maxChunkSizeLog2: 25, + }); + // Opting in on the write side alone is not enough: the reader must opt in too. + expect(() => openChunked(dekLocal, optedIn)).toThrowError(/exceeds the limit/i); + expect(() => inspectChunked(optedIn)).toThrowError(/exceeds the limit/i); + expect(inspectChunked(optedIn, { maxChunkSizeLog2: 25 }).chunkSize).toBe(2 ** 25); + expect(openChunked(dekLocal, optedIn, { maxChunkSizeLog2: 25 })).toHaveLength(4); + }); + + it("requires a whole header before reporting on one", () => { + // 496 bytes is the authenticated content but not a complete header; reporting on it + // would describe an object no reader could ever authenticate. + expect(() => inspectChunked(sealed.subarray(0, NMCS_HEADER_BYTES - 1))).toThrowError( + /truncated/i, + ); + expect(inspectChunked(sealed.subarray(0, NMCS_HEADER_BYTES)).keyReference).toBe(KEY_REFERENCE); + }); + + it("rejects unknown versions and suites", () => { + const badVersion = Uint8Array.from(sealed); + badVersion[4] = 0x02; + expect(() => inspectChunked(badVersion)).toThrowError(/version is unsupported/i); + + const badSuite = Uint8Array.from(sealed); + badSuite[5] = 0x02; + expect(() => inspectChunked(badSuite)).toThrowError(/suite is unsupported/i); + + const badMagic = Uint8Array.from(sealed); + badMagic[0] = 0x78; + expect(() => inspectChunked(badMagic)).toThrowError(/magic is invalid/i); + }); + + it("refuses to seal a declared length that disagrees with the payload", () => { + const dekLocal = key(); + // Emitting the object would produce ciphertext whose own header makes it + // permanently unopenable, so the contradiction is rejected up front. + expect(() => + sealChunked(dekLocal, randomBytes(100), { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + plaintextLength: 99, + }), + ).toThrowError(/declared plaintext length does not match/i); + }); +}); + +describe("chunked streams", () => { + it("produces bytes identical to the buffered seal when the length is declared", async () => { + const dek = key(); + const plaintext = randomBytes(SMALL_CHUNK * 2 + 511); + // The file identifier is drawn fresh per seal, so byte identity is only observable + // when both paths are pinned to the same one through the testing seam. + const options: ChunkedSealOptions = withFixedFileId( + { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + aad: "ctx", + plaintextLength: plaintext.byteLength, + }, + randomBytes(16), + ); + + const buffered = sealChunked(dek, plaintext, options); + const streamed = await pipe(plaintext, createChunkedSealStream(dek, options)); + expect(hex(streamed)).toBe(hex(buffered)); + }); + + it("differs from the buffered seal only in the length flag when undeclared", async () => { + const dek = key(); + const plaintext = randomBytes(SMALL_CHUNK + 7); + const options: ChunkedSealOptions = withFixedFileId( + { keyReference: KEY_REFERENCE, chunkSizeLog2: SMALL_LOG2 }, + randomBytes(16), + ); + + // `sealChunked` always knows the length and declares it; a stream cannot. + const buffered = sealChunked(dek, plaintext, options); + const streamed = await pipe(plaintext, createChunkedSealStream(dek, options)); + + expect(inspectChunked(buffered).plaintextLength).toBe(plaintext.byteLength); + expect(inspectChunked(streamed).plaintextLength).toBeUndefined(); + expect(streamed.byteLength).toBe(buffered.byteLength); + // Byte 6 is the flag field; the declared-length bit and the length field are the + // only header differences, and the chunk keys diverge because the header is hashed. + expect(buffered[6]! ^ streamed[6]!).toBe(0b0000_0010); + expect(hex(buffered.subarray(0, 6))).toBe(hex(streamed.subarray(0, 6))); + expect(hex(openChunked(dek, streamed))).toBe(hex(plaintext)); + }); + + it("round-trips through both stream directions", async () => { + const dek = key(); + for (const size of [0, 1, SMALL_CHUNK, SMALL_CHUNK + 1, SMALL_CHUNK * 3 + 7]) { + const plaintext = randomBytes(size); + const sealed = await pipe( + plaintext, + createChunkedSealStream(dek, { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }), + ); + expect(sealed.byteLength).toBe(chunkedCiphertextLength(size, SMALL_LOG2)); + expect(hex(await pipe(sealed, createChunkedOpenStream(dek)))).toBe(hex(plaintext)); + expect(hex(openChunked(dek, sealed))).toBe(hex(plaintext)); + } + }); + + it("opens a buffered seal and seals for a buffered open", async () => { + const dek = key(); + const plaintext = randomBytes(SMALL_CHUNK + 128); + const options: ChunkedSealOptions = { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }; + const buffered = sealChunked(dek, plaintext, options); + expect(hex(await pipe(buffered, createChunkedOpenStream(dek), 999))).toBe(hex(plaintext)); + + const streamed = await pipe(plaintext, createChunkedSealStream(dek, options), 999); + expect(hex(openChunked(dek, streamed))).toBe(hex(plaintext)); + }); + + it("omits the declared length when the size is unknown up front", async () => { + const dek = key(); + const sealed = await pipe( + randomBytes(300), + createChunkedSealStream(dek, { keyReference: KEY_REFERENCE, chunkSizeLog2: SMALL_LOG2 }), + ); + expect(inspectChunked(sealed).plaintextLength).toBeUndefined(); + expect(openChunked(dek, sealed).byteLength).toBe(300); + }); + + it("fails a stream truncated before its final chunk", async () => { + const dek = key(); + const sealed = sealChunked(dek, randomBytes(SMALL_CHUNK * 2), { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }); + await expect( + pipe(sealed.subarray(0, sealed.byteLength - 40), createChunkedOpenStream(dek)), + ).rejects.toThrow(); + await expect( + pipe(sealed.subarray(0, NMCS_HEADER_BYTES - 1), createChunkedOpenStream(dek)), + ).rejects.toThrowError(/truncated/i); + }); + + it("rejects a declared length that disagrees with the streamed payload", async () => { + const dek = key(); + await expect( + pipe( + randomBytes(100), + createChunkedSealStream(dek, { keyReference: KEY_REFERENCE, plaintextLength: 99 }), + ), + ).rejects.toThrowError(/does not match/i); + }); +}); + +describe("chunked size math", () => { + it("inverts exactly across chunk boundaries", () => { + for (const log2 of [SMALL_LOG2, NMCS_DEFAULT_CHUNK_SIZE_LOG2]) { + const chunk = 2 ** log2; + const sizes = [0, 1, 2, chunk - 1, chunk, chunk + 1, chunk * 2, chunk * 2 + 1, chunk * 5]; + for (const size of sizes) { + expect(chunkedPlaintextLength(chunkedCiphertextLength(size, log2), log2)).toBe(size); + } + } + }); + + it("returns undefined for sizes outside the format image", () => { + expect(chunkedPlaintextLength(0, SMALL_LOG2)).toBeUndefined(); + expect(chunkedPlaintextLength(NMCS_HEADER_BYTES, SMALL_LOG2)).toBeUndefined(); + expect(chunkedPlaintextLength(NMCS_HEADER_BYTES + 19, SMALL_LOG2)).toBeUndefined(); + expect(chunkedPlaintextLength(-1, SMALL_LOG2)).toBeUndefined(); + // One byte past a full single chunk cannot exist: it would need a second chunk's overhead. + expect( + chunkedPlaintextLength(NMCS_HEADER_BYTES + SMALL_CHUNK + 21, SMALL_LOG2), + ).toBeUndefined(); + }); + + it("computes chunk ciphertext ranges", () => { + const stride = SMALL_CHUNK + 20; + expect(chunkedChunkRange(0, SMALL_LOG2)).toEqual({ + start: NMCS_HEADER_BYTES, + end: NMCS_HEADER_BYTES + stride, + }); + expect(chunkedChunkRange(2, SMALL_LOG2)).toEqual({ + start: NMCS_HEADER_BYTES + stride * 2, + end: NMCS_HEADER_BYTES + stride * 3, + }); + expect(() => chunkedChunkRange(-1, SMALL_LOG2)).toThrowError(/out of range/i); + }); + + it("caps the chunk count at 2^20 (index is an exclusive bound)", () => { + // The policy allows 2^20 chunks, i.e. indices 0 .. 2^20 - 1. The last valid index + // resolves; the 2^20th index does not (off-by-one guard). + const maxIndex = 0x10_0000; + expect(chunkedChunkRange(maxIndex - 1, SMALL_LOG2)).toBeDefined(); + expect(() => chunkedChunkRange(maxIndex, SMALL_LOG2)).toThrowError(/out of range/i); + }); +}); + +describe("integration: sealed data key + chunked object", () => { + it("round-trips the full storage-adapter path", () => { + // 1. A random per-object data key, sealed to the workspace public key. + const workspace = generateX25519KeyPair(); + const dek = createSecretKey(randomBytes(32)); + const wrap = wrapKeyToRecipient(workspace.publicKey, dek, { + recipientId: "ws:1f9e", + info: "wsk.seal", + }); + + // 2. The object is encrypted under that key with the wrap carried inline. + const plaintext = randomBytes(SMALL_CHUNK * 2 + 42); + const sealed = sealChunked(dek, plaintext, { + keyReference: "ws:1f9e:e1", + aad: "org:acme|ws:1f9e", + chunkSizeLog2: SMALL_LOG2, + wrapRecords: [wrap], + }); + + // 3. A reader holding only the workspace private key recovers the object. + const header = inspectChunked(sealed.subarray(0, NMCS_HEADER_BYTES)); + const found = selectKeyWrapRecord(header.wrapRecords, { + recipientType: "x25519", + recipientId: "ws:1f9e", + }); + expect(found).toBeDefined(); + const recovered = unwrapKeyFromRecipient(workspace.privateKey, found!, { info: "wsk.seal" }); + expect(hex(openChunked(recovered, sealed, { aad: "org:acme|ws:1f9e" }))).toBe(hex(plaintext)); + + // 4. Someone without the workspace key cannot get the data key at all. + const stranger = generateX25519KeyPair(); + expect(() => + unwrapKeyFromRecipient(stranger.privateKey, found!, { info: "wsk.seal" }), + ).toThrowError(/authentication failed/i); + }); +}); + +describe("nmcs1 hardening regressions", () => { + const dek = key(); + const sealed = sealChunked(dek, randomBytes(SMALL_CHUNK + 32), { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }); + + it("gives callers no way to pin the file identifier", () => { + // The file identifier is the only per-object randomness in the key schedule, so a + // public seam would let a caller repeat a chunk key and nonce prefix. It exists only + // on `@nestm/crypto/testing`, and the public options type must not accept it. + const options: ChunkedSealOptions = { + keyReference: KEY_REFERENCE, + // @ts-expect-error -- fileId is deliberately absent from ChunkedSealOptions. + fileId: new Uint8Array(16), + }; + const a = sealChunked(dek, Buffer.from("same"), options); + const b = sealChunked(dek, Buffer.from("same"), options); + // An ignored stray property must not make two seals share a file identifier. + expect(hex(a.subarray(8, 24))).not.toBe(hex(b.subarray(8, 24))); + expect(hex(a)).not.toBe(hex(b)); + }); + + it("keeps the keystream distinct across seals of identical input", () => { + const plaintext = randomBytes(SMALL_CHUNK); + const a = sealChunked(dek, plaintext, { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }); + const b = sealChunked(dek, plaintext, { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }); + const bodyA = a.subarray(NMCS_HEADER_BYTES + 4, NMCS_HEADER_BYTES + 4 + plaintext.byteLength); + const bodyB = b.subarray(NMCS_HEADER_BYTES + 4, NMCS_HEADER_BYTES + 4 + plaintext.byteLength); + // Equal keystreams would make ct_a XOR ct_b equal pt_a XOR pt_b, i.e. all zeroes here. + expect(bodyA.some((byte, index) => (byte ^ (bodyB[index] as number)) !== 0)).toBe(true); + }); + + it("validates byte limits instead of letting NaN disable them", () => { + for (const bad of [Number.NaN, -1, 1.5, Number.POSITIVE_INFINITY]) { + expect(() => + sealChunked(dek, randomBytes(8), { keyReference: KEY_REFERENCE, maxPlaintextBytes: bad }), + ).toThrowError(/non-negative integer/i); + expect(() => openChunked(dek, sealed, { maxPlaintextBytes: bad })).toThrowError( + /non-negative integer/i, + ); + } + expect(() => createChunkedOpenStream(dek, { maxChunkSizeLog2: Number.NaN })).toThrowError( + /out of range/i, + ); + }); + + it("rejects unstable byte views at the bulk boundaries", () => { + const shared = new Uint8Array(new SharedArrayBuffer(16)); + expect(() => sealChunked(dek, shared, { keyReference: KEY_REFERENCE })).toThrowError( + /must be bytes/i, + ); + expect(() => openChunked(dek, shared)).toThrowError(/must be bytes/i); + + class Exotic extends Uint8Array {} + expect(() => sealChunked(dek, new Exotic(16), { keyReference: KEY_REFERENCE })).toThrowError( + /must be bytes/i, + ); + }); + + it("rejects a header whose text is not valid UTF-8 as malformed", () => { + const tampered = Uint8Array.from(sealed); + // Byte 38 begins the key reference; a bare continuation byte is not valid UTF-8. + tampered[38] = 0x80; + expect(() => inspectChunked(tampered)).toThrowError(/malformed/i); + expect(() => openChunked(dek, tampered)).toThrowError(/malformed/i); + }); + + it("rejects a header key reference carrying control characters", () => { + const tampered = Uint8Array.from(sealed); + tampered[38] = 0x07; + expect(() => inspectChunked(tampered)).toThrowError(/key reference is invalid/i); + }); + + it("fills the header content region exactly", () => { + // 38 fixed bytes + key reference must be able to reach the 496-byte boundary and no + // further; one byte more is a header that cannot be encoded. + const fitting = "k".repeat(496 - 38); + expect(() => sealChunked(key(), randomBytes(4), { keyReference: fitting })).not.toThrow(); + expect(() => sealChunked(key(), randomBytes(4), { keyReference: `${fitting}k` })).toThrowError( + /too large/i, + ); + }); + + it("bounds streaming work by the plaintext budget before buffering a whole chunk", async () => { + const big = sealChunked(dek, randomBytes(SMALL_CHUNK + 64), { + keyReference: KEY_REFERENCE, + chunkSizeLog2: SMALL_LOG2, + }); + // Feeding a byte at a time must trip the limit almost immediately rather than after a + // full chunk has accumulated in memory. + await expect( + pipe(big, createChunkedOpenStream(dek, { maxPlaintextBytes: 32 }), 1), + ).rejects.toThrow(/exceeds the configured limit/i); + await expect( + pipe(big, createChunkedOpenStream(dek, { maxPlaintextBytes: 32 })), + ).rejects.toThrow(/exceeds the configured limit/i); + }); + + it("produces identical output whatever the source write sizes are", async () => { + const plaintext = randomBytes(SMALL_CHUNK + 13); + const options = withFixedFileId( + { keyReference: KEY_REFERENCE, chunkSizeLog2: SMALL_LOG2 }, + randomBytes(16), + ); + const oneByteAtATime = await pipe(plaintext, createChunkedSealStream(dek, options), 1); + const allAtOnce = await pipe( + plaintext, + createChunkedSealStream(dek, options), + plaintext.byteLength, + ); + expect(hex(oneByteAtATime)).toBe(hex(allAtOnce)); + expect(hex(await pipe(oneByteAtATime, createChunkedOpenStream(dek), 1))).toBe(hex(plaintext)); + }); +}); diff --git a/tests/unit/vectors/format-vectors.json b/tests/unit/vectors/format-vectors.json new file mode 100644 index 0000000..4451f0e --- /dev/null +++ b/tests/unit/vectors/format-vectors.json @@ -0,0 +1,23 @@ +{ + "_comment": "FROZEN FORMAT VECTORS. Changing any value here is a wire-format break: it invalidates every object already written. Regenerate only with a format version bump.", + "nmcs1": { + "key": "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff", + "fileId": "000102030405060708090a0b0c0d0e0f", + "keyReference": "ws:vector:1", + "chunkSizeLog2": 16, + "empty": "bm1jcwEBAhAAAQIDBAUGBwgJCgsMDQ4PAAAAAAAAAAAACwAAAAB3czp2ZWN0b3I6MQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMeg7DUxNoDSbmAAtB4y3v8AAAAAhCpiSk/nbesHiZr4rAGznQ==", + "oneByte": "bm1jcwEBAhAAAQIDBAUGBwgJCgsMDQ4PAAAAAAAAAAEACwAAAAB3czp2ZWN0b3I6MQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIr5qCkpLRYqOGiMZ0mPBHsAAAABwBoqhnRKllXb7b9gnE8GHUQ=", + "exactChunk": "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", + "multiChunk": "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", + "withAad": "bm1jcwEBAhAAAQIDBAUGBwgJCgsMDQ4PAAAAAAAAAEAACwANAAB3czp2ZWN0b3I6MW9yZzphY21lfHdzOjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAzjNDaSqLNPEEFvrSQsycAAABAlKfUf+gcqMu9+i9Jcs9e7GQGjvYNcFPJs8VZTQTc9jblpF7x3ObLki5bZLIct/AL6q2viKJFr8smICVMV/V2w7hG260FvQufz4Un55tXkso=", + "undeclaredLength": "bm1jcwEBABAAAQIDBAUGBwgJCgsMDQ4P//////////8ACwAAAAB3czp2ZWN0b3I6MQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFLh8gH7z6A+0dA0jjfzd7UAAAAQWdAgpL4SoG+b7DW9yPAuD0p2hcESNbKqmWkBsUHduh0=", + "withWrapRecord": "bm1jcwEBAxAAAQIDBAUGBwgJCgsMDQ4PAAAAAAAAAAcACwAAAAF3czp2ZWN0b3I6MQEBCQkAPUEyNTZHQ01LV3dzOnZlY3RvcgEIDxYdJCsyOUBHTlVcY2pxeH+GjZSboqmwt77FzNPa4ejv9v0ECxIZICcuNTxDSlFYX2ZtdHuCiZCXnqUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMfeJwl+y62fpM3RJ2GRK8sAAAAHzU0axaGJocvFn2xopWKR4EI4Zhed530=" + }, + "keyWrapRecord": { + "kek": "ffeeddccbbaa99887766554433221100ffeeddccbbaa99887766554433221100", + "secret": "AQEJCwA9QTI1NkdDTUtXdXNlcjp2ZWN0b3IBjvT8P7HrN7vCRd5pz6o/HSMUSvP2qmsvmod4m5OeGOi8QZLUnyq1URe5CRdXcfuCNflssGOETX43ynj1", + "x25519RecipientPrivateKey": "1111111111111111111111111111111111111111111111111111111111111111", + "x25519": "AQIaCwBSWDI1NTE5LUhLREYtU0hBMjU2LUEyNTZHQ011c2VyOnZlY3RvcgEBpWgfv7voBBLxXw5/4Db+wvRFhCa97Lfm0zSUfERfsGO9gdDg+jNQybjg0HvUF4GU1gjRaFKK5yy74nukfvDWNfw9mjhneRpSHkmPrnPvcYw=", + "inlineSecret": "AQEJCQA9QTI1NkdDTUtXd3M6dmVjdG9yAQgPFh0kKzI5QEdOVVxjanF4f4aNlJuiqbC3vsXM09rh6O/2/QQLEhkgJy41PENKUVhfZm10e4KJkJeepQ==" + } +} diff --git a/tsdown.config.ts b/tsdown.config.ts index 14a70a2..1f7aa4a 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -9,6 +9,8 @@ export default defineConfig({ "src/http/index.ts", "src/prisma/index.ts", "src/keys/index.ts", + "src/password/index.ts", + "src/stream/index.ts", "src/key-wrap/rsa/index.ts", "src/kms/aws/index.ts", "src/kms/gcp/index.ts",