diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c333bfe..ed99479 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,17 +5,23 @@ on: pull_request: jobs: - pkvault: + test: runs-on: ubuntu-latest strategy: matrix: node: [18.x, 22.x] - defaults: - run: - working-directory: apps/pkvault steps: - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + with: + version: 10.33.0 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} - - run: npm test + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: node -e 'require("@pksuite/crypto")' + working-directory: apps/pkvault + - run: pnpm build + - run: git diff --exit-code -- packages/crypto/dist + - run: pnpm test diff --git a/.gitignore b/.gitignore index e32de2f..b1f4ba3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,9 @@ node_modules/ dist/ +!packages/crypto/dist/ +!packages/crypto/dist/** +packages/crypto/test/mobile/runner.bundle.js +packages/crypto/test/mobile/runner.bundle.js.LEGAL.txt .wrangler/ .env .env.* diff --git a/README.md b/README.md index dc6ec52..372bca7 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ hosted service to trust. |---|---|---| | [pkvault](apps/pkvault) | pre-release | Passkey-encrypted `.env` files for teams | +The independently specified [`@pksuite/crypto`](packages/crypto) package +provides the product-neutral streaming file envelope shared by future tools. + A product appears here only when it has a maintained implementation or a committed near-term release. @@ -45,6 +48,7 @@ live in [`spec/research`](spec/research). - `apps/` — deployable products (imported with full history; pkvault's pre-monorepo commits are preserved from `ddyy/pkvault`) +- `packages/` — reusable, product-neutral libraries - `spec/` — specifications; `spec/research/` is exploratory, non-roadmap ## Releases diff --git a/apps/pkvault/README.md b/apps/pkvault/README.md index 4edd3d9..f3e6571 100644 --- a/apps/pkvault/README.md +++ b/apps/pkvault/README.md @@ -111,7 +111,7 @@ running `setup` for a new identity and being re-added to every vault. ## Development ```sh -npm test +pnpm --filter pkvault test ``` ## License diff --git a/apps/pkvault/package.json b/apps/pkvault/package.json index ff30292..9cbbbb5 100644 --- a/apps/pkvault/package.json +++ b/apps/pkvault/package.json @@ -39,5 +39,8 @@ ], "engines": { "node": ">=18" + }, + "dependencies": { + "@pksuite/crypto": "0.1.0" } } diff --git a/apps/pkvault/src/age.js b/apps/pkvault/src/age.js index a823b77..3df3191 100644 --- a/apps/pkvault/src/age.js +++ b/apps/pkvault/src/age.js @@ -1,10 +1,12 @@ "use strict"; -// Minimal age v1 (X25519 recipients only) on node:crypto. Zero deps. +// Minimal age v1 (X25519 recipients only) on node:crypto. Identity string +// encoding and public-key derivation are shared with @pksuite/crypto; the +// legacy vault envelope remains implemented with node:crypto. // Writer: never greases, X25519 stanzas only (SPEC §5 writer obligations). // Reader: strict — any non-X25519 stanza rejects (SPEC §5 reader validation). const crypto = require("node:crypto"); -const bech32 = require("./bech32"); +const shared = require("@pksuite/crypto"); const VERSION_LINE = "age-encryption.org/v1"; // Fixed DER prefixes for raw X25519 key import (RFC 8410 structures). @@ -26,8 +28,7 @@ function pubKey(raw32) { return crypto.createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw32]), format: "der", type: "spki" }); } function publicFromScalar(scalar32) { - const pub = crypto.createPublicKey(privKey(scalar32)); - return pub.export({ format: "der", type: "spki" }).subarray(-32); + return Buffer.from(shared.publicKeyFromSecretKey(scalar32)); } function x25519(scalar32, theirPub32) { return crypto.diffieHellman({ privateKey: privKey(scalar32), publicKey: pubKey(theirPub32) }); @@ -52,21 +53,18 @@ function chachaOpen(key, nonce, ctAndTag) { // --- identity / recipient strings ------------------------------------------- function keygen(scalar32 = crypto.randomBytes(32)) { - return { scalar: scalar32, recipient: encodeRecipient(publicFromScalar(scalar32)), identity: encodeIdentity(scalar32) }; + const pair = shared.createRecipientIdentityFromSecret(scalar32); + return { scalar: scalar32, recipient: pair.recipient, identity: pair.identity }; } -const encodeRecipient = (pub32) => bech32.encode("age", pub32); -const encodeIdentity = (scalar32) => bech32.encode("age-secret-key-", scalar32).toUpperCase(); +const encodeRecipient = (pub32) => shared.formatRecipient(pub32); +const encodeIdentity = (scalar32) => shared.formatIdentity(scalar32); function decodeRecipient(str) { - if (str !== str.toLowerCase()) return null; // canonical lowercase required (SPEC §2.2) - const d = bech32.decode(str); - if (!d || d.hrp !== "age" || d.data.length !== 32) return null; - if (bech32.encode("age", d.data) !== str) return null; // canonical re-encoding - return d.data; + try { return Buffer.from(shared.parseRecipient(str)); } + catch { return null; } } function decodeIdentity(str) { - const d = bech32.decode(str); - if (!d || d.hrp !== "age-secret-key-" || d.data.length !== 32) return null; - return d.data; + try { return Buffer.from(shared.parseIdentity(str)); } + catch { return null; } } // --- encrypt ------------------------------------------------------------------ diff --git a/apps/pkvault/src/bech32.js b/apps/pkvault/src/bech32.js deleted file mode 100644 index 67cf37f..0000000 --- a/apps/pkvault/src/bech32.js +++ /dev/null @@ -1,66 +0,0 @@ -"use strict"; -// Minimal Bech32 (BIP-173, checksum constant 1) for age recipient/identity strings. - -const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; -const GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; - -function polymod(values) { - let chk = 1; - for (const v of values) { - const top = chk >>> 25; - chk = ((chk & 0x1ffffff) << 5) ^ v; - for (let i = 0; i < 5; i++) if ((top >>> i) & 1) chk ^= GEN[i]; - } - return chk >>> 0; -} - -function hrpExpand(hrp) { - const out = []; - for (const c of hrp) out.push(c.charCodeAt(0) >>> 5); - out.push(0); - for (const c of hrp) out.push(c.charCodeAt(0) & 31); - return out; -} - -function convertBits(data, from, to, pad) { - let acc = 0, bits = 0; - const out = [], maxv = (1 << to) - 1; - for (const b of data) { - if (b < 0 || b >>> from) return null; - acc = (acc << from) | b; - bits += from; - while (bits >= to) { bits -= to; out.push((acc >>> bits) & maxv); } - } - if (pad) { if (bits) out.push((acc << (to - bits)) & maxv); } - else if (bits >= from || ((acc << (to - bits)) & maxv)) return null; - return out; -} - -function encode(hrp, data /* Buffer */) { - const words = convertBits([...data], 8, 5, true); - const values = [...hrpExpand(hrp), ...words]; - const mod = polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1; - const checksum = []; - for (let i = 0; i < 6; i++) checksum.push((mod >>> (5 * (5 - i))) & 31); - return hrp + "1" + [...words, ...checksum].map((w) => CHARSET[w]).join(""); -} - -function decode(str) { - if (str !== str.toLowerCase() && str !== str.toUpperCase()) return null; // mixed case forbidden - const s = str.toLowerCase(); - const pos = s.lastIndexOf("1"); - if (pos < 1 || pos + 7 > s.length) return null; - const hrp = s.slice(0, pos); - const words = []; - for (const c of s.slice(pos + 1)) { - const v = CHARSET.indexOf(c); - if (v === -1) return null; - words.push(v); - } - if (polymod([...hrpExpand(hrp), ...words]) !== 1) return null; - const data = convertBits(words.slice(0, -6), 5, 8, false); - if (data === null) return null; - return { hrp, data: Buffer.from(data) }; -} - -module.exports = { encode, decode }; diff --git a/apps/pkvault/src/format.js b/apps/pkvault/src/format.js index 821112d..ab04fc4 100644 --- a/apps/pkvault/src/format.js +++ b/apps/pkvault/src/format.js @@ -1,5 +1,5 @@ "use strict"; -// pkvault v1 wire format (SPEC.md draft 0.10). Zero deps beyond node:crypto. +// pkvault v1 wire format (SPEC.md draft 0.10). This module uses node:crypto. // Verification order (SPEC §6.2): structural parse → unseal FK → verify MAC → decrypt. const crypto = require("node:crypto"); diff --git a/apps/pkvault/test/agent.test.js b/apps/pkvault/test/agent.test.js index fdacfa8..6b30b5f 100644 --- a/apps/pkvault/test/agent.test.js +++ b/apps/pkvault/test/agent.test.js @@ -1,6 +1,6 @@ "use strict"; // Agent suite: lazy one-ceremony unlock, caching, TTL, lock, socket hygiene, -// and the full sync-CLI-through-agent path. node:test, zero deps. +// and the full sync-CLI-through-agent path. Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); diff --git a/apps/pkvault/test/bin.test.js b/apps/pkvault/test/bin.test.js index 91dcca9..21d1ae0 100644 --- a/apps/pkvault/test/bin.test.js +++ b/apps/pkvault/test/bin.test.js @@ -1,6 +1,6 @@ "use strict"; // CLI arg-parsing safety (bin.js): fail-closed on unknown/misspelled/conflicting -// flags and invalid --ttl. Exercises the real binary via spawnSync. zero deps. +// flags and invalid --ttl. Exercises the real binary via spawnSync. const test = require("node:test"); const assert = require("node:assert/strict"); const path = require("node:path"); diff --git a/apps/pkvault/test/bridge.test.js b/apps/pkvault/test/bridge.test.js index 5bcdc36..f3aeaad 100644 --- a/apps/pkvault/test/bridge.test.js +++ b/apps/pkvault/test/bridge.test.js @@ -1,7 +1,7 @@ "use strict"; // Bridge suite: the ceremony transport protocol with a Node-simulated page // (same ECDH/HKDF/AES-GCM the browser does in WebCrypto), plus setup/recover/ -// ceremonyUnlock end-to-end with a simulated authenticator. node:test, zero deps. +// ceremonyUnlock end-to-end with a simulated authenticator. Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); diff --git a/apps/pkvault/test/cli.test.js b/apps/pkvault/test/cli.test.js index b1a033c..0cd2782 100644 --- a/apps/pkvault/test/cli.test.js +++ b/apps/pkvault/test/cli.test.js @@ -1,7 +1,7 @@ "use strict"; // CLI integration suite: init → set/get/run → add (guard) → remove (rotation + // checklist + accepted-exposure marker) → status, plus non-interactive refusals -// and interrupted-txn recovery on next mutation. node:test, zero deps. +// and interrupted-txn recovery on next mutation. Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); diff --git a/apps/pkvault/test/editor.test.js b/apps/pkvault/test/editor.test.js index 59bce2c..30aebc6 100644 --- a/apps/pkvault/test/editor.test.js +++ b/apps/pkvault/test/editor.test.js @@ -1,6 +1,6 @@ "use strict"; // Editor suite: F1 classification, F2 sentinels, F8 set-stdin, F10 sentinel -// rename, F11 public-refusal (SPEC §§9, 11.4). node:test, zero deps. +// rename and F11 public-refusal (SPEC §§9, 11.4). Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); diff --git a/apps/pkvault/test/identity.test.js b/apps/pkvault/test/identity.test.js index 23b38fe..6695ae1 100644 --- a/apps/pkvault/test/identity.test.js +++ b/apps/pkvault/test/identity.test.js @@ -1,6 +1,6 @@ "use strict"; // Identity-blob suite (SPEC-IDENTITY v1): PRF/recovery wraps, self-authentication, -// recovery-burns-both semantics, parse/version errors. node:test, zero deps. +// recovery-burns-both semantics and parse/version errors. Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); diff --git a/apps/pkvault/test/merge.test.js b/apps/pkvault/test/merge.test.js index ec768e5..215884a 100644 --- a/apps/pkvault/test/merge.test.js +++ b/apps/pkvault/test/merge.test.js @@ -1,6 +1,6 @@ "use strict"; // Merge suite: F3 disjoint, F4 rotated, F5 conflicts, F6 override boundary, -// F12 case-fold (SPEC §§8, 11.4). node:test, zero deps. +// F12 case-fold (SPEC §§8, 11.4). Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const crypto = require("node:crypto"); diff --git a/apps/pkvault/test/wire.test.js b/apps/pkvault/test/wire.test.js index 0d3d8e9..711ba4f 100644 --- a/apps/pkvault/test/wire.test.js +++ b/apps/pkvault/test/wire.test.js @@ -1,7 +1,7 @@ "use strict"; // Wire-fixture suite (SPEC 0.10 §§11.2–11.3). Runs every fixtures/manifest.json // entry through the pipeline and asserts the exact expected error code or -// success properties. node:test, zero deps. +// success properties. Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); diff --git a/apps/pkvault/test/workflow.test.js b/apps/pkvault/test/workflow.test.js index 24a5027..26cbe85 100644 --- a/apps/pkvault/test/workflow.test.js +++ b/apps/pkvault/test/workflow.test.js @@ -1,6 +1,6 @@ "use strict"; // Workflow suite: F7 crash/adversary matrix (SPEC §8.2), F9 locking (§8.3), -// manifest + config parsing (SPEC-MANIFEST). node:test, zero deps. +// Manifest + config parsing (SPEC-MANIFEST). Uses node:test. const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); diff --git a/package.json b/package.json index b190585..c4ab02a 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,14 @@ "engines": { "node": ">=18" }, + "scripts": { + "build": "pnpm --filter @pksuite/crypto build", + "test": "pnpm --filter @pksuite/crypto test && pnpm --filter pkvault test" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + }, "packageManager": "pnpm@10.33.0" } diff --git a/packages/crypto/LICENSE b/packages/crypto/LICENSE new file mode 100644 index 0000000..b769ce3 --- /dev/null +++ b/packages/crypto/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Daniel Yang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/crypto/README.md b/packages/crypto/README.md new file mode 100644 index 0000000..7401ab5 --- /dev/null +++ b/packages/crypto/README.md @@ -0,0 +1,74 @@ +# `@pksuite/crypto` + +Browser and Node cryptographic primitives shared by the pk suite. Version +0.1.0 provides canonical age/X25519 identities and the streaming `PKFILE1` +file envelope. It has no workflow, storage-provider, or network concepts. +The ESM browser entry is intended for an application bundler. Its declared +`@noble/*` dependencies remain external in both ESM and CommonJS artifacts so +consumer lockfile updates change the code that actually executes. + +```js +import { + createRecipientIdentity, + createFileEncryption, + disposeEnvelope, + encryptChunk, + finalizeEnvelope, + openFileEnvelope, + decryptChunk, +} from "@pksuite/crypto"; + +const recipient = createRecipientIdentity(); +const encryption = createFileEncryption({ + recipient: recipient.recipient, + plaintextSize: file.size, + metadata: { + name: file.name, + mediaType: file.type || "application/octet-stream", + lastModified: file.lastModified, + }, +}); + +for (let index = 0; index < encryption.chunkCount; index++) { + const start = index * encryption.chunkSize; + const plaintext = new Uint8Array(await file.slice(start, start + encryption.chunkSize).arrayBuffer()); + const ciphertext = encryptChunk({ state: encryption, index, plaintext }); + // Persist or upload this independently authenticated chunk before reading + // the next slice. Retrying an index produces a fresh XChaCha nonce. + await storeChunk(index, ciphertext); +} +finalizeEnvelope(encryption); + +const decryption = openFileEnvelope({ identity: recipient.identity, header: encryption.header }); +for (let index = 0; index < decryption.chunkCount; index++) { + const plaintext = decryptChunk({ state: decryption, index, ciphertext: await loadChunk(index) }); + await consumePlaintext(plaintext); +} +finalizeEnvelope(decryption); +``` + +`finalizeEnvelope` best-effort overwrites the file key retained by its state +and makes that state unusable. Call `disposeEnvelope(state)` on cancellation or +any abandoned upload/decryption. JavaScript cannot guarantee erasure of engine +or cryptographic-library internal copies. + +Metadata names are encrypted portable filename components, not paths. A +consumer that writes files must still join the name beneath an explicitly +chosen directory and must not decode or reinterpret it as path syntax. + +The encrypted header exposes file size, chunk geometry, recipient count, and +header length. Filename, media type, and modification time are encrypted. See +[`dist/SPEC-FILE.md`](dist/SPEC-FILE.md) in the package, or `spec/SPEC-FILE.md` +in the repository, for the implementation-independent wire format and threat +contract. + +`vectors/file-v1.json` contains fixed identity, header, metadata, nonce, AAD, +and chunk outputs. An independent Node reference reader opens that frozen +vector in the test suite. Every structured case in +`vectors/file-v1-tampering.json` is executed by the conformance tests. The +frozen `age-cli-v1.age` artifact was written by the external age CLI. + +The mobile release gate is recorded under `test/mobile/reports/` for Android +Chrome and iOS Safari. Both devices completed foreground and background-resume +runs with matching byte counts. Safari does not expose `performance.memory`; +the harness records that measurement as unavailable. diff --git a/packages/crypto/dist/SPEC-FILE.md b/packages/crypto/dist/SPEC-FILE.md new file mode 100644 index 0000000..62d02c4 --- /dev/null +++ b/packages/crypto/dist/SPEC-FILE.md @@ -0,0 +1,214 @@ +# pk file envelope v1 + +Status: implementation draft for `@pksuite/crypto` 0.1.0 +Wire identifier: `PKFILE1\n` +Date: 2026-07-17 + +## 1. Scope + +This document specifies a product-independent, seekable file envelope for +browser encryption, multipart upload, and browser or Node decryption. It does +not identify firms, requests, checklist items, storage providers, object keys, +or upload sessions. + +The format provides: + +- X25519 recipient identities encoded according to age v1. +- A fresh 128-bit file key sealed in a strict age v1 X25519 header. +- Encrypted filename, media type, and modification time. +- Independently authenticated chunks that can be retried or resumed. +- Authentication of version, file id, header, chunk index, chunk count, total + plaintext size, configured chunk size, and actual chunk size. + +It does not hide ciphertext size, chunk geometry, recipient count, or timing. +It does not authenticate who selected the recipient key. A malicious hosted +origin can substitute code or keys before encryption. + +## 2. Primitive profile + +- X25519, HKDF-SHA-256, HMAC-SHA-256, and ChaCha20-Poly1305 exactly as used by + the age v1 X25519 recipient stanza. +- XChaCha20-Poly1305 for metadata and file chunks. +- SHA-256 for public header commitments. +- All integers are unsigned big-endian. +- Strings are UTF-8. Domain labels below are literal UTF-8 bytes. +- AEAD ciphertext is `ciphertext || 16-byte tag`. + +Each envelope has a fresh random 16-byte age file key and 16-byte `file_id`. +Each metadata or chunk encryption has a fresh random 24-byte XChaCha nonce. +Randomness MUST come from the operating system CSPRNG. + +## 3. Public header + +```text +magic 8 bytes "PKFILE1\n" +header_body_length u32 +chunk_size u32 +plaintext_size u64 +chunk_count u32 +file_id 16 bytes +age_header_length u32 +age_header bytes +metadata_nonce 24 bytes +metadata_ciphertext_len u32 +metadata_ciphertext bytes +``` + +`header_body_length` covers every field after itself. There are no trailing +header bytes. Implementations MUST reject lengths beyond their documented +limits before allocating. + +`chunk_count = max(1, ceil(plaintext_size / chunk_size))`. An empty file has +one authenticated zero-length plaintext chunk. Every non-final chunk has +exactly `chunk_size` plaintext bytes. The final chunk has the remaining bytes. + +The age header is a complete textual age v1 header ending in LF. V1 accepts +only native `X25519` stanzas. It contains no age payload: its sole purpose is +to authenticate and seal the 16-byte file key. + +## 4. Encrypted metadata + +Metadata plaintext is canonical UTF-8 JSON with exactly these keys in order: + +```json +{"name":"client.pdf","mediaType":"application/pdf","lastModified":1784313600000} +``` + +`name` is a non-empty portable filename no longer than 4096 UTF-8 bytes, not a +path. It MUST contain only Unicode scalar values; MUST NOT contain control +characters, `/`, `\\`, or `:`; +MUST NOT be `.` or `..`; MUST NOT end in a space or period; and MUST NOT be a +case-insensitive Windows device basename (`CON`, `PRN`, `AUX`, `NUL`, `COM1` +through `COM9`, or `LPT1` through `LPT9`, with or without an extension). +Consumers MUST still join it beneath an explicitly chosen directory and must +not decode or reinterpret it as path syntax. `mediaType` is at most 255 bytes +of printable ASCII. `lastModified` is an integer from 0 through +9007199254740991 inclusive, or `null`. + +“Canonical JSON” is defined here independently of any JSON library. The three +members occur exactly in the order shown, with no whitespace. Member names use +the literal ASCII bytes shown. String values are surrounded by `"`; `"` and +`\\` are escaped as `\\"` and `\\\\`; U+0008, U+0009, U+000A, U+000C, and +U+000D use `\\b`, `\\t`, `\\n`, `\\f`, and `\\r`; any other U+0000 through +U+001F uses lowercase `\\u00xx`; every other Unicode scalar value is emitted +directly as UTF-8. Lone UTF-16 surrogates are invalid. `lastModified` uses +unprefixed base-10 ASCII with no leading zeroes, or literal `null`. + +```text +metadata_key = HKDF-SHA-256( + ikm = file_key, + salt = file_id, + info = "pksuite/file/metadata-key/v1", + L = 32 +) + +structural_prefix = chunk_size || plaintext_size || chunk_count || file_id + || age_header_length || age_header + +metadata_aad = "pksuite/file/metadata/v1" || 0x00 || u32(1) + || SHA-256("PKFILE1\n" || structural_prefix) + +metadata_ciphertext = XChaCha20-Poly1305( + key = metadata_key, + nonce = metadata_nonce, + aad = metadata_aad, + plaintext = canonical_metadata +) +``` + +## 5. Chunks + +Every stored chunk is: + +```text +chunk_nonce 24 bytes +chunk_ciphertext expected_plaintext_length + 16 bytes +``` + +For zero-based chunk index `i`: + +```text +chunk_key = HKDF-SHA-256( + ikm = file_key, + salt = file_id || u32(i), + info = "pksuite/file/chunk-key/v1", + L = 32 +) + +header_commitment = SHA-256(complete_public_header) + +chunk_aad = "pksuite/file/chunk/v1" || 0x00 + || u32(1) + || file_id + || header_commitment + || u32(i) + || u32(chunk_count) + || u64(plaintext_size) + || u32(chunk_size) + || u32(expected_plaintext_length(i)) +``` + +The chunk is encrypted with XChaCha20-Poly1305 using `chunk_key`, a fresh +random `chunk_nonce`, and `chunk_aad`. + +Multipart retries MAY re-encrypt the same index. A retry MUST generate a fresh +24-byte nonce. Consequently, a restarted client does not need to persist nonce +state, and changing the local plaintext before a retry does not deterministically +reuse a nonce. A multipart implementation SHOULD reuse already-produced +ciphertext when available to avoid unnecessary work. + +The receiver MUST know the index associated with each chunk and MUST reject a +wrong length or authentication failure. Completion succeeds only after every +index in `[0, chunk_count)` has authenticated. Missing chunks are truncation; +passing a chunk under another index is reordering; moving a chunk between +envelopes is substitution. All fail closed. + +## 6. Parsing and limits + +The 0.1.0 implementation accepts: + +- Chunk sizes from 64 KiB through 16 MiB; default 1 MiB. +- Public headers no larger than 1 MiB. +- Embedded age headers no larger than 256 KiB. +- Metadata ciphertext no larger than 64 KiB. +- Plaintext sizes within JavaScript's safe integer range and no more than + `2^32 - 1` chunks. + +Readers MUST reject unknown magic/version, inconsistent geometry, non-canonical +age or metadata encoding, foreign age stanzas, trailing header bytes, and any +integer or length outside supported bounds. + +## 7. Resume and storage contract + +The envelope does not prescribe a storage API. A storage adapter records the +public header once and associates each ciphertext with its zero-based index. +Replacing an index atomically is safe because each encryption uses a fresh +XChaCha nonce. The adapter must not declare the object complete until every +index exists and the application has run the package finalization check. + +Successful finalization or explicit disposal makes the state unusable and +best-effort overwrites its retained file key. Applications MUST explicitly +dispose abandoned states; JavaScript cannot guarantee that engine or AEAD +internal copies are erased. + +Storage-specific identifiers and authorization capabilities are deliberately +outside the envelope and MUST NOT be added as authenticated fields in v1. + +## 8. Test requirements + +Published vectors cover identity encoding, age-header sealing, header bytes, +metadata ciphertext, chunk ciphertexts, and AAD commitments. Implementations +must additionally reject: + +- Wrong identities. +- Header or encrypted-metadata tampering. +- Truncated or extended chunks. +- Reordered chunks. +- Chunks substituted across indices or files. +- Missing chunks at finalization. +- Retry output that reuses the prior nonce. + +Before a stable release, representative 50–100 MB PDF/image inputs must be +measured on supported iOS Safari and Android Chrome versions. Record duration, +configured chunk size, peak observable memory when available, browser/device, +and whether backgrounding or network interruption affected resume. diff --git a/packages/crypto/dist/index.cjs b/packages/crypto/dist/index.cjs new file mode 100644 index 0000000..fd24945 --- /dev/null +++ b/packages/crypto/dist/index.cjs @@ -0,0 +1,784 @@ +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// src/index.js +var index_exports = {}; +__export(index_exports, { + PkCryptoError: () => PkCryptoError, + ageHeader: () => ageHeader, + createFileEncryption: () => createFileEncryption, + createRecipientIdentity: () => createRecipientIdentity, + createRecipientIdentityFromSecret: () => createRecipientIdentityFromSecret, + decryptChunk: () => decryptChunk, + disposeEnvelope: () => disposeEnvelope, + encryptChunk: () => encryptChunk, + finalizeEnvelope: () => finalizeEnvelope, + formatIdentity: () => formatIdentity, + formatRecipient: () => formatRecipient, + openFileEnvelope: () => openFileEnvelope, + parseIdentity: () => parseIdentity, + parseRecipient: () => parseRecipient, + publicKeyFromSecretKey: () => publicKeyFromSecretKey, + recipientFromIdentity: () => recipientFromIdentity +}); +module.exports = __toCommonJS(index_exports); + +// src/error.js +var PkCryptoError = class extends Error { + constructor(code, message) { + super(`${code}: ${message}`); + this.name = "PkCryptoError"; + this.code = code; + } +}; +var fail = (code, message) => { + throw new PkCryptoError(code, message); +}; + +// src/age.js +var import_chacha = require("@noble/ciphers/chacha.js"); +var import_ed25519 = require("@noble/curves/ed25519.js"); +var import_hkdf = require("@noble/hashes/hkdf.js"); +var import_hmac = require("@noble/hashes/hmac.js"); +var import_sha256 = require("@noble/hashes/sha256.js"); +var import_utils = require("@noble/hashes/utils.js"); + +// src/bytes.js +var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var BASE64_VALUE = new Int16Array(128).fill(-1); +for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i; +var EMPTY = new Uint8Array(0); +var utf8 = (value) => new TextEncoder().encode(value); +function bytes(value, length, name = "value") { + if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Uint8Array`); + if (length !== void 0 && value.length !== length) throw new TypeError(`${name} must be ${length} bytes`); + return value; +} +function concat(...parts) { + const length = parts.reduce((sum, part) => sum + bytes(part).length, 0); + const out = new Uint8Array(length); + let offset = 0; + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } + return out; +} +function equal(a, b) { + if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array) || a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; + return diff === 0; +} +function allZero(value) { + let acc = 0; + for (const byte of value) acc |= byte; + return acc === 0; +} +function u32(value) { + if (!Number.isInteger(value) || value < 0 || value > 4294967295) throw new RangeError("value is not a uint32"); + const out = new Uint8Array(4); + new DataView(out.buffer).setUint32(0, value, false); + return out; +} +function u64(value) { + if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("value is not a non-negative safe integer"); + const out = new Uint8Array(8); + const view = new DataView(out.buffer); + const big = BigInt(value); + view.setUint32(0, Number(big >> 32n), false); + view.setUint32(4, Number(big & 0xffffffffn), false); + return out; +} +function readU32(value, offset) { + if (offset < 0 || offset + 4 > value.length) throw new RangeError("truncated uint32"); + return new DataView(value.buffer, value.byteOffset + offset, 4).getUint32(0, false); +} +function readU64(value, offset) { + if (offset < 0 || offset + 8 > value.length) throw new RangeError("truncated uint64"); + const view = new DataView(value.buffer, value.byteOffset + offset, 8); + const big = BigInt(view.getUint32(0, false)) << 32n | BigInt(view.getUint32(4, false)); + if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError("uint64 exceeds JavaScript safe integer range"); + return Number(big); +} +function encodeBase64(value, { padding = false } = {}) { + bytes(value); + let out = ""; + for (let i = 0; i < value.length; i += 3) { + const a = value[i]; + const haveB = i + 1 < value.length; + const haveC = i + 2 < value.length; + const b = haveB ? value[i + 1] : 0; + const c = haveC ? value[i + 2] : 0; + out += BASE64[a >>> 2]; + out += BASE64[(a & 3) << 4 | b >>> 4]; + if (haveB) out += BASE64[(b & 15) << 2 | c >>> 6]; + else if (padding) out += "="; + if (haveC) out += BASE64[c & 63]; + else if (padding) out += "="; + } + return out; +} +function decodeBase64(value, { padding = false } = {}) { + if (typeof value !== "string") return null; + if (padding) { + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) return null; + } else if (!/^[A-Za-z0-9+/]*$/.test(value) || value.length % 4 === 1) return null; + const stripped = padding ? value.replace(/=+$/, "") : value; + const outputLength = Math.floor(stripped.length * 6 / 8); + const out = new Uint8Array(outputLength); + let accumulator = 0; + let bits = 0; + let offset = 0; + for (let i = 0; i < stripped.length; i++) { + const code = stripped.charCodeAt(i); + if (code >= BASE64_VALUE.length || BASE64_VALUE[code] < 0) return null; + accumulator = accumulator << 6 | BASE64_VALUE[code]; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[offset++] = accumulator >>> bits & 255; + } + } + if (bits > 0 && (accumulator & (1 << bits) - 1) !== 0) return null; + if (encodeBase64(out, { padding }) !== value) return null; + return out; +} + +// src/bech32.js +var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +var GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059]; +function polymod(values) { + let checksum = 1; + for (const value of values) { + const top = checksum >>> 25; + checksum = (checksum & 33554431) << 5 ^ value; + for (let i = 0; i < 5; i++) if (top >>> i & 1) checksum ^= GENERATORS[i]; + } + return checksum >>> 0; +} +function expandHrp(hrp) { + const out = []; + for (const char of hrp) out.push(char.charCodeAt(0) >>> 5); + out.push(0); + for (const char of hrp) out.push(char.charCodeAt(0) & 31); + return out; +} +function convertBits(data, from, to, pad) { + let accumulator = 0; + let bits = 0; + const out = []; + const maximum = (1 << to) - 1; + for (const value of data) { + if (value < 0 || value >>> from) return null; + accumulator = accumulator << from | value; + bits += from; + while (bits >= to) { + bits -= to; + out.push(accumulator >>> bits & maximum); + } + } + if (pad && bits) out.push(accumulator << to - bits & maximum); + else if (!pad && (bits >= from || accumulator << to - bits & maximum)) return null; + return out; +} +function encodeBech32(hrp, data) { + const words = convertBits(data, 8, 5, true); + const values = [...expandHrp(hrp), ...words]; + const mod = polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1; + const checksum = []; + for (let i = 0; i < 6; i++) checksum.push(mod >>> 5 * (5 - i) & 31); + return hrp + "1" + [...words, ...checksum].map((word) => CHARSET[word]).join(""); +} +function decodeBech32(value) { + if (typeof value !== "string" || value !== value.toLowerCase() && value !== value.toUpperCase()) return null; + const normalized = value.toLowerCase(); + const separator = normalized.lastIndexOf("1"); + if (separator < 1 || separator + 7 > normalized.length) return null; + const hrp = normalized.slice(0, separator); + const words = []; + for (const char of normalized.slice(separator + 1)) { + const word = CHARSET.indexOf(char); + if (word === -1) return null; + words.push(word); + } + if (polymod([...expandHrp(hrp), ...words]) !== 1) return null; + const data = convertBits(words.slice(0, -6), 5, 8, false); + return data === null ? null : { hrp, data: new Uint8Array(data) }; +} + +// src/age.js +var VERSION_LINE = "age-encryption.org/v1"; +var AGE_X25519_INFO = utf8("age-encryption.org/v1/X25519"); +var AGE_HEADER_INFO = utf8("header"); +var ZERO_NONCE = new Uint8Array(12); +var random = (length) => (0, import_utils.randomBytes)(length); +function parseRecipient(recipient) { + if (typeof recipient !== "string" || recipient !== recipient.toLowerCase()) fail("E_RECIPIENT", "recipient must be canonical lowercase age Bech32"); + const decoded = decodeBech32(recipient); + if (!decoded || decoded.hrp !== "age" || decoded.data.length !== 32 || encodeBech32("age", decoded.data) !== recipient) + fail("E_RECIPIENT", "invalid X25519 age recipient"); + return decoded.data; +} +function parseIdentity(identity) { + if (typeof identity !== "string" || identity !== identity.toUpperCase() && identity !== identity.toLowerCase()) + fail("E_IDENTITY", "identity must use uniform Bech32 case"); + const decoded = decodeBech32(identity); + if (!decoded || decoded.hrp !== "age-secret-key-" || decoded.data.length !== 32 || encodeBech32("age-secret-key-", decoded.data) !== identity.toLowerCase()) + fail("E_IDENTITY", "invalid X25519 age identity"); + return decoded.data; +} +function recipientFromIdentity(identity) { + return encodeBech32("age", import_ed25519.x25519.getPublicKey(parseIdentity(identity))); +} +function formatRecipient(publicKey) { + return encodeBech32("age", bytes(publicKey, 32, "publicKey")); +} +function formatIdentity(secretKey) { + return encodeBech32("age-secret-key-", bytes(secretKey, 32, "secretKey")).toUpperCase(); +} +function publicKeyFromSecretKey(secretKey) { + return import_ed25519.x25519.getPublicKey(bytes(secretKey, 32, "secretKey")); +} +function createRecipientIdentity() { + return createRecipientIdentityFromSecret(random(32)); +} +function createRecipientIdentityFromSecret(secretKey) { + const scalar = bytes(secretKey, 32, "secretKey"); + return { identity: formatIdentity(scalar), recipient: formatRecipient(publicKeyFromSecretKey(scalar)) }; +} +function parseHeader(header) { + bytes(header); + let position = 0; + const line = () => { + const newline = header.indexOf(10, position); + if (newline === -1) fail("E_AGE_HEADER_PARSE", "truncated age header"); + const value = new TextDecoder("utf-8", { fatal: true }).decode(header.subarray(position, newline)); + position = newline + 1; + return value; + }; + let version; + try { + version = line(); + } catch (error) { + if (error?.code) throw error; + fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); + } + if (version !== VERSION_LINE) fail("E_AGE_HEADER_VERSION", "unsupported age header version"); + const stanzas = []; + for (; ; ) { + let current; + try { + current = line(); + } catch (error) { + if (error?.code) throw error; + fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); + } + if (current.startsWith("--- ")) { + const mac = decodeBase64(current.slice(4)); + if (!mac || mac.length !== 32 || position !== header.length) fail("E_AGE_HEADER_PARSE", "invalid age header MAC line"); + const markerEnd = position - current.length - 1 + 3; + return { stanzas, mac, authenticated: header.subarray(0, markerEnd) }; + } + if (!current.startsWith("-> ")) fail("E_AGE_HEADER_PARSE", "invalid age recipient stanza"); + const args = current.slice(3).split(" "); + if (args.some((arg) => arg.length === 0)) fail("E_AGE_HEADER_PARSE", "invalid age recipient arguments"); + const bodyLines = []; + for (; ; ) { + let bodyLine; + try { + bodyLine = line(); + } catch (error) { + if (error?.code) throw error; + fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); + } + if (!/^[A-Za-z0-9+/]*$/.test(bodyLine) || bodyLine.length > 64) fail("E_AGE_HEADER_PARSE", "invalid age stanza body"); + bodyLines.push(bodyLine); + if (bodyLine.length < 64) break; + } + const body = decodeBase64(bodyLines.join("")); + if (!body) fail("E_AGE_HEADER_PARSE", "non-canonical age stanza body"); + stanzas.push({ args, body }); + } +} +function encryptAgeHeader(fileKey, recipients, rng) { + bytes(fileKey, 16, "fileKey"); + if (!Array.isArray(recipients) || recipients.length === 0) fail("E_RECIPIENT", "at least one recipient is required"); + let text = `${VERSION_LINE} +`; + for (const recipient of recipients) { + const recipientPublic = typeof recipient === "string" ? parseRecipient(recipient) : bytes(recipient, 32, "recipient"); + const ephemeralSecret = rng(32); + bytes(ephemeralSecret, 32, "random output"); + let shared; + let wrapKey; + try { + const ephemeralPublic = import_ed25519.x25519.getPublicKey(ephemeralSecret); + try { + shared = import_ed25519.x25519.getSharedSecret(ephemeralSecret, recipientPublic); + } catch { + fail("E_RECIPIENT", "recipient is a low-order X25519 point"); + } + if (allZero(shared)) fail("E_RECIPIENT", "recipient is a low-order X25519 point"); + wrapKey = (0, import_hkdf.hkdf)(import_sha256.sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32); + const wrapped = (0, import_chacha.chacha20poly1305)(wrapKey, ZERO_NONCE).encrypt(fileKey); + text += `-> X25519 ${encodeBase64(ephemeralPublic)} +${encodeBase64(wrapped)} +`; + } finally { + ephemeralSecret.fill(0); + shared?.fill(0); + wrapKey?.fill(0); + } + } + text += "---"; + const authenticated = utf8(text); + const macKey = (0, import_hkdf.hkdf)(import_sha256.sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32); + try { + const mac = (0, import_hmac.hmac)(import_sha256.sha256, macKey, authenticated); + return utf8(`${text} ${encodeBase64(mac)} +`); + } finally { + macKey.fill(0); + } +} +function decryptAgeHeader(header, identity) { + const ownsScalar = typeof identity === "string"; + const scalar = ownsScalar ? parseIdentity(identity) : bytes(identity, 32, "identity"); + let fileKey = null; + let authenticated = false; + try { + const parsed = parseHeader(header); + if (parsed.stanzas.length === 0) fail("E_AGE_HEADER_PARSE", "age header has no recipients"); + const recipientPublic = import_ed25519.x25519.getPublicKey(scalar); + for (const stanza of parsed.stanzas) { + if (stanza.args[0] !== "X25519") fail("E_AGE_HEADER_RECIPIENT", "foreign recipient stanza is not supported"); + if (stanza.args.length !== 2 || stanza.body.length !== 32) fail("E_AGE_HEADER_PARSE", "invalid X25519 stanza"); + const ephemeralPublic = decodeBase64(stanza.args[1]); + if (!ephemeralPublic || ephemeralPublic.length !== 32) fail("E_AGE_HEADER_PARSE", "invalid X25519 ephemeral share"); + } + for (const stanza of parsed.stanzas) { + const ephemeralPublic = decodeBase64(stanza.args[1]); + let shared; + let wrapKey; + try { + try { + shared = import_ed25519.x25519.getSharedSecret(scalar, ephemeralPublic); + } catch { + continue; + } + if (allZero(shared)) continue; + wrapKey = (0, import_hkdf.hkdf)(import_sha256.sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32); + try { + const candidate = (0, import_chacha.chacha20poly1305)(wrapKey, ZERO_NONCE).decrypt(stanza.body); + if (candidate.length === 16) { + fileKey = candidate; + break; + } + candidate.fill(0); + } catch { + } + } finally { + shared?.fill(0); + wrapKey?.fill(0); + } + } + if (!fileKey) fail("E_WRONG_RECIPIENT", "identity does not match this file envelope"); + const macKey = (0, import_hkdf.hkdf)(import_sha256.sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32); + const expectedMac = (0, import_hmac.hmac)(import_sha256.sha256, macKey, parsed.authenticated); + try { + if (!equal(expectedMac, parsed.mac)) fail("E_ENVELOPE_AUTH", "age header authentication failed"); + authenticated = true; + return fileKey; + } finally { + macKey.fill(0); + expectedMac.fill(0); + } + } finally { + if (ownsScalar) scalar.fill(0); + if (!authenticated) fileKey?.fill(0); + } +} +var ageHeader = Object.freeze({ + seal: (fileKey, recipients) => encryptAgeHeader(fileKey, recipients, random), + open: decryptAgeHeader +}); +var ageInternals = Object.freeze({ encryptAgeHeader, decryptAgeHeader }); + +// src/file.js +var import_chacha2 = require("@noble/ciphers/chacha.js"); +var import_hkdf2 = require("@noble/hashes/hkdf.js"); +var import_sha2562 = require("@noble/hashes/sha256.js"); +var import_utils2 = require("@noble/hashes/utils.js"); +var MAGIC = utf8("PKFILE1\n"); +var VERSION = 1; +var FILE_ID_BYTES = 16; +var NONCE_BYTES = 24; +var TAG_BYTES = 16; +var DEFAULT_CHUNK_SIZE = 1024 * 1024; +var MIN_CHUNK_SIZE = 64 * 1024; +var MAX_CHUNK_SIZE = 16 * 1024 * 1024; +var MAX_HEADER_SIZE = 1024 * 1024; +var MAX_AGE_HEADER_SIZE = 256 * 1024; +var MAX_METADATA_CIPHERTEXT_SIZE = 64 * 1024; +var METADATA_KEY_INFO = utf8("pksuite/file/metadata-key/v1"); +var CHUNK_KEY_INFO = utf8("pksuite/file/chunk-key/v1"); +var METADATA_AAD_DOMAIN = utf8("pksuite/file/metadata/v1"); +var CHUNK_AAD_DOMAIN = utf8("pksuite/file/chunk/v1"); +var DOMAIN_SEPARATOR = new Uint8Array([0]); +var encryptionStates = /* @__PURE__ */ new WeakMap(); +var decryptionStates = /* @__PURE__ */ new WeakMap(); +function validateGeometry(plaintextSize, chunkSize) { + if (!Number.isSafeInteger(plaintextSize) || plaintextSize < 0) fail("E_INPUT", "plaintextSize must be a non-negative safe integer"); + if (!Number.isInteger(chunkSize) || chunkSize < MIN_CHUNK_SIZE || chunkSize > MAX_CHUNK_SIZE) + fail("E_INPUT", `chunkSize must be between ${MIN_CHUNK_SIZE} and ${MAX_CHUNK_SIZE} bytes`); + const chunkCount = Math.max(1, Math.ceil(plaintextSize / chunkSize)); + if (chunkCount > 4294967295) fail("E_INPUT", "file requires too many chunks"); + return chunkCount; +} +function expectedPlaintextLength({ plaintextSize, chunkSize, chunkCount }, index) { + if (!Number.isInteger(index) || index < 0 || index >= chunkCount) fail("E_CHUNK_INDEX", "chunk index is outside the envelope"); + if (plaintextSize === 0) return 0; + return index === chunkCount - 1 ? plaintextSize - chunkSize * index : chunkSize; +} +function canonicalJsonString(value, label) { + let result = '"'; + for (let offset = 0; offset < value.length; ) { + const first = value.charCodeAt(offset); + let character; + if (first >= 55296 && first <= 56319) { + const second = value.charCodeAt(offset + 1); + if (!(second >= 56320 && second <= 57343)) fail("E_METADATA", `${label} must contain only Unicode scalar values`); + character = value.slice(offset, offset + 2); + offset += 2; + } else { + if (first >= 56320 && first <= 57343) fail("E_METADATA", `${label} must contain only Unicode scalar values`); + character = value[offset++]; + } + if (character === '"') result += '\\"'; + else if (character === "\\") result += "\\\\"; + else if (first === 8) result += "\\b"; + else if (first === 9) result += "\\t"; + else if (first === 10) result += "\\n"; + else if (first === 12) result += "\\f"; + else if (first === 13) result += "\\r"; + else if (first < 32) result += `\\u${first.toString(16).padStart(4, "0")}`; + else result += character; + } + return `${result}"`; +} +function canonicalMetadata(metadata) { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) fail("E_METADATA", "metadata must be an object"); + if (Object.keys(metadata).some((key) => !["name", "mediaType", "lastModified"].includes(key))) + fail("E_METADATA", "metadata contains an unknown field"); + const { name, mediaType = "application/octet-stream", lastModified = null } = metadata; + if (typeof name !== "string" || name.length === 0 || utf8(name).length > 4096 || /[\u0000-\u001f\u007f]/u.test(name)) + fail("E_METADATA", "metadata.name must be a non-empty filename without control characters"); + if (name === "." || name === ".." || /[\\/:]/u.test(name) || /[ .]$/u.test(name) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(name)) + fail("E_METADATA", "metadata.name must be a portable single filename, not a path or device name"); + if (typeof mediaType !== "string" || utf8(mediaType).length > 255 || !/^[\x20-\x7e]*$/.test(mediaType)) + fail("E_METADATA", "metadata.mediaType must be printable ASCII no longer than 255 bytes"); + if (lastModified !== null && (!Number.isSafeInteger(lastModified) || lastModified < 0)) + fail("E_METADATA", "metadata.lastModified must be null or a non-negative safe integer"); + return utf8(`{"name":${canonicalJsonString(name, "metadata.name")},"mediaType":${canonicalJsonString(mediaType, "metadata.mediaType")},"lastModified":${lastModified === null ? "null" : String(lastModified)}}`); +} +function parseMetadata(plaintext) { + let parsed; + try { + parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext)); + } catch { + fail("E_METADATA", "encrypted metadata is not canonical UTF-8 JSON"); + } + const canonical = canonicalMetadata(parsed); + if (canonical.length !== plaintext.length || canonical.some((byte, index) => byte !== plaintext[index])) + fail("E_METADATA", "encrypted metadata is not canonical"); + return parsed; +} +function structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader: ageHeader2 }) { + return concat(u32(chunkSize), u64(plaintextSize), u32(chunkCount), fileId, u32(ageHeader2.length), ageHeader2); +} +function metadataAad(prefix) { + return concat(METADATA_AAD_DOMAIN, DOMAIN_SEPARATOR, u32(VERSION), (0, import_sha2562.sha256)(concat(MAGIC, prefix))); +} +function chunkAad(record, index) { + const expected = expectedPlaintextLength(record, index); + return concat( + CHUNK_AAD_DOMAIN, + DOMAIN_SEPARATOR, + u32(VERSION), + record.fileId, + record.headerCommitment, + u32(index), + u32(record.chunkCount), + u64(record.plaintextSize), + u32(record.chunkSize), + u32(expected) + ); +} +function deriveMetadataKey(fileKey, fileId) { + return (0, import_hkdf2.hkdf)(import_sha2562.sha256, fileKey, fileId, METADATA_KEY_INFO, 32); +} +function deriveChunkKey(fileKey, fileId, index) { + return (0, import_hkdf2.hkdf)(import_sha2562.sha256, fileKey, concat(fileId, u32(index)), CHUNK_KEY_INFO, 32); +} +function parseEnvelopeHeader(header) { + bytes(header); + if (header.length < MAGIC.length + 4 || MAGIC.some((byte, index) => header[index] !== byte)) + fail("E_ENVELOPE_VERSION", "unsupported file envelope magic or version"); + let bodyLength; + try { + bodyLength = readU32(header, MAGIC.length); + } catch { + fail("E_ENVELOPE_PARSE", "truncated file envelope prelude"); + } + if (bodyLength > MAX_HEADER_SIZE || bodyLength !== header.length - MAGIC.length - 4) + fail("E_ENVELOPE_PARSE", "file envelope header length mismatch"); + let offset = MAGIC.length + 4; + const take = (length, label) => { + if (!Number.isInteger(length) || length < 0 || offset + length > header.length) fail("E_ENVELOPE_PARSE", `truncated ${label}`); + const value = header.subarray(offset, offset + length); + offset += length; + return value; + }; + let chunkSize, plaintextSize, chunkCount, ageHeaderLength, metadataLength; + try { + chunkSize = readU32(header, offset); + offset += 4; + plaintextSize = readU64(header, offset); + offset += 8; + chunkCount = readU32(header, offset); + offset += 4; + } catch { + fail("E_ENVELOPE_PARSE", "invalid file geometry"); + } + const fileId = take(FILE_ID_BYTES, "file id"); + try { + ageHeaderLength = readU32(header, offset); + offset += 4; + } catch { + fail("E_ENVELOPE_PARSE", "truncated age header length"); + } + if (ageHeaderLength === 0 || ageHeaderLength > MAX_AGE_HEADER_SIZE) fail("E_ENVELOPE_PARSE", "invalid age header length"); + const ageHeader2 = take(ageHeaderLength, "age header"); + const prefixEnd = offset; + const metadataNonce = take(NONCE_BYTES, "metadata nonce"); + try { + metadataLength = readU32(header, offset); + offset += 4; + } catch { + fail("E_ENVELOPE_PARSE", "truncated metadata length"); + } + if (metadataLength < TAG_BYTES || metadataLength > MAX_METADATA_CIPHERTEXT_SIZE) fail("E_ENVELOPE_PARSE", "invalid metadata ciphertext length"); + const metadataCiphertext = take(metadataLength, "metadata ciphertext"); + if (offset !== header.length) fail("E_ENVELOPE_PARSE", "trailing bytes in file envelope header"); + const expectedCount = validateGeometry(plaintextSize, chunkSize); + if (chunkCount !== expectedCount) fail("E_ENVELOPE_PARSE", "chunk count does not match file geometry"); + return { + chunkSize, + plaintextSize, + chunkCount, + fileId, + ageHeader: ageHeader2, + prefix: header.subarray(MAGIC.length + 4, prefixEnd), + metadataNonce, + metadataCiphertext, + headerCommitment: (0, import_sha2562.sha256)(header) + }; +} +function makePublicState(kind, record, header, metadata) { + return Object.freeze({ + kind, + version: VERSION, + header, + metadata, + plaintextSize: record.plaintextSize, + chunkSize: record.chunkSize, + chunkCount: record.chunkCount + }); +} +function createFileEncryptionImpl({ recipient, recipients, metadata, plaintextSize, chunkSize = DEFAULT_CHUNK_SIZE }, rng) { + if (recipient !== void 0 && recipients !== void 0) fail("E_INPUT", "provide recipient or recipients, not both"); + const recipientList = recipients ?? (recipient === void 0 ? [] : [recipient]); + const chunkCount = validateGeometry(plaintextSize, chunkSize); + const fileKey = bytes(rng(16), 16, "random file key"); + let retained = false; + try { + const fileId = bytes(rng(FILE_ID_BYTES), FILE_ID_BYTES, "random file id"); + const ageHeader2 = ageInternals.encryptAgeHeader(fileKey, recipientList, rng); + if (ageHeader2.length > MAX_AGE_HEADER_SIZE) fail("E_INPUT", "recipient header is too large"); + const prefix = structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader: ageHeader2 }); + const metadataNonce = bytes(rng(NONCE_BYTES), NONCE_BYTES, "random metadata nonce"); + const metadataPlaintext = canonicalMetadata(metadata); + const metadataKey = deriveMetadataKey(fileKey, fileId); + let metadataCiphertext; + try { + metadataCiphertext = (0, import_chacha2.xchacha20poly1305)(metadataKey, metadataNonce, metadataAad(prefix)).encrypt(metadataPlaintext); + } finally { + metadataKey.fill(0); + metadataPlaintext.fill(0); + } + if (metadataCiphertext.length > MAX_METADATA_CIPHERTEXT_SIZE) fail("E_METADATA", "encrypted metadata is too large"); + const body = concat(prefix, metadataNonce, u32(metadataCiphertext.length), metadataCiphertext); + if (body.length > MAX_HEADER_SIZE) fail("E_INPUT", "file envelope header is too large"); + const header = concat(MAGIC, u32(body.length), body); + const record = { + fileKey, + fileId: new Uint8Array(fileId), + plaintextSize, + chunkSize, + chunkCount, + headerCommitment: (0, import_sha2562.sha256)(header), + rng, + seen: /* @__PURE__ */ new Set() + }; + const state = makePublicState("encryption", record, header, void 0); + encryptionStates.set(state, record); + retained = true; + return state; + } finally { + if (!retained) fileKey.fill(0); + } +} +function createFileEncryption(options) { + return createFileEncryptionImpl(options, import_utils2.randomBytes); +} +function encryptChunk({ state, index, plaintext }) { + const record = encryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-encryption state"); + bytes(plaintext); + const expected = expectedPlaintextLength(record, index); + if (plaintext.length !== expected) fail("E_CHUNK_SIZE", `chunk ${index} must contain exactly ${expected} plaintext bytes`); + const nonce = bytes(record.rng(NONCE_BYTES), NONCE_BYTES, "random chunk nonce"); + const key = deriveChunkKey(record.fileKey, record.fileId, index); + let ciphertext; + try { + ciphertext = (0, import_chacha2.xchacha20poly1305)(key, nonce, chunkAad(record, index)).encrypt(plaintext); + } finally { + key.fill(0); + } + record.seen.add(index); + return concat(nonce, ciphertext); +} +function openFileEnvelope({ identity, header }) { + const parsed = parseEnvelopeHeader(header); + const fileKey = ageInternals.decryptAgeHeader(parsed.ageHeader, identity); + let retained = false; + try { + const metadataKey = deriveMetadataKey(fileKey, parsed.fileId); + let metadataPlaintext; + try { + metadataPlaintext = (0, import_chacha2.xchacha20poly1305)(metadataKey, parsed.metadataNonce, metadataAad(parsed.prefix)).decrypt(parsed.metadataCiphertext); + } catch { + fail("E_ENVELOPE_AUTH", "file metadata authentication failed"); + } finally { + metadataKey.fill(0); + } + let metadata; + try { + metadata = parseMetadata(metadataPlaintext); + } finally { + metadataPlaintext.fill(0); + } + const record = { + fileKey, + fileId: new Uint8Array(parsed.fileId), + plaintextSize: parsed.plaintextSize, + chunkSize: parsed.chunkSize, + chunkCount: parsed.chunkCount, + headerCommitment: new Uint8Array(parsed.headerCommitment), + seen: /* @__PURE__ */ new Set() + }; + const state = makePublicState("decryption", record, new Uint8Array(header), metadata); + decryptionStates.set(state, record); + retained = true; + return state; + } finally { + if (!retained) fileKey.fill(0); + } +} +function decryptChunk({ state, index, ciphertext }) { + const record = decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-decryption state"); + bytes(ciphertext); + const expected = expectedPlaintextLength(record, index); + if (ciphertext.length !== NONCE_BYTES + expected + TAG_BYTES) + fail("E_CHUNK_SIZE", `chunk ${index} ciphertext has the wrong length`); + const nonce = ciphertext.subarray(0, NONCE_BYTES); + const body = ciphertext.subarray(NONCE_BYTES); + const key = deriveChunkKey(record.fileKey, record.fileId, index); + let plaintext; + try { + plaintext = (0, import_chacha2.xchacha20poly1305)(key, nonce, chunkAad(record, index)).decrypt(body); + } catch { + fail("E_CHUNK_AUTH", `chunk ${index} authentication failed`); + } finally { + key.fill(0); + } + record.seen.add(index); + return plaintext; +} +function finalizeEnvelope(state) { + const record = encryptionStates.get(state) ?? decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-envelope state"); + if (record.seen.size !== record.chunkCount) fail("E_ENVELOPE_INCOMPLETE", `expected ${record.chunkCount} authenticated chunks, observed ${record.seen.size}`); + const finalized = Object.freeze({ + version: VERSION, + header: state.header, + metadata: state.metadata, + plaintextSize: record.plaintextSize, + chunkSize: record.chunkSize, + chunkCount: record.chunkCount + }); + disposeEnvelope(state); + return finalized; +} +function disposeEnvelope(state) { + const record = encryptionStates.get(state) ?? decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not an active file-envelope state"); + record.fileKey.fill(0); + record.seen.clear(); + encryptionStates.delete(state); + decryptionStates.delete(state); +} +var fileInternals = Object.freeze({ + createFileEncryptionWithRandom: createFileEncryptionImpl, + parseEnvelopeHeader, + metadataAad, + chunkAad, + constants: Object.freeze({ MAGIC, VERSION, NONCE_BYTES, TAG_BYTES, DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE }) +}); +// Annotate the CommonJS export names for ESM import in node: +0 && (module.exports = { + PkCryptoError, + ageHeader, + createFileEncryption, + createRecipientIdentity, + createRecipientIdentityFromSecret, + decryptChunk, + disposeEnvelope, + encryptChunk, + finalizeEnvelope, + formatIdentity, + formatRecipient, + openFileEnvelope, + parseIdentity, + parseRecipient, + publicKeyFromSecretKey, + recipientFromIdentity +}); +//# sourceMappingURL=index.cjs.map diff --git a/packages/crypto/dist/index.cjs.map b/packages/crypto/dist/index.cjs.map new file mode 100644 index 0000000..499ce1e --- /dev/null +++ b/packages/crypto/dist/index.cjs.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/index.js", "../src/error.js", "../src/age.js", "../src/bytes.js", "../src/bech32.js", "../src/file.js"], + "sourcesContent": ["export { PkCryptoError } from \"./error.js\";\nexport {\n ageHeader,\n createRecipientIdentity,\n createRecipientIdentityFromSecret,\n formatIdentity,\n formatRecipient,\n parseIdentity,\n parseRecipient,\n publicKeyFromSecretKey,\n recipientFromIdentity,\n} from \"./age.js\";\nexport {\n createFileEncryption,\n decryptChunk,\n disposeEnvelope,\n encryptChunk,\n finalizeEnvelope,\n openFileEnvelope,\n} from \"./file.js\";\n", "export class PkCryptoError extends Error {\n constructor(code, message) {\n super(`${code}: ${message}`);\n this.name = \"PkCryptoError\";\n this.code = code;\n }\n}\n\nexport const fail = (code, message) => {\n throw new PkCryptoError(code, message);\n};\n", "import { chacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { x25519 } from \"@noble/curves/ed25519.js\";\nimport { hkdf } from \"@noble/hashes/hkdf.js\";\nimport { hmac } from \"@noble/hashes/hmac.js\";\nimport { sha256 } from \"@noble/hashes/sha256.js\";\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport { allZero, bytes, concat, decodeBase64, EMPTY, encodeBase64, equal, utf8 } from \"./bytes.js\";\nimport { decodeBech32, encodeBech32 } from \"./bech32.js\";\nimport { fail } from \"./error.js\";\n\nconst VERSION_LINE = \"age-encryption.org/v1\";\nconst AGE_X25519_INFO = utf8(\"age-encryption.org/v1/X25519\");\nconst AGE_HEADER_INFO = utf8(\"header\");\nconst ZERO_NONCE = new Uint8Array(12);\n\nconst random = (length) => randomBytes(length);\n\nexport function parseRecipient(recipient) {\n if (typeof recipient !== \"string\" || recipient !== recipient.toLowerCase()) fail(\"E_RECIPIENT\", \"recipient must be canonical lowercase age Bech32\");\n const decoded = decodeBech32(recipient);\n if (!decoded || decoded.hrp !== \"age\" || decoded.data.length !== 32 || encodeBech32(\"age\", decoded.data) !== recipient)\n fail(\"E_RECIPIENT\", \"invalid X25519 age recipient\");\n return decoded.data;\n}\n\nexport function parseIdentity(identity) {\n if (typeof identity !== \"string\" || (identity !== identity.toUpperCase() && identity !== identity.toLowerCase()))\n fail(\"E_IDENTITY\", \"identity must use uniform Bech32 case\");\n const decoded = decodeBech32(identity);\n if (!decoded || decoded.hrp !== \"age-secret-key-\" || decoded.data.length !== 32 || encodeBech32(\"age-secret-key-\", decoded.data) !== identity.toLowerCase())\n fail(\"E_IDENTITY\", \"invalid X25519 age identity\");\n return decoded.data;\n}\n\nexport function recipientFromIdentity(identity) {\n return encodeBech32(\"age\", x25519.getPublicKey(parseIdentity(identity)));\n}\n\nexport function formatRecipient(publicKey) {\n return encodeBech32(\"age\", bytes(publicKey, 32, \"publicKey\"));\n}\n\nexport function formatIdentity(secretKey) {\n return encodeBech32(\"age-secret-key-\", bytes(secretKey, 32, \"secretKey\")).toUpperCase();\n}\n\nexport function publicKeyFromSecretKey(secretKey) {\n return x25519.getPublicKey(bytes(secretKey, 32, \"secretKey\"));\n}\n\nexport function createRecipientIdentity() {\n return createRecipientIdentityFromSecret(random(32));\n}\n\nexport function createRecipientIdentityFromSecret(secretKey) {\n const scalar = bytes(secretKey, 32, \"secretKey\");\n return { identity: formatIdentity(scalar), recipient: formatRecipient(publicKeyFromSecretKey(scalar)) };\n}\n\nfunction parseHeader(header) {\n bytes(header);\n let position = 0;\n const line = () => {\n const newline = header.indexOf(0x0a, position);\n if (newline === -1) fail(\"E_AGE_HEADER_PARSE\", \"truncated age header\");\n const value = new TextDecoder(\"utf-8\", { fatal: true }).decode(header.subarray(position, newline));\n position = newline + 1;\n return value;\n };\n\n let version;\n try { version = line(); }\n catch (error) { if (error?.code) throw error; fail(\"E_AGE_HEADER_PARSE\", \"age header is not valid UTF-8\"); }\n if (version !== VERSION_LINE) fail(\"E_AGE_HEADER_VERSION\", \"unsupported age header version\");\n\n const stanzas = [];\n for (;;) {\n let current;\n try { current = line(); }\n catch (error) { if (error?.code) throw error; fail(\"E_AGE_HEADER_PARSE\", \"age header is not valid UTF-8\"); }\n if (current.startsWith(\"--- \")) {\n const mac = decodeBase64(current.slice(4));\n if (!mac || mac.length !== 32 || position !== header.length) fail(\"E_AGE_HEADER_PARSE\", \"invalid age header MAC line\");\n const markerEnd = position - current.length - 1 + 3;\n return { stanzas, mac, authenticated: header.subarray(0, markerEnd) };\n }\n if (!current.startsWith(\"-> \")) fail(\"E_AGE_HEADER_PARSE\", \"invalid age recipient stanza\");\n const args = current.slice(3).split(\" \");\n if (args.some((arg) => arg.length === 0)) fail(\"E_AGE_HEADER_PARSE\", \"invalid age recipient arguments\");\n const bodyLines = [];\n for (;;) {\n let bodyLine;\n try { bodyLine = line(); }\n catch (error) { if (error?.code) throw error; fail(\"E_AGE_HEADER_PARSE\", \"age header is not valid UTF-8\"); }\n if (!/^[A-Za-z0-9+/]*$/.test(bodyLine) || bodyLine.length > 64) fail(\"E_AGE_HEADER_PARSE\", \"invalid age stanza body\");\n bodyLines.push(bodyLine);\n if (bodyLine.length < 64) break;\n }\n const body = decodeBase64(bodyLines.join(\"\"));\n if (!body) fail(\"E_AGE_HEADER_PARSE\", \"non-canonical age stanza body\");\n stanzas.push({ args, body });\n }\n}\n\nfunction encryptAgeHeader(fileKey, recipients, rng) {\n bytes(fileKey, 16, \"fileKey\");\n if (!Array.isArray(recipients) || recipients.length === 0) fail(\"E_RECIPIENT\", \"at least one recipient is required\");\n let text = `${VERSION_LINE}\\n`;\n for (const recipient of recipients) {\n const recipientPublic = typeof recipient === \"string\" ? parseRecipient(recipient) : bytes(recipient, 32, \"recipient\");\n const ephemeralSecret = rng(32);\n bytes(ephemeralSecret, 32, \"random output\");\n let shared;\n let wrapKey;\n try {\n const ephemeralPublic = x25519.getPublicKey(ephemeralSecret);\n try { shared = x25519.getSharedSecret(ephemeralSecret, recipientPublic); }\n catch { fail(\"E_RECIPIENT\", \"recipient is a low-order X25519 point\"); }\n if (allZero(shared)) fail(\"E_RECIPIENT\", \"recipient is a low-order X25519 point\");\n wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32);\n const wrapped = chacha20poly1305(wrapKey, ZERO_NONCE).encrypt(fileKey);\n text += `-> X25519 ${encodeBase64(ephemeralPublic)}\\n${encodeBase64(wrapped)}\\n`;\n } finally {\n ephemeralSecret.fill(0);\n shared?.fill(0);\n wrapKey?.fill(0);\n }\n }\n text += \"---\";\n const authenticated = utf8(text);\n const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32);\n try {\n const mac = hmac(sha256, macKey, authenticated);\n return utf8(`${text} ${encodeBase64(mac)}\\n`);\n } finally { macKey.fill(0); }\n}\n\nfunction decryptAgeHeader(header, identity) {\n const ownsScalar = typeof identity === \"string\";\n const scalar = ownsScalar ? parseIdentity(identity) : bytes(identity, 32, \"identity\");\n let fileKey = null;\n let authenticated = false;\n try {\n const parsed = parseHeader(header);\n if (parsed.stanzas.length === 0) fail(\"E_AGE_HEADER_PARSE\", \"age header has no recipients\");\n const recipientPublic = x25519.getPublicKey(scalar);\n for (const stanza of parsed.stanzas) {\n if (stanza.args[0] !== \"X25519\") fail(\"E_AGE_HEADER_RECIPIENT\", \"foreign recipient stanza is not supported\");\n if (stanza.args.length !== 2 || stanza.body.length !== 32) fail(\"E_AGE_HEADER_PARSE\", \"invalid X25519 stanza\");\n const ephemeralPublic = decodeBase64(stanza.args[1]);\n if (!ephemeralPublic || ephemeralPublic.length !== 32) fail(\"E_AGE_HEADER_PARSE\", \"invalid X25519 ephemeral share\");\n }\n for (const stanza of parsed.stanzas) {\n const ephemeralPublic = decodeBase64(stanza.args[1]);\n let shared;\n let wrapKey;\n try {\n try { shared = x25519.getSharedSecret(scalar, ephemeralPublic); }\n catch { continue; }\n if (allZero(shared)) continue;\n wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32);\n try {\n const candidate = chacha20poly1305(wrapKey, ZERO_NONCE).decrypt(stanza.body);\n if (candidate.length === 16) { fileKey = candidate; break; }\n candidate.fill(0);\n } catch { /* stanza is for another identity */ }\n } finally {\n shared?.fill(0);\n wrapKey?.fill(0);\n }\n }\n if (!fileKey) fail(\"E_WRONG_RECIPIENT\", \"identity does not match this file envelope\");\n const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32);\n const expectedMac = hmac(sha256, macKey, parsed.authenticated);\n try {\n if (!equal(expectedMac, parsed.mac)) fail(\"E_ENVELOPE_AUTH\", \"age header authentication failed\");\n authenticated = true;\n return fileKey;\n } finally {\n macKey.fill(0);\n expectedMac.fill(0);\n }\n } finally {\n if (ownsScalar) scalar.fill(0);\n if (!authenticated) fileKey?.fill(0);\n }\n}\n\nexport const ageHeader = Object.freeze({\n seal: (fileKey, recipients) => encryptAgeHeader(fileKey, recipients, random),\n open: decryptAgeHeader,\n});\n\nexport const ageInternals = Object.freeze({ encryptAgeHeader, decryptAgeHeader });\n", "const BASE64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\nconst BASE64_VALUE = new Int16Array(128).fill(-1);\nfor (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;\n\nexport const EMPTY = new Uint8Array(0);\nexport const utf8 = (value) => new TextEncoder().encode(value);\n\nexport function bytes(value, length, name = \"value\") {\n if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Uint8Array`);\n if (length !== undefined && value.length !== length) throw new TypeError(`${name} must be ${length} bytes`);\n return value;\n}\n\nexport function concat(...parts) {\n const length = parts.reduce((sum, part) => sum + bytes(part).length, 0);\n const out = new Uint8Array(length);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nexport function equal(a, b) {\n if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array) || a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];\n return diff === 0;\n}\n\nexport function allZero(value) {\n let acc = 0;\n for (const byte of value) acc |= byte;\n return acc === 0;\n}\n\nexport function u32(value) {\n if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) throw new RangeError(\"value is not a uint32\");\n const out = new Uint8Array(4);\n new DataView(out.buffer).setUint32(0, value, false);\n return out;\n}\n\nexport function u64(value) {\n if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(\"value is not a non-negative safe integer\");\n const out = new Uint8Array(8);\n const view = new DataView(out.buffer);\n const big = BigInt(value);\n view.setUint32(0, Number(big >> 32n), false);\n view.setUint32(4, Number(big & 0xffffffffn), false);\n return out;\n}\n\nexport function readU32(value, offset) {\n if (offset < 0 || offset + 4 > value.length) throw new RangeError(\"truncated uint32\");\n return new DataView(value.buffer, value.byteOffset + offset, 4).getUint32(0, false);\n}\n\nexport function readU64(value, offset) {\n if (offset < 0 || offset + 8 > value.length) throw new RangeError(\"truncated uint64\");\n const view = new DataView(value.buffer, value.byteOffset + offset, 8);\n const big = (BigInt(view.getUint32(0, false)) << 32n) | BigInt(view.getUint32(4, false));\n if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError(\"uint64 exceeds JavaScript safe integer range\");\n return Number(big);\n}\n\nexport function encodeBase64(value, { padding = false } = {}) {\n bytes(value);\n let out = \"\";\n for (let i = 0; i < value.length; i += 3) {\n const a = value[i];\n const haveB = i + 1 < value.length;\n const haveC = i + 2 < value.length;\n const b = haveB ? value[i + 1] : 0;\n const c = haveC ? value[i + 2] : 0;\n out += BASE64[a >>> 2];\n out += BASE64[((a & 3) << 4) | (b >>> 4)];\n if (haveB) out += BASE64[((b & 15) << 2) | (c >>> 6)];\n else if (padding) out += \"=\";\n if (haveC) out += BASE64[c & 63];\n else if (padding) out += \"=\";\n }\n return out;\n}\n\nexport function decodeBase64(value, { padding = false } = {}) {\n if (typeof value !== \"string\") return null;\n if (padding) {\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) return null;\n } else if (!/^[A-Za-z0-9+/]*$/.test(value) || value.length % 4 === 1) return null;\n\n const stripped = padding ? value.replace(/=+$/, \"\") : value;\n const outputLength = Math.floor((stripped.length * 6) / 8);\n const out = new Uint8Array(outputLength);\n let accumulator = 0;\n let bits = 0;\n let offset = 0;\n for (let i = 0; i < stripped.length; i++) {\n const code = stripped.charCodeAt(i);\n if (code >= BASE64_VALUE.length || BASE64_VALUE[code] < 0) return null;\n accumulator = (accumulator << 6) | BASE64_VALUE[code];\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[offset++] = (accumulator >>> bits) & 0xff;\n }\n }\n if (bits > 0 && (accumulator & ((1 << bits) - 1)) !== 0) return null;\n if (encodeBase64(out, { padding }) !== value) return null;\n return out;\n}\n", "const CHARSET = \"qpzry9x8gf2tvdw0s3jn54khce6mua7l\";\nconst GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];\n\nfunction polymod(values) {\n let checksum = 1;\n for (const value of values) {\n const top = checksum >>> 25;\n checksum = ((checksum & 0x1ffffff) << 5) ^ value;\n for (let i = 0; i < 5; i++) if ((top >>> i) & 1) checksum ^= GENERATORS[i];\n }\n return checksum >>> 0;\n}\n\nfunction expandHrp(hrp) {\n const out = [];\n for (const char of hrp) out.push(char.charCodeAt(0) >>> 5);\n out.push(0);\n for (const char of hrp) out.push(char.charCodeAt(0) & 31);\n return out;\n}\n\nfunction convertBits(data, from, to, pad) {\n let accumulator = 0;\n let bits = 0;\n const out = [];\n const maximum = (1 << to) - 1;\n for (const value of data) {\n if (value < 0 || value >>> from) return null;\n accumulator = (accumulator << from) | value;\n bits += from;\n while (bits >= to) {\n bits -= to;\n out.push((accumulator >>> bits) & maximum);\n }\n }\n if (pad && bits) out.push((accumulator << (to - bits)) & maximum);\n else if (!pad && (bits >= from || ((accumulator << (to - bits)) & maximum))) return null;\n return out;\n}\n\nexport function encodeBech32(hrp, data) {\n const words = convertBits(data, 8, 5, true);\n const values = [...expandHrp(hrp), ...words];\n const mod = polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1;\n const checksum = [];\n for (let i = 0; i < 6; i++) checksum.push((mod >>> (5 * (5 - i))) & 31);\n return hrp + \"1\" + [...words, ...checksum].map((word) => CHARSET[word]).join(\"\");\n}\n\nexport function decodeBech32(value) {\n if (typeof value !== \"string\" || (value !== value.toLowerCase() && value !== value.toUpperCase())) return null;\n const normalized = value.toLowerCase();\n const separator = normalized.lastIndexOf(\"1\");\n if (separator < 1 || separator + 7 > normalized.length) return null;\n const hrp = normalized.slice(0, separator);\n const words = [];\n for (const char of normalized.slice(separator + 1)) {\n const word = CHARSET.indexOf(char);\n if (word === -1) return null;\n words.push(word);\n }\n if (polymod([...expandHrp(hrp), ...words]) !== 1) return null;\n const data = convertBits(words.slice(0, -6), 5, 8, false);\n return data === null ? null : { hrp, data: new Uint8Array(data) };\n}\n", "import { xchacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { hkdf } from \"@noble/hashes/hkdf.js\";\nimport { sha256 } from \"@noble/hashes/sha256.js\";\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport { ageInternals } from \"./age.js\";\nimport { bytes, concat, readU32, readU64, u32, u64, utf8 } from \"./bytes.js\";\nimport { fail } from \"./error.js\";\n\nconst MAGIC = utf8(\"PKFILE1\\n\");\nconst VERSION = 1;\nconst FILE_ID_BYTES = 16;\nconst NONCE_BYTES = 24;\nconst TAG_BYTES = 16;\nconst DEFAULT_CHUNK_SIZE = 1024 * 1024;\nconst MIN_CHUNK_SIZE = 64 * 1024;\nconst MAX_CHUNK_SIZE = 16 * 1024 * 1024;\nconst MAX_HEADER_SIZE = 1024 * 1024;\nconst MAX_AGE_HEADER_SIZE = 256 * 1024;\nconst MAX_METADATA_CIPHERTEXT_SIZE = 64 * 1024;\n\nconst METADATA_KEY_INFO = utf8(\"pksuite/file/metadata-key/v1\");\nconst CHUNK_KEY_INFO = utf8(\"pksuite/file/chunk-key/v1\");\nconst METADATA_AAD_DOMAIN = utf8(\"pksuite/file/metadata/v1\");\nconst CHUNK_AAD_DOMAIN = utf8(\"pksuite/file/chunk/v1\");\nconst DOMAIN_SEPARATOR = new Uint8Array([0]);\n\nconst encryptionStates = new WeakMap();\nconst decryptionStates = new WeakMap();\n\nfunction validateGeometry(plaintextSize, chunkSize) {\n if (!Number.isSafeInteger(plaintextSize) || plaintextSize < 0) fail(\"E_INPUT\", \"plaintextSize must be a non-negative safe integer\");\n if (!Number.isInteger(chunkSize) || chunkSize < MIN_CHUNK_SIZE || chunkSize > MAX_CHUNK_SIZE)\n fail(\"E_INPUT\", `chunkSize must be between ${MIN_CHUNK_SIZE} and ${MAX_CHUNK_SIZE} bytes`);\n const chunkCount = Math.max(1, Math.ceil(plaintextSize / chunkSize));\n if (chunkCount > 0xffffffff) fail(\"E_INPUT\", \"file requires too many chunks\");\n return chunkCount;\n}\n\nfunction expectedPlaintextLength({ plaintextSize, chunkSize, chunkCount }, index) {\n if (!Number.isInteger(index) || index < 0 || index >= chunkCount) fail(\"E_CHUNK_INDEX\", \"chunk index is outside the envelope\");\n if (plaintextSize === 0) return 0;\n return index === chunkCount - 1 ? plaintextSize - chunkSize * index : chunkSize;\n}\n\nfunction canonicalJsonString(value, label) {\n let result = '\"';\n for (let offset = 0; offset < value.length;) {\n const first = value.charCodeAt(offset);\n let character;\n if (first >= 0xd800 && first <= 0xdbff) {\n const second = value.charCodeAt(offset + 1);\n if (!(second >= 0xdc00 && second <= 0xdfff)) fail(\"E_METADATA\", `${label} must contain only Unicode scalar values`);\n character = value.slice(offset, offset + 2);\n offset += 2;\n } else {\n if (first >= 0xdc00 && first <= 0xdfff) fail(\"E_METADATA\", `${label} must contain only Unicode scalar values`);\n character = value[offset++];\n }\n if (character === '\"') result += '\\\\\"';\n else if (character === \"\\\\\") result += \"\\\\\\\\\";\n else if (first === 0x08) result += \"\\\\b\";\n else if (first === 0x09) result += \"\\\\t\";\n else if (first === 0x0a) result += \"\\\\n\";\n else if (first === 0x0c) result += \"\\\\f\";\n else if (first === 0x0d) result += \"\\\\r\";\n else if (first < 0x20) result += `\\\\u${first.toString(16).padStart(4, \"0\")}`;\n else result += character;\n }\n return `${result}\"`;\n}\n\nfunction canonicalMetadata(metadata) {\n if (!metadata || typeof metadata !== \"object\" || Array.isArray(metadata)) fail(\"E_METADATA\", \"metadata must be an object\");\n if (Object.keys(metadata).some((key) => ![\"name\", \"mediaType\", \"lastModified\"].includes(key)))\n fail(\"E_METADATA\", \"metadata contains an unknown field\");\n const { name, mediaType = \"application/octet-stream\", lastModified = null } = metadata;\n if (typeof name !== \"string\" || name.length === 0 || utf8(name).length > 4096 || /[\\u0000-\\u001f\\u007f]/u.test(name))\n fail(\"E_METADATA\", \"metadata.name must be a non-empty filename without control characters\");\n if (name === \".\" || name === \"..\" || /[\\\\/:]/u.test(name) || /[ .]$/u.test(name) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\.|$)/iu.test(name))\n fail(\"E_METADATA\", \"metadata.name must be a portable single filename, not a path or device name\");\n if (typeof mediaType !== \"string\" || utf8(mediaType).length > 255 || !/^[\\x20-\\x7e]*$/.test(mediaType))\n fail(\"E_METADATA\", \"metadata.mediaType must be printable ASCII no longer than 255 bytes\");\n if (lastModified !== null && (!Number.isSafeInteger(lastModified) || lastModified < 0))\n fail(\"E_METADATA\", \"metadata.lastModified must be null or a non-negative safe integer\");\n return utf8(`{\"name\":${canonicalJsonString(name, \"metadata.name\")},\"mediaType\":${canonicalJsonString(mediaType, \"metadata.mediaType\")},\"lastModified\":${lastModified === null ? \"null\" : String(lastModified)}}`);\n}\n\nfunction parseMetadata(plaintext) {\n let parsed;\n try { parsed = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(plaintext)); }\n catch { fail(\"E_METADATA\", \"encrypted metadata is not canonical UTF-8 JSON\"); }\n const canonical = canonicalMetadata(parsed);\n if (canonical.length !== plaintext.length || canonical.some((byte, index) => byte !== plaintext[index]))\n fail(\"E_METADATA\", \"encrypted metadata is not canonical\");\n return parsed;\n}\n\nfunction structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader }) {\n return concat(u32(chunkSize), u64(plaintextSize), u32(chunkCount), fileId, u32(ageHeader.length), ageHeader);\n}\n\nfunction metadataAad(prefix) {\n return concat(METADATA_AAD_DOMAIN, DOMAIN_SEPARATOR, u32(VERSION), sha256(concat(MAGIC, prefix)));\n}\n\nfunction chunkAad(record, index) {\n const expected = expectedPlaintextLength(record, index);\n return concat(\n CHUNK_AAD_DOMAIN,\n DOMAIN_SEPARATOR,\n u32(VERSION),\n record.fileId,\n record.headerCommitment,\n u32(index),\n u32(record.chunkCount),\n u64(record.plaintextSize),\n u32(record.chunkSize),\n u32(expected),\n );\n}\n\nfunction deriveMetadataKey(fileKey, fileId) {\n return hkdf(sha256, fileKey, fileId, METADATA_KEY_INFO, 32);\n}\n\nfunction deriveChunkKey(fileKey, fileId, index) {\n return hkdf(sha256, fileKey, concat(fileId, u32(index)), CHUNK_KEY_INFO, 32);\n}\n\nfunction parseEnvelopeHeader(header) {\n bytes(header);\n if (header.length < MAGIC.length + 4 || MAGIC.some((byte, index) => header[index] !== byte))\n fail(\"E_ENVELOPE_VERSION\", \"unsupported file envelope magic or version\");\n let bodyLength;\n try { bodyLength = readU32(header, MAGIC.length); }\n catch { fail(\"E_ENVELOPE_PARSE\", \"truncated file envelope prelude\"); }\n if (bodyLength > MAX_HEADER_SIZE || bodyLength !== header.length - MAGIC.length - 4)\n fail(\"E_ENVELOPE_PARSE\", \"file envelope header length mismatch\");\n let offset = MAGIC.length + 4;\n const take = (length, label) => {\n if (!Number.isInteger(length) || length < 0 || offset + length > header.length) fail(\"E_ENVELOPE_PARSE\", `truncated ${label}`);\n const value = header.subarray(offset, offset + length);\n offset += length;\n return value;\n };\n\n let chunkSize, plaintextSize, chunkCount, ageHeaderLength, metadataLength;\n try {\n chunkSize = readU32(header, offset); offset += 4;\n plaintextSize = readU64(header, offset); offset += 8;\n chunkCount = readU32(header, offset); offset += 4;\n } catch { fail(\"E_ENVELOPE_PARSE\", \"invalid file geometry\"); }\n const fileId = take(FILE_ID_BYTES, \"file id\");\n try { ageHeaderLength = readU32(header, offset); offset += 4; }\n catch { fail(\"E_ENVELOPE_PARSE\", \"truncated age header length\"); }\n if (ageHeaderLength === 0 || ageHeaderLength > MAX_AGE_HEADER_SIZE) fail(\"E_ENVELOPE_PARSE\", \"invalid age header length\");\n const ageHeader = take(ageHeaderLength, \"age header\");\n const prefixEnd = offset;\n const metadataNonce = take(NONCE_BYTES, \"metadata nonce\");\n try { metadataLength = readU32(header, offset); offset += 4; }\n catch { fail(\"E_ENVELOPE_PARSE\", \"truncated metadata length\"); }\n if (metadataLength < TAG_BYTES || metadataLength > MAX_METADATA_CIPHERTEXT_SIZE) fail(\"E_ENVELOPE_PARSE\", \"invalid metadata ciphertext length\");\n const metadataCiphertext = take(metadataLength, \"metadata ciphertext\");\n if (offset !== header.length) fail(\"E_ENVELOPE_PARSE\", \"trailing bytes in file envelope header\");\n\n const expectedCount = validateGeometry(plaintextSize, chunkSize);\n if (chunkCount !== expectedCount) fail(\"E_ENVELOPE_PARSE\", \"chunk count does not match file geometry\");\n return {\n chunkSize,\n plaintextSize,\n chunkCount,\n fileId,\n ageHeader,\n prefix: header.subarray(MAGIC.length + 4, prefixEnd),\n metadataNonce,\n metadataCiphertext,\n headerCommitment: sha256(header),\n };\n}\n\nfunction makePublicState(kind, record, header, metadata) {\n return Object.freeze({\n kind,\n version: VERSION,\n header,\n metadata,\n plaintextSize: record.plaintextSize,\n chunkSize: record.chunkSize,\n chunkCount: record.chunkCount,\n });\n}\n\nfunction createFileEncryptionImpl({ recipient, recipients, metadata, plaintextSize, chunkSize = DEFAULT_CHUNK_SIZE }, rng) {\n if (recipient !== undefined && recipients !== undefined) fail(\"E_INPUT\", \"provide recipient or recipients, not both\");\n const recipientList = recipients ?? (recipient === undefined ? [] : [recipient]);\n const chunkCount = validateGeometry(plaintextSize, chunkSize);\n const fileKey = bytes(rng(16), 16, \"random file key\");\n let retained = false;\n try {\n const fileId = bytes(rng(FILE_ID_BYTES), FILE_ID_BYTES, \"random file id\");\n const ageHeader = ageInternals.encryptAgeHeader(fileKey, recipientList, rng);\n if (ageHeader.length > MAX_AGE_HEADER_SIZE) fail(\"E_INPUT\", \"recipient header is too large\");\n const prefix = structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader });\n const metadataNonce = bytes(rng(NONCE_BYTES), NONCE_BYTES, \"random metadata nonce\");\n const metadataPlaintext = canonicalMetadata(metadata);\n const metadataKey = deriveMetadataKey(fileKey, fileId);\n let metadataCiphertext;\n try { metadataCiphertext = xchacha20poly1305(metadataKey, metadataNonce, metadataAad(prefix)).encrypt(metadataPlaintext); }\n finally { metadataKey.fill(0); metadataPlaintext.fill(0); }\n if (metadataCiphertext.length > MAX_METADATA_CIPHERTEXT_SIZE) fail(\"E_METADATA\", \"encrypted metadata is too large\");\n const body = concat(prefix, metadataNonce, u32(metadataCiphertext.length), metadataCiphertext);\n if (body.length > MAX_HEADER_SIZE) fail(\"E_INPUT\", \"file envelope header is too large\");\n const header = concat(MAGIC, u32(body.length), body);\n const record = {\n fileKey,\n fileId: new Uint8Array(fileId),\n plaintextSize,\n chunkSize,\n chunkCount,\n headerCommitment: sha256(header),\n rng,\n seen: new Set(),\n };\n const state = makePublicState(\"encryption\", record, header, undefined);\n encryptionStates.set(state, record);\n retained = true;\n return state;\n } finally {\n if (!retained) fileKey.fill(0);\n }\n}\n\nexport function createFileEncryption(options) {\n return createFileEncryptionImpl(options, randomBytes);\n}\n\nexport function encryptChunk({ state, index, plaintext }) {\n const record = encryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not a file-encryption state\");\n bytes(plaintext);\n const expected = expectedPlaintextLength(record, index);\n if (plaintext.length !== expected) fail(\"E_CHUNK_SIZE\", `chunk ${index} must contain exactly ${expected} plaintext bytes`);\n const nonce = bytes(record.rng(NONCE_BYTES), NONCE_BYTES, \"random chunk nonce\");\n const key = deriveChunkKey(record.fileKey, record.fileId, index);\n let ciphertext;\n try { ciphertext = xchacha20poly1305(key, nonce, chunkAad(record, index)).encrypt(plaintext); }\n finally { key.fill(0); }\n record.seen.add(index);\n return concat(nonce, ciphertext);\n}\n\nexport function openFileEnvelope({ identity, header }) {\n const parsed = parseEnvelopeHeader(header);\n const fileKey = ageInternals.decryptAgeHeader(parsed.ageHeader, identity);\n let retained = false;\n try {\n const metadataKey = deriveMetadataKey(fileKey, parsed.fileId);\n let metadataPlaintext;\n try { metadataPlaintext = xchacha20poly1305(metadataKey, parsed.metadataNonce, metadataAad(parsed.prefix)).decrypt(parsed.metadataCiphertext); }\n catch { fail(\"E_ENVELOPE_AUTH\", \"file metadata authentication failed\"); }\n finally { metadataKey.fill(0); }\n let metadata;\n try { metadata = parseMetadata(metadataPlaintext); }\n finally { metadataPlaintext.fill(0); }\n const record = {\n fileKey,\n fileId: new Uint8Array(parsed.fileId),\n plaintextSize: parsed.plaintextSize,\n chunkSize: parsed.chunkSize,\n chunkCount: parsed.chunkCount,\n headerCommitment: new Uint8Array(parsed.headerCommitment),\n seen: new Set(),\n };\n const state = makePublicState(\"decryption\", record, new Uint8Array(header), metadata);\n decryptionStates.set(state, record);\n retained = true;\n return state;\n } finally {\n if (!retained) fileKey.fill(0);\n }\n}\n\nexport function decryptChunk({ state, index, ciphertext }) {\n const record = decryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not a file-decryption state\");\n bytes(ciphertext);\n const expected = expectedPlaintextLength(record, index);\n if (ciphertext.length !== NONCE_BYTES + expected + TAG_BYTES)\n fail(\"E_CHUNK_SIZE\", `chunk ${index} ciphertext has the wrong length`);\n const nonce = ciphertext.subarray(0, NONCE_BYTES);\n const body = ciphertext.subarray(NONCE_BYTES);\n const key = deriveChunkKey(record.fileKey, record.fileId, index);\n let plaintext;\n try { plaintext = xchacha20poly1305(key, nonce, chunkAad(record, index)).decrypt(body); }\n catch { fail(\"E_CHUNK_AUTH\", `chunk ${index} authentication failed`); }\n finally { key.fill(0); }\n record.seen.add(index);\n return plaintext;\n}\n\nexport function finalizeEnvelope(state) {\n const record = encryptionStates.get(state) ?? decryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not a file-envelope state\");\n if (record.seen.size !== record.chunkCount) fail(\"E_ENVELOPE_INCOMPLETE\", `expected ${record.chunkCount} authenticated chunks, observed ${record.seen.size}`);\n const finalized = Object.freeze({\n version: VERSION,\n header: state.header,\n metadata: state.metadata,\n plaintextSize: record.plaintextSize,\n chunkSize: record.chunkSize,\n chunkCount: record.chunkCount,\n });\n disposeEnvelope(state);\n return finalized;\n}\n\nexport function disposeEnvelope(state) {\n const record = encryptionStates.get(state) ?? decryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not an active file-envelope state\");\n record.fileKey.fill(0);\n record.seen.clear();\n encryptionStates.delete(state);\n decryptionStates.delete(state);\n}\n\nexport const fileInternals = Object.freeze({\n createFileEncryptionWithRandom: createFileEncryptionImpl,\n parseEnvelopeHeader,\n metadataAad,\n chunkAad,\n constants: Object.freeze({ MAGIC, VERSION, NONCE_BYTES, TAG_BYTES, DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE }),\n});\n"], + "mappings": "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", + "names": ["import_chacha", "import_hkdf", "import_sha256", "import_utils", "ageHeader"] +} diff --git a/packages/crypto/dist/index.d.ts b/packages/crypto/dist/index.d.ts new file mode 100644 index 0000000..1645cdf --- /dev/null +++ b/packages/crypto/dist/index.d.ts @@ -0,0 +1,78 @@ +export class PkCryptoError extends Error { + readonly code: string; +} + +export interface RecipientIdentity { + identity: string; + recipient: string; +} + +export interface FileMetadata { + name: string; + mediaType?: string; + lastModified?: number | null; +} + +export interface FileEnvelopeState { + readonly kind: "encryption" | "decryption"; + readonly version: 1; + readonly header: Uint8Array; + readonly metadata?: Required; + readonly plaintextSize: number; + readonly chunkSize: number; + readonly chunkCount: number; +} + +export interface FinalizedEnvelope { + readonly version: 1; + readonly header: Uint8Array; + readonly metadata?: Required; + readonly plaintextSize: number; + readonly chunkSize: number; + readonly chunkCount: number; +} + +export function createRecipientIdentity(): RecipientIdentity; +export function createRecipientIdentityFromSecret(secretKey: Uint8Array): RecipientIdentity; +export function formatIdentity(secretKey: Uint8Array): string; +export function formatRecipient(publicKey: Uint8Array): string; +export function parseIdentity(identity: string): Uint8Array; +export function parseRecipient(recipient: string): Uint8Array; +export function publicKeyFromSecretKey(secretKey: Uint8Array): Uint8Array; +export function recipientFromIdentity(identity: string): string; + +export const ageHeader: Readonly<{ + seal(fileKey: Uint8Array, recipients: Array): Uint8Array; + /** The caller owns the returned secret key bytes and should overwrite them after use. */ + open(header: Uint8Array, identity: string | Uint8Array): Uint8Array; +}>; + +export function createFileEncryption(options: { + recipient?: string; + recipients?: string[]; + metadata: FileMetadata; + plaintextSize: number; + chunkSize?: number; +}): FileEnvelopeState; + +export function encryptChunk(options: { + state: FileEnvelopeState; + index: number; + plaintext: Uint8Array; +}): Uint8Array; + +export function openFileEnvelope(options: { + identity: string | Uint8Array; + header: Uint8Array; +}): FileEnvelopeState & { readonly kind: "decryption"; readonly metadata: Required }; + +export function decryptChunk(options: { + state: FileEnvelopeState; + index: number; + ciphertext: Uint8Array; +}): Uint8Array; + +/** Best-effort zeroization for an incomplete or abandoned envelope state. */ +export function disposeEnvelope(state: FileEnvelopeState): void; + +export function finalizeEnvelope(state: FileEnvelopeState): FinalizedEnvelope; diff --git a/packages/crypto/dist/index.js b/packages/crypto/dist/index.js new file mode 100644 index 0000000..f3980e9 --- /dev/null +++ b/packages/crypto/dist/index.js @@ -0,0 +1,743 @@ +// src/error.js +var PkCryptoError = class extends Error { + constructor(code, message) { + super(`${code}: ${message}`); + this.name = "PkCryptoError"; + this.code = code; + } +}; +var fail = (code, message) => { + throw new PkCryptoError(code, message); +}; + +// src/age.js +import { chacha20poly1305 } from "@noble/ciphers/chacha.js"; +import { x25519 } from "@noble/curves/ed25519.js"; +import { hkdf } from "@noble/hashes/hkdf.js"; +import { hmac } from "@noble/hashes/hmac.js"; +import { sha256 } from "@noble/hashes/sha256.js"; +import { randomBytes } from "@noble/hashes/utils.js"; + +// src/bytes.js +var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var BASE64_VALUE = new Int16Array(128).fill(-1); +for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i; +var EMPTY = new Uint8Array(0); +var utf8 = (value) => new TextEncoder().encode(value); +function bytes(value, length, name = "value") { + if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Uint8Array`); + if (length !== void 0 && value.length !== length) throw new TypeError(`${name} must be ${length} bytes`); + return value; +} +function concat(...parts) { + const length = parts.reduce((sum, part) => sum + bytes(part).length, 0); + const out = new Uint8Array(length); + let offset = 0; + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } + return out; +} +function equal(a, b) { + if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array) || a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; + return diff === 0; +} +function allZero(value) { + let acc = 0; + for (const byte of value) acc |= byte; + return acc === 0; +} +function u32(value) { + if (!Number.isInteger(value) || value < 0 || value > 4294967295) throw new RangeError("value is not a uint32"); + const out = new Uint8Array(4); + new DataView(out.buffer).setUint32(0, value, false); + return out; +} +function u64(value) { + if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("value is not a non-negative safe integer"); + const out = new Uint8Array(8); + const view = new DataView(out.buffer); + const big = BigInt(value); + view.setUint32(0, Number(big >> 32n), false); + view.setUint32(4, Number(big & 0xffffffffn), false); + return out; +} +function readU32(value, offset) { + if (offset < 0 || offset + 4 > value.length) throw new RangeError("truncated uint32"); + return new DataView(value.buffer, value.byteOffset + offset, 4).getUint32(0, false); +} +function readU64(value, offset) { + if (offset < 0 || offset + 8 > value.length) throw new RangeError("truncated uint64"); + const view = new DataView(value.buffer, value.byteOffset + offset, 8); + const big = BigInt(view.getUint32(0, false)) << 32n | BigInt(view.getUint32(4, false)); + if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError("uint64 exceeds JavaScript safe integer range"); + return Number(big); +} +function encodeBase64(value, { padding = false } = {}) { + bytes(value); + let out = ""; + for (let i = 0; i < value.length; i += 3) { + const a = value[i]; + const haveB = i + 1 < value.length; + const haveC = i + 2 < value.length; + const b = haveB ? value[i + 1] : 0; + const c = haveC ? value[i + 2] : 0; + out += BASE64[a >>> 2]; + out += BASE64[(a & 3) << 4 | b >>> 4]; + if (haveB) out += BASE64[(b & 15) << 2 | c >>> 6]; + else if (padding) out += "="; + if (haveC) out += BASE64[c & 63]; + else if (padding) out += "="; + } + return out; +} +function decodeBase64(value, { padding = false } = {}) { + if (typeof value !== "string") return null; + if (padding) { + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) return null; + } else if (!/^[A-Za-z0-9+/]*$/.test(value) || value.length % 4 === 1) return null; + const stripped = padding ? value.replace(/=+$/, "") : value; + const outputLength = Math.floor(stripped.length * 6 / 8); + const out = new Uint8Array(outputLength); + let accumulator = 0; + let bits = 0; + let offset = 0; + for (let i = 0; i < stripped.length; i++) { + const code = stripped.charCodeAt(i); + if (code >= BASE64_VALUE.length || BASE64_VALUE[code] < 0) return null; + accumulator = accumulator << 6 | BASE64_VALUE[code]; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[offset++] = accumulator >>> bits & 255; + } + } + if (bits > 0 && (accumulator & (1 << bits) - 1) !== 0) return null; + if (encodeBase64(out, { padding }) !== value) return null; + return out; +} + +// src/bech32.js +var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +var GENERATORS = [996825010, 642813549, 513874426, 1027748829, 705979059]; +function polymod(values) { + let checksum = 1; + for (const value of values) { + const top = checksum >>> 25; + checksum = (checksum & 33554431) << 5 ^ value; + for (let i = 0; i < 5; i++) if (top >>> i & 1) checksum ^= GENERATORS[i]; + } + return checksum >>> 0; +} +function expandHrp(hrp) { + const out = []; + for (const char of hrp) out.push(char.charCodeAt(0) >>> 5); + out.push(0); + for (const char of hrp) out.push(char.charCodeAt(0) & 31); + return out; +} +function convertBits(data, from, to, pad) { + let accumulator = 0; + let bits = 0; + const out = []; + const maximum = (1 << to) - 1; + for (const value of data) { + if (value < 0 || value >>> from) return null; + accumulator = accumulator << from | value; + bits += from; + while (bits >= to) { + bits -= to; + out.push(accumulator >>> bits & maximum); + } + } + if (pad && bits) out.push(accumulator << to - bits & maximum); + else if (!pad && (bits >= from || accumulator << to - bits & maximum)) return null; + return out; +} +function encodeBech32(hrp, data) { + const words = convertBits(data, 8, 5, true); + const values = [...expandHrp(hrp), ...words]; + const mod = polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1; + const checksum = []; + for (let i = 0; i < 6; i++) checksum.push(mod >>> 5 * (5 - i) & 31); + return hrp + "1" + [...words, ...checksum].map((word) => CHARSET[word]).join(""); +} +function decodeBech32(value) { + if (typeof value !== "string" || value !== value.toLowerCase() && value !== value.toUpperCase()) return null; + const normalized = value.toLowerCase(); + const separator = normalized.lastIndexOf("1"); + if (separator < 1 || separator + 7 > normalized.length) return null; + const hrp = normalized.slice(0, separator); + const words = []; + for (const char of normalized.slice(separator + 1)) { + const word = CHARSET.indexOf(char); + if (word === -1) return null; + words.push(word); + } + if (polymod([...expandHrp(hrp), ...words]) !== 1) return null; + const data = convertBits(words.slice(0, -6), 5, 8, false); + return data === null ? null : { hrp, data: new Uint8Array(data) }; +} + +// src/age.js +var VERSION_LINE = "age-encryption.org/v1"; +var AGE_X25519_INFO = utf8("age-encryption.org/v1/X25519"); +var AGE_HEADER_INFO = utf8("header"); +var ZERO_NONCE = new Uint8Array(12); +var random = (length) => randomBytes(length); +function parseRecipient(recipient) { + if (typeof recipient !== "string" || recipient !== recipient.toLowerCase()) fail("E_RECIPIENT", "recipient must be canonical lowercase age Bech32"); + const decoded = decodeBech32(recipient); + if (!decoded || decoded.hrp !== "age" || decoded.data.length !== 32 || encodeBech32("age", decoded.data) !== recipient) + fail("E_RECIPIENT", "invalid X25519 age recipient"); + return decoded.data; +} +function parseIdentity(identity) { + if (typeof identity !== "string" || identity !== identity.toUpperCase() && identity !== identity.toLowerCase()) + fail("E_IDENTITY", "identity must use uniform Bech32 case"); + const decoded = decodeBech32(identity); + if (!decoded || decoded.hrp !== "age-secret-key-" || decoded.data.length !== 32 || encodeBech32("age-secret-key-", decoded.data) !== identity.toLowerCase()) + fail("E_IDENTITY", "invalid X25519 age identity"); + return decoded.data; +} +function recipientFromIdentity(identity) { + return encodeBech32("age", x25519.getPublicKey(parseIdentity(identity))); +} +function formatRecipient(publicKey) { + return encodeBech32("age", bytes(publicKey, 32, "publicKey")); +} +function formatIdentity(secretKey) { + return encodeBech32("age-secret-key-", bytes(secretKey, 32, "secretKey")).toUpperCase(); +} +function publicKeyFromSecretKey(secretKey) { + return x25519.getPublicKey(bytes(secretKey, 32, "secretKey")); +} +function createRecipientIdentity() { + return createRecipientIdentityFromSecret(random(32)); +} +function createRecipientIdentityFromSecret(secretKey) { + const scalar = bytes(secretKey, 32, "secretKey"); + return { identity: formatIdentity(scalar), recipient: formatRecipient(publicKeyFromSecretKey(scalar)) }; +} +function parseHeader(header) { + bytes(header); + let position = 0; + const line = () => { + const newline = header.indexOf(10, position); + if (newline === -1) fail("E_AGE_HEADER_PARSE", "truncated age header"); + const value = new TextDecoder("utf-8", { fatal: true }).decode(header.subarray(position, newline)); + position = newline + 1; + return value; + }; + let version; + try { + version = line(); + } catch (error) { + if (error?.code) throw error; + fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); + } + if (version !== VERSION_LINE) fail("E_AGE_HEADER_VERSION", "unsupported age header version"); + const stanzas = []; + for (; ; ) { + let current; + try { + current = line(); + } catch (error) { + if (error?.code) throw error; + fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); + } + if (current.startsWith("--- ")) { + const mac = decodeBase64(current.slice(4)); + if (!mac || mac.length !== 32 || position !== header.length) fail("E_AGE_HEADER_PARSE", "invalid age header MAC line"); + const markerEnd = position - current.length - 1 + 3; + return { stanzas, mac, authenticated: header.subarray(0, markerEnd) }; + } + if (!current.startsWith("-> ")) fail("E_AGE_HEADER_PARSE", "invalid age recipient stanza"); + const args = current.slice(3).split(" "); + if (args.some((arg) => arg.length === 0)) fail("E_AGE_HEADER_PARSE", "invalid age recipient arguments"); + const bodyLines = []; + for (; ; ) { + let bodyLine; + try { + bodyLine = line(); + } catch (error) { + if (error?.code) throw error; + fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); + } + if (!/^[A-Za-z0-9+/]*$/.test(bodyLine) || bodyLine.length > 64) fail("E_AGE_HEADER_PARSE", "invalid age stanza body"); + bodyLines.push(bodyLine); + if (bodyLine.length < 64) break; + } + const body = decodeBase64(bodyLines.join("")); + if (!body) fail("E_AGE_HEADER_PARSE", "non-canonical age stanza body"); + stanzas.push({ args, body }); + } +} +function encryptAgeHeader(fileKey, recipients, rng) { + bytes(fileKey, 16, "fileKey"); + if (!Array.isArray(recipients) || recipients.length === 0) fail("E_RECIPIENT", "at least one recipient is required"); + let text = `${VERSION_LINE} +`; + for (const recipient of recipients) { + const recipientPublic = typeof recipient === "string" ? parseRecipient(recipient) : bytes(recipient, 32, "recipient"); + const ephemeralSecret = rng(32); + bytes(ephemeralSecret, 32, "random output"); + let shared; + let wrapKey; + try { + const ephemeralPublic = x25519.getPublicKey(ephemeralSecret); + try { + shared = x25519.getSharedSecret(ephemeralSecret, recipientPublic); + } catch { + fail("E_RECIPIENT", "recipient is a low-order X25519 point"); + } + if (allZero(shared)) fail("E_RECIPIENT", "recipient is a low-order X25519 point"); + wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32); + const wrapped = chacha20poly1305(wrapKey, ZERO_NONCE).encrypt(fileKey); + text += `-> X25519 ${encodeBase64(ephemeralPublic)} +${encodeBase64(wrapped)} +`; + } finally { + ephemeralSecret.fill(0); + shared?.fill(0); + wrapKey?.fill(0); + } + } + text += "---"; + const authenticated = utf8(text); + const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32); + try { + const mac = hmac(sha256, macKey, authenticated); + return utf8(`${text} ${encodeBase64(mac)} +`); + } finally { + macKey.fill(0); + } +} +function decryptAgeHeader(header, identity) { + const ownsScalar = typeof identity === "string"; + const scalar = ownsScalar ? parseIdentity(identity) : bytes(identity, 32, "identity"); + let fileKey = null; + let authenticated = false; + try { + const parsed = parseHeader(header); + if (parsed.stanzas.length === 0) fail("E_AGE_HEADER_PARSE", "age header has no recipients"); + const recipientPublic = x25519.getPublicKey(scalar); + for (const stanza of parsed.stanzas) { + if (stanza.args[0] !== "X25519") fail("E_AGE_HEADER_RECIPIENT", "foreign recipient stanza is not supported"); + if (stanza.args.length !== 2 || stanza.body.length !== 32) fail("E_AGE_HEADER_PARSE", "invalid X25519 stanza"); + const ephemeralPublic = decodeBase64(stanza.args[1]); + if (!ephemeralPublic || ephemeralPublic.length !== 32) fail("E_AGE_HEADER_PARSE", "invalid X25519 ephemeral share"); + } + for (const stanza of parsed.stanzas) { + const ephemeralPublic = decodeBase64(stanza.args[1]); + let shared; + let wrapKey; + try { + try { + shared = x25519.getSharedSecret(scalar, ephemeralPublic); + } catch { + continue; + } + if (allZero(shared)) continue; + wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32); + try { + const candidate = chacha20poly1305(wrapKey, ZERO_NONCE).decrypt(stanza.body); + if (candidate.length === 16) { + fileKey = candidate; + break; + } + candidate.fill(0); + } catch { + } + } finally { + shared?.fill(0); + wrapKey?.fill(0); + } + } + if (!fileKey) fail("E_WRONG_RECIPIENT", "identity does not match this file envelope"); + const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32); + const expectedMac = hmac(sha256, macKey, parsed.authenticated); + try { + if (!equal(expectedMac, parsed.mac)) fail("E_ENVELOPE_AUTH", "age header authentication failed"); + authenticated = true; + return fileKey; + } finally { + macKey.fill(0); + expectedMac.fill(0); + } + } finally { + if (ownsScalar) scalar.fill(0); + if (!authenticated) fileKey?.fill(0); + } +} +var ageHeader = Object.freeze({ + seal: (fileKey, recipients) => encryptAgeHeader(fileKey, recipients, random), + open: decryptAgeHeader +}); +var ageInternals = Object.freeze({ encryptAgeHeader, decryptAgeHeader }); + +// src/file.js +import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"; +import { hkdf as hkdf2 } from "@noble/hashes/hkdf.js"; +import { sha256 as sha2562 } from "@noble/hashes/sha256.js"; +import { randomBytes as randomBytes2 } from "@noble/hashes/utils.js"; +var MAGIC = utf8("PKFILE1\n"); +var VERSION = 1; +var FILE_ID_BYTES = 16; +var NONCE_BYTES = 24; +var TAG_BYTES = 16; +var DEFAULT_CHUNK_SIZE = 1024 * 1024; +var MIN_CHUNK_SIZE = 64 * 1024; +var MAX_CHUNK_SIZE = 16 * 1024 * 1024; +var MAX_HEADER_SIZE = 1024 * 1024; +var MAX_AGE_HEADER_SIZE = 256 * 1024; +var MAX_METADATA_CIPHERTEXT_SIZE = 64 * 1024; +var METADATA_KEY_INFO = utf8("pksuite/file/metadata-key/v1"); +var CHUNK_KEY_INFO = utf8("pksuite/file/chunk-key/v1"); +var METADATA_AAD_DOMAIN = utf8("pksuite/file/metadata/v1"); +var CHUNK_AAD_DOMAIN = utf8("pksuite/file/chunk/v1"); +var DOMAIN_SEPARATOR = new Uint8Array([0]); +var encryptionStates = /* @__PURE__ */ new WeakMap(); +var decryptionStates = /* @__PURE__ */ new WeakMap(); +function validateGeometry(plaintextSize, chunkSize) { + if (!Number.isSafeInteger(plaintextSize) || plaintextSize < 0) fail("E_INPUT", "plaintextSize must be a non-negative safe integer"); + if (!Number.isInteger(chunkSize) || chunkSize < MIN_CHUNK_SIZE || chunkSize > MAX_CHUNK_SIZE) + fail("E_INPUT", `chunkSize must be between ${MIN_CHUNK_SIZE} and ${MAX_CHUNK_SIZE} bytes`); + const chunkCount = Math.max(1, Math.ceil(plaintextSize / chunkSize)); + if (chunkCount > 4294967295) fail("E_INPUT", "file requires too many chunks"); + return chunkCount; +} +function expectedPlaintextLength({ plaintextSize, chunkSize, chunkCount }, index) { + if (!Number.isInteger(index) || index < 0 || index >= chunkCount) fail("E_CHUNK_INDEX", "chunk index is outside the envelope"); + if (plaintextSize === 0) return 0; + return index === chunkCount - 1 ? plaintextSize - chunkSize * index : chunkSize; +} +function canonicalJsonString(value, label) { + let result = '"'; + for (let offset = 0; offset < value.length; ) { + const first = value.charCodeAt(offset); + let character; + if (first >= 55296 && first <= 56319) { + const second = value.charCodeAt(offset + 1); + if (!(second >= 56320 && second <= 57343)) fail("E_METADATA", `${label} must contain only Unicode scalar values`); + character = value.slice(offset, offset + 2); + offset += 2; + } else { + if (first >= 56320 && first <= 57343) fail("E_METADATA", `${label} must contain only Unicode scalar values`); + character = value[offset++]; + } + if (character === '"') result += '\\"'; + else if (character === "\\") result += "\\\\"; + else if (first === 8) result += "\\b"; + else if (first === 9) result += "\\t"; + else if (first === 10) result += "\\n"; + else if (first === 12) result += "\\f"; + else if (first === 13) result += "\\r"; + else if (first < 32) result += `\\u${first.toString(16).padStart(4, "0")}`; + else result += character; + } + return `${result}"`; +} +function canonicalMetadata(metadata) { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) fail("E_METADATA", "metadata must be an object"); + if (Object.keys(metadata).some((key) => !["name", "mediaType", "lastModified"].includes(key))) + fail("E_METADATA", "metadata contains an unknown field"); + const { name, mediaType = "application/octet-stream", lastModified = null } = metadata; + if (typeof name !== "string" || name.length === 0 || utf8(name).length > 4096 || /[\u0000-\u001f\u007f]/u.test(name)) + fail("E_METADATA", "metadata.name must be a non-empty filename without control characters"); + if (name === "." || name === ".." || /[\\/:]/u.test(name) || /[ .]$/u.test(name) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(name)) + fail("E_METADATA", "metadata.name must be a portable single filename, not a path or device name"); + if (typeof mediaType !== "string" || utf8(mediaType).length > 255 || !/^[\x20-\x7e]*$/.test(mediaType)) + fail("E_METADATA", "metadata.mediaType must be printable ASCII no longer than 255 bytes"); + if (lastModified !== null && (!Number.isSafeInteger(lastModified) || lastModified < 0)) + fail("E_METADATA", "metadata.lastModified must be null or a non-negative safe integer"); + return utf8(`{"name":${canonicalJsonString(name, "metadata.name")},"mediaType":${canonicalJsonString(mediaType, "metadata.mediaType")},"lastModified":${lastModified === null ? "null" : String(lastModified)}}`); +} +function parseMetadata(plaintext) { + let parsed; + try { + parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext)); + } catch { + fail("E_METADATA", "encrypted metadata is not canonical UTF-8 JSON"); + } + const canonical = canonicalMetadata(parsed); + if (canonical.length !== plaintext.length || canonical.some((byte, index) => byte !== plaintext[index])) + fail("E_METADATA", "encrypted metadata is not canonical"); + return parsed; +} +function structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader: ageHeader2 }) { + return concat(u32(chunkSize), u64(plaintextSize), u32(chunkCount), fileId, u32(ageHeader2.length), ageHeader2); +} +function metadataAad(prefix) { + return concat(METADATA_AAD_DOMAIN, DOMAIN_SEPARATOR, u32(VERSION), sha2562(concat(MAGIC, prefix))); +} +function chunkAad(record, index) { + const expected = expectedPlaintextLength(record, index); + return concat( + CHUNK_AAD_DOMAIN, + DOMAIN_SEPARATOR, + u32(VERSION), + record.fileId, + record.headerCommitment, + u32(index), + u32(record.chunkCount), + u64(record.plaintextSize), + u32(record.chunkSize), + u32(expected) + ); +} +function deriveMetadataKey(fileKey, fileId) { + return hkdf2(sha2562, fileKey, fileId, METADATA_KEY_INFO, 32); +} +function deriveChunkKey(fileKey, fileId, index) { + return hkdf2(sha2562, fileKey, concat(fileId, u32(index)), CHUNK_KEY_INFO, 32); +} +function parseEnvelopeHeader(header) { + bytes(header); + if (header.length < MAGIC.length + 4 || MAGIC.some((byte, index) => header[index] !== byte)) + fail("E_ENVELOPE_VERSION", "unsupported file envelope magic or version"); + let bodyLength; + try { + bodyLength = readU32(header, MAGIC.length); + } catch { + fail("E_ENVELOPE_PARSE", "truncated file envelope prelude"); + } + if (bodyLength > MAX_HEADER_SIZE || bodyLength !== header.length - MAGIC.length - 4) + fail("E_ENVELOPE_PARSE", "file envelope header length mismatch"); + let offset = MAGIC.length + 4; + const take = (length, label) => { + if (!Number.isInteger(length) || length < 0 || offset + length > header.length) fail("E_ENVELOPE_PARSE", `truncated ${label}`); + const value = header.subarray(offset, offset + length); + offset += length; + return value; + }; + let chunkSize, plaintextSize, chunkCount, ageHeaderLength, metadataLength; + try { + chunkSize = readU32(header, offset); + offset += 4; + plaintextSize = readU64(header, offset); + offset += 8; + chunkCount = readU32(header, offset); + offset += 4; + } catch { + fail("E_ENVELOPE_PARSE", "invalid file geometry"); + } + const fileId = take(FILE_ID_BYTES, "file id"); + try { + ageHeaderLength = readU32(header, offset); + offset += 4; + } catch { + fail("E_ENVELOPE_PARSE", "truncated age header length"); + } + if (ageHeaderLength === 0 || ageHeaderLength > MAX_AGE_HEADER_SIZE) fail("E_ENVELOPE_PARSE", "invalid age header length"); + const ageHeader2 = take(ageHeaderLength, "age header"); + const prefixEnd = offset; + const metadataNonce = take(NONCE_BYTES, "metadata nonce"); + try { + metadataLength = readU32(header, offset); + offset += 4; + } catch { + fail("E_ENVELOPE_PARSE", "truncated metadata length"); + } + if (metadataLength < TAG_BYTES || metadataLength > MAX_METADATA_CIPHERTEXT_SIZE) fail("E_ENVELOPE_PARSE", "invalid metadata ciphertext length"); + const metadataCiphertext = take(metadataLength, "metadata ciphertext"); + if (offset !== header.length) fail("E_ENVELOPE_PARSE", "trailing bytes in file envelope header"); + const expectedCount = validateGeometry(plaintextSize, chunkSize); + if (chunkCount !== expectedCount) fail("E_ENVELOPE_PARSE", "chunk count does not match file geometry"); + return { + chunkSize, + plaintextSize, + chunkCount, + fileId, + ageHeader: ageHeader2, + prefix: header.subarray(MAGIC.length + 4, prefixEnd), + metadataNonce, + metadataCiphertext, + headerCommitment: sha2562(header) + }; +} +function makePublicState(kind, record, header, metadata) { + return Object.freeze({ + kind, + version: VERSION, + header, + metadata, + plaintextSize: record.plaintextSize, + chunkSize: record.chunkSize, + chunkCount: record.chunkCount + }); +} +function createFileEncryptionImpl({ recipient, recipients, metadata, plaintextSize, chunkSize = DEFAULT_CHUNK_SIZE }, rng) { + if (recipient !== void 0 && recipients !== void 0) fail("E_INPUT", "provide recipient or recipients, not both"); + const recipientList = recipients ?? (recipient === void 0 ? [] : [recipient]); + const chunkCount = validateGeometry(plaintextSize, chunkSize); + const fileKey = bytes(rng(16), 16, "random file key"); + let retained = false; + try { + const fileId = bytes(rng(FILE_ID_BYTES), FILE_ID_BYTES, "random file id"); + const ageHeader2 = ageInternals.encryptAgeHeader(fileKey, recipientList, rng); + if (ageHeader2.length > MAX_AGE_HEADER_SIZE) fail("E_INPUT", "recipient header is too large"); + const prefix = structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader: ageHeader2 }); + const metadataNonce = bytes(rng(NONCE_BYTES), NONCE_BYTES, "random metadata nonce"); + const metadataPlaintext = canonicalMetadata(metadata); + const metadataKey = deriveMetadataKey(fileKey, fileId); + let metadataCiphertext; + try { + metadataCiphertext = xchacha20poly1305(metadataKey, metadataNonce, metadataAad(prefix)).encrypt(metadataPlaintext); + } finally { + metadataKey.fill(0); + metadataPlaintext.fill(0); + } + if (metadataCiphertext.length > MAX_METADATA_CIPHERTEXT_SIZE) fail("E_METADATA", "encrypted metadata is too large"); + const body = concat(prefix, metadataNonce, u32(metadataCiphertext.length), metadataCiphertext); + if (body.length > MAX_HEADER_SIZE) fail("E_INPUT", "file envelope header is too large"); + const header = concat(MAGIC, u32(body.length), body); + const record = { + fileKey, + fileId: new Uint8Array(fileId), + plaintextSize, + chunkSize, + chunkCount, + headerCommitment: sha2562(header), + rng, + seen: /* @__PURE__ */ new Set() + }; + const state = makePublicState("encryption", record, header, void 0); + encryptionStates.set(state, record); + retained = true; + return state; + } finally { + if (!retained) fileKey.fill(0); + } +} +function createFileEncryption(options) { + return createFileEncryptionImpl(options, randomBytes2); +} +function encryptChunk({ state, index, plaintext }) { + const record = encryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-encryption state"); + bytes(plaintext); + const expected = expectedPlaintextLength(record, index); + if (plaintext.length !== expected) fail("E_CHUNK_SIZE", `chunk ${index} must contain exactly ${expected} plaintext bytes`); + const nonce = bytes(record.rng(NONCE_BYTES), NONCE_BYTES, "random chunk nonce"); + const key = deriveChunkKey(record.fileKey, record.fileId, index); + let ciphertext; + try { + ciphertext = xchacha20poly1305(key, nonce, chunkAad(record, index)).encrypt(plaintext); + } finally { + key.fill(0); + } + record.seen.add(index); + return concat(nonce, ciphertext); +} +function openFileEnvelope({ identity, header }) { + const parsed = parseEnvelopeHeader(header); + const fileKey = ageInternals.decryptAgeHeader(parsed.ageHeader, identity); + let retained = false; + try { + const metadataKey = deriveMetadataKey(fileKey, parsed.fileId); + let metadataPlaintext; + try { + metadataPlaintext = xchacha20poly1305(metadataKey, parsed.metadataNonce, metadataAad(parsed.prefix)).decrypt(parsed.metadataCiphertext); + } catch { + fail("E_ENVELOPE_AUTH", "file metadata authentication failed"); + } finally { + metadataKey.fill(0); + } + let metadata; + try { + metadata = parseMetadata(metadataPlaintext); + } finally { + metadataPlaintext.fill(0); + } + const record = { + fileKey, + fileId: new Uint8Array(parsed.fileId), + plaintextSize: parsed.plaintextSize, + chunkSize: parsed.chunkSize, + chunkCount: parsed.chunkCount, + headerCommitment: new Uint8Array(parsed.headerCommitment), + seen: /* @__PURE__ */ new Set() + }; + const state = makePublicState("decryption", record, new Uint8Array(header), metadata); + decryptionStates.set(state, record); + retained = true; + return state; + } finally { + if (!retained) fileKey.fill(0); + } +} +function decryptChunk({ state, index, ciphertext }) { + const record = decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-decryption state"); + bytes(ciphertext); + const expected = expectedPlaintextLength(record, index); + if (ciphertext.length !== NONCE_BYTES + expected + TAG_BYTES) + fail("E_CHUNK_SIZE", `chunk ${index} ciphertext has the wrong length`); + const nonce = ciphertext.subarray(0, NONCE_BYTES); + const body = ciphertext.subarray(NONCE_BYTES); + const key = deriveChunkKey(record.fileKey, record.fileId, index); + let plaintext; + try { + plaintext = xchacha20poly1305(key, nonce, chunkAad(record, index)).decrypt(body); + } catch { + fail("E_CHUNK_AUTH", `chunk ${index} authentication failed`); + } finally { + key.fill(0); + } + record.seen.add(index); + return plaintext; +} +function finalizeEnvelope(state) { + const record = encryptionStates.get(state) ?? decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-envelope state"); + if (record.seen.size !== record.chunkCount) fail("E_ENVELOPE_INCOMPLETE", `expected ${record.chunkCount} authenticated chunks, observed ${record.seen.size}`); + const finalized = Object.freeze({ + version: VERSION, + header: state.header, + metadata: state.metadata, + plaintextSize: record.plaintextSize, + chunkSize: record.chunkSize, + chunkCount: record.chunkCount + }); + disposeEnvelope(state); + return finalized; +} +function disposeEnvelope(state) { + const record = encryptionStates.get(state) ?? decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not an active file-envelope state"); + record.fileKey.fill(0); + record.seen.clear(); + encryptionStates.delete(state); + decryptionStates.delete(state); +} +var fileInternals = Object.freeze({ + createFileEncryptionWithRandom: createFileEncryptionImpl, + parseEnvelopeHeader, + metadataAad, + chunkAad, + constants: Object.freeze({ MAGIC, VERSION, NONCE_BYTES, TAG_BYTES, DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE }) +}); +export { + PkCryptoError, + ageHeader, + createFileEncryption, + createRecipientIdentity, + createRecipientIdentityFromSecret, + decryptChunk, + disposeEnvelope, + encryptChunk, + finalizeEnvelope, + formatIdentity, + formatRecipient, + openFileEnvelope, + parseIdentity, + parseRecipient, + publicKeyFromSecretKey, + recipientFromIdentity +}; +//# sourceMappingURL=index.js.map diff --git a/packages/crypto/dist/index.js.map b/packages/crypto/dist/index.js.map new file mode 100644 index 0000000..52ac919 --- /dev/null +++ b/packages/crypto/dist/index.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["../src/error.js", "../src/age.js", "../src/bytes.js", "../src/bech32.js", "../src/file.js"], + "sourcesContent": ["export class PkCryptoError extends Error {\n constructor(code, message) {\n super(`${code}: ${message}`);\n this.name = \"PkCryptoError\";\n this.code = code;\n }\n}\n\nexport const fail = (code, message) => {\n throw new PkCryptoError(code, message);\n};\n", "import { chacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { x25519 } from \"@noble/curves/ed25519.js\";\nimport { hkdf } from \"@noble/hashes/hkdf.js\";\nimport { hmac } from \"@noble/hashes/hmac.js\";\nimport { sha256 } from \"@noble/hashes/sha256.js\";\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport { allZero, bytes, concat, decodeBase64, EMPTY, encodeBase64, equal, utf8 } from \"./bytes.js\";\nimport { decodeBech32, encodeBech32 } from \"./bech32.js\";\nimport { fail } from \"./error.js\";\n\nconst VERSION_LINE = \"age-encryption.org/v1\";\nconst AGE_X25519_INFO = utf8(\"age-encryption.org/v1/X25519\");\nconst AGE_HEADER_INFO = utf8(\"header\");\nconst ZERO_NONCE = new Uint8Array(12);\n\nconst random = (length) => randomBytes(length);\n\nexport function parseRecipient(recipient) {\n if (typeof recipient !== \"string\" || recipient !== recipient.toLowerCase()) fail(\"E_RECIPIENT\", \"recipient must be canonical lowercase age Bech32\");\n const decoded = decodeBech32(recipient);\n if (!decoded || decoded.hrp !== \"age\" || decoded.data.length !== 32 || encodeBech32(\"age\", decoded.data) !== recipient)\n fail(\"E_RECIPIENT\", \"invalid X25519 age recipient\");\n return decoded.data;\n}\n\nexport function parseIdentity(identity) {\n if (typeof identity !== \"string\" || (identity !== identity.toUpperCase() && identity !== identity.toLowerCase()))\n fail(\"E_IDENTITY\", \"identity must use uniform Bech32 case\");\n const decoded = decodeBech32(identity);\n if (!decoded || decoded.hrp !== \"age-secret-key-\" || decoded.data.length !== 32 || encodeBech32(\"age-secret-key-\", decoded.data) !== identity.toLowerCase())\n fail(\"E_IDENTITY\", \"invalid X25519 age identity\");\n return decoded.data;\n}\n\nexport function recipientFromIdentity(identity) {\n return encodeBech32(\"age\", x25519.getPublicKey(parseIdentity(identity)));\n}\n\nexport function formatRecipient(publicKey) {\n return encodeBech32(\"age\", bytes(publicKey, 32, \"publicKey\"));\n}\n\nexport function formatIdentity(secretKey) {\n return encodeBech32(\"age-secret-key-\", bytes(secretKey, 32, \"secretKey\")).toUpperCase();\n}\n\nexport function publicKeyFromSecretKey(secretKey) {\n return x25519.getPublicKey(bytes(secretKey, 32, \"secretKey\"));\n}\n\nexport function createRecipientIdentity() {\n return createRecipientIdentityFromSecret(random(32));\n}\n\nexport function createRecipientIdentityFromSecret(secretKey) {\n const scalar = bytes(secretKey, 32, \"secretKey\");\n return { identity: formatIdentity(scalar), recipient: formatRecipient(publicKeyFromSecretKey(scalar)) };\n}\n\nfunction parseHeader(header) {\n bytes(header);\n let position = 0;\n const line = () => {\n const newline = header.indexOf(0x0a, position);\n if (newline === -1) fail(\"E_AGE_HEADER_PARSE\", \"truncated age header\");\n const value = new TextDecoder(\"utf-8\", { fatal: true }).decode(header.subarray(position, newline));\n position = newline + 1;\n return value;\n };\n\n let version;\n try { version = line(); }\n catch (error) { if (error?.code) throw error; fail(\"E_AGE_HEADER_PARSE\", \"age header is not valid UTF-8\"); }\n if (version !== VERSION_LINE) fail(\"E_AGE_HEADER_VERSION\", \"unsupported age header version\");\n\n const stanzas = [];\n for (;;) {\n let current;\n try { current = line(); }\n catch (error) { if (error?.code) throw error; fail(\"E_AGE_HEADER_PARSE\", \"age header is not valid UTF-8\"); }\n if (current.startsWith(\"--- \")) {\n const mac = decodeBase64(current.slice(4));\n if (!mac || mac.length !== 32 || position !== header.length) fail(\"E_AGE_HEADER_PARSE\", \"invalid age header MAC line\");\n const markerEnd = position - current.length - 1 + 3;\n return { stanzas, mac, authenticated: header.subarray(0, markerEnd) };\n }\n if (!current.startsWith(\"-> \")) fail(\"E_AGE_HEADER_PARSE\", \"invalid age recipient stanza\");\n const args = current.slice(3).split(\" \");\n if (args.some((arg) => arg.length === 0)) fail(\"E_AGE_HEADER_PARSE\", \"invalid age recipient arguments\");\n const bodyLines = [];\n for (;;) {\n let bodyLine;\n try { bodyLine = line(); }\n catch (error) { if (error?.code) throw error; fail(\"E_AGE_HEADER_PARSE\", \"age header is not valid UTF-8\"); }\n if (!/^[A-Za-z0-9+/]*$/.test(bodyLine) || bodyLine.length > 64) fail(\"E_AGE_HEADER_PARSE\", \"invalid age stanza body\");\n bodyLines.push(bodyLine);\n if (bodyLine.length < 64) break;\n }\n const body = decodeBase64(bodyLines.join(\"\"));\n if (!body) fail(\"E_AGE_HEADER_PARSE\", \"non-canonical age stanza body\");\n stanzas.push({ args, body });\n }\n}\n\nfunction encryptAgeHeader(fileKey, recipients, rng) {\n bytes(fileKey, 16, \"fileKey\");\n if (!Array.isArray(recipients) || recipients.length === 0) fail(\"E_RECIPIENT\", \"at least one recipient is required\");\n let text = `${VERSION_LINE}\\n`;\n for (const recipient of recipients) {\n const recipientPublic = typeof recipient === \"string\" ? parseRecipient(recipient) : bytes(recipient, 32, \"recipient\");\n const ephemeralSecret = rng(32);\n bytes(ephemeralSecret, 32, \"random output\");\n let shared;\n let wrapKey;\n try {\n const ephemeralPublic = x25519.getPublicKey(ephemeralSecret);\n try { shared = x25519.getSharedSecret(ephemeralSecret, recipientPublic); }\n catch { fail(\"E_RECIPIENT\", \"recipient is a low-order X25519 point\"); }\n if (allZero(shared)) fail(\"E_RECIPIENT\", \"recipient is a low-order X25519 point\");\n wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32);\n const wrapped = chacha20poly1305(wrapKey, ZERO_NONCE).encrypt(fileKey);\n text += `-> X25519 ${encodeBase64(ephemeralPublic)}\\n${encodeBase64(wrapped)}\\n`;\n } finally {\n ephemeralSecret.fill(0);\n shared?.fill(0);\n wrapKey?.fill(0);\n }\n }\n text += \"---\";\n const authenticated = utf8(text);\n const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32);\n try {\n const mac = hmac(sha256, macKey, authenticated);\n return utf8(`${text} ${encodeBase64(mac)}\\n`);\n } finally { macKey.fill(0); }\n}\n\nfunction decryptAgeHeader(header, identity) {\n const ownsScalar = typeof identity === \"string\";\n const scalar = ownsScalar ? parseIdentity(identity) : bytes(identity, 32, \"identity\");\n let fileKey = null;\n let authenticated = false;\n try {\n const parsed = parseHeader(header);\n if (parsed.stanzas.length === 0) fail(\"E_AGE_HEADER_PARSE\", \"age header has no recipients\");\n const recipientPublic = x25519.getPublicKey(scalar);\n for (const stanza of parsed.stanzas) {\n if (stanza.args[0] !== \"X25519\") fail(\"E_AGE_HEADER_RECIPIENT\", \"foreign recipient stanza is not supported\");\n if (stanza.args.length !== 2 || stanza.body.length !== 32) fail(\"E_AGE_HEADER_PARSE\", \"invalid X25519 stanza\");\n const ephemeralPublic = decodeBase64(stanza.args[1]);\n if (!ephemeralPublic || ephemeralPublic.length !== 32) fail(\"E_AGE_HEADER_PARSE\", \"invalid X25519 ephemeral share\");\n }\n for (const stanza of parsed.stanzas) {\n const ephemeralPublic = decodeBase64(stanza.args[1]);\n let shared;\n let wrapKey;\n try {\n try { shared = x25519.getSharedSecret(scalar, ephemeralPublic); }\n catch { continue; }\n if (allZero(shared)) continue;\n wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32);\n try {\n const candidate = chacha20poly1305(wrapKey, ZERO_NONCE).decrypt(stanza.body);\n if (candidate.length === 16) { fileKey = candidate; break; }\n candidate.fill(0);\n } catch { /* stanza is for another identity */ }\n } finally {\n shared?.fill(0);\n wrapKey?.fill(0);\n }\n }\n if (!fileKey) fail(\"E_WRONG_RECIPIENT\", \"identity does not match this file envelope\");\n const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32);\n const expectedMac = hmac(sha256, macKey, parsed.authenticated);\n try {\n if (!equal(expectedMac, parsed.mac)) fail(\"E_ENVELOPE_AUTH\", \"age header authentication failed\");\n authenticated = true;\n return fileKey;\n } finally {\n macKey.fill(0);\n expectedMac.fill(0);\n }\n } finally {\n if (ownsScalar) scalar.fill(0);\n if (!authenticated) fileKey?.fill(0);\n }\n}\n\nexport const ageHeader = Object.freeze({\n seal: (fileKey, recipients) => encryptAgeHeader(fileKey, recipients, random),\n open: decryptAgeHeader,\n});\n\nexport const ageInternals = Object.freeze({ encryptAgeHeader, decryptAgeHeader });\n", "const BASE64 = \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/\";\nconst BASE64_VALUE = new Int16Array(128).fill(-1);\nfor (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i;\n\nexport const EMPTY = new Uint8Array(0);\nexport const utf8 = (value) => new TextEncoder().encode(value);\n\nexport function bytes(value, length, name = \"value\") {\n if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Uint8Array`);\n if (length !== undefined && value.length !== length) throw new TypeError(`${name} must be ${length} bytes`);\n return value;\n}\n\nexport function concat(...parts) {\n const length = parts.reduce((sum, part) => sum + bytes(part).length, 0);\n const out = new Uint8Array(length);\n let offset = 0;\n for (const part of parts) {\n out.set(part, offset);\n offset += part.length;\n }\n return out;\n}\n\nexport function equal(a, b) {\n if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array) || a.length !== b.length) return false;\n let diff = 0;\n for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i];\n return diff === 0;\n}\n\nexport function allZero(value) {\n let acc = 0;\n for (const byte of value) acc |= byte;\n return acc === 0;\n}\n\nexport function u32(value) {\n if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) throw new RangeError(\"value is not a uint32\");\n const out = new Uint8Array(4);\n new DataView(out.buffer).setUint32(0, value, false);\n return out;\n}\n\nexport function u64(value) {\n if (!Number.isSafeInteger(value) || value < 0) throw new RangeError(\"value is not a non-negative safe integer\");\n const out = new Uint8Array(8);\n const view = new DataView(out.buffer);\n const big = BigInt(value);\n view.setUint32(0, Number(big >> 32n), false);\n view.setUint32(4, Number(big & 0xffffffffn), false);\n return out;\n}\n\nexport function readU32(value, offset) {\n if (offset < 0 || offset + 4 > value.length) throw new RangeError(\"truncated uint32\");\n return new DataView(value.buffer, value.byteOffset + offset, 4).getUint32(0, false);\n}\n\nexport function readU64(value, offset) {\n if (offset < 0 || offset + 8 > value.length) throw new RangeError(\"truncated uint64\");\n const view = new DataView(value.buffer, value.byteOffset + offset, 8);\n const big = (BigInt(view.getUint32(0, false)) << 32n) | BigInt(view.getUint32(4, false));\n if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError(\"uint64 exceeds JavaScript safe integer range\");\n return Number(big);\n}\n\nexport function encodeBase64(value, { padding = false } = {}) {\n bytes(value);\n let out = \"\";\n for (let i = 0; i < value.length; i += 3) {\n const a = value[i];\n const haveB = i + 1 < value.length;\n const haveC = i + 2 < value.length;\n const b = haveB ? value[i + 1] : 0;\n const c = haveC ? value[i + 2] : 0;\n out += BASE64[a >>> 2];\n out += BASE64[((a & 3) << 4) | (b >>> 4)];\n if (haveB) out += BASE64[((b & 15) << 2) | (c >>> 6)];\n else if (padding) out += \"=\";\n if (haveC) out += BASE64[c & 63];\n else if (padding) out += \"=\";\n }\n return out;\n}\n\nexport function decodeBase64(value, { padding = false } = {}) {\n if (typeof value !== \"string\") return null;\n if (padding) {\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) return null;\n } else if (!/^[A-Za-z0-9+/]*$/.test(value) || value.length % 4 === 1) return null;\n\n const stripped = padding ? value.replace(/=+$/, \"\") : value;\n const outputLength = Math.floor((stripped.length * 6) / 8);\n const out = new Uint8Array(outputLength);\n let accumulator = 0;\n let bits = 0;\n let offset = 0;\n for (let i = 0; i < stripped.length; i++) {\n const code = stripped.charCodeAt(i);\n if (code >= BASE64_VALUE.length || BASE64_VALUE[code] < 0) return null;\n accumulator = (accumulator << 6) | BASE64_VALUE[code];\n bits += 6;\n if (bits >= 8) {\n bits -= 8;\n out[offset++] = (accumulator >>> bits) & 0xff;\n }\n }\n if (bits > 0 && (accumulator & ((1 << bits) - 1)) !== 0) return null;\n if (encodeBase64(out, { padding }) !== value) return null;\n return out;\n}\n", "const CHARSET = \"qpzry9x8gf2tvdw0s3jn54khce6mua7l\";\nconst GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];\n\nfunction polymod(values) {\n let checksum = 1;\n for (const value of values) {\n const top = checksum >>> 25;\n checksum = ((checksum & 0x1ffffff) << 5) ^ value;\n for (let i = 0; i < 5; i++) if ((top >>> i) & 1) checksum ^= GENERATORS[i];\n }\n return checksum >>> 0;\n}\n\nfunction expandHrp(hrp) {\n const out = [];\n for (const char of hrp) out.push(char.charCodeAt(0) >>> 5);\n out.push(0);\n for (const char of hrp) out.push(char.charCodeAt(0) & 31);\n return out;\n}\n\nfunction convertBits(data, from, to, pad) {\n let accumulator = 0;\n let bits = 0;\n const out = [];\n const maximum = (1 << to) - 1;\n for (const value of data) {\n if (value < 0 || value >>> from) return null;\n accumulator = (accumulator << from) | value;\n bits += from;\n while (bits >= to) {\n bits -= to;\n out.push((accumulator >>> bits) & maximum);\n }\n }\n if (pad && bits) out.push((accumulator << (to - bits)) & maximum);\n else if (!pad && (bits >= from || ((accumulator << (to - bits)) & maximum))) return null;\n return out;\n}\n\nexport function encodeBech32(hrp, data) {\n const words = convertBits(data, 8, 5, true);\n const values = [...expandHrp(hrp), ...words];\n const mod = polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1;\n const checksum = [];\n for (let i = 0; i < 6; i++) checksum.push((mod >>> (5 * (5 - i))) & 31);\n return hrp + \"1\" + [...words, ...checksum].map((word) => CHARSET[word]).join(\"\");\n}\n\nexport function decodeBech32(value) {\n if (typeof value !== \"string\" || (value !== value.toLowerCase() && value !== value.toUpperCase())) return null;\n const normalized = value.toLowerCase();\n const separator = normalized.lastIndexOf(\"1\");\n if (separator < 1 || separator + 7 > normalized.length) return null;\n const hrp = normalized.slice(0, separator);\n const words = [];\n for (const char of normalized.slice(separator + 1)) {\n const word = CHARSET.indexOf(char);\n if (word === -1) return null;\n words.push(word);\n }\n if (polymod([...expandHrp(hrp), ...words]) !== 1) return null;\n const data = convertBits(words.slice(0, -6), 5, 8, false);\n return data === null ? null : { hrp, data: new Uint8Array(data) };\n}\n", "import { xchacha20poly1305 } from \"@noble/ciphers/chacha.js\";\nimport { hkdf } from \"@noble/hashes/hkdf.js\";\nimport { sha256 } from \"@noble/hashes/sha256.js\";\nimport { randomBytes } from \"@noble/hashes/utils.js\";\nimport { ageInternals } from \"./age.js\";\nimport { bytes, concat, readU32, readU64, u32, u64, utf8 } from \"./bytes.js\";\nimport { fail } from \"./error.js\";\n\nconst MAGIC = utf8(\"PKFILE1\\n\");\nconst VERSION = 1;\nconst FILE_ID_BYTES = 16;\nconst NONCE_BYTES = 24;\nconst TAG_BYTES = 16;\nconst DEFAULT_CHUNK_SIZE = 1024 * 1024;\nconst MIN_CHUNK_SIZE = 64 * 1024;\nconst MAX_CHUNK_SIZE = 16 * 1024 * 1024;\nconst MAX_HEADER_SIZE = 1024 * 1024;\nconst MAX_AGE_HEADER_SIZE = 256 * 1024;\nconst MAX_METADATA_CIPHERTEXT_SIZE = 64 * 1024;\n\nconst METADATA_KEY_INFO = utf8(\"pksuite/file/metadata-key/v1\");\nconst CHUNK_KEY_INFO = utf8(\"pksuite/file/chunk-key/v1\");\nconst METADATA_AAD_DOMAIN = utf8(\"pksuite/file/metadata/v1\");\nconst CHUNK_AAD_DOMAIN = utf8(\"pksuite/file/chunk/v1\");\nconst DOMAIN_SEPARATOR = new Uint8Array([0]);\n\nconst encryptionStates = new WeakMap();\nconst decryptionStates = new WeakMap();\n\nfunction validateGeometry(plaintextSize, chunkSize) {\n if (!Number.isSafeInteger(plaintextSize) || plaintextSize < 0) fail(\"E_INPUT\", \"plaintextSize must be a non-negative safe integer\");\n if (!Number.isInteger(chunkSize) || chunkSize < MIN_CHUNK_SIZE || chunkSize > MAX_CHUNK_SIZE)\n fail(\"E_INPUT\", `chunkSize must be between ${MIN_CHUNK_SIZE} and ${MAX_CHUNK_SIZE} bytes`);\n const chunkCount = Math.max(1, Math.ceil(plaintextSize / chunkSize));\n if (chunkCount > 0xffffffff) fail(\"E_INPUT\", \"file requires too many chunks\");\n return chunkCount;\n}\n\nfunction expectedPlaintextLength({ plaintextSize, chunkSize, chunkCount }, index) {\n if (!Number.isInteger(index) || index < 0 || index >= chunkCount) fail(\"E_CHUNK_INDEX\", \"chunk index is outside the envelope\");\n if (plaintextSize === 0) return 0;\n return index === chunkCount - 1 ? plaintextSize - chunkSize * index : chunkSize;\n}\n\nfunction canonicalJsonString(value, label) {\n let result = '\"';\n for (let offset = 0; offset < value.length;) {\n const first = value.charCodeAt(offset);\n let character;\n if (first >= 0xd800 && first <= 0xdbff) {\n const second = value.charCodeAt(offset + 1);\n if (!(second >= 0xdc00 && second <= 0xdfff)) fail(\"E_METADATA\", `${label} must contain only Unicode scalar values`);\n character = value.slice(offset, offset + 2);\n offset += 2;\n } else {\n if (first >= 0xdc00 && first <= 0xdfff) fail(\"E_METADATA\", `${label} must contain only Unicode scalar values`);\n character = value[offset++];\n }\n if (character === '\"') result += '\\\\\"';\n else if (character === \"\\\\\") result += \"\\\\\\\\\";\n else if (first === 0x08) result += \"\\\\b\";\n else if (first === 0x09) result += \"\\\\t\";\n else if (first === 0x0a) result += \"\\\\n\";\n else if (first === 0x0c) result += \"\\\\f\";\n else if (first === 0x0d) result += \"\\\\r\";\n else if (first < 0x20) result += `\\\\u${first.toString(16).padStart(4, \"0\")}`;\n else result += character;\n }\n return `${result}\"`;\n}\n\nfunction canonicalMetadata(metadata) {\n if (!metadata || typeof metadata !== \"object\" || Array.isArray(metadata)) fail(\"E_METADATA\", \"metadata must be an object\");\n if (Object.keys(metadata).some((key) => ![\"name\", \"mediaType\", \"lastModified\"].includes(key)))\n fail(\"E_METADATA\", \"metadata contains an unknown field\");\n const { name, mediaType = \"application/octet-stream\", lastModified = null } = metadata;\n if (typeof name !== \"string\" || name.length === 0 || utf8(name).length > 4096 || /[\\u0000-\\u001f\\u007f]/u.test(name))\n fail(\"E_METADATA\", \"metadata.name must be a non-empty filename without control characters\");\n if (name === \".\" || name === \"..\" || /[\\\\/:]/u.test(name) || /[ .]$/u.test(name) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\\.|$)/iu.test(name))\n fail(\"E_METADATA\", \"metadata.name must be a portable single filename, not a path or device name\");\n if (typeof mediaType !== \"string\" || utf8(mediaType).length > 255 || !/^[\\x20-\\x7e]*$/.test(mediaType))\n fail(\"E_METADATA\", \"metadata.mediaType must be printable ASCII no longer than 255 bytes\");\n if (lastModified !== null && (!Number.isSafeInteger(lastModified) || lastModified < 0))\n fail(\"E_METADATA\", \"metadata.lastModified must be null or a non-negative safe integer\");\n return utf8(`{\"name\":${canonicalJsonString(name, \"metadata.name\")},\"mediaType\":${canonicalJsonString(mediaType, \"metadata.mediaType\")},\"lastModified\":${lastModified === null ? \"null\" : String(lastModified)}}`);\n}\n\nfunction parseMetadata(plaintext) {\n let parsed;\n try { parsed = JSON.parse(new TextDecoder(\"utf-8\", { fatal: true }).decode(plaintext)); }\n catch { fail(\"E_METADATA\", \"encrypted metadata is not canonical UTF-8 JSON\"); }\n const canonical = canonicalMetadata(parsed);\n if (canonical.length !== plaintext.length || canonical.some((byte, index) => byte !== plaintext[index]))\n fail(\"E_METADATA\", \"encrypted metadata is not canonical\");\n return parsed;\n}\n\nfunction structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader }) {\n return concat(u32(chunkSize), u64(plaintextSize), u32(chunkCount), fileId, u32(ageHeader.length), ageHeader);\n}\n\nfunction metadataAad(prefix) {\n return concat(METADATA_AAD_DOMAIN, DOMAIN_SEPARATOR, u32(VERSION), sha256(concat(MAGIC, prefix)));\n}\n\nfunction chunkAad(record, index) {\n const expected = expectedPlaintextLength(record, index);\n return concat(\n CHUNK_AAD_DOMAIN,\n DOMAIN_SEPARATOR,\n u32(VERSION),\n record.fileId,\n record.headerCommitment,\n u32(index),\n u32(record.chunkCount),\n u64(record.plaintextSize),\n u32(record.chunkSize),\n u32(expected),\n );\n}\n\nfunction deriveMetadataKey(fileKey, fileId) {\n return hkdf(sha256, fileKey, fileId, METADATA_KEY_INFO, 32);\n}\n\nfunction deriveChunkKey(fileKey, fileId, index) {\n return hkdf(sha256, fileKey, concat(fileId, u32(index)), CHUNK_KEY_INFO, 32);\n}\n\nfunction parseEnvelopeHeader(header) {\n bytes(header);\n if (header.length < MAGIC.length + 4 || MAGIC.some((byte, index) => header[index] !== byte))\n fail(\"E_ENVELOPE_VERSION\", \"unsupported file envelope magic or version\");\n let bodyLength;\n try { bodyLength = readU32(header, MAGIC.length); }\n catch { fail(\"E_ENVELOPE_PARSE\", \"truncated file envelope prelude\"); }\n if (bodyLength > MAX_HEADER_SIZE || bodyLength !== header.length - MAGIC.length - 4)\n fail(\"E_ENVELOPE_PARSE\", \"file envelope header length mismatch\");\n let offset = MAGIC.length + 4;\n const take = (length, label) => {\n if (!Number.isInteger(length) || length < 0 || offset + length > header.length) fail(\"E_ENVELOPE_PARSE\", `truncated ${label}`);\n const value = header.subarray(offset, offset + length);\n offset += length;\n return value;\n };\n\n let chunkSize, plaintextSize, chunkCount, ageHeaderLength, metadataLength;\n try {\n chunkSize = readU32(header, offset); offset += 4;\n plaintextSize = readU64(header, offset); offset += 8;\n chunkCount = readU32(header, offset); offset += 4;\n } catch { fail(\"E_ENVELOPE_PARSE\", \"invalid file geometry\"); }\n const fileId = take(FILE_ID_BYTES, \"file id\");\n try { ageHeaderLength = readU32(header, offset); offset += 4; }\n catch { fail(\"E_ENVELOPE_PARSE\", \"truncated age header length\"); }\n if (ageHeaderLength === 0 || ageHeaderLength > MAX_AGE_HEADER_SIZE) fail(\"E_ENVELOPE_PARSE\", \"invalid age header length\");\n const ageHeader = take(ageHeaderLength, \"age header\");\n const prefixEnd = offset;\n const metadataNonce = take(NONCE_BYTES, \"metadata nonce\");\n try { metadataLength = readU32(header, offset); offset += 4; }\n catch { fail(\"E_ENVELOPE_PARSE\", \"truncated metadata length\"); }\n if (metadataLength < TAG_BYTES || metadataLength > MAX_METADATA_CIPHERTEXT_SIZE) fail(\"E_ENVELOPE_PARSE\", \"invalid metadata ciphertext length\");\n const metadataCiphertext = take(metadataLength, \"metadata ciphertext\");\n if (offset !== header.length) fail(\"E_ENVELOPE_PARSE\", \"trailing bytes in file envelope header\");\n\n const expectedCount = validateGeometry(plaintextSize, chunkSize);\n if (chunkCount !== expectedCount) fail(\"E_ENVELOPE_PARSE\", \"chunk count does not match file geometry\");\n return {\n chunkSize,\n plaintextSize,\n chunkCount,\n fileId,\n ageHeader,\n prefix: header.subarray(MAGIC.length + 4, prefixEnd),\n metadataNonce,\n metadataCiphertext,\n headerCommitment: sha256(header),\n };\n}\n\nfunction makePublicState(kind, record, header, metadata) {\n return Object.freeze({\n kind,\n version: VERSION,\n header,\n metadata,\n plaintextSize: record.plaintextSize,\n chunkSize: record.chunkSize,\n chunkCount: record.chunkCount,\n });\n}\n\nfunction createFileEncryptionImpl({ recipient, recipients, metadata, plaintextSize, chunkSize = DEFAULT_CHUNK_SIZE }, rng) {\n if (recipient !== undefined && recipients !== undefined) fail(\"E_INPUT\", \"provide recipient or recipients, not both\");\n const recipientList = recipients ?? (recipient === undefined ? [] : [recipient]);\n const chunkCount = validateGeometry(plaintextSize, chunkSize);\n const fileKey = bytes(rng(16), 16, \"random file key\");\n let retained = false;\n try {\n const fileId = bytes(rng(FILE_ID_BYTES), FILE_ID_BYTES, \"random file id\");\n const ageHeader = ageInternals.encryptAgeHeader(fileKey, recipientList, rng);\n if (ageHeader.length > MAX_AGE_HEADER_SIZE) fail(\"E_INPUT\", \"recipient header is too large\");\n const prefix = structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader });\n const metadataNonce = bytes(rng(NONCE_BYTES), NONCE_BYTES, \"random metadata nonce\");\n const metadataPlaintext = canonicalMetadata(metadata);\n const metadataKey = deriveMetadataKey(fileKey, fileId);\n let metadataCiphertext;\n try { metadataCiphertext = xchacha20poly1305(metadataKey, metadataNonce, metadataAad(prefix)).encrypt(metadataPlaintext); }\n finally { metadataKey.fill(0); metadataPlaintext.fill(0); }\n if (metadataCiphertext.length > MAX_METADATA_CIPHERTEXT_SIZE) fail(\"E_METADATA\", \"encrypted metadata is too large\");\n const body = concat(prefix, metadataNonce, u32(metadataCiphertext.length), metadataCiphertext);\n if (body.length > MAX_HEADER_SIZE) fail(\"E_INPUT\", \"file envelope header is too large\");\n const header = concat(MAGIC, u32(body.length), body);\n const record = {\n fileKey,\n fileId: new Uint8Array(fileId),\n plaintextSize,\n chunkSize,\n chunkCount,\n headerCommitment: sha256(header),\n rng,\n seen: new Set(),\n };\n const state = makePublicState(\"encryption\", record, header, undefined);\n encryptionStates.set(state, record);\n retained = true;\n return state;\n } finally {\n if (!retained) fileKey.fill(0);\n }\n}\n\nexport function createFileEncryption(options) {\n return createFileEncryptionImpl(options, randomBytes);\n}\n\nexport function encryptChunk({ state, index, plaintext }) {\n const record = encryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not a file-encryption state\");\n bytes(plaintext);\n const expected = expectedPlaintextLength(record, index);\n if (plaintext.length !== expected) fail(\"E_CHUNK_SIZE\", `chunk ${index} must contain exactly ${expected} plaintext bytes`);\n const nonce = bytes(record.rng(NONCE_BYTES), NONCE_BYTES, \"random chunk nonce\");\n const key = deriveChunkKey(record.fileKey, record.fileId, index);\n let ciphertext;\n try { ciphertext = xchacha20poly1305(key, nonce, chunkAad(record, index)).encrypt(plaintext); }\n finally { key.fill(0); }\n record.seen.add(index);\n return concat(nonce, ciphertext);\n}\n\nexport function openFileEnvelope({ identity, header }) {\n const parsed = parseEnvelopeHeader(header);\n const fileKey = ageInternals.decryptAgeHeader(parsed.ageHeader, identity);\n let retained = false;\n try {\n const metadataKey = deriveMetadataKey(fileKey, parsed.fileId);\n let metadataPlaintext;\n try { metadataPlaintext = xchacha20poly1305(metadataKey, parsed.metadataNonce, metadataAad(parsed.prefix)).decrypt(parsed.metadataCiphertext); }\n catch { fail(\"E_ENVELOPE_AUTH\", \"file metadata authentication failed\"); }\n finally { metadataKey.fill(0); }\n let metadata;\n try { metadata = parseMetadata(metadataPlaintext); }\n finally { metadataPlaintext.fill(0); }\n const record = {\n fileKey,\n fileId: new Uint8Array(parsed.fileId),\n plaintextSize: parsed.plaintextSize,\n chunkSize: parsed.chunkSize,\n chunkCount: parsed.chunkCount,\n headerCommitment: new Uint8Array(parsed.headerCommitment),\n seen: new Set(),\n };\n const state = makePublicState(\"decryption\", record, new Uint8Array(header), metadata);\n decryptionStates.set(state, record);\n retained = true;\n return state;\n } finally {\n if (!retained) fileKey.fill(0);\n }\n}\n\nexport function decryptChunk({ state, index, ciphertext }) {\n const record = decryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not a file-decryption state\");\n bytes(ciphertext);\n const expected = expectedPlaintextLength(record, index);\n if (ciphertext.length !== NONCE_BYTES + expected + TAG_BYTES)\n fail(\"E_CHUNK_SIZE\", `chunk ${index} ciphertext has the wrong length`);\n const nonce = ciphertext.subarray(0, NONCE_BYTES);\n const body = ciphertext.subarray(NONCE_BYTES);\n const key = deriveChunkKey(record.fileKey, record.fileId, index);\n let plaintext;\n try { plaintext = xchacha20poly1305(key, nonce, chunkAad(record, index)).decrypt(body); }\n catch { fail(\"E_CHUNK_AUTH\", `chunk ${index} authentication failed`); }\n finally { key.fill(0); }\n record.seen.add(index);\n return plaintext;\n}\n\nexport function finalizeEnvelope(state) {\n const record = encryptionStates.get(state) ?? decryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not a file-envelope state\");\n if (record.seen.size !== record.chunkCount) fail(\"E_ENVELOPE_INCOMPLETE\", `expected ${record.chunkCount} authenticated chunks, observed ${record.seen.size}`);\n const finalized = Object.freeze({\n version: VERSION,\n header: state.header,\n metadata: state.metadata,\n plaintextSize: record.plaintextSize,\n chunkSize: record.chunkSize,\n chunkCount: record.chunkCount,\n });\n disposeEnvelope(state);\n return finalized;\n}\n\nexport function disposeEnvelope(state) {\n const record = encryptionStates.get(state) ?? decryptionStates.get(state);\n if (!record) fail(\"E_STATE\", \"state is not an active file-envelope state\");\n record.fileKey.fill(0);\n record.seen.clear();\n encryptionStates.delete(state);\n decryptionStates.delete(state);\n}\n\nexport const fileInternals = Object.freeze({\n createFileEncryptionWithRandom: createFileEncryptionImpl,\n parseEnvelopeHeader,\n metadataAad,\n chunkAad,\n constants: Object.freeze({ MAGIC, VERSION, NONCE_BYTES, TAG_BYTES, DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE }),\n});\n"], + "mappings": "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", + "names": ["hkdf", "sha256", "randomBytes", "ageHeader", "sha256", "hkdf", "randomBytes"] +} diff --git a/packages/crypto/package.json b/packages/crypto/package.json new file mode 100644 index 0000000..3705032 --- /dev/null +++ b/packages/crypto/package.json @@ -0,0 +1,54 @@ +{ + "name": "@pksuite/crypto", + "version": "0.1.0", + "description": "Browser and Node cryptographic primitives for the pk suite.", + "type": "module", + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist/", + "src/", + "vectors/", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "node scripts/build.mjs", + "gen-age-fixture": "node scripts/gen-age-cli-fixture.mjs", + "gen-vectors": "node scripts/gen-vectors.mjs", + "prepack": "node scripts/build.mjs", + "prepublishOnly": "npm test", + "test": "node scripts/build.mjs && node --test test/*.test.js" + }, + "engines": { + "node": ">=18" + }, + "license": "MIT", + "author": "Daniel Yang ", + "repository": { + "type": "git", + "url": "git+https://github.com/ddyy/pk.git", + "directory": "packages/crypto" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "dependencies": { + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0" + }, + "devDependencies": { + "esbuild": "0.28.1" + } +} diff --git a/packages/crypto/scripts/build.mjs b/packages/crypto/scripts/build.mjs new file mode 100644 index 0000000..23b278f --- /dev/null +++ b/packages/crypto/scripts/build.mjs @@ -0,0 +1,32 @@ +import { build } from "esbuild"; +import { mkdir, copyFile, rm } from "node:fs/promises"; + +const dist = new URL("../dist/", import.meta.url); +await rm(dist, { recursive: true, force: true }); +await mkdir(dist, { recursive: true }); + +const common = { + entryPoints: [new URL("../src/index.js", import.meta.url).pathname], + bundle: true, + sourcemap: true, + legalComments: "linked", + packages: "external", + target: ["es2020"], +}; + +await Promise.all([ + build({ ...common, format: "esm", platform: "browser", outfile: new URL("../dist/index.js", import.meta.url).pathname }), + build({ ...common, format: "cjs", platform: "node", outfile: new URL("../dist/index.cjs", import.meta.url).pathname }), + copyFile(new URL("../src/index.d.ts", import.meta.url), new URL("../dist/index.d.ts", import.meta.url)), + copyFile(new URL("../../../spec/SPEC-FILE.md", import.meta.url), new URL("../dist/SPEC-FILE.md", import.meta.url)), +]); + +await build({ + entryPoints: [new URL("../test/mobile/runner.js", import.meta.url).pathname], + bundle: true, + format: "esm", + legalComments: "linked", + platform: "browser", + target: ["es2020"], + outfile: new URL("../test/mobile/runner.bundle.js", import.meta.url).pathname, +}); diff --git a/packages/crypto/scripts/gen-age-cli-fixture.mjs b/packages/crypto/scripts/gen-age-cli-fixture.mjs new file mode 100644 index 0000000..c6dd86f --- /dev/null +++ b/packages/crypto/scripts/gen-age-cli-fixture.mjs @@ -0,0 +1,27 @@ +// Maintainer-only interoperability fixture. Requires the external age CLI. +// The output is intentionally nondeterministic and is committed as a frozen +// external-writer artifact; normal tests never regenerate it. +import { createHash } from "node:crypto"; +import { readFileSync, writeFileSync } from "node:fs"; +import { spawnSync } from "node:child_process"; + +const vector = JSON.parse(readFileSync(new URL("../vectors/file-v1.json", import.meta.url), "utf8")); +const plaintext = createHash("sha256").update("external age CLI interoperability fixture").digest(); +const version = spawnSync("age", ["--version"], { encoding: "utf8" }); +if (version.status !== 0) throw new Error(version.stderr || "age --version failed"); +const derived = spawnSync("age-keygen", ["-y"], { input: `${vector.identity.identity}\n`, encoding: "utf8" }); +if (derived.status !== 0) throw new Error(derived.stderr || "age-keygen -y failed"); +if (derived.stdout.trim() !== vector.identity.recipient) throw new Error("age-keygen derived a different recipient"); +const encrypted = spawnSync("age", ["-r", vector.identity.recipient], { input: plaintext, maxBuffer: 1024 * 1024 }); +if (encrypted.status !== 0) throw new Error(encrypted.stderr?.toString() || "age encryption failed"); +writeFileSync(new URL("../vectors/age-cli-v1.age", import.meta.url), encrypted.stdout); +writeFileSync(new URL("../vectors/age-cli-v1.json", import.meta.url), JSON.stringify({ + note: "Frozen artifact written by the external age CLI, not by @pksuite/crypto.", + writer: version.stdout.trim() || version.stderr.trim(), + identity_verified_by: "age-keygen -y", + identity: vector.identity.identity, + recipient: vector.identity.recipient, + secret_key_hex: vector.identity.secret_key, + plaintext_hex: plaintext.toString("hex"), + ciphertext_sha256: createHash("sha256").update(encrypted.stdout).digest("hex"), +}, null, 2) + "\n"); diff --git a/packages/crypto/scripts/gen-vectors.mjs b/packages/crypto/scripts/gen-vectors.mjs new file mode 100644 index 0000000..5b6d334 --- /dev/null +++ b/packages/crypto/scripts/gen-vectors.mjs @@ -0,0 +1,112 @@ +import { createHash } from "node:crypto"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { createRecipientIdentityFromSecret } from "../src/age.js"; +import { encryptChunk, fileInternals, finalizeEnvelope } from "../src/file.js"; + +const root = join(dirname(fileURLToPath(import.meta.url)), ".."); +const sequence = (start, length) => Uint8Array.from({ length }, (_, index) => (start + index) & 0xff); +const hex = (value) => Buffer.from(value).toString("hex"); +const b64 = (value) => Buffer.from(value).toString("base64"); +const digest = (value) => createHash("sha256").update(value).digest("hex"); + +const secretKey = sequence(0xa0, 32); +const identity = createRecipientIdentityFromSecret(secretKey); +const wrongIdentity = createRecipientIdentityFromSecret(sequence(0xc0, 32)); +const randomValues = [ + sequence(0x00, 16), // file key + sequence(0x10, 16), // file id + sequence(0x20, 32), // age ephemeral secret + sequence(0x40, 24), // metadata nonce + sequence(0x60, 24), // chunk 0 nonce + sequence(0x80, 24), // chunk 1 nonce +]; +const rng = (length) => { + const next = randomValues.shift(); + if (!next || next.length !== length) throw new Error(`deterministic RNG expected ${next?.length ?? "no more"} bytes, requested ${length}`); + return next; +}; + +const chunkSize = 64 * 1024; +const plaintext0 = Uint8Array.from({ length: chunkSize }, (_, index) => index & 0xff); +const plaintext1 = new Uint8Array([0xfa, 0xce, 0x01]); +const state = fileInternals.createFileEncryptionWithRandom({ + recipient: identity.recipient, + plaintextSize: plaintext0.length + plaintext1.length, + chunkSize, + metadata: { name: "Smith 2025 W-2.pdf", mediaType: "application/pdf", lastModified: 1784313600000 }, +}, rng); +const ciphertext0 = encryptChunk({ state, index: 0, plaintext: plaintext0 }); +const ciphertext1 = encryptChunk({ state, index: 1, plaintext: plaintext1 }); +finalizeEnvelope(state); +if (randomValues.length !== 0) throw new Error("deterministic RNG values were not fully consumed"); +const parsed = fileInternals.parseEnvelopeHeader(state.header); +const metadataAssociatedData = fileInternals.metadataAad(parsed.prefix); + +const vector = { + note: "PKFILE1 v1 known-answer vector. Fixed keys and nonces are test data only and MUST NOT be reused.", + identity: { + secret_key: hex(secretKey), + identity: identity.identity, + recipient: identity.recipient, + }, + file: { + plaintext_size: state.plaintextSize, + chunk_size: state.chunkSize, + chunk_count: state.chunkCount, + metadata: { + name: "Smith 2025 W-2.pdf", + mediaType: "application/pdf", + lastModified: 1784313600000, + }, + file_id_hex: hex(parsed.fileId), + age_header_base64: b64(parsed.ageHeader), + metadata_nonce_hex: hex(parsed.metadataNonce), + metadata_ciphertext_base64: b64(parsed.metadataCiphertext), + metadata_aad_hex: hex(metadataAssociatedData), + header_base64: b64(state.header), + header_sha256: digest(state.header), + }, + chunks: [ + { + index: 0, + plaintext_pattern: "byte[index mod 256] repeated to chunk_size", + plaintext_sha256: digest(plaintext0), + nonce_hex: hex(ciphertext0.subarray(0, 24)), + aad_hex: hex(fileInternals.chunkAad(parsed, 0)), + ciphertext_base64: b64(ciphertext0), + ciphertext_sha256: digest(ciphertext0), + }, + { + index: 1, + plaintext_hex: hex(plaintext1), + plaintext_sha256: digest(plaintext1), + nonce_hex: hex(ciphertext1.subarray(0, 24)), + aad_hex: hex(fileInternals.chunkAad(parsed, 1)), + ciphertext_base64: b64(ciphertext1), + ciphertext_sha256: digest(ciphertext1), + }, + ], +}; + +mkdirSync(join(root, "vectors"), { recursive: true }); +writeFileSync(join(root, "vectors/file-v1.json"), JSON.stringify(vector, null, 2) + "\n"); +const tampering = { + note: "Executable mutation cases for file-v1.json. packages/crypto/test/file.test.js MUST execute every listed case.", + base_vector: "file-v1.json", + wrong_identity: wrongIdentity.identity, + cases: [ + { id: "wrong-recipient", operation: "open_wrong_identity", expected_code: "E_WRONG_RECIPIENT" }, + { id: "metadata-tamper", operation: "xor_header_byte", offset_from_end: 1, xor: 1, expected_code: "E_ENVELOPE_AUTH" }, + { id: "chunk-tamper", operation: "xor_chunk_byte", chunk: 0, offset_from_end: 1, xor: 1, expected_code: "E_CHUNK_AUTH" }, + { id: "chunk-truncation", operation: "truncate_chunk", chunk: 0, bytes: 1, expected_code: "E_CHUNK_SIZE" }, + { id: "chunk-extension", operation: "extend_chunk", chunk: 0, hex: "00", expected_code: "E_CHUNK_SIZE" }, + { id: "chunk-reordering", operation: "present_chunk_at_index", chunk: 0, index: 1, expected_code: "E_CHUNK_SIZE" }, + { id: "file-truncation", operation: "omit_chunk_then_finalize", omit: 1, expected_code: "E_ENVELOPE_INCOMPLETE" }, + { id: "cross-file-substitution", operation: "substitute_same_geometry_file", chunk: 0, expected_code: "E_CHUNK_AUTH" }, + { id: "retry-nonce-reuse", operation: "assert_retry_nonce_distinct", chunk: 0 }, + ], +}; +writeFileSync(join(root, "vectors/file-v1-tampering.json"), JSON.stringify(tampering, null, 2) + "\n"); +console.log(`wrote vectors/file-v1.json (${state.header.length} header bytes, ${ciphertext0.length + ciphertext1.length} ciphertext bytes)`); diff --git a/packages/crypto/src/age.js b/packages/crypto/src/age.js new file mode 100644 index 0000000..066d24a --- /dev/null +++ b/packages/crypto/src/age.js @@ -0,0 +1,194 @@ +import { chacha20poly1305 } from "@noble/ciphers/chacha.js"; +import { x25519 } from "@noble/curves/ed25519.js"; +import { hkdf } from "@noble/hashes/hkdf.js"; +import { hmac } from "@noble/hashes/hmac.js"; +import { sha256 } from "@noble/hashes/sha256.js"; +import { randomBytes } from "@noble/hashes/utils.js"; +import { allZero, bytes, concat, decodeBase64, EMPTY, encodeBase64, equal, utf8 } from "./bytes.js"; +import { decodeBech32, encodeBech32 } from "./bech32.js"; +import { fail } from "./error.js"; + +const VERSION_LINE = "age-encryption.org/v1"; +const AGE_X25519_INFO = utf8("age-encryption.org/v1/X25519"); +const AGE_HEADER_INFO = utf8("header"); +const ZERO_NONCE = new Uint8Array(12); + +const random = (length) => randomBytes(length); + +export function parseRecipient(recipient) { + if (typeof recipient !== "string" || recipient !== recipient.toLowerCase()) fail("E_RECIPIENT", "recipient must be canonical lowercase age Bech32"); + const decoded = decodeBech32(recipient); + if (!decoded || decoded.hrp !== "age" || decoded.data.length !== 32 || encodeBech32("age", decoded.data) !== recipient) + fail("E_RECIPIENT", "invalid X25519 age recipient"); + return decoded.data; +} + +export function parseIdentity(identity) { + if (typeof identity !== "string" || (identity !== identity.toUpperCase() && identity !== identity.toLowerCase())) + fail("E_IDENTITY", "identity must use uniform Bech32 case"); + const decoded = decodeBech32(identity); + if (!decoded || decoded.hrp !== "age-secret-key-" || decoded.data.length !== 32 || encodeBech32("age-secret-key-", decoded.data) !== identity.toLowerCase()) + fail("E_IDENTITY", "invalid X25519 age identity"); + return decoded.data; +} + +export function recipientFromIdentity(identity) { + return encodeBech32("age", x25519.getPublicKey(parseIdentity(identity))); +} + +export function formatRecipient(publicKey) { + return encodeBech32("age", bytes(publicKey, 32, "publicKey")); +} + +export function formatIdentity(secretKey) { + return encodeBech32("age-secret-key-", bytes(secretKey, 32, "secretKey")).toUpperCase(); +} + +export function publicKeyFromSecretKey(secretKey) { + return x25519.getPublicKey(bytes(secretKey, 32, "secretKey")); +} + +export function createRecipientIdentity() { + return createRecipientIdentityFromSecret(random(32)); +} + +export function createRecipientIdentityFromSecret(secretKey) { + const scalar = bytes(secretKey, 32, "secretKey"); + return { identity: formatIdentity(scalar), recipient: formatRecipient(publicKeyFromSecretKey(scalar)) }; +} + +function parseHeader(header) { + bytes(header); + let position = 0; + const line = () => { + const newline = header.indexOf(0x0a, position); + if (newline === -1) fail("E_AGE_HEADER_PARSE", "truncated age header"); + const value = new TextDecoder("utf-8", { fatal: true }).decode(header.subarray(position, newline)); + position = newline + 1; + return value; + }; + + let version; + try { version = line(); } + catch (error) { if (error?.code) throw error; fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); } + if (version !== VERSION_LINE) fail("E_AGE_HEADER_VERSION", "unsupported age header version"); + + const stanzas = []; + for (;;) { + let current; + try { current = line(); } + catch (error) { if (error?.code) throw error; fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); } + if (current.startsWith("--- ")) { + const mac = decodeBase64(current.slice(4)); + if (!mac || mac.length !== 32 || position !== header.length) fail("E_AGE_HEADER_PARSE", "invalid age header MAC line"); + const markerEnd = position - current.length - 1 + 3; + return { stanzas, mac, authenticated: header.subarray(0, markerEnd) }; + } + if (!current.startsWith("-> ")) fail("E_AGE_HEADER_PARSE", "invalid age recipient stanza"); + const args = current.slice(3).split(" "); + if (args.some((arg) => arg.length === 0)) fail("E_AGE_HEADER_PARSE", "invalid age recipient arguments"); + const bodyLines = []; + for (;;) { + let bodyLine; + try { bodyLine = line(); } + catch (error) { if (error?.code) throw error; fail("E_AGE_HEADER_PARSE", "age header is not valid UTF-8"); } + if (!/^[A-Za-z0-9+/]*$/.test(bodyLine) || bodyLine.length > 64) fail("E_AGE_HEADER_PARSE", "invalid age stanza body"); + bodyLines.push(bodyLine); + if (bodyLine.length < 64) break; + } + const body = decodeBase64(bodyLines.join("")); + if (!body) fail("E_AGE_HEADER_PARSE", "non-canonical age stanza body"); + stanzas.push({ args, body }); + } +} + +function encryptAgeHeader(fileKey, recipients, rng) { + bytes(fileKey, 16, "fileKey"); + if (!Array.isArray(recipients) || recipients.length === 0) fail("E_RECIPIENT", "at least one recipient is required"); + let text = `${VERSION_LINE}\n`; + for (const recipient of recipients) { + const recipientPublic = typeof recipient === "string" ? parseRecipient(recipient) : bytes(recipient, 32, "recipient"); + const ephemeralSecret = rng(32); + bytes(ephemeralSecret, 32, "random output"); + let shared; + let wrapKey; + try { + const ephemeralPublic = x25519.getPublicKey(ephemeralSecret); + try { shared = x25519.getSharedSecret(ephemeralSecret, recipientPublic); } + catch { fail("E_RECIPIENT", "recipient is a low-order X25519 point"); } + if (allZero(shared)) fail("E_RECIPIENT", "recipient is a low-order X25519 point"); + wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32); + const wrapped = chacha20poly1305(wrapKey, ZERO_NONCE).encrypt(fileKey); + text += `-> X25519 ${encodeBase64(ephemeralPublic)}\n${encodeBase64(wrapped)}\n`; + } finally { + ephemeralSecret.fill(0); + shared?.fill(0); + wrapKey?.fill(0); + } + } + text += "---"; + const authenticated = utf8(text); + const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32); + try { + const mac = hmac(sha256, macKey, authenticated); + return utf8(`${text} ${encodeBase64(mac)}\n`); + } finally { macKey.fill(0); } +} + +function decryptAgeHeader(header, identity) { + const ownsScalar = typeof identity === "string"; + const scalar = ownsScalar ? parseIdentity(identity) : bytes(identity, 32, "identity"); + let fileKey = null; + let authenticated = false; + try { + const parsed = parseHeader(header); + if (parsed.stanzas.length === 0) fail("E_AGE_HEADER_PARSE", "age header has no recipients"); + const recipientPublic = x25519.getPublicKey(scalar); + for (const stanza of parsed.stanzas) { + if (stanza.args[0] !== "X25519") fail("E_AGE_HEADER_RECIPIENT", "foreign recipient stanza is not supported"); + if (stanza.args.length !== 2 || stanza.body.length !== 32) fail("E_AGE_HEADER_PARSE", "invalid X25519 stanza"); + const ephemeralPublic = decodeBase64(stanza.args[1]); + if (!ephemeralPublic || ephemeralPublic.length !== 32) fail("E_AGE_HEADER_PARSE", "invalid X25519 ephemeral share"); + } + for (const stanza of parsed.stanzas) { + const ephemeralPublic = decodeBase64(stanza.args[1]); + let shared; + let wrapKey; + try { + try { shared = x25519.getSharedSecret(scalar, ephemeralPublic); } + catch { continue; } + if (allZero(shared)) continue; + wrapKey = hkdf(sha256, shared, concat(ephemeralPublic, recipientPublic), AGE_X25519_INFO, 32); + try { + const candidate = chacha20poly1305(wrapKey, ZERO_NONCE).decrypt(stanza.body); + if (candidate.length === 16) { fileKey = candidate; break; } + candidate.fill(0); + } catch { /* stanza is for another identity */ } + } finally { + shared?.fill(0); + wrapKey?.fill(0); + } + } + if (!fileKey) fail("E_WRONG_RECIPIENT", "identity does not match this file envelope"); + const macKey = hkdf(sha256, fileKey, EMPTY, AGE_HEADER_INFO, 32); + const expectedMac = hmac(sha256, macKey, parsed.authenticated); + try { + if (!equal(expectedMac, parsed.mac)) fail("E_ENVELOPE_AUTH", "age header authentication failed"); + authenticated = true; + return fileKey; + } finally { + macKey.fill(0); + expectedMac.fill(0); + } + } finally { + if (ownsScalar) scalar.fill(0); + if (!authenticated) fileKey?.fill(0); + } +} + +export const ageHeader = Object.freeze({ + seal: (fileKey, recipients) => encryptAgeHeader(fileKey, recipients, random), + open: decryptAgeHeader, +}); + +export const ageInternals = Object.freeze({ encryptAgeHeader, decryptAgeHeader }); diff --git a/packages/crypto/src/bech32.js b/packages/crypto/src/bech32.js new file mode 100644 index 0000000..a882ab4 --- /dev/null +++ b/packages/crypto/src/bech32.js @@ -0,0 +1,65 @@ +const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; +const GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + +function polymod(values) { + let checksum = 1; + for (const value of values) { + const top = checksum >>> 25; + checksum = ((checksum & 0x1ffffff) << 5) ^ value; + for (let i = 0; i < 5; i++) if ((top >>> i) & 1) checksum ^= GENERATORS[i]; + } + return checksum >>> 0; +} + +function expandHrp(hrp) { + const out = []; + for (const char of hrp) out.push(char.charCodeAt(0) >>> 5); + out.push(0); + for (const char of hrp) out.push(char.charCodeAt(0) & 31); + return out; +} + +function convertBits(data, from, to, pad) { + let accumulator = 0; + let bits = 0; + const out = []; + const maximum = (1 << to) - 1; + for (const value of data) { + if (value < 0 || value >>> from) return null; + accumulator = (accumulator << from) | value; + bits += from; + while (bits >= to) { + bits -= to; + out.push((accumulator >>> bits) & maximum); + } + } + if (pad && bits) out.push((accumulator << (to - bits)) & maximum); + else if (!pad && (bits >= from || ((accumulator << (to - bits)) & maximum))) return null; + return out; +} + +export function encodeBech32(hrp, data) { + const words = convertBits(data, 8, 5, true); + const values = [...expandHrp(hrp), ...words]; + const mod = polymod([...values, 0, 0, 0, 0, 0, 0]) ^ 1; + const checksum = []; + for (let i = 0; i < 6; i++) checksum.push((mod >>> (5 * (5 - i))) & 31); + return hrp + "1" + [...words, ...checksum].map((word) => CHARSET[word]).join(""); +} + +export function decodeBech32(value) { + if (typeof value !== "string" || (value !== value.toLowerCase() && value !== value.toUpperCase())) return null; + const normalized = value.toLowerCase(); + const separator = normalized.lastIndexOf("1"); + if (separator < 1 || separator + 7 > normalized.length) return null; + const hrp = normalized.slice(0, separator); + const words = []; + for (const char of normalized.slice(separator + 1)) { + const word = CHARSET.indexOf(char); + if (word === -1) return null; + words.push(word); + } + if (polymod([...expandHrp(hrp), ...words]) !== 1) return null; + const data = convertBits(words.slice(0, -6), 5, 8, false); + return data === null ? null : { hrp, data: new Uint8Array(data) }; +} diff --git a/packages/crypto/src/bytes.js b/packages/crypto/src/bytes.js new file mode 100644 index 0000000..6c35634 --- /dev/null +++ b/packages/crypto/src/bytes.js @@ -0,0 +1,112 @@ +const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +const BASE64_VALUE = new Int16Array(128).fill(-1); +for (let i = 0; i < BASE64.length; i++) BASE64_VALUE[BASE64.charCodeAt(i)] = i; + +export const EMPTY = new Uint8Array(0); +export const utf8 = (value) => new TextEncoder().encode(value); + +export function bytes(value, length, name = "value") { + if (!(value instanceof Uint8Array)) throw new TypeError(`${name} must be a Uint8Array`); + if (length !== undefined && value.length !== length) throw new TypeError(`${name} must be ${length} bytes`); + return value; +} + +export function concat(...parts) { + const length = parts.reduce((sum, part) => sum + bytes(part).length, 0); + const out = new Uint8Array(length); + let offset = 0; + for (const part of parts) { + out.set(part, offset); + offset += part.length; + } + return out; +} + +export function equal(a, b) { + if (!(a instanceof Uint8Array) || !(b instanceof Uint8Array) || a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; + return diff === 0; +} + +export function allZero(value) { + let acc = 0; + for (const byte of value) acc |= byte; + return acc === 0; +} + +export function u32(value) { + if (!Number.isInteger(value) || value < 0 || value > 0xffffffff) throw new RangeError("value is not a uint32"); + const out = new Uint8Array(4); + new DataView(out.buffer).setUint32(0, value, false); + return out; +} + +export function u64(value) { + if (!Number.isSafeInteger(value) || value < 0) throw new RangeError("value is not a non-negative safe integer"); + const out = new Uint8Array(8); + const view = new DataView(out.buffer); + const big = BigInt(value); + view.setUint32(0, Number(big >> 32n), false); + view.setUint32(4, Number(big & 0xffffffffn), false); + return out; +} + +export function readU32(value, offset) { + if (offset < 0 || offset + 4 > value.length) throw new RangeError("truncated uint32"); + return new DataView(value.buffer, value.byteOffset + offset, 4).getUint32(0, false); +} + +export function readU64(value, offset) { + if (offset < 0 || offset + 8 > value.length) throw new RangeError("truncated uint64"); + const view = new DataView(value.buffer, value.byteOffset + offset, 8); + const big = (BigInt(view.getUint32(0, false)) << 32n) | BigInt(view.getUint32(4, false)); + if (big > BigInt(Number.MAX_SAFE_INTEGER)) throw new RangeError("uint64 exceeds JavaScript safe integer range"); + return Number(big); +} + +export function encodeBase64(value, { padding = false } = {}) { + bytes(value); + let out = ""; + for (let i = 0; i < value.length; i += 3) { + const a = value[i]; + const haveB = i + 1 < value.length; + const haveC = i + 2 < value.length; + const b = haveB ? value[i + 1] : 0; + const c = haveC ? value[i + 2] : 0; + out += BASE64[a >>> 2]; + out += BASE64[((a & 3) << 4) | (b >>> 4)]; + if (haveB) out += BASE64[((b & 15) << 2) | (c >>> 6)]; + else if (padding) out += "="; + if (haveC) out += BASE64[c & 63]; + else if (padding) out += "="; + } + return out; +} + +export function decodeBase64(value, { padding = false } = {}) { + if (typeof value !== "string") return null; + if (padding) { + if (!/^[A-Za-z0-9+/]*={0,2}$/.test(value) || value.length % 4 !== 0) return null; + } else if (!/^[A-Za-z0-9+/]*$/.test(value) || value.length % 4 === 1) return null; + + const stripped = padding ? value.replace(/=+$/, "") : value; + const outputLength = Math.floor((stripped.length * 6) / 8); + const out = new Uint8Array(outputLength); + let accumulator = 0; + let bits = 0; + let offset = 0; + for (let i = 0; i < stripped.length; i++) { + const code = stripped.charCodeAt(i); + if (code >= BASE64_VALUE.length || BASE64_VALUE[code] < 0) return null; + accumulator = (accumulator << 6) | BASE64_VALUE[code]; + bits += 6; + if (bits >= 8) { + bits -= 8; + out[offset++] = (accumulator >>> bits) & 0xff; + } + } + if (bits > 0 && (accumulator & ((1 << bits) - 1)) !== 0) return null; + if (encodeBase64(out, { padding }) !== value) return null; + return out; +} diff --git a/packages/crypto/src/error.js b/packages/crypto/src/error.js new file mode 100644 index 0000000..92300aa --- /dev/null +++ b/packages/crypto/src/error.js @@ -0,0 +1,11 @@ +export class PkCryptoError extends Error { + constructor(code, message) { + super(`${code}: ${message}`); + this.name = "PkCryptoError"; + this.code = code; + } +} + +export const fail = (code, message) => { + throw new PkCryptoError(code, message); +}; diff --git a/packages/crypto/src/file.js b/packages/crypto/src/file.js new file mode 100644 index 0000000..6a82f8d --- /dev/null +++ b/packages/crypto/src/file.js @@ -0,0 +1,332 @@ +import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"; +import { hkdf } from "@noble/hashes/hkdf.js"; +import { sha256 } from "@noble/hashes/sha256.js"; +import { randomBytes } from "@noble/hashes/utils.js"; +import { ageInternals } from "./age.js"; +import { bytes, concat, readU32, readU64, u32, u64, utf8 } from "./bytes.js"; +import { fail } from "./error.js"; + +const MAGIC = utf8("PKFILE1\n"); +const VERSION = 1; +const FILE_ID_BYTES = 16; +const NONCE_BYTES = 24; +const TAG_BYTES = 16; +const DEFAULT_CHUNK_SIZE = 1024 * 1024; +const MIN_CHUNK_SIZE = 64 * 1024; +const MAX_CHUNK_SIZE = 16 * 1024 * 1024; +const MAX_HEADER_SIZE = 1024 * 1024; +const MAX_AGE_HEADER_SIZE = 256 * 1024; +const MAX_METADATA_CIPHERTEXT_SIZE = 64 * 1024; + +const METADATA_KEY_INFO = utf8("pksuite/file/metadata-key/v1"); +const CHUNK_KEY_INFO = utf8("pksuite/file/chunk-key/v1"); +const METADATA_AAD_DOMAIN = utf8("pksuite/file/metadata/v1"); +const CHUNK_AAD_DOMAIN = utf8("pksuite/file/chunk/v1"); +const DOMAIN_SEPARATOR = new Uint8Array([0]); + +const encryptionStates = new WeakMap(); +const decryptionStates = new WeakMap(); + +function validateGeometry(plaintextSize, chunkSize) { + if (!Number.isSafeInteger(plaintextSize) || plaintextSize < 0) fail("E_INPUT", "plaintextSize must be a non-negative safe integer"); + if (!Number.isInteger(chunkSize) || chunkSize < MIN_CHUNK_SIZE || chunkSize > MAX_CHUNK_SIZE) + fail("E_INPUT", `chunkSize must be between ${MIN_CHUNK_SIZE} and ${MAX_CHUNK_SIZE} bytes`); + const chunkCount = Math.max(1, Math.ceil(plaintextSize / chunkSize)); + if (chunkCount > 0xffffffff) fail("E_INPUT", "file requires too many chunks"); + return chunkCount; +} + +function expectedPlaintextLength({ plaintextSize, chunkSize, chunkCount }, index) { + if (!Number.isInteger(index) || index < 0 || index >= chunkCount) fail("E_CHUNK_INDEX", "chunk index is outside the envelope"); + if (plaintextSize === 0) return 0; + return index === chunkCount - 1 ? plaintextSize - chunkSize * index : chunkSize; +} + +function canonicalJsonString(value, label) { + let result = '"'; + for (let offset = 0; offset < value.length;) { + const first = value.charCodeAt(offset); + let character; + if (first >= 0xd800 && first <= 0xdbff) { + const second = value.charCodeAt(offset + 1); + if (!(second >= 0xdc00 && second <= 0xdfff)) fail("E_METADATA", `${label} must contain only Unicode scalar values`); + character = value.slice(offset, offset + 2); + offset += 2; + } else { + if (first >= 0xdc00 && first <= 0xdfff) fail("E_METADATA", `${label} must contain only Unicode scalar values`); + character = value[offset++]; + } + if (character === '"') result += '\\"'; + else if (character === "\\") result += "\\\\"; + else if (first === 0x08) result += "\\b"; + else if (first === 0x09) result += "\\t"; + else if (first === 0x0a) result += "\\n"; + else if (first === 0x0c) result += "\\f"; + else if (first === 0x0d) result += "\\r"; + else if (first < 0x20) result += `\\u${first.toString(16).padStart(4, "0")}`; + else result += character; + } + return `${result}"`; +} + +function canonicalMetadata(metadata) { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) fail("E_METADATA", "metadata must be an object"); + if (Object.keys(metadata).some((key) => !["name", "mediaType", "lastModified"].includes(key))) + fail("E_METADATA", "metadata contains an unknown field"); + const { name, mediaType = "application/octet-stream", lastModified = null } = metadata; + if (typeof name !== "string" || name.length === 0 || utf8(name).length > 4096 || /[\u0000-\u001f\u007f]/u.test(name)) + fail("E_METADATA", "metadata.name must be a non-empty filename without control characters"); + if (name === "." || name === ".." || /[\\/:]/u.test(name) || /[ .]$/u.test(name) || /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu.test(name)) + fail("E_METADATA", "metadata.name must be a portable single filename, not a path or device name"); + if (typeof mediaType !== "string" || utf8(mediaType).length > 255 || !/^[\x20-\x7e]*$/.test(mediaType)) + fail("E_METADATA", "metadata.mediaType must be printable ASCII no longer than 255 bytes"); + if (lastModified !== null && (!Number.isSafeInteger(lastModified) || lastModified < 0)) + fail("E_METADATA", "metadata.lastModified must be null or a non-negative safe integer"); + return utf8(`{"name":${canonicalJsonString(name, "metadata.name")},"mediaType":${canonicalJsonString(mediaType, "metadata.mediaType")},"lastModified":${lastModified === null ? "null" : String(lastModified)}}`); +} + +function parseMetadata(plaintext) { + let parsed; + try { parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(plaintext)); } + catch { fail("E_METADATA", "encrypted metadata is not canonical UTF-8 JSON"); } + const canonical = canonicalMetadata(parsed); + if (canonical.length !== plaintext.length || canonical.some((byte, index) => byte !== plaintext[index])) + fail("E_METADATA", "encrypted metadata is not canonical"); + return parsed; +} + +function structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader }) { + return concat(u32(chunkSize), u64(plaintextSize), u32(chunkCount), fileId, u32(ageHeader.length), ageHeader); +} + +function metadataAad(prefix) { + return concat(METADATA_AAD_DOMAIN, DOMAIN_SEPARATOR, u32(VERSION), sha256(concat(MAGIC, prefix))); +} + +function chunkAad(record, index) { + const expected = expectedPlaintextLength(record, index); + return concat( + CHUNK_AAD_DOMAIN, + DOMAIN_SEPARATOR, + u32(VERSION), + record.fileId, + record.headerCommitment, + u32(index), + u32(record.chunkCount), + u64(record.plaintextSize), + u32(record.chunkSize), + u32(expected), + ); +} + +function deriveMetadataKey(fileKey, fileId) { + return hkdf(sha256, fileKey, fileId, METADATA_KEY_INFO, 32); +} + +function deriveChunkKey(fileKey, fileId, index) { + return hkdf(sha256, fileKey, concat(fileId, u32(index)), CHUNK_KEY_INFO, 32); +} + +function parseEnvelopeHeader(header) { + bytes(header); + if (header.length < MAGIC.length + 4 || MAGIC.some((byte, index) => header[index] !== byte)) + fail("E_ENVELOPE_VERSION", "unsupported file envelope magic or version"); + let bodyLength; + try { bodyLength = readU32(header, MAGIC.length); } + catch { fail("E_ENVELOPE_PARSE", "truncated file envelope prelude"); } + if (bodyLength > MAX_HEADER_SIZE || bodyLength !== header.length - MAGIC.length - 4) + fail("E_ENVELOPE_PARSE", "file envelope header length mismatch"); + let offset = MAGIC.length + 4; + const take = (length, label) => { + if (!Number.isInteger(length) || length < 0 || offset + length > header.length) fail("E_ENVELOPE_PARSE", `truncated ${label}`); + const value = header.subarray(offset, offset + length); + offset += length; + return value; + }; + + let chunkSize, plaintextSize, chunkCount, ageHeaderLength, metadataLength; + try { + chunkSize = readU32(header, offset); offset += 4; + plaintextSize = readU64(header, offset); offset += 8; + chunkCount = readU32(header, offset); offset += 4; + } catch { fail("E_ENVELOPE_PARSE", "invalid file geometry"); } + const fileId = take(FILE_ID_BYTES, "file id"); + try { ageHeaderLength = readU32(header, offset); offset += 4; } + catch { fail("E_ENVELOPE_PARSE", "truncated age header length"); } + if (ageHeaderLength === 0 || ageHeaderLength > MAX_AGE_HEADER_SIZE) fail("E_ENVELOPE_PARSE", "invalid age header length"); + const ageHeader = take(ageHeaderLength, "age header"); + const prefixEnd = offset; + const metadataNonce = take(NONCE_BYTES, "metadata nonce"); + try { metadataLength = readU32(header, offset); offset += 4; } + catch { fail("E_ENVELOPE_PARSE", "truncated metadata length"); } + if (metadataLength < TAG_BYTES || metadataLength > MAX_METADATA_CIPHERTEXT_SIZE) fail("E_ENVELOPE_PARSE", "invalid metadata ciphertext length"); + const metadataCiphertext = take(metadataLength, "metadata ciphertext"); + if (offset !== header.length) fail("E_ENVELOPE_PARSE", "trailing bytes in file envelope header"); + + const expectedCount = validateGeometry(plaintextSize, chunkSize); + if (chunkCount !== expectedCount) fail("E_ENVELOPE_PARSE", "chunk count does not match file geometry"); + return { + chunkSize, + plaintextSize, + chunkCount, + fileId, + ageHeader, + prefix: header.subarray(MAGIC.length + 4, prefixEnd), + metadataNonce, + metadataCiphertext, + headerCommitment: sha256(header), + }; +} + +function makePublicState(kind, record, header, metadata) { + return Object.freeze({ + kind, + version: VERSION, + header, + metadata, + plaintextSize: record.plaintextSize, + chunkSize: record.chunkSize, + chunkCount: record.chunkCount, + }); +} + +function createFileEncryptionImpl({ recipient, recipients, metadata, plaintextSize, chunkSize = DEFAULT_CHUNK_SIZE }, rng) { + if (recipient !== undefined && recipients !== undefined) fail("E_INPUT", "provide recipient or recipients, not both"); + const recipientList = recipients ?? (recipient === undefined ? [] : [recipient]); + const chunkCount = validateGeometry(plaintextSize, chunkSize); + const fileKey = bytes(rng(16), 16, "random file key"); + let retained = false; + try { + const fileId = bytes(rng(FILE_ID_BYTES), FILE_ID_BYTES, "random file id"); + const ageHeader = ageInternals.encryptAgeHeader(fileKey, recipientList, rng); + if (ageHeader.length > MAX_AGE_HEADER_SIZE) fail("E_INPUT", "recipient header is too large"); + const prefix = structuralPrefix({ chunkSize, plaintextSize, chunkCount, fileId, ageHeader }); + const metadataNonce = bytes(rng(NONCE_BYTES), NONCE_BYTES, "random metadata nonce"); + const metadataPlaintext = canonicalMetadata(metadata); + const metadataKey = deriveMetadataKey(fileKey, fileId); + let metadataCiphertext; + try { metadataCiphertext = xchacha20poly1305(metadataKey, metadataNonce, metadataAad(prefix)).encrypt(metadataPlaintext); } + finally { metadataKey.fill(0); metadataPlaintext.fill(0); } + if (metadataCiphertext.length > MAX_METADATA_CIPHERTEXT_SIZE) fail("E_METADATA", "encrypted metadata is too large"); + const body = concat(prefix, metadataNonce, u32(metadataCiphertext.length), metadataCiphertext); + if (body.length > MAX_HEADER_SIZE) fail("E_INPUT", "file envelope header is too large"); + const header = concat(MAGIC, u32(body.length), body); + const record = { + fileKey, + fileId: new Uint8Array(fileId), + plaintextSize, + chunkSize, + chunkCount, + headerCommitment: sha256(header), + rng, + seen: new Set(), + }; + const state = makePublicState("encryption", record, header, undefined); + encryptionStates.set(state, record); + retained = true; + return state; + } finally { + if (!retained) fileKey.fill(0); + } +} + +export function createFileEncryption(options) { + return createFileEncryptionImpl(options, randomBytes); +} + +export function encryptChunk({ state, index, plaintext }) { + const record = encryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-encryption state"); + bytes(plaintext); + const expected = expectedPlaintextLength(record, index); + if (plaintext.length !== expected) fail("E_CHUNK_SIZE", `chunk ${index} must contain exactly ${expected} plaintext bytes`); + const nonce = bytes(record.rng(NONCE_BYTES), NONCE_BYTES, "random chunk nonce"); + const key = deriveChunkKey(record.fileKey, record.fileId, index); + let ciphertext; + try { ciphertext = xchacha20poly1305(key, nonce, chunkAad(record, index)).encrypt(plaintext); } + finally { key.fill(0); } + record.seen.add(index); + return concat(nonce, ciphertext); +} + +export function openFileEnvelope({ identity, header }) { + const parsed = parseEnvelopeHeader(header); + const fileKey = ageInternals.decryptAgeHeader(parsed.ageHeader, identity); + let retained = false; + try { + const metadataKey = deriveMetadataKey(fileKey, parsed.fileId); + let metadataPlaintext; + try { metadataPlaintext = xchacha20poly1305(metadataKey, parsed.metadataNonce, metadataAad(parsed.prefix)).decrypt(parsed.metadataCiphertext); } + catch { fail("E_ENVELOPE_AUTH", "file metadata authentication failed"); } + finally { metadataKey.fill(0); } + let metadata; + try { metadata = parseMetadata(metadataPlaintext); } + finally { metadataPlaintext.fill(0); } + const record = { + fileKey, + fileId: new Uint8Array(parsed.fileId), + plaintextSize: parsed.plaintextSize, + chunkSize: parsed.chunkSize, + chunkCount: parsed.chunkCount, + headerCommitment: new Uint8Array(parsed.headerCommitment), + seen: new Set(), + }; + const state = makePublicState("decryption", record, new Uint8Array(header), metadata); + decryptionStates.set(state, record); + retained = true; + return state; + } finally { + if (!retained) fileKey.fill(0); + } +} + +export function decryptChunk({ state, index, ciphertext }) { + const record = decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-decryption state"); + bytes(ciphertext); + const expected = expectedPlaintextLength(record, index); + if (ciphertext.length !== NONCE_BYTES + expected + TAG_BYTES) + fail("E_CHUNK_SIZE", `chunk ${index} ciphertext has the wrong length`); + const nonce = ciphertext.subarray(0, NONCE_BYTES); + const body = ciphertext.subarray(NONCE_BYTES); + const key = deriveChunkKey(record.fileKey, record.fileId, index); + let plaintext; + try { plaintext = xchacha20poly1305(key, nonce, chunkAad(record, index)).decrypt(body); } + catch { fail("E_CHUNK_AUTH", `chunk ${index} authentication failed`); } + finally { key.fill(0); } + record.seen.add(index); + return plaintext; +} + +export function finalizeEnvelope(state) { + const record = encryptionStates.get(state) ?? decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not a file-envelope state"); + if (record.seen.size !== record.chunkCount) fail("E_ENVELOPE_INCOMPLETE", `expected ${record.chunkCount} authenticated chunks, observed ${record.seen.size}`); + const finalized = Object.freeze({ + version: VERSION, + header: state.header, + metadata: state.metadata, + plaintextSize: record.plaintextSize, + chunkSize: record.chunkSize, + chunkCount: record.chunkCount, + }); + disposeEnvelope(state); + return finalized; +} + +export function disposeEnvelope(state) { + const record = encryptionStates.get(state) ?? decryptionStates.get(state); + if (!record) fail("E_STATE", "state is not an active file-envelope state"); + record.fileKey.fill(0); + record.seen.clear(); + encryptionStates.delete(state); + decryptionStates.delete(state); +} + +export const fileInternals = Object.freeze({ + createFileEncryptionWithRandom: createFileEncryptionImpl, + parseEnvelopeHeader, + metadataAad, + chunkAad, + constants: Object.freeze({ MAGIC, VERSION, NONCE_BYTES, TAG_BYTES, DEFAULT_CHUNK_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE }), +}); diff --git a/packages/crypto/src/index.d.ts b/packages/crypto/src/index.d.ts new file mode 100644 index 0000000..1645cdf --- /dev/null +++ b/packages/crypto/src/index.d.ts @@ -0,0 +1,78 @@ +export class PkCryptoError extends Error { + readonly code: string; +} + +export interface RecipientIdentity { + identity: string; + recipient: string; +} + +export interface FileMetadata { + name: string; + mediaType?: string; + lastModified?: number | null; +} + +export interface FileEnvelopeState { + readonly kind: "encryption" | "decryption"; + readonly version: 1; + readonly header: Uint8Array; + readonly metadata?: Required; + readonly plaintextSize: number; + readonly chunkSize: number; + readonly chunkCount: number; +} + +export interface FinalizedEnvelope { + readonly version: 1; + readonly header: Uint8Array; + readonly metadata?: Required; + readonly plaintextSize: number; + readonly chunkSize: number; + readonly chunkCount: number; +} + +export function createRecipientIdentity(): RecipientIdentity; +export function createRecipientIdentityFromSecret(secretKey: Uint8Array): RecipientIdentity; +export function formatIdentity(secretKey: Uint8Array): string; +export function formatRecipient(publicKey: Uint8Array): string; +export function parseIdentity(identity: string): Uint8Array; +export function parseRecipient(recipient: string): Uint8Array; +export function publicKeyFromSecretKey(secretKey: Uint8Array): Uint8Array; +export function recipientFromIdentity(identity: string): string; + +export const ageHeader: Readonly<{ + seal(fileKey: Uint8Array, recipients: Array): Uint8Array; + /** The caller owns the returned secret key bytes and should overwrite them after use. */ + open(header: Uint8Array, identity: string | Uint8Array): Uint8Array; +}>; + +export function createFileEncryption(options: { + recipient?: string; + recipients?: string[]; + metadata: FileMetadata; + plaintextSize: number; + chunkSize?: number; +}): FileEnvelopeState; + +export function encryptChunk(options: { + state: FileEnvelopeState; + index: number; + plaintext: Uint8Array; +}): Uint8Array; + +export function openFileEnvelope(options: { + identity: string | Uint8Array; + header: Uint8Array; +}): FileEnvelopeState & { readonly kind: "decryption"; readonly metadata: Required }; + +export function decryptChunk(options: { + state: FileEnvelopeState; + index: number; + ciphertext: Uint8Array; +}): Uint8Array; + +/** Best-effort zeroization for an incomplete or abandoned envelope state. */ +export function disposeEnvelope(state: FileEnvelopeState): void; + +export function finalizeEnvelope(state: FileEnvelopeState): FinalizedEnvelope; diff --git a/packages/crypto/src/index.js b/packages/crypto/src/index.js new file mode 100644 index 0000000..5a7aaca --- /dev/null +++ b/packages/crypto/src/index.js @@ -0,0 +1,20 @@ +export { PkCryptoError } from "./error.js"; +export { + ageHeader, + createRecipientIdentity, + createRecipientIdentityFromSecret, + formatIdentity, + formatRecipient, + parseIdentity, + parseRecipient, + publicKeyFromSecretKey, + recipientFromIdentity, +} from "./age.js"; +export { + createFileEncryption, + decryptChunk, + disposeEnvelope, + encryptChunk, + finalizeEnvelope, + openFileEnvelope, +} from "./file.js"; diff --git a/packages/crypto/test/file.test.js b/packages/crypto/test/file.test.js new file mode 100644 index 0000000..6da49bd --- /dev/null +++ b/packages/crypto/test/file.test.js @@ -0,0 +1,407 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createCipheriv, createDecipheriv, createHash, createHmac, hkdfSync } from "node:crypto"; +import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import { + createFileEncryption, + createRecipientIdentity, + createRecipientIdentityFromSecret, + decryptChunk, + disposeEnvelope, + encryptChunk, + finalizeEnvelope, + openFileEnvelope, + ageHeader, + parseIdentity, + parseRecipient, + recipientFromIdentity, +} from "../dist/index.js"; +import { ageInternals } from "../src/age.js"; +import { + disposeEnvelope as disposeSourceEnvelope, + encryptChunk as encryptSourceChunk, + fileInternals, + finalizeEnvelope as finalizeSourceEnvelope, +} from "../src/file.js"; + +const require = createRequire(import.meta.url); +const cjs = require("../dist/index.cjs"); +const pkvaultAge = require("../../../apps/pkvault/src/age.js"); +const vector = JSON.parse(readFileSync(new URL("../vectors/file-v1.json", import.meta.url), "utf8")); +const tamperingVector = JSON.parse(readFileSync(new URL("../vectors/file-v1-tampering.json", import.meta.url), "utf8")); +const externalAge = JSON.parse(readFileSync(new URL("../vectors/age-cli-v1.json", import.meta.url), "utf8")); +const externalAgeBytes = readFileSync(new URL("../vectors/age-cli-v1.age", import.meta.url)); +const b64 = (value) => new Uint8Array(Buffer.from(value, "base64")); +const hex = (value) => Buffer.from(value, "hex"); +const sha = (value) => createHash("sha256").update(value).digest("hex"); +const throwsCode = (fn, code) => assert.throws(fn, (error) => { + assert.equal(error.code, code, error.message); + return true; +}); + +test("published artifacts execute declared noble dependencies instead of vendoring them", () => { + const esm = readFileSync(new URL("../dist/index.js", import.meta.url), "utf8"); + const commonjs = readFileSync(new URL("../dist/index.cjs", import.meta.url), "utf8"); + assert.match(esm, /from "@noble\/ciphers\/chacha\.js"/); + assert.match(commonjs, /require\("@noble\/ciphers\/chacha\.js"\)/); + for (const name of ["index.js.map", "index.cjs.map"]) { + const map = JSON.parse(readFileSync(new URL(`../dist/${name}`, import.meta.url), "utf8")); + assert.equal(map.sources.some((source) => source.includes("node_modules/@noble") || source.includes("node_modules/.pnpm/@noble")), false); + } +}); + +function chunksFor(state, fill = (index, length) => new Uint8Array(length).fill(index + 1)) { + const chunks = []; + for (let index = 0; index < state.chunkCount; index++) { + const remaining = state.plaintextSize - index * state.chunkSize; + const length = state.plaintextSize === 0 ? 0 : Math.min(state.chunkSize, remaining); + chunks.push(fill(index, length)); + } + return chunks; +} + +test("canonical age identity handling is shared with pkvault", () => { + const secret = createHash("sha256").update("shared-age-identity").digest(); + const shared = createRecipientIdentityFromSecret(secret); + const legacy = pkvaultAge.keygen(secret); + assert.equal(shared.identity, legacy.identity); + assert.equal(shared.recipient, legacy.recipient); + assert.deepEqual(Buffer.from(parseIdentity(shared.identity)), secret); + assert.equal(recipientFromIdentity(shared.identity), shared.recipient); + assert.deepEqual(Buffer.from(parseRecipient(shared.recipient)), legacy.scalar.length === 32 ? pkvaultAge.decodeRecipient(legacy.recipient) : null); +}); + +test("pkvault integration reads an age payload built around the package header", () => { + const secret = createHash("sha256").update("node-age-reader").digest(); + const identity = createRecipientIdentityFromSecret(secret); + const fileKey = Uint8Array.from({ length: 16 }, (_, index) => index); + const ephemeral = Uint8Array.from({ length: 32 }, (_, index) => 0x20 + index); + const header = ageInternals.encryptAgeHeader(fileKey, [identity.recipient], (length) => { + assert.equal(length, 32); + return ephemeral; + }); + + const plaintext = createHash("sha256").update("age payload").digest(); + const nonce16 = Buffer.alloc(16, 0x44); + const payloadKey = Buffer.from(hkdfSync("sha256", fileKey, nonce16, Buffer.from("payload"), 32)); + const chunkNonce = Buffer.alloc(12); + chunkNonce[11] = 1; + const cipher = createCipheriv("chacha20-poly1305", payloadKey, chunkNonce, { authTagLength: 16 }); + const body = Buffer.concat([cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]); + const ageFile = Buffer.concat([Buffer.from(header), nonce16, body]); + assert.deepEqual(pkvaultAge.decrypt(ageFile, secret, 1), plaintext); +}); + +test("frozen external age CLI output opens in package and independent pkvault readers", () => { + assert.equal(sha(externalAgeBytes), externalAge.ciphertext_sha256); + const secret = Buffer.from(externalAge.secret_key_hex, "hex"); + const expectedPlaintext = Buffer.from(externalAge.plaintext_hex, "hex"); + assert.deepEqual(pkvaultAge.decrypt(externalAgeBytes, secret, 1), expectedPlaintext); + + let position = 0; + let headerEnd = -1; + while (position < externalAgeBytes.length) { + const newline = externalAgeBytes.indexOf(0x0a, position); + assert.notEqual(newline, -1); + if (externalAgeBytes.subarray(position, newline).toString().startsWith("--- ")) { headerEnd = newline + 1; break; } + position = newline + 1; + } + assert.notEqual(headerEnd, -1); + const fileKey = ageHeader.open(externalAgeBytes.subarray(0, headerEnd), externalAge.identity); + const nonce16 = externalAgeBytes.subarray(headerEnd, headerEnd + 16); + const body = externalAgeBytes.subarray(headerEnd + 16); + const payloadKey = Buffer.from(hkdfSync("sha256", fileKey, nonce16, Buffer.from("payload"), 32)); + const chunkNonce = Buffer.alloc(12); + chunkNonce[11] = 1; + const decipher = createDecipheriv("chacha20-poly1305", payloadKey, chunkNonce, { authTagLength: 16 }); + decipher.setAuthTag(body.subarray(-16)); + const plaintext = Buffer.concat([decipher.update(body.subarray(0, -16)), decipher.final()]); + assert.deepEqual(plaintext, expectedPlaintext); + fileKey.fill(0); + payloadKey.fill(0); +}); + +test("strict age reader rejects a foreign stanza even after a matching recipient", () => { + const secret = createHash("sha256").update("strict-age-reader").digest(); + const identity = createRecipientIdentityFromSecret(secret); + const fileKey = Uint8Array.from({ length: 16 }, (_, index) => index + 1); + const base = new TextDecoder().decode(ageInternals.encryptAgeHeader(fileKey, [identity.recipient], () => new Uint8Array(32).fill(7))); + const marker = base.lastIndexOf("--- "); + assert.notEqual(marker, -1); + const authenticated = `${base.slice(0, marker)}-> future-recipient test\nAAAA\n---`; + const macKey = Buffer.from(hkdfSync("sha256", fileKey, Buffer.alloc(0), Buffer.from("header"), 32)); + const mac = createHmac("sha256", macKey).update(authenticated).digest("base64").replace(/=+$/, ""); + const header = new TextEncoder().encode(`${authenticated} ${mac}\n`); + throwsCode(() => ageInternals.decryptAgeHeader(header, identity.identity), "E_AGE_HEADER_RECIPIENT"); +}); + +for (const [label, api] of [["browser ESM bundle", { openFileEnvelope, decryptChunk, finalizeEnvelope }], ["Node CJS bundle", cjs]]) { + test(`${label} opens the frozen PKFILE1 vector`, () => { + const header = b64(vector.file.header_base64); + assert.equal(sha(header), vector.file.header_sha256); + const state = api.openFileEnvelope({ identity: vector.identity.identity, header }); + assert.deepEqual(state.metadata, vector.file.metadata); + assert.equal(state.chunkCount, 2); + for (const entry of vector.chunks) { + const ciphertext = b64(entry.ciphertext_base64); + assert.equal(sha(ciphertext), entry.ciphertext_sha256); + const plaintext = api.decryptChunk({ state, index: entry.index, ciphertext }); + assert.equal(sha(plaintext), entry.plaintext_sha256); + if (entry.plaintext_hex) assert.equal(Buffer.from(plaintext).toString("hex"), entry.plaintext_hex); + } + api.finalizeEnvelope(state); + }); +} + +test("every published tampering-vector case executes and fails closed", () => { + assert.equal(tamperingVector.cases.length, 9); + const header = b64(vector.file.header_base64); + const chunks = vector.chunks.map((entry) => b64(entry.ciphertext_base64)); + const open = (candidateHeader = header) => openFileEnvelope({ identity: vector.identity.identity, header: candidateHeader }); + for (const entry of tamperingVector.cases) { + switch (entry.operation) { + case "open_wrong_identity": + throwsCode(() => openFileEnvelope({ identity: tamperingVector.wrong_identity, header }), entry.expected_code); + break; + case "xor_header_byte": { + const mutated = new Uint8Array(header); + mutated[mutated.length - entry.offset_from_end] ^= entry.xor; + throwsCode(() => open(mutated), entry.expected_code); + break; + } + case "xor_chunk_byte": { + const mutated = new Uint8Array(chunks[entry.chunk]); + mutated[mutated.length - entry.offset_from_end] ^= entry.xor; + throwsCode(() => decryptChunk({ state: open(), index: entry.chunk, ciphertext: mutated }), entry.expected_code); + break; + } + case "truncate_chunk": + throwsCode(() => decryptChunk({ state: open(), index: entry.chunk, ciphertext: chunks[entry.chunk].subarray(0, -entry.bytes) }), entry.expected_code); + break; + case "extend_chunk": + throwsCode(() => decryptChunk({ state: open(), index: entry.chunk, ciphertext: new Uint8Array([...chunks[entry.chunk], ...hex(entry.hex)]) }), entry.expected_code); + break; + case "present_chunk_at_index": + throwsCode(() => decryptChunk({ state: open(), index: entry.index, ciphertext: chunks[entry.chunk] }), entry.expected_code); + break; + case "omit_chunk_then_finalize": { + const state = open(); + for (const item of vector.chunks) if (item.index !== entry.omit) + decryptChunk({ state, index: item.index, ciphertext: chunks[item.index] }); + throwsCode(() => finalizeEnvelope(state), entry.expected_code); + disposeEnvelope(state); + break; + } + case "substitute_same_geometry_file": { + const other = createFileEncryption({ + recipient: vector.identity.recipient, + plaintextSize: vector.file.plaintext_size, + chunkSize: vector.file.chunk_size, + metadata: { name: "other.bin" }, + }); + const substitute = encryptChunk({ state: other, index: entry.chunk, plaintext: new Uint8Array(vector.file.chunk_size) }); + throwsCode(() => decryptChunk({ state: open(), index: entry.chunk, ciphertext: substitute }), entry.expected_code); + disposeEnvelope(other); + break; + } + case "assert_retry_nonce_distinct": { + const state = createFileEncryption({ recipient: vector.identity.recipient, plaintextSize: vector.file.chunk_size, chunkSize: vector.file.chunk_size, metadata: { name: "retry.bin" } }); + const plaintext = new Uint8Array(vector.file.chunk_size); + const first = encryptChunk({ state, index: entry.chunk, plaintext }); + const second = encryptChunk({ state, index: entry.chunk, plaintext }); + assert.notDeepEqual(first.subarray(0, 24), second.subarray(0, 24), entry.id); + disposeEnvelope(state); + break; + } + default: + assert.fail(`unimplemented tampering-vector operation: ${entry.operation}`); + } + } +}); + +test("streaming round-trip, retry, and empty files do not require whole-file buffering", () => { + const identity = createRecipientIdentity(); + for (const size of [0, 2 * 65536 + 17, 8 * 1024 * 1024 + 3]) { + const encryption = createFileEncryption({ + recipient: identity.recipient, + plaintextSize: size, + chunkSize: 65536, + metadata: { name: `file-${size}.bin` }, + }); + const plaintextChunks = chunksFor(encryption, (index, length) => Uint8Array.from({ length }, (_, offset) => (index + offset) & 0xff)); + const ciphertextChunks = plaintextChunks.map((plaintext, index) => encryptChunk({ state: encryption, index, plaintext })); + const retry = encryptChunk({ state: encryption, index: 0, plaintext: plaintextChunks[0] }); + assert.notDeepEqual(retry.subarray(0, 24), ciphertextChunks[0].subarray(0, 24), "retry must use a fresh XChaCha nonce"); + finalizeEnvelope(encryption); + + const decryption = openFileEnvelope({ identity: identity.identity, header: encryption.header }); + for (let index = 0; index < ciphertextChunks.length; index++) { + assert.deepEqual(decryptChunk({ state: decryption, index, ciphertext: ciphertextChunks[index] }), plaintextChunks[index]); + } + finalizeEnvelope(decryption); + + // A multipart adapter may atomically replace an index with a freshly + // encrypted retry. Both the original and replacement must authenticate, + // while the replacement nonce must be independent. + const resumed = openFileEnvelope({ identity: identity.identity, header: encryption.header }); + assert.deepEqual(decryptChunk({ state: resumed, index: 0, ciphertext: retry }), plaintextChunks[0]); + for (let index = 1; index < ciphertextChunks.length; index++) { + decryptChunk({ state: resumed, index, ciphertext: ciphertextChunks[index] }); + } + finalizeEnvelope(resumed); + } +}); + +test("filename and file metadata are encrypted", () => { + const identity = createRecipientIdentity(); + const marker = "private-client-name-7f3d.pdf"; + const state = createFileEncryption({ + recipient: identity.recipient, + plaintextSize: 0, + metadata: { name: marker, mediaType: "application/pdf", lastModified: 1234 }, + }); + assert.equal(Buffer.from(state.header).includes(Buffer.from(marker)), false); + assert.equal(Buffer.from(state.header).includes(Buffer.from("application/pdf")), false); + const ciphertext = encryptChunk({ state, index: 0, plaintext: new Uint8Array() }); + finalizeEnvelope(state); + const opened = openFileEnvelope({ identity: identity.identity, header: state.header }); + assert.deepEqual(opened.metadata, { name: marker, mediaType: "application/pdf", lastModified: 1234 }); + decryptChunk({ state: opened, index: 0, ciphertext }); + finalizeEnvelope(opened); +}); + +test("metadata filenames are portable single components, never paths or device names", () => { + const recipient = createRecipientIdentity().recipient; + const options = (name) => ({ recipient, plaintextSize: 0, metadata: { name } }); + for (const name of ["../../.ssh/authorized_keys", "..\\..\\boot.ini", "/etc/passwd", "C:\\boot.ini", ".", "..", "NUL.txt", "report. "]) + throwsCode(() => createFileEncryption(options(name)), "E_METADATA"); + const active = createFileEncryption(options("safe report.pdf")); + disposeEnvelope(active); + throwsCode(() => encryptChunk({ state: active, index: 0, plaintext: new Uint8Array() }), "E_STATE"); +}); + +test("metadata encoding is explicit, canonical, and rejects unknown or non-scalar input", () => { + const identity = createRecipientIdentity(); + const base = { recipient: identity.recipient, plaintextSize: 0 }; + throwsCode(() => createFileEncryption({ ...base, metadata: { name: "x", unexpected: true } }), "E_METADATA"); + throwsCode(() => createFileEncryption({ ...base, metadata: { name: "bad\ud800.pdf" } }), "E_METADATA"); + const metadata = { name: 'quote " only.pdf', mediaType: 'text/x-"quoted"\\value' }; + const state = createFileEncryption({ ...base, metadata }); + const ciphertext = encryptChunk({ state, index: 0, plaintext: new Uint8Array() }); + finalizeEnvelope(state); + const opened = openFileEnvelope({ identity: identity.identity, header: state.header }); + assert.deepEqual(opened.metadata, { ...metadata, lastModified: null }); + decryptChunk({ state: opened, index: 0, ciphertext }); + finalizeEnvelope(opened); +}); + +test("finalization and disposal overwrite retained file-key bytes", () => { + const recipient = createRecipientIdentity().recipient; + const make = () => { + const fileKey = new Uint8Array(16).fill(0x11); + const ephemeral = new Uint8Array(32).fill(0x33); + const draws = [fileKey, new Uint8Array(16).fill(0x22), ephemeral, new Uint8Array(24).fill(0x44), new Uint8Array(24).fill(0x55)]; + const state = fileInternals.createFileEncryptionWithRandom({ recipient, plaintextSize: 0, metadata: { name: "wipe.bin" } }, (length) => { + const value = draws.shift(); + assert.equal(value?.length, length); + return value; + }); + assert.ok(ephemeral.every((byte) => byte === 0), "age ephemeral secret should be overwritten immediately"); + return { state, fileKey }; + }; + + const completed = make(); + encryptSourceChunk({ state: completed.state, index: 0, plaintext: new Uint8Array() }); + finalizeSourceEnvelope(completed.state); + assert.ok(completed.fileKey.every((byte) => byte === 0)); + + const abandoned = make(); + disposeSourceEnvelope(abandoned.state); + assert.ok(abandoned.fileKey.every((byte) => byte === 0)); +}); + +test("wrong recipients, non-canonical recipients, and mixed-case identities fail", () => { + const owner = createRecipientIdentity(); + const stranger = createRecipientIdentity(); + const state = createFileEncryption({ recipient: owner.recipient, plaintextSize: 0, metadata: { name: "x" } }); + throwsCode(() => openFileEnvelope({ identity: stranger.identity, header: state.header }), "E_WRONG_RECIPIENT"); + throwsCode(() => parseRecipient(owner.recipient.toUpperCase()), "E_RECIPIENT"); + assert.deepEqual(parseIdentity(owner.identity.toLowerCase()), parseIdentity(owner.identity)); + throwsCode(() => parseIdentity(`${owner.identity.slice(0, 1).toLowerCase()}${owner.identity.slice(1)}`), "E_IDENTITY"); +}); + +test("header and encrypted metadata tampering fail closed", () => { + const identity = createRecipientIdentity(); + const state = createFileEncryption({ recipient: identity.recipient, plaintextSize: 10, metadata: { name: "secret.pdf" } }); + const tamperedMetadata = new Uint8Array(state.header); + tamperedMetadata[tamperedMetadata.length - 1] ^= 1; + throwsCode(() => openFileEnvelope({ identity: identity.identity, header: tamperedMetadata }), "E_ENVELOPE_AUTH"); + + const tamperedGeometry = new Uint8Array(state.header); + tamperedGeometry[15] ^= 1; // chunk_size, authenticated through metadata AAD + assert.throws(() => openFileEnvelope({ identity: identity.identity, header: tamperedGeometry }), + (error) => ["E_ENVELOPE_PARSE", "E_ENVELOPE_AUTH"].includes(error.code)); +}); + +test("opened state is detached from the caller's mutable header buffer", () => { + const identity = createRecipientIdentity(); + const encryption = createFileEncryption({ recipient: identity.recipient, plaintextSize: 65536, chunkSize: 65536, metadata: { name: "detached.bin" } }); + const plaintext = new Uint8Array(65536).fill(0x5a); + const ciphertext = encryptChunk({ state: encryption, index: 0, plaintext }); + finalizeEnvelope(encryption); + + const callerHeader = new Uint8Array(encryption.header); + const opened = openFileEnvelope({ identity: identity.identity, header: callerHeader }); + callerHeader.fill(0); + assert.equal(new TextDecoder().decode(opened.header.subarray(0, 8)), "PKFILE1\n"); + assert.deepEqual(decryptChunk({ state: opened, index: 0, ciphertext }), plaintext); + finalizeEnvelope(opened); +}); + +test("chunk tampering, truncation, extension, and wrong index fail closed", () => { + const identity = createRecipientIdentity(); + const encryption = createFileEncryption({ recipient: identity.recipient, plaintextSize: 2 * 65536, chunkSize: 65536, metadata: { name: "two.bin" } }); + const [plain0, plain1] = chunksFor(encryption); + const chunk0 = encryptChunk({ state: encryption, index: 0, plaintext: plain0 }); + const chunk1 = encryptChunk({ state: encryption, index: 1, plaintext: plain1 }); + finalizeEnvelope(encryption); + + const tampered = new Uint8Array(chunk0); + tampered[tampered.length - 1] ^= 1; + throwsCode(() => decryptChunk({ state: openFileEnvelope({ identity: identity.identity, header: encryption.header }), index: 0, ciphertext: tampered }), "E_CHUNK_AUTH"); + throwsCode(() => decryptChunk({ state: openFileEnvelope({ identity: identity.identity, header: encryption.header }), index: 0, ciphertext: chunk0.subarray(0, -1) }), "E_CHUNK_SIZE"); + throwsCode(() => decryptChunk({ state: openFileEnvelope({ identity: identity.identity, header: encryption.header }), index: 0, ciphertext: new Uint8Array([...chunk0, 0]) }), "E_CHUNK_SIZE"); + throwsCode(() => decryptChunk({ state: openFileEnvelope({ identity: identity.identity, header: encryption.header }), index: 1, ciphertext: chunk0 }), "E_CHUNK_AUTH"); + + const valid = openFileEnvelope({ identity: identity.identity, header: encryption.header }); + decryptChunk({ state: valid, index: 0, ciphertext: chunk0 }); + decryptChunk({ state: valid, index: 1, ciphertext: chunk1 }); + finalizeEnvelope(valid); +}); + +test("cross-file chunk substitution fails even with the same recipient and geometry", () => { + const identity = createRecipientIdentity(); + const make = (name, fill) => { + const state = createFileEncryption({ recipient: identity.recipient, plaintextSize: 65536, chunkSize: 65536, metadata: { name } }); + const ciphertext = encryptChunk({ state, index: 0, plaintext: new Uint8Array(65536).fill(fill) }); + finalizeEnvelope(state); + return { state, ciphertext }; + }; + const a = make("a.bin", 1); + const b = make("b.bin", 2); + throwsCode(() => decryptChunk({ state: openFileEnvelope({ identity: identity.identity, header: b.state.header }), index: 0, ciphertext: a.ciphertext }), "E_CHUNK_AUTH"); +}); + +test("finalization detects missing chunks after an interrupted resume", () => { + const identity = createRecipientIdentity(); + const encryption = createFileEncryption({ recipient: identity.recipient, plaintextSize: 3 * 65536, chunkSize: 65536, metadata: { name: "resume.bin" } }); + const chunks = chunksFor(encryption); + const ciphertext0 = encryptChunk({ state: encryption, index: 0, plaintext: chunks[0] }); + encryptChunk({ state: encryption, index: 2, plaintext: chunks[2] }); + throwsCode(() => finalizeEnvelope(encryption), "E_ENVELOPE_INCOMPLETE"); + + const decryption = openFileEnvelope({ identity: identity.identity, header: encryption.header }); + decryptChunk({ state: decryption, index: 0, ciphertext: ciphertext0 }); + throwsCode(() => finalizeEnvelope(decryption), "E_ENVELOPE_INCOMPLETE"); +}); diff --git a/packages/crypto/test/mobile/README.md b/packages/crypto/test/mobile/README.md new file mode 100644 index 0000000..e1d7d4e --- /dev/null +++ b/packages/crypto/test/mobile/README.md @@ -0,0 +1,39 @@ +# Mobile release gate + +This is a manual release gate, not a simulated mobile test. Build the package, +serve the repository over HTTPS or a trusted local-network origin, and open +`packages/crypto/test/mobile/` on each target device. + +The build creates the ignored `runner.bundle.js` browser harness. Published +package entry points deliberately leave their declared `@noble/*` runtime +dependencies external so application lockfile updates affect executed code. + +For both current iOS Safari and Android Chrome: + +1. Select a representative file between 50 and 100 MB. The crypto and memory + gate is content-type agnostic; separately test PDF and camera-image picking + when integrating the package into an intake UI. +2. Run the harness once without interruption. +3. Run it again while backgrounding the browser during encryption, then return + and let the operation complete. +4. Save the JSON report with the device model, OS/browser versions, and the + observed outcome of the interrupted run. + +The report records encryption/decryption duration and JavaScript heap use when +the browser exposes it. Safari normally does not expose `performance.memory`; +record that value as unavailable rather than estimating it. The fixed 1 MiB +chunk size and the fact that the harness never retains prior plaintext chunks +are still part of the result. + +Do not publish 0.1.0 until both device reports show a successful byte count and +acceptable memory behavior. Commit reports only if they contain no private +filenames or user-agent details you do not want public. + +## Recorded results + +- Google Pixel 8, Android 16, Chrome 150.0.0.0: pass. +- iPhone 16 Pro, iOS 18.4, Safari 18.4: pass. + +See `reports/` for the foreground and background-resume measurements. Chrome's +version is the reduced value exposed by its user agent. Safari does not expose +`performance.memory`. diff --git a/packages/crypto/test/mobile/index.html b/packages/crypto/test/mobile/index.html new file mode 100644 index 0000000..b2822e5 --- /dev/null +++ b/packages/crypto/test/mobile/index.html @@ -0,0 +1,16 @@ + + + +@pksuite/crypto mobile file test + +

PKFILE1 mobile test

+

Select a representative 50–100 MB file. Chunks are encrypted into IndexedDB, then read back and decrypted without retaining the whole file in JavaScript memory.

+ + + +
Waiting.
+ diff --git a/packages/crypto/test/mobile/reports/android-chrome-pixel-8-android-16.json b/packages/crypto/test/mobile/reports/android-chrome-pixel-8-android-16.json new file mode 100644 index 0000000..f7418d3 --- /dev/null +++ b/packages/crypto/test/mobile/reports/android-chrome-pixel-8-android-16.json @@ -0,0 +1,50 @@ +{ + "schema": "pksuite-crypto-mobile-gate/v1", + "recordedAt": "2026-07-18", + "status": "pass", + "device": { + "manufacturer": "Google", + "model": "Pixel 8", + "os": "Android", + "osVersion": "16", + "browser": "Chrome", + "browserVersion": "150.0.0.0", + "browserVersionSource": "reduced user agent" + }, + "fixture": { + "repositoryPath": "spec/test.dmg", + "sizeBytes": 78575566, + "mediaType": "application/x-apple-diskimage", + "chunkSizeBytes": 1048576 + }, + "runs": [ + { + "mode": "foreground", + "startedAt": "2026-07-18T04:17:41.037Z", + "completedAt": "2026-07-18T04:17:49.580Z", + "encryptMilliseconds": 6214, + "decryptMilliseconds": 2210, + "decryptedBytes": 78575566, + "maxObservedHeapBytes": 10000000, + "success": true + }, + { + "mode": "backgrounded-during-encryption", + "startedAt": "2026-07-18T04:19:24.224Z", + "completedAt": "2026-07-18T04:19:38.245Z", + "encryptMilliseconds": 11790, + "decryptMilliseconds": 2117, + "decryptedBytes": 78575566, + "maxObservedHeapBytes": 10000000, + "resumedWithoutReload": true, + "success": true + } + ], + "assessment": { + "byteCountMatched": true, + "foregroundCompleted": true, + "backgroundedRunCompleted": true, + "observedHeapStayedBelowFileSize": true, + "notes": "Chrome may quantize performance.memory. File content type does not affect PKFILE1 chunk encryption or browser Blob slicing." + } +} diff --git a/packages/crypto/test/mobile/reports/ios-safari-iphone-16-pro-ios-18.4.json b/packages/crypto/test/mobile/reports/ios-safari-iphone-16-pro-ios-18.4.json new file mode 100644 index 0000000..00a9ca2 --- /dev/null +++ b/packages/crypto/test/mobile/reports/ios-safari-iphone-16-pro-ios-18.4.json @@ -0,0 +1,53 @@ +{ + "schema": "pksuite-crypto-mobile-gate/v1", + "recordedAt": "2026-07-18", + "status": "pass", + "device": { + "manufacturer": "Apple", + "model": "iPhone 16 Pro", + "os": "iOS", + "osVersion": "18.4", + "browser": "Safari", + "browserVersion": "18.4", + "browserVersionSource": "user agent" + }, + "fixture": { + "sizeBytes": 63114560, + "mediaType": "image/png", + "chunkSizeBytes": 1048576 + }, + "runs": [ + { + "mode": "foreground", + "startedAt": "2026-07-18T04:33:44.289Z", + "completedAt": "2026-07-18T04:33:45.737Z", + "encryptMilliseconds": 832, + "decryptMilliseconds": 584, + "decryptedBytes": 63114560, + "maxObservedHeapBytes": null, + "success": true + }, + { + "mode": "background-resume", + "startedAt": "2026-07-18T04:38:37.623Z", + "backgroundedAt": "2026-07-18T04:38:43.543Z", + "resumedAt": "2026-07-18T04:38:57.348Z", + "completedAt": "2026-07-18T04:38:58.757Z", + "backgroundMilliseconds": 13805, + "backgroundObserved": true, + "encryptMilliseconds": 20518, + "decryptMilliseconds": 582, + "decryptedBytes": 63114560, + "maxObservedHeapBytes": null, + "success": true + } + ], + "assessment": { + "byteCountMatched": true, + "foregroundCompleted": true, + "backgroundedRunCompleted": true, + "backgroundTransitionObservedByPage": true, + "heapMeasurementAvailable": false, + "notes": "Safari does not expose performance.memory. The harness retained only one 1 MiB plaintext chunk at a time and completed after an observed hidden-visible cycle." + } +} diff --git a/packages/crypto/test/mobile/runner.js b/packages/crypto/test/mobile/runner.js new file mode 100644 index 0000000..6d3181c --- /dev/null +++ b/packages/crypto/test/mobile/runner.js @@ -0,0 +1,124 @@ +import { + createFileEncryption, + createRecipientIdentity, + decryptChunk, + disposeEnvelope, + encryptChunk, + finalizeEnvelope, + openFileEnvelope, +} from "../../dist/index.js"; + +const output = document.querySelector("#output"); +const fileInput = document.querySelector("#file"); +const backgroundInput = document.querySelector("#background"); +const runButton = document.querySelector("#run"); + +const openDb = () => new Promise((resolve, reject) => { + const request = indexedDB.open("pk-crypto-mobile-test", 1); + request.onupgradeneeded = () => request.result.createObjectStore("chunks"); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); +}); + +const transaction = (db, mode, operation) => new Promise((resolve, reject) => { + const tx = db.transaction("chunks", mode); + const store = tx.objectStore("chunks"); + const request = operation(store); + request.onerror = () => reject(request.error); + request.onsuccess = () => resolve(request.result); +}); + +const heap = () => performance.memory?.usedJSHeapSize ?? null; + +const waitForBackgroundCycle = (report) => new Promise((resolve) => { + let hiddenAt; + const onVisibilityChange = () => { + if (document.visibilityState === "hidden" && hiddenAt === undefined) { + hiddenAt = Date.now(); + report.backgroundedAt = new Date(hiddenAt).toISOString(); + return; + } + if (document.visibilityState === "visible" && hiddenAt !== undefined) { + const resumedAt = Date.now(); + report.resumedAt = new Date(resumedAt).toISOString(); + report.backgroundMilliseconds = resumedAt - hiddenAt; + report.backgroundObserved = true; + document.removeEventListener("visibilitychange", onVisibilityChange); + resolve(); + } + }; + document.addEventListener("visibilitychange", onVisibilityChange); + output.textContent = "First chunk encrypted. Background the browser now, wait 10–30 seconds, then return."; +}); + +runButton.addEventListener("click", async () => { + const file = fileInput.files[0]; + if (!file) return; + const backgroundRequested = backgroundInput.checked; + runButton.disabled = true; + const report = { + userAgent: navigator.userAgent, + fileSize: file.size, + fileType: file.type, + chunkSize: 1024 * 1024, + startedAt: new Date().toISOString(), + maxObservedHeapBytes: heap(), + mode: backgroundRequested ? "background-resume" : "foreground", + backgroundObserved: false, + }; + let encryption; + let decryption; + let header; + try { + const db = await openDb(); + await transaction(db, "readwrite", (store) => store.clear()); + const identity = createRecipientIdentity(); + encryption = createFileEncryption({ + recipient: identity.recipient, + plaintextSize: file.size, + chunkSize: report.chunkSize, + metadata: { name: file.name, mediaType: file.type || "application/octet-stream", lastModified: file.lastModified }, + }); + const encryptStart = performance.now(); + for (let index = 0; index < encryption.chunkCount; index++) { + const start = index * encryption.chunkSize; + const plaintext = new Uint8Array(await file.slice(start, start + encryption.chunkSize).arrayBuffer()); + const ciphertext = encryptChunk({ state: encryption, index, plaintext }); + await transaction(db, "readwrite", (store) => store.put(ciphertext, index)); + report.maxObservedHeapBytes = Math.max(report.maxObservedHeapBytes ?? 0, heap() ?? 0) || null; + output.textContent = `Encrypting ${index + 1}/${encryption.chunkCount}`; + if (backgroundRequested && index === 0) await waitForBackgroundCycle(report); + } + header = encryption.header; + finalizeEnvelope(encryption); + encryption = null; + report.encryptMilliseconds = Math.round(performance.now() - encryptStart); + + decryption = openFileEnvelope({ identity: identity.identity, header }); + const decryptStart = performance.now(); + let decryptedBytes = 0; + for (let index = 0; index < decryption.chunkCount; index++) { + const stored = await transaction(db, "readonly", (store) => store.get(index)); + const plaintext = decryptChunk({ state: decryption, index, ciphertext: new Uint8Array(stored) }); + decryptedBytes += plaintext.length; + report.maxObservedHeapBytes = Math.max(report.maxObservedHeapBytes ?? 0, heap() ?? 0) || null; + output.textContent = `Decrypting ${index + 1}/${decryption.chunkCount}`; + } + finalizeEnvelope(decryption); + decryption = null; + report.decryptMilliseconds = Math.round(performance.now() - decryptStart); + report.decryptedBytes = decryptedBytes; + report.completedAt = new Date().toISOString(); + report.success = decryptedBytes === file.size; + output.textContent = JSON.stringify(report, null, 2); + db.close(); + } catch (error) { + report.success = false; + report.error = `${error?.code ?? error?.name ?? "Error"}: ${error?.message ?? error}`; + output.textContent = JSON.stringify(report, null, 2); + } finally { + if (encryption) disposeEnvelope(encryption); + if (decryption) disposeEnvelope(decryption); + runButton.disabled = false; + } +}); diff --git a/packages/crypto/test/reference-reader.mjs b/packages/crypto/test/reference-reader.mjs new file mode 100644 index 0000000..9e53d03 --- /dev/null +++ b/packages/crypto/test/reference-reader.mjs @@ -0,0 +1,125 @@ +// Deliberately independent PKFILE1 reader used only for conformance tests. +// It does not import @pksuite/crypto source or bundles. Node's crypto handles +// age/X25519, HKDF, HMAC, SHA-256, and ChaCha20-Poly1305; noble supplies only +// the XChaCha20-Poly1305 primitive absent from node:crypto. +import { + createDecipheriv, + createHmac, + createPrivateKey, + createPublicKey, + diffieHellman, + hkdfSync, + timingSafeEqual, + createHash, +} from "node:crypto"; +import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"; + +const PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex"); +const SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex"); +const MAGIC = Buffer.from("PKFILE1\n"); +const u32 = (value) => { const out = Buffer.alloc(4); out.writeUInt32BE(value); return out; }; +const u64 = (value) => { const out = Buffer.alloc(8); out.writeBigUInt64BE(BigInt(value)); return out; }; +const sha256 = (value) => createHash("sha256").update(value).digest(); +const hkdf = (ikm, salt, info, length = 32) => Buffer.from(hkdfSync("sha256", ikm, salt, Buffer.from(info), length)); + +function privateKey(raw) { + return createPrivateKey({ key: Buffer.concat([PKCS8_PREFIX, raw]), format: "der", type: "pkcs8" }); +} + +function publicKey(raw) { + return createPublicKey({ key: Buffer.concat([SPKI_PREFIX, raw]), format: "der", type: "spki" }); +} + +function publicFromPrivate(raw) { + return createPublicKey(privateKey(raw)).export({ format: "der", type: "spki" }).subarray(-32); +} + +function chachaOpen(key, ciphertext) { + const decipher = createDecipheriv("chacha20-poly1305", key, Buffer.alloc(12), { authTagLength: 16 }); + decipher.setAuthTag(ciphertext.subarray(-16)); + return Buffer.concat([decipher.update(ciphertext.subarray(0, -16)), decipher.final()]); +} + +function openAgeHeader(header, secret) { + const text = Buffer.from(header).toString("utf8"); + const lines = text.split("\n"); + if (lines.shift() !== "age-encryption.org/v1") throw new Error("reference: bad age version"); + const recipientPublic = publicFromPrivate(secret); + let fileKey; + let marker; + for (let index = 0; index < lines.length;) { + const line = lines[index++]; + if (line.startsWith("--- ")) { marker = line; break; } + const match = /^-> X25519 ([A-Za-z0-9+/]+)$/.exec(line); + if (!match) throw new Error("reference: unsupported age stanza"); + const ephemeral = Buffer.from(match[1], "base64"); + const body = Buffer.from(lines[index++], "base64"); + const shared = diffieHellman({ privateKey: privateKey(secret), publicKey: publicKey(ephemeral) }); + const wrapKey = hkdf(shared, Buffer.concat([ephemeral, recipientPublic]), "age-encryption.org/v1/X25519"); + try { fileKey ??= chachaOpen(wrapKey, body); } + catch { /* stanza belongs to another recipient */ } + finally { shared.fill(0); wrapKey.fill(0); } + } + if (!fileKey || !marker) throw new Error("reference: no matching age stanza"); + const markerOffset = text.lastIndexOf("--- "); + const authenticated = Buffer.from(`${text.slice(0, markerOffset)}---`); + const macKey = hkdf(fileKey, Buffer.alloc(0), "header"); + const expected = createHmac("sha256", macKey).update(authenticated).digest(); + const actual = Buffer.from(marker.slice(4), "base64"); + macKey.fill(0); + if (actual.length !== expected.length || !timingSafeEqual(actual, expected)) throw new Error("reference: bad age MAC"); + expected.fill(0); + return fileKey; +} + +export function readVectorWithIndependentNodeImplementation(vector) { + const header = Buffer.from(vector.file.header_base64, "base64"); + if (!header.subarray(0, 8).equals(MAGIC)) throw new Error("reference: bad PKFILE1 magic"); + if (header.readUInt32BE(8) !== header.length - 12) throw new Error("reference: bad header length"); + let offset = 12; + const chunkSize = header.readUInt32BE(offset); offset += 4; + const plaintextSize = Number(header.readBigUInt64BE(offset)); offset += 8; + const chunkCount = header.readUInt32BE(offset); offset += 4; + const fileId = header.subarray(offset, offset + 16); offset += 16; + const ageLength = header.readUInt32BE(offset); offset += 4; + const ageHeader = header.subarray(offset, offset + ageLength); offset += ageLength; + const structuralPrefix = header.subarray(12, offset); + const metadataNonce = header.subarray(offset, offset + 24); offset += 24; + const metadataLength = header.readUInt32BE(offset); offset += 4; + const metadataCiphertext = header.subarray(offset, offset + metadataLength); offset += metadataLength; + if (offset !== header.length) throw new Error("reference: trailing header data"); + + const fileKey = openAgeHeader(ageHeader, Buffer.from(vector.identity.secret_key, "hex")); + try { + const metadataKey = hkdf(fileKey, fileId, "pksuite/file/metadata-key/v1"); + const metadataAad = Buffer.concat([ + Buffer.from("pksuite/file/metadata/v1\0"), + u32(1), + sha256(Buffer.concat([MAGIC, structuralPrefix])), + ]); + let metadataPlaintext; + try { metadataPlaintext = xchacha20poly1305(metadataKey, metadataNonce, metadataAad).decrypt(metadataCiphertext); } + finally { metadataKey.fill(0); } + const metadataText = new TextDecoder("utf-8", { fatal: true }).decode(metadataPlaintext); + metadataPlaintext.fill(0); + const metadata = JSON.parse(metadataText); + + const plaintextDigests = []; + for (const entry of vector.chunks) { + const expectedLength = entry.index === chunkCount - 1 ? plaintextSize - chunkSize * entry.index : chunkSize; + const chunkAad = Buffer.concat([ + Buffer.from("pksuite/file/chunk/v1\0"), + u32(1), fileId, sha256(header), u32(entry.index), u32(chunkCount), + u64(plaintextSize), u32(chunkSize), u32(expectedLength), + ]); + const chunkKey = hkdf(fileKey, Buffer.concat([fileId, u32(entry.index)]), "pksuite/file/chunk-key/v1"); + const stored = Buffer.from(entry.ciphertext_base64, "base64"); + let plaintext; + try { plaintext = xchacha20poly1305(chunkKey, stored.subarray(0, 24), chunkAad).decrypt(stored.subarray(24)); } + finally { chunkKey.fill(0); } + plaintextDigests.push(sha256(plaintext).toString("hex")); + plaintext.fill(0); + } + return { metadata, chunkSize, plaintextSize, chunkCount, plaintextDigests }; + } finally { fileKey.fill(0); } +} diff --git a/packages/crypto/test/reference.test.js b/packages/crypto/test/reference.test.js new file mode 100644 index 0000000..03dfb54 --- /dev/null +++ b/packages/crypto/test/reference.test.js @@ -0,0 +1,15 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { readVectorWithIndependentNodeImplementation } from "./reference-reader.mjs"; + +const vector = JSON.parse(readFileSync(new URL("../vectors/file-v1.json", import.meta.url), "utf8")); + +test("frozen PKFILE1 vector opens in the independent Node reference reader", () => { + const result = readVectorWithIndependentNodeImplementation(vector); + assert.deepEqual(result.metadata, vector.file.metadata); + assert.equal(result.chunkSize, vector.file.chunk_size); + assert.equal(result.plaintextSize, vector.file.plaintext_size); + assert.equal(result.chunkCount, vector.file.chunk_count); + assert.deepEqual(result.plaintextDigests, vector.chunks.map((chunk) => chunk.plaintext_sha256)); +}); diff --git a/packages/crypto/vectors/age-cli-v1.age b/packages/crypto/vectors/age-cli-v1.age new file mode 100644 index 0000000..5b60ad8 --- /dev/null +++ b/packages/crypto/vectors/age-cli-v1.age @@ -0,0 +1,5 @@ +age-encryption.org/v1 +-> X25519 PoWIqszPsIfHYD3MuVc6k0rBmugRxuOY/87NaTS6VwM +ud7UzqJrMutPVKeJYkfTVNeatnzndL+4kzV1sJeyVOo +--- 0W/hgiPptstbmLpLJoQUbOKClUev61egRssh5mC9mTo +d2S+gP=` 1~#EYU9v QpL.LF\cvD!+rjG,1 \ No newline at end of file diff --git a/packages/crypto/vectors/age-cli-v1.json b/packages/crypto/vectors/age-cli-v1.json new file mode 100644 index 0000000..8a876c4 --- /dev/null +++ b/packages/crypto/vectors/age-cli-v1.json @@ -0,0 +1,10 @@ +{ + "note": "Frozen artifact written by the external age CLI, not by @pksuite/crypto.", + "writer": "v1.3.1", + "identity_verified_by": "age-keygen -y", + "identity": "AGE-SECRET-KEY-15ZS69GAY5KN2029F4246ETDW47CTRV4NKJ6MDDACHXATH09AH6LSSSLT45", + "recipient": "age1vpd8yhf2ft07avdznct7m43pcxm4j0hgek7yftrvf2mw97q96g7qz68n63", + "secret_key_hex": "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", + "plaintext_hex": "e7a4b0664dfbe5833fee83726b21e88a71f31dae26f118cb80a9874d11d2bc7f", + "ciphertext_sha256": "4328565985a6406b005a83fc29494b9099242ebdaebcdc4c562169da39efc50e" +} diff --git a/packages/crypto/vectors/file-v1-tampering.json b/packages/crypto/vectors/file-v1-tampering.json new file mode 100644 index 0000000..f240caa --- /dev/null +++ b/packages/crypto/vectors/file-v1-tampering.json @@ -0,0 +1,65 @@ +{ + "note": "Executable mutation cases for file-v1.json. packages/crypto/test/file.test.js MUST execute every listed case.", + "base_vector": "file-v1.json", + "wrong_identity": "AGE-SECRET-KEY-1CRQU9S7YCHRV0JXFET9UENWWELGDR5KN6N2AD47CM8DDHHXAMM0S2L2P3P", + "cases": [ + { + "id": "wrong-recipient", + "operation": "open_wrong_identity", + "expected_code": "E_WRONG_RECIPIENT" + }, + { + "id": "metadata-tamper", + "operation": "xor_header_byte", + "offset_from_end": 1, + "xor": 1, + "expected_code": "E_ENVELOPE_AUTH" + }, + { + "id": "chunk-tamper", + "operation": "xor_chunk_byte", + "chunk": 0, + "offset_from_end": 1, + "xor": 1, + "expected_code": "E_CHUNK_AUTH" + }, + { + "id": "chunk-truncation", + "operation": "truncate_chunk", + "chunk": 0, + "bytes": 1, + "expected_code": "E_CHUNK_SIZE" + }, + { + "id": "chunk-extension", + "operation": "extend_chunk", + "chunk": 0, + "hex": "00", + "expected_code": "E_CHUNK_SIZE" + }, + { + "id": "chunk-reordering", + "operation": "present_chunk_at_index", + "chunk": 0, + "index": 1, + "expected_code": "E_CHUNK_SIZE" + }, + { + "id": "file-truncation", + "operation": "omit_chunk_then_finalize", + "omit": 1, + "expected_code": "E_ENVELOPE_INCOMPLETE" + }, + { + "id": "cross-file-substitution", + "operation": "substitute_same_geometry_file", + "chunk": 0, + "expected_code": "E_CHUNK_AUTH" + }, + { + "id": "retry-nonce-reuse", + "operation": "assert_retry_nonce_distinct", + "chunk": 0 + } + ] +} diff --git a/packages/crypto/vectors/file-v1.json b/packages/crypto/vectors/file-v1.json new file mode 100644 index 0000000..b74c1f7 --- /dev/null +++ b/packages/crypto/vectors/file-v1.json @@ -0,0 +1,45 @@ +{ + "note": "PKFILE1 v1 known-answer vector. Fixed keys and nonces are test data only and MUST NOT be reused.", + "identity": { + "secret_key": "a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf", + "identity": "AGE-SECRET-KEY-15ZS69GAY5KN2029F4246ETDW47CTRV4NKJ6MDDACHXATH09AH6LSSSLT45", + "recipient": "age1vpd8yhf2ft07avdznct7m43pcxm4j0hgek7yftrvf2mw97q96g7qz68n63" + }, + "file": { + "plaintext_size": 65539, + "chunk_size": 65536, + "chunk_count": 2, + "metadata": { + "name": "Smith 2025 W-2.pdf", + "mediaType": "application/pdf", + "lastModified": 1784313600000 + }, + "file_id_hex": "101112131415161718191a1b1c1d1e1f", + "age_header_base64": "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOWUJ5MWpaWWdOR3U2akthMzVFaE9EaFI3U0dpamp0MTZXWFEwczBXWWxRCnAxdjhYSzdQbWZQZU5KMlNlMzUwQ0lnWEpmdTVQUnAybUZoMjRLTkZiekkKLS0tIGZFWFVNOU91V0loM3VuTTZYaEFneHlnN0xkbnpmQ3hFV09xdHpoT05JSW8K", + "metadata_nonce_hex": "404142434445464748494a4b4c4d4e4f5051525354555657", + "metadata_ciphertext_base64": "2BmhtdsB/6HQ6uWCMlGKvaYWWe4mJx377yU3EAgRcO2IsIoXNahxMbkC0uwqCATruFQIg29zRpzk3lXzkDW1OL1c9in5kqp2poT4/mwy2AngGLmfFrJ6AePoCZW9T1TjSb+T7NMlQIE=", + "metadata_aad_hex": "706b73756974652f66696c652f6d657461646174612f76310000000001375b4422cef93be854418918b75b0465e9cd48e157bc01d4e042a07c8aaaed53", + "header_base64": "UEtGSUxFMQoAAAFQAAEAAAAAAAAAAQADAAAAAhAREhMUFRYXGBkaGxwdHh8AAACoYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOWUJ5MWpaWWdOR3U2akthMzVFaE9EaFI3U0dpamp0MTZXWFEwczBXWWxRCnAxdjhYSzdQbWZQZU5KMlNlMzUwQ0lnWEpmdTVQUnAybUZoMjRLTkZiekkKLS0tIGZFWFVNOU91V0loM3VuTTZYaEFneHlnN0xkbnpmQ3hFV09xdHpoT05JSW8KQEFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXAAAAaNgZobXbAf+h0OrlgjJRir2mFlnuJicd++8lNxAIEXDtiLCKFzWocTG5AtLsKggE67hUCINvc0ac5N5V85A1tTi9XPYp+ZKqdqaE+P5sMtgJ4Bi5nxayegHj6AmVvU9U40m/k+zTJUCB", + "header_sha256": "3f1dff9b652f231ffa2bf1ae96f31b20e412decfabaaa23e8440801755f6a6da" + }, + "chunks": [ + { + "index": 0, + "plaintext_pattern": "byte[index mod 256] repeated to chunk_size", + "plaintext_sha256": "7daca2095d0438260fa849183dfc67faa459fdf4936e1bc91eec6b281b27e4c2", + "nonce_hex": "606162636465666768696a6b6c6d6e6f7071727374757677", + "aad_hex": "706b73756974652f66696c652f6368756e6b2f76310000000001101112131415161718191a1b1c1d1e1f3f1dff9b652f231ffa2bf1ae96f31b20e412decfabaaa23e8440801755f6a6da000000000000000200000000000100030001000000010000", + "ciphertext_base64": "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", + "ciphertext_sha256": "074fce33f04eb661369f822d8df812f579ba8d52ab32a46456df76df21ef19a8" + }, + { + "index": 1, + "plaintext_hex": "face01", + "plaintext_sha256": "f80a2bde55e679e954a0850d83a2336e15fd3ce98c8b4f91f7279420ba7dbd54", + "nonce_hex": "808182838485868788898a8b8c8d8e8f9091929394959697", + "aad_hex": "706b73756974652f66696c652f6368756e6b2f76310000000001101112131415161718191a1b1c1d1e1f3f1dff9b652f231ffa2bf1ae96f31b20e412decfabaaa23e8440801755f6a6da000000010000000200000000000100030001000000000003", + "ciphertext_base64": "gIGCg4SFhoeIiYqLjI2Oj5CRkpOUlZaXQcX/ykUcmFYBOuqH9aEkIK8Pzw==", + "ciphertext_sha256": "2be2193468ed3c73f071283ba727632fd1ef745ce9679237780a339ddde33e5f" + } + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..77e92a9 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,323 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: {} + + apps/pkvault: + dependencies: + '@pksuite/crypto': + specifier: 0.1.0 + version: link:../../packages/crypto + + packages/crypto: + dependencies: + '@noble/ciphers': + specifier: 1.3.0 + version: 1.3.0 + '@noble/curves': + specifier: 1.9.7 + version: 1.9.7 + '@noble/hashes': + specifier: 1.8.0 + version: 1.8.0 + devDependencies: + esbuild: + specifier: 0.28.1 + version: 0.28.1 + +packages: + + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.28.1': + optional: true + + '@esbuild/android-arm64@0.28.1': + optional: true + + '@esbuild/android-arm@0.28.1': + optional: true + + '@esbuild/android-x64@0.28.1': + optional: true + + '@esbuild/darwin-arm64@0.28.1': + optional: true + + '@esbuild/darwin-x64@0.28.1': + optional: true + + '@esbuild/freebsd-arm64@0.28.1': + optional: true + + '@esbuild/freebsd-x64@0.28.1': + optional: true + + '@esbuild/linux-arm64@0.28.1': + optional: true + + '@esbuild/linux-arm@0.28.1': + optional: true + + '@esbuild/linux-ia32@0.28.1': + optional: true + + '@esbuild/linux-loong64@0.28.1': + optional: true + + '@esbuild/linux-mips64el@0.28.1': + optional: true + + '@esbuild/linux-ppc64@0.28.1': + optional: true + + '@esbuild/linux-riscv64@0.28.1': + optional: true + + '@esbuild/linux-s390x@0.28.1': + optional: true + + '@esbuild/linux-x64@0.28.1': + optional: true + + '@esbuild/netbsd-arm64@0.28.1': + optional: true + + '@esbuild/netbsd-x64@0.28.1': + optional: true + + '@esbuild/openbsd-arm64@0.28.1': + optional: true + + '@esbuild/openbsd-x64@0.28.1': + optional: true + + '@esbuild/openharmony-arm64@0.28.1': + optional: true + + '@esbuild/sunos-x64@0.28.1': + optional: true + + '@esbuild/win32-arm64@0.28.1': + optional: true + + '@esbuild/win32-ia32@0.28.1': + optional: true + + '@esbuild/win32-x64@0.28.1': + optional: true + + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3ff5faa..1b24730 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ packages: - "apps/*" - "packages/*" + +linkWorkspacePackages: true diff --git a/spec/SPEC-FILE.md b/spec/SPEC-FILE.md new file mode 100644 index 0000000..62d02c4 --- /dev/null +++ b/spec/SPEC-FILE.md @@ -0,0 +1,214 @@ +# pk file envelope v1 + +Status: implementation draft for `@pksuite/crypto` 0.1.0 +Wire identifier: `PKFILE1\n` +Date: 2026-07-17 + +## 1. Scope + +This document specifies a product-independent, seekable file envelope for +browser encryption, multipart upload, and browser or Node decryption. It does +not identify firms, requests, checklist items, storage providers, object keys, +or upload sessions. + +The format provides: + +- X25519 recipient identities encoded according to age v1. +- A fresh 128-bit file key sealed in a strict age v1 X25519 header. +- Encrypted filename, media type, and modification time. +- Independently authenticated chunks that can be retried or resumed. +- Authentication of version, file id, header, chunk index, chunk count, total + plaintext size, configured chunk size, and actual chunk size. + +It does not hide ciphertext size, chunk geometry, recipient count, or timing. +It does not authenticate who selected the recipient key. A malicious hosted +origin can substitute code or keys before encryption. + +## 2. Primitive profile + +- X25519, HKDF-SHA-256, HMAC-SHA-256, and ChaCha20-Poly1305 exactly as used by + the age v1 X25519 recipient stanza. +- XChaCha20-Poly1305 for metadata and file chunks. +- SHA-256 for public header commitments. +- All integers are unsigned big-endian. +- Strings are UTF-8. Domain labels below are literal UTF-8 bytes. +- AEAD ciphertext is `ciphertext || 16-byte tag`. + +Each envelope has a fresh random 16-byte age file key and 16-byte `file_id`. +Each metadata or chunk encryption has a fresh random 24-byte XChaCha nonce. +Randomness MUST come from the operating system CSPRNG. + +## 3. Public header + +```text +magic 8 bytes "PKFILE1\n" +header_body_length u32 +chunk_size u32 +plaintext_size u64 +chunk_count u32 +file_id 16 bytes +age_header_length u32 +age_header bytes +metadata_nonce 24 bytes +metadata_ciphertext_len u32 +metadata_ciphertext bytes +``` + +`header_body_length` covers every field after itself. There are no trailing +header bytes. Implementations MUST reject lengths beyond their documented +limits before allocating. + +`chunk_count = max(1, ceil(plaintext_size / chunk_size))`. An empty file has +one authenticated zero-length plaintext chunk. Every non-final chunk has +exactly `chunk_size` plaintext bytes. The final chunk has the remaining bytes. + +The age header is a complete textual age v1 header ending in LF. V1 accepts +only native `X25519` stanzas. It contains no age payload: its sole purpose is +to authenticate and seal the 16-byte file key. + +## 4. Encrypted metadata + +Metadata plaintext is canonical UTF-8 JSON with exactly these keys in order: + +```json +{"name":"client.pdf","mediaType":"application/pdf","lastModified":1784313600000} +``` + +`name` is a non-empty portable filename no longer than 4096 UTF-8 bytes, not a +path. It MUST contain only Unicode scalar values; MUST NOT contain control +characters, `/`, `\\`, or `:`; +MUST NOT be `.` or `..`; MUST NOT end in a space or period; and MUST NOT be a +case-insensitive Windows device basename (`CON`, `PRN`, `AUX`, `NUL`, `COM1` +through `COM9`, or `LPT1` through `LPT9`, with or without an extension). +Consumers MUST still join it beneath an explicitly chosen directory and must +not decode or reinterpret it as path syntax. `mediaType` is at most 255 bytes +of printable ASCII. `lastModified` is an integer from 0 through +9007199254740991 inclusive, or `null`. + +“Canonical JSON” is defined here independently of any JSON library. The three +members occur exactly in the order shown, with no whitespace. Member names use +the literal ASCII bytes shown. String values are surrounded by `"`; `"` and +`\\` are escaped as `\\"` and `\\\\`; U+0008, U+0009, U+000A, U+000C, and +U+000D use `\\b`, `\\t`, `\\n`, `\\f`, and `\\r`; any other U+0000 through +U+001F uses lowercase `\\u00xx`; every other Unicode scalar value is emitted +directly as UTF-8. Lone UTF-16 surrogates are invalid. `lastModified` uses +unprefixed base-10 ASCII with no leading zeroes, or literal `null`. + +```text +metadata_key = HKDF-SHA-256( + ikm = file_key, + salt = file_id, + info = "pksuite/file/metadata-key/v1", + L = 32 +) + +structural_prefix = chunk_size || plaintext_size || chunk_count || file_id + || age_header_length || age_header + +metadata_aad = "pksuite/file/metadata/v1" || 0x00 || u32(1) + || SHA-256("PKFILE1\n" || structural_prefix) + +metadata_ciphertext = XChaCha20-Poly1305( + key = metadata_key, + nonce = metadata_nonce, + aad = metadata_aad, + plaintext = canonical_metadata +) +``` + +## 5. Chunks + +Every stored chunk is: + +```text +chunk_nonce 24 bytes +chunk_ciphertext expected_plaintext_length + 16 bytes +``` + +For zero-based chunk index `i`: + +```text +chunk_key = HKDF-SHA-256( + ikm = file_key, + salt = file_id || u32(i), + info = "pksuite/file/chunk-key/v1", + L = 32 +) + +header_commitment = SHA-256(complete_public_header) + +chunk_aad = "pksuite/file/chunk/v1" || 0x00 + || u32(1) + || file_id + || header_commitment + || u32(i) + || u32(chunk_count) + || u64(plaintext_size) + || u32(chunk_size) + || u32(expected_plaintext_length(i)) +``` + +The chunk is encrypted with XChaCha20-Poly1305 using `chunk_key`, a fresh +random `chunk_nonce`, and `chunk_aad`. + +Multipart retries MAY re-encrypt the same index. A retry MUST generate a fresh +24-byte nonce. Consequently, a restarted client does not need to persist nonce +state, and changing the local plaintext before a retry does not deterministically +reuse a nonce. A multipart implementation SHOULD reuse already-produced +ciphertext when available to avoid unnecessary work. + +The receiver MUST know the index associated with each chunk and MUST reject a +wrong length or authentication failure. Completion succeeds only after every +index in `[0, chunk_count)` has authenticated. Missing chunks are truncation; +passing a chunk under another index is reordering; moving a chunk between +envelopes is substitution. All fail closed. + +## 6. Parsing and limits + +The 0.1.0 implementation accepts: + +- Chunk sizes from 64 KiB through 16 MiB; default 1 MiB. +- Public headers no larger than 1 MiB. +- Embedded age headers no larger than 256 KiB. +- Metadata ciphertext no larger than 64 KiB. +- Plaintext sizes within JavaScript's safe integer range and no more than + `2^32 - 1` chunks. + +Readers MUST reject unknown magic/version, inconsistent geometry, non-canonical +age or metadata encoding, foreign age stanzas, trailing header bytes, and any +integer or length outside supported bounds. + +## 7. Resume and storage contract + +The envelope does not prescribe a storage API. A storage adapter records the +public header once and associates each ciphertext with its zero-based index. +Replacing an index atomically is safe because each encryption uses a fresh +XChaCha nonce. The adapter must not declare the object complete until every +index exists and the application has run the package finalization check. + +Successful finalization or explicit disposal makes the state unusable and +best-effort overwrites its retained file key. Applications MUST explicitly +dispose abandoned states; JavaScript cannot guarantee that engine or AEAD +internal copies are erased. + +Storage-specific identifiers and authorization capabilities are deliberately +outside the envelope and MUST NOT be added as authenticated fields in v1. + +## 8. Test requirements + +Published vectors cover identity encoding, age-header sealing, header bytes, +metadata ciphertext, chunk ciphertexts, and AAD commitments. Implementations +must additionally reject: + +- Wrong identities. +- Header or encrypted-metadata tampering. +- Truncated or extended chunks. +- Reordered chunks. +- Chunks substituted across indices or files. +- Missing chunks at finalization. +- Retry output that reuses the prior nonce. + +Before a stable release, representative 50–100 MB PDF/image inputs must be +measured on supported iOS Safari and Android Chrome versions. Record duration, +configured chunk size, peak observable memory when available, browser/device, +and whether backgrounding or network interruption affected resume. diff --git a/spec/derivation-registry.md b/spec/derivation-registry.md index 9f89729..0ec892e 100644 --- a/spec/derivation-registry.md +++ b/spec/derivation-registry.md @@ -42,8 +42,9 @@ the same namespace so no future HKDF `info` string can collide with them. ### age spec internal (reserved — never reuse) -Used inside the age implementation (`apps/pkvault/src/age.js`) exactly as the -age v1 spec defines them. Not pk-owned; never derive pk keys with these. +Used inside the age implementations (`apps/pkvault/src/age.js` and +`packages/crypto/src/age.js`) exactly as the age v1 spec defines them. Not +pk-owned; never derive pk keys with these. | `info` string | Defined by | |---|---| @@ -51,6 +52,20 @@ age v1 spec defines them. Not pk-owned; never derive pk keys with these. | `header` | age v1 header HMAC key | | `payload` | age v1 payload key | +### pksuite file envelope (0.1.0) + +Product-independent labels used by `@pksuite/crypto` and the `PKFILE1` +envelope in `spec/SPEC-FILE.md`. + +| `info` string | KDF | IKM | Salt | Output | Source | +|---|---|---|---|---|---| +| `pksuite/file/metadata-key/v1` | HKDF-SHA256 | random 16-byte age file key | random 16-byte file id | 32-byte XChaCha20-Poly1305 metadata key | `packages/crypto/src/file.js` | +| `pksuite/file/chunk-key/v1` | HKDF-SHA256 | random 16-byte age file key | file id (16 bytes) `\|\|` chunk index (u32be) | 32-byte XChaCha20-Poly1305 key for one chunk index | `packages/crypto/src/file.js` | + +AAD/domain labels reserved: `pksuite/file/metadata/v1` and +`pksuite/file/chunk/v1`. They bind the versioned structural header and each +chunk's geometry as specified in `SPEC-FILE.md`. + ### pklinks (planned — reserve before M0 code) To be finalized when `@pksuite/crypto` lands. Expected shape, per rule 4