sealkeep 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +163 -14
- package/CHANGELOG.md +223 -1
- package/CONTROL_PLANE.md +2 -2
- package/LICENSE +1 -1
- package/README.md +108 -22
- package/THIRD_PARTY.md +2 -2
- package/THREAT_MODEL.md +23 -4
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/chunk-access.d.ts +26 -4
- package/dist/packages/sealkeep-crypto/src/chunk-access.js +219 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.d.ts +11 -1
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/envelope.js +58 -21
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.d.ts +2 -2
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.d.ts +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/index.js +1 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.js +4 -4
- package/dist/packages/sealkeep-crypto/src/squeeze.d.ts +6 -0
- package/dist/packages/sealkeep-crypto/src/squeeze.js +39 -0
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.d.ts +36 -3
- package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/stream.js +196 -37
- package/dist/site/index.html +1808 -1904
- package/dist/site/llms.txt +67 -0
- package/dist/site/trust/architecture-data-flow.html +53 -0
- package/dist/site/trust/audit-roadmap.html +37 -0
- package/dist/site/trust/deployment-responsibility.html +11 -0
- package/dist/site/trust/dpa-sample.html +30 -0
- package/dist/site/trust/release-provenance.html +21 -0
- package/dist/site/trust/subprocessors.html +15 -0
- package/dist/site/trust/threat-model.html +34 -0
- package/dist/site/trust/toms.html +41 -0
- package/dist/site/trust-document.css +32 -0
- package/dist/site/trust.html +73 -0
- package/dist/site/visual/assets/index-8Nxnkc7o.js +207 -0
- package/dist/site/visual/assets/index-oxLy1bpy.css +1 -0
- package/dist/site/visual/index.html +18 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +9 -0
- package/dist/src/activity.js +90 -1
- package/dist/src/adapters.d.ts +175 -5
- package/dist/src/adapters.js +961 -73
- package/dist/src/agent-context.d.ts +135 -0
- package/dist/src/agent-context.js +1059 -0
- package/dist/src/archive-copies.d.ts +47 -0
- package/dist/src/archive-copies.js +179 -0
- package/dist/src/audit.d.ts +1 -1
- package/dist/src/audit.js +29 -4
- package/dist/src/autopilot.d.ts +52 -7
- package/dist/src/autopilot.js +143 -25
- package/dist/src/background-bandwidth.d.ts +46 -0
- package/dist/src/background-bandwidth.js +301 -0
- package/dist/src/background-cpu.d.ts +82 -0
- package/dist/src/background-cpu.js +212 -0
- package/dist/src/background-worker-error.d.ts +12 -0
- package/dist/src/background-worker-error.js +18 -0
- package/dist/src/branding.d.ts +24 -1
- package/dist/src/branding.js +31 -1
- package/dist/src/bridge.d.ts +233 -0
- package/dist/src/bridge.js +604 -0
- package/dist/src/byte-stream.d.ts +91 -0
- package/dist/src/byte-stream.js +385 -0
- package/dist/src/chunk-store.d.ts +41 -8
- package/dist/src/chunk-store.js +161 -65
- package/dist/src/cli.js +1698 -163
- package/dist/src/cloud.d.ts +841 -31
- package/dist/src/cloud.js +3196 -277
- package/dist/src/context-background.d.ts +37 -0
- package/dist/src/context-background.js +309 -0
- package/dist/src/context-drain-child.d.ts +1 -0
- package/dist/src/context-drain-child.js +98 -0
- package/dist/src/context-reader.d.ts +118 -0
- package/dist/src/context-reader.js +447 -0
- package/dist/src/control-plane/auth.d.ts +32 -4
- package/dist/src/control-plane/auth.js +85 -24
- package/dist/src/control-plane/server.js +19 -6
- package/dist/src/control-plane.d.ts +17 -1
- package/dist/src/control-plane.js +32 -6
- package/dist/src/crypto.d.ts +1 -1
- package/dist/src/crypto.js +5 -5
- package/dist/src/daemon-lease.d.ts +70 -0
- package/dist/src/daemon-lease.js +420 -0
- package/dist/src/daemon.d.ts +94 -1
- package/dist/src/daemon.js +1082 -105
- package/dist/src/darwin-service-policy.d.ts +41 -0
- package/dist/src/darwin-service-policy.js +60 -0
- package/dist/src/dashboard-cli.js +15 -15
- package/dist/src/device-authorization.d.ts +37 -0
- package/dist/src/device-authorization.js +199 -0
- package/dist/src/device-enrollment.d.ts +91 -0
- package/dist/src/device-enrollment.js +349 -0
- package/dist/src/disk.d.ts +17 -12
- package/dist/src/disk.js +43 -17
- package/dist/src/doctor.d.ts +35 -1
- package/dist/src/doctor.js +316 -41
- package/dist/src/durable-ticket-lock.d.ts +24 -0
- package/dist/src/durable-ticket-lock.js +232 -0
- package/dist/src/enroll.d.ts +1 -1
- package/dist/src/enroll.js +13 -7
- package/dist/src/env.d.ts +10 -1
- package/dist/src/env.js +11 -3
- package/dist/src/errors.d.ts +8 -8
- package/dist/src/errors.js +6 -6
- package/dist/src/flush.d.ts +12 -0
- package/dist/src/flush.js +37 -0
- package/dist/src/heartbeat.d.ts +86 -12
- package/dist/src/heartbeat.js +415 -29
- package/dist/src/index-background-watchdog.d.ts +1 -0
- package/dist/src/index-background-watchdog.js +94 -0
- package/dist/src/index-background-work.d.ts +21 -0
- package/dist/src/index-background-work.js +25 -0
- package/dist/src/index-background.d.ts +64 -0
- package/dist/src/index-background.js +394 -0
- package/dist/src/index-build-child.d.ts +1 -0
- package/dist/src/index-build-child.js +109 -0
- package/dist/src/index-manifest.d.ts +52 -0
- package/dist/src/index-manifest.js +444 -0
- package/dist/src/index-publication-proof.d.ts +84 -0
- package/dist/src/index-publication-proof.js +380 -0
- package/dist/src/index-publication-state.d.ts +149 -0
- package/dist/src/index-publication-state.js +696 -0
- package/dist/src/index-publication-verifier.d.ts +89 -0
- package/dist/src/index-publication-verifier.js +341 -0
- package/dist/src/index-publish.d.ts +62 -0
- package/dist/src/index-publish.js +540 -0
- package/dist/src/index-scratch-cleanup.d.ts +19 -0
- package/dist/src/index-scratch-cleanup.js +166 -0
- package/dist/src/index-segment-types.d.ts +132 -0
- package/dist/src/index-segment-types.js +21 -0
- package/dist/src/index-segments.d.ts +9 -0
- package/dist/src/index-segments.js +516 -0
- package/dist/src/index-store.d.ts +123 -0
- package/dist/src/index-store.js +495 -0
- package/dist/src/index-sync.d.ts +91 -15
- package/dist/src/index-sync.js +286 -53
- package/dist/src/index-upgrade-publication.d.ts +30 -0
- package/dist/src/index-upgrade-publication.js +179 -0
- package/dist/src/integration-manager.d.ts +32 -0
- package/dist/src/integration-manager.js +394 -0
- package/dist/src/leakscan.js +1 -1
- package/dist/src/local-api.d.ts +56 -15
- package/dist/src/local-api.js +4979 -541
- package/dist/src/machine-settings.d.ts +51 -0
- package/dist/src/machine-settings.js +166 -0
- package/dist/src/managed-chunks.d.ts +5 -2
- package/dist/src/managed-chunks.js +14 -14
- package/dist/src/mcp-install.d.ts +11 -9
- package/dist/src/mcp-install.js +73 -28
- package/dist/src/mcp-workspace.d.ts +18 -0
- package/dist/src/mcp-workspace.js +50 -0
- package/dist/src/mcp.js +294 -25
- package/dist/src/migrate.js +27 -21
- package/dist/src/notify.d.ts +1 -1
- package/dist/src/notify.js +5 -5
- package/dist/src/offload.d.ts +201 -14
- package/dist/src/offload.js +1848 -140
- package/dist/src/onboarding.d.ts +8 -1
- package/dist/src/onboarding.js +4 -4
- package/dist/src/packages.d.ts +2 -2
- package/dist/src/packages.js +10 -2
- package/dist/src/passkey.d.ts +0 -1
- package/dist/src/passkey.js +2 -7
- package/dist/src/password-lock.d.ts +2 -2
- package/dist/src/password-lock.js +6 -6
- package/dist/src/paths.d.ts +2 -0
- package/dist/src/paths.js +2 -0
- package/dist/src/presence.d.ts +86 -0
- package/dist/src/presence.js +240 -0
- package/dist/src/progress-deadline.d.ts +21 -0
- package/dist/src/progress-deadline.js +91 -0
- package/dist/src/project-repair.d.ts +55 -0
- package/dist/src/project-repair.js +131 -0
- package/dist/src/providers/gcs.d.ts +28 -7
- package/dist/src/providers/gcs.js +35 -24
- package/dist/src/providers/gdrive.d.ts +71 -8
- package/dist/src/providers/gdrive.js +223 -50
- package/dist/src/providers/index.d.ts +11 -3
- package/dist/src/providers/index.js +30 -10
- package/dist/src/providers/s3.d.ts +30 -8
- package/dist/src/providers/s3.js +41 -30
- package/dist/src/providers/safe-storage-fetch.d.ts +12 -0
- package/dist/src/providers/safe-storage-fetch.js +72 -0
- package/dist/src/queue.d.ts +136 -19
- package/dist/src/queue.js +862 -96
- package/dist/src/reclaim-transaction.d.ts +156 -0
- package/dist/src/reclaim-transaction.js +1027 -0
- package/dist/src/recovery-codes.d.ts +32 -0
- package/dist/src/recovery-codes.js +338 -0
- package/dist/src/recovery.js +12 -9
- package/dist/src/rehydrate.d.ts +25 -22
- package/dist/src/rehydrate.js +319 -23
- package/dist/src/restore.d.ts +57 -4
- package/dist/src/restore.js +272 -36
- package/dist/src/resume-stub.d.ts +92 -0
- package/dist/src/resume-stub.js +417 -0
- package/dist/src/retention.d.ts +98 -7
- package/dist/src/retention.js +1066 -62
- package/dist/src/rotate.js +3 -3
- package/dist/src/search.d.ts +566 -8
- package/dist/src/search.js +5897 -290
- package/dist/src/secrets.d.ts +51 -7
- package/dist/src/secrets.js +316 -24
- package/dist/src/service.d.ts +49 -11
- package/dist/src/service.js +776 -35
- package/dist/src/share.js +3 -3
- package/dist/src/shared-spaces.d.ts +98 -0
- package/dist/src/shared-spaces.js +214 -0
- package/dist/src/source-reader.d.ts +73 -0
- package/dist/src/source-reader.js +715 -0
- package/dist/src/spool.d.ts +1 -1
- package/dist/src/spool.js +1 -1
- package/dist/src/start-tui.js +2 -1
- package/dist/src/start.js +2 -2
- package/dist/src/storage-endpoint.d.ts +21 -0
- package/dist/src/storage-endpoint.js +122 -0
- package/dist/src/storage-setup.js +12 -12
- package/dist/src/storage-targets.d.ts +109 -6
- package/dist/src/storage-targets.js +975 -67
- package/dist/src/stream-to-cloud.d.ts +5 -1
- package/dist/src/stream-to-cloud.js +34 -14
- package/dist/src/sync-rules.d.ts +31 -6
- package/dist/src/sync-rules.js +153 -14
- package/dist/src/team-backfill-scheduling.d.ts +8 -0
- package/dist/src/team-backfill-scheduling.js +33 -0
- package/dist/src/team-backfill.d.ts +116 -0
- package/dist/src/team-backfill.js +1429 -0
- package/dist/src/team-index-cache.d.ts +16 -0
- package/dist/src/team-index-cache.js +152 -0
- package/dist/src/team-offboarding.d.ts +38 -0
- package/dist/src/team-offboarding.js +1043 -0
- package/dist/src/team-presence.d.ts +127 -0
- package/dist/src/team-presence.js +904 -0
- package/dist/src/team-publication-policy.d.ts +20 -0
- package/dist/src/team-publication-policy.js +140 -0
- package/dist/src/team-realtime.d.ts +68 -0
- package/dist/src/team-realtime.js +816 -0
- package/dist/src/team-source-facts-cache.d.ts +23 -0
- package/dist/src/team-source-facts-cache.js +255 -0
- package/dist/src/trash.d.ts +1 -1
- package/dist/src/trash.js +2 -2
- package/dist/src/tui.js +11 -12
- package/dist/src/types.d.ts +173 -7
- package/dist/src/types.js +20 -0
- package/dist/src/ui-server.d.ts +163 -35
- package/dist/src/ui-server.js +712 -72
- package/dist/src/ui.d.ts +1 -2
- package/dist/src/ui.js +1 -2
- package/dist/src/upload.d.ts +27 -0
- package/dist/src/upload.js +383 -43
- package/dist/src/vault.d.ts +226 -30
- package/dist/src/vault.js +1776 -192
- package/dist/src/watcher.d.ts +7 -1
- package/dist/src/watcher.js +198 -55
- package/dist/src/worker.d.ts +27 -3
- package/dist/src/worker.js +274 -55
- package/package.json +33 -12
- package/scripts/native-reboot-rehearsal.mjs +90 -0
- package/web/app.js +6032 -343
- package/web/bootstrap.js +17 -0
- package/web/index.html +255 -57
- package/web/rail.js +317 -40
- package/web/retention.html +2 -2
- package/web/rules-view.js +188 -16
- package/web/sessions-view.js +485 -62
- package/web/sessions.html +2 -2
- package/web/setup-api.js +152 -29
- package/web/setup-logic.js +68 -9
- package/web/setup.html +113 -44
- package/web/setup.js +604 -71
- package/web/style.css +513 -98
- package/dist/packages/vaultline-crypto/src/chunk-access.js +0 -93
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/aead.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/format.js +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/recipients.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.d.ts +0 -0
- /package/dist/packages/{vaultline-crypto → sealkeep-crypto}/src/sha256-stream.js +0 -0
|
@@ -3,6 +3,7 @@ import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { aeadCipher, aeadDecipher } from "./aead.js";
|
|
4
4
|
import { CryptoError, unwrapArchiveKey, wrapForKey, wrapForPhrase, x25519PublicKeyFromRaw, zeroize } from "./recipients.js";
|
|
5
5
|
import { DEFAULT_CHUNK_BYTES, ENVELOPE_VERSION, KEY_BYTES, NONCE_BYTES, SUITES, TAG_BYTES, chunkAad, chunkNonce } from "./format.js";
|
|
6
|
+
import { squeezeSync, unsqueezeSync } from "./squeeze.js";
|
|
6
7
|
const sha256 = (input) => createHash("sha256").update(input).digest("hex");
|
|
7
8
|
function toPublicKey(value) {
|
|
8
9
|
return Buffer.isBuffer(value) ? x25519PublicKeyFromRaw(value) : value;
|
|
@@ -38,9 +39,10 @@ export function encryptArchive(plaintext, options) {
|
|
|
38
39
|
const noncePrefix = options.deterministic?.noncePrefix ?? randomBytes(4);
|
|
39
40
|
if (archiveKey.length !== KEY_BYTES)
|
|
40
41
|
throw new CryptoError("key_malformed", `An archive key must be ${KEY_BYTES} bytes`);
|
|
41
|
-
if ((options.compression ?? "none")
|
|
42
|
-
throw new CryptoError("envelope_malformed",
|
|
43
|
-
const
|
|
42
|
+
if (/-chunk$/.test(options.compression ?? "none"))
|
|
43
|
+
throw new CryptoError("envelope_malformed", `${options.compression} sealing is streaming-only; use sealArchiveToFile`);
|
|
44
|
+
const codecIn = options.compression ?? "none";
|
|
45
|
+
const body = codecIn === "gzip" ? gzipSync(plaintext, { level: 6 }) : codecIn === "zstd" ? squeezeSync(plaintext) : plaintext;
|
|
44
46
|
const totalChunks = Math.max(1, Math.ceil(body.length / chunkBytes));
|
|
45
47
|
const chunks = [];
|
|
46
48
|
const blocks = [];
|
|
@@ -58,8 +60,8 @@ export function encryptArchive(plaintext, options) {
|
|
|
58
60
|
const envelope = {
|
|
59
61
|
version: ENVELOPE_VERSION, archiveId, suite, chunkBytes, chunks,
|
|
60
62
|
manifest: { plaintextSha256: sha256(body), plaintextBytes: body.length,
|
|
61
|
-
...(
|
|
62
|
-
? { compression:
|
|
63
|
+
...(codecIn === "gzip" || codecIn === "zstd"
|
|
64
|
+
? { compression: codecIn, originalBytes: plaintext.length, originalSha256: sha256(plaintext) }
|
|
63
65
|
: {}), createdAt: options.createdAt ?? new Date().toISOString(), adapter: options.adapter },
|
|
64
66
|
wrappedKeys: wrapAll(archiveKey, options.recipients, suite, archiveId, options.deterministic)
|
|
65
67
|
};
|
|
@@ -100,20 +102,25 @@ export function validateEnvelope(envelope, ciphertextBytes) {
|
|
|
100
102
|
throw new CryptoError("envelope_malformed", "Chunk lengths do not sum to the manifest plaintext length");
|
|
101
103
|
}
|
|
102
104
|
/**
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* the
|
|
106
|
-
*
|
|
105
|
+
* Authenticates and decrypts the stored body without expanding its compression.
|
|
106
|
+
*
|
|
107
|
+
* Keeping the authenticated chunks separate matters for callers that can stream
|
|
108
|
+
* decompression. A highly-compressible search index may be tens of megabytes on
|
|
109
|
+
* disk and more than a gigabyte after gzip; forcing that expanded body through a
|
|
110
|
+
* single Buffer is enough to OOM a long-lived agent process before it can inspect
|
|
111
|
+
* one query term.
|
|
107
112
|
*/
|
|
108
|
-
export function
|
|
113
|
+
export function decryptArchiveBodyChunks(envelope, ciphertext, unlock) {
|
|
109
114
|
validateEnvelope(envelope, ciphertext.length);
|
|
110
115
|
const archiveKey = unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
|
|
111
116
|
if (archiveKey.length !== KEY_BYTES) {
|
|
112
117
|
zeroize(archiveKey);
|
|
113
118
|
throw new CryptoError("key_malformed", "Unwrapped archive key has the wrong length");
|
|
114
119
|
}
|
|
120
|
+
const parts = [];
|
|
115
121
|
try {
|
|
116
|
-
const
|
|
122
|
+
const bodyHash = createHash("sha256");
|
|
123
|
+
let bodyBytes = 0;
|
|
117
124
|
let offset = 0;
|
|
118
125
|
for (const chunk of envelope.chunks) {
|
|
119
126
|
const stored = ciphertext.subarray(offset, offset + chunk.storedBytes);
|
|
@@ -124,30 +131,57 @@ export function decryptArchive(envelope, ciphertext, unlock) {
|
|
|
124
131
|
decipher.setAAD(chunkAad({ archiveId: envelope.archiveId, suite: envelope.suite, index: chunk.index, totalChunks: envelope.chunks.length, plaintextBytes: chunk.plaintextBytes, final: chunk.final }));
|
|
125
132
|
decipher.setAuthTag(stored.subarray(stored.length - TAG_BYTES));
|
|
126
133
|
try {
|
|
127
|
-
|
|
134
|
+
const part = Buffer.concat([decipher.update(stored.subarray(0, stored.length - TAG_BYTES)), decipher.final()]);
|
|
135
|
+
parts.push(part);
|
|
136
|
+
bodyHash.update(part);
|
|
137
|
+
bodyBytes += part.length;
|
|
128
138
|
}
|
|
129
139
|
catch {
|
|
130
140
|
throw new CryptoError("chunk_auth_failed", `Chunk ${chunk.index} failed authentication`);
|
|
131
141
|
}
|
|
132
142
|
}
|
|
133
|
-
|
|
134
|
-
if (plaintext.length !== envelope.manifest.plaintextBytes)
|
|
143
|
+
if (bodyBytes !== envelope.manifest.plaintextBytes)
|
|
135
144
|
throw new CryptoError("plaintext_length_mismatch", "Decrypted length does not match the manifest");
|
|
136
|
-
if (
|
|
145
|
+
if (bodyHash.digest("hex") !== envelope.manifest.plaintextSha256)
|
|
137
146
|
throw new CryptoError("plaintext_hash_mismatch", "Decrypted bytes do not match the manifest hash");
|
|
147
|
+
return parts;
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
for (const part of parts)
|
|
151
|
+
zeroize(part);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
zeroize(archiveKey);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Decrypts an archive after verifying its structure and every stored chunk hash.
|
|
160
|
+
* Every chunk is authenticated against associated data that pins its position and
|
|
161
|
+
* the total chunk count, so a reordered or truncated archive fails rather than
|
|
162
|
+
* returning partial plaintext.
|
|
163
|
+
*/
|
|
164
|
+
export function decryptArchive(envelope, ciphertext, unlock) {
|
|
165
|
+
const parts = decryptArchiveBodyChunks(envelope, ciphertext, unlock);
|
|
166
|
+
const plaintext = Buffer.concat(parts);
|
|
167
|
+
try {
|
|
138
168
|
// Absent on archives written without compression, where the authenticated
|
|
139
169
|
// body already is the caller's plaintext.
|
|
140
170
|
if (envelope.manifest.compression === undefined)
|
|
141
171
|
return plaintext;
|
|
142
|
-
|
|
172
|
+
const codec = envelope.manifest.compression;
|
|
173
|
+
if (codec !== "gzip" && codec !== "gzip-chunk" && codec !== "zstd" && codec !== "zstd-chunk")
|
|
143
174
|
throw new CryptoError("envelope_malformed", "Unsupported compression");
|
|
175
|
+
// Which compressor sealed this archive is recorded in its own envelope,
|
|
176
|
+
// so gzip archives written before zstd existed keep opening unchanged.
|
|
177
|
+
const expand = codec === "zstd" || codec === "zstd-chunk" ? unsqueezeSync : gunzipSync;
|
|
144
178
|
let original;
|
|
145
179
|
try {
|
|
146
|
-
original =
|
|
180
|
+
original = codec.endsWith("-chunk")
|
|
147
181
|
// Each part is one chunk's compressed slice, independently made and
|
|
148
182
|
// independently undone — concatenating the results reproduces the file.
|
|
149
|
-
? Buffer.concat(parts.map((part) =>
|
|
150
|
-
:
|
|
183
|
+
? Buffer.concat(parts.map((part) => expand(part)))
|
|
184
|
+
: expand(plaintext);
|
|
151
185
|
}
|
|
152
186
|
catch {
|
|
153
187
|
throw new CryptoError("plaintext_hash_mismatch", "Decompression failed after authentication");
|
|
@@ -160,8 +194,11 @@ export function decryptArchive(envelope, ciphertext, unlock) {
|
|
|
160
194
|
throw new CryptoError("plaintext_hash_mismatch", "Decompressed bytes do not match the manifest hash");
|
|
161
195
|
return original;
|
|
162
196
|
}
|
|
163
|
-
|
|
164
|
-
zeroize(
|
|
197
|
+
catch (error) {
|
|
198
|
+
zeroize(plaintext);
|
|
199
|
+
for (const part of parts)
|
|
200
|
+
zeroize(part);
|
|
201
|
+
throw error;
|
|
165
202
|
}
|
|
166
203
|
}
|
|
167
204
|
/**
|
|
@@ -45,7 +45,7 @@ export type ChunkHeader = {
|
|
|
45
45
|
index: number;
|
|
46
46
|
/** Bytes this chunk's ciphertext decrypts to. Under "gzip-chunk" that is the compressed slice; the raw length lives in `rawBytes`. */
|
|
47
47
|
plaintextBytes: number;
|
|
48
|
-
/** Original (pre-compression) bytes this chunk covers. Present
|
|
48
|
+
/** Original (pre-compression) bytes this chunk covers. Present under either chunked codec, where it is what makes byte-ranged retrieval computable without decompressing anything. */
|
|
49
49
|
rawBytes?: number;
|
|
50
50
|
/** Stored bytes for this chunk: ciphertext followed by its authentication tag. */
|
|
51
51
|
storedBytes: number;
|
|
@@ -66,7 +66,7 @@ export type Envelope = {
|
|
|
66
66
|
* every chunk decrypts AND decompresses on its own — what makes streaming
|
|
67
67
|
* seals and fetch-only-the-chunks retrieval possible. Absent means none.
|
|
68
68
|
*/
|
|
69
|
-
compression?: "gzip" | "gzip-chunk";
|
|
69
|
+
compression?: "gzip" | "gzip-chunk" | "zstd" | "zstd-chunk";
|
|
70
70
|
originalBytes?: number;
|
|
71
71
|
originalSha256?: string;
|
|
72
72
|
plaintextSha256: string;
|
|
@@ -38,10 +38,10 @@ function openKey(suite, kek, nonce, sealed, aad) {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
export function phraseRecipientId(salt) {
|
|
41
|
-
return createHash("sha256").update(Buffer.concat([Buffer.from("
|
|
41
|
+
return createHash("sha256").update(Buffer.concat([Buffer.from("sealkeep-phrase-recipient:v2"), salt])).digest("hex").slice(0, 32);
|
|
42
42
|
}
|
|
43
43
|
export function keyRecipientId(publicKeyRaw) {
|
|
44
|
-
return createHash("sha256").update(Buffer.concat([Buffer.from("
|
|
44
|
+
return createHash("sha256").update(Buffer.concat([Buffer.from("sealkeep-key-recipient:v2"), publicKeyRaw])).digest("hex").slice(0, 32);
|
|
45
45
|
}
|
|
46
46
|
export function rawPublicKey(key) {
|
|
47
47
|
// The last 32 bytes of an X25519 SPKI DER encoding are the raw public key.
|
|
@@ -67,7 +67,7 @@ export function wrapForKey(archiveKey, recipientPublicKey, suite, archiveId, opt
|
|
|
67
67
|
const recipientRaw = rawPublicKey(recipientPublicKey);
|
|
68
68
|
const ephemeralRaw = rawPublicKey(ephemeral.publicKey);
|
|
69
69
|
const shared = diffieHellman({ privateKey: ephemeral.privateKey, publicKey: recipientPublicKey });
|
|
70
|
-
const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([recipientRaw, ephemeralRaw]), Buffer.from("
|
|
70
|
+
const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([recipientRaw, ephemeralRaw]), Buffer.from("sealkeep-recipient:v2"), KEY_BYTES));
|
|
71
71
|
const id = keyRecipientId(recipientRaw);
|
|
72
72
|
try {
|
|
73
73
|
return { type: "x25519", id, recipientPublicKey: b64(recipientRaw), ephemeralPublicKey: b64(ephemeralRaw), nonce: b64(options.nonce), ciphertext: b64(sealKey(suite, kek, options.nonce, archiveKey, wrapAad(archiveId, id))) };
|
|
@@ -112,7 +112,7 @@ export function unwrapArchiveKey(wrappedKeys, suite, archiveId, unlock) {
|
|
|
112
112
|
if ("privateKey" in unlock && wrapped.type === "x25519") {
|
|
113
113
|
const privateKey = typeof unlock.privateKey === "string" ? createPrivateKey(unlock.privateKey) : unlock.privateKey;
|
|
114
114
|
const shared = diffieHellman({ privateKey, publicKey: x25519PublicKeyFromRaw(unb64(wrapped.ephemeralPublicKey)) });
|
|
115
|
-
const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([unb64(wrapped.recipientPublicKey), unb64(wrapped.ephemeralPublicKey)]), Buffer.from("
|
|
115
|
+
const kek = Buffer.from(hkdfSync("sha256", shared, Buffer.concat([unb64(wrapped.recipientPublicKey), unb64(wrapped.ephemeralPublicKey)]), Buffer.from("sealkeep-recipient:v2"), KEY_BYTES));
|
|
116
116
|
try {
|
|
117
117
|
return openKey(suite, kek, unb64(wrapped.nonce), unb64(wrapped.ciphertext), wrapAad(archiveId, wrapped.id));
|
|
118
118
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const squeezeStream: () => import("zlib").ZstdCompress;
|
|
2
|
+
export declare const unsqueezeStream: () => import("zlib").ZstdDecompress;
|
|
3
|
+
export declare const squeezeSync: (input: Buffer) => NonSharedBuffer;
|
|
4
|
+
export declare const unsqueezeSync: (input: Buffer, options?: {
|
|
5
|
+
maxOutputLength?: number;
|
|
6
|
+
}) => NonSharedBuffer;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { constants as Z, createZstdCompress, createZstdDecompress, zstdCompressSync, zstdDecompressSync } from "node:zlib";
|
|
2
|
+
/**
|
|
3
|
+
* The compressor, and why it is not gzip.
|
|
4
|
+
*
|
|
5
|
+
* An agent transcript is not ordinary text. Every time the agent compacts its
|
|
6
|
+
* context it writes the whole conversation back to disk again — screenshots
|
|
7
|
+
* included — so one session file can hold the same 300 KB PNG fifty-odd times.
|
|
8
|
+
* Measured on a real 1.9 GB Codex session: 28 distinct images, each stored an
|
|
9
|
+
* average of 56 times, 90% of the file.
|
|
10
|
+
*
|
|
11
|
+
* gzip cannot see any of that. Its window is 32 KB, so a duplicate 300 KB image
|
|
12
|
+
* a hundred megabytes upstream is invisible and gets stored again in full; on
|
|
13
|
+
* that file it managed 1.5x. zstd with long-distance matching keeps a 128 MB
|
|
14
|
+
* window and collapses the repeats: the same file goes to 36 MB — 54x — and it
|
|
15
|
+
* does it faster than the gzip it replaces, because most of the input turns
|
|
16
|
+
* into a back-reference instead of being scanned.
|
|
17
|
+
*
|
|
18
|
+
* WINDOW_LOG is the whole trick and the one number worth guarding. At 4 MB
|
|
19
|
+
* chunks the same compressor gets 2x, because nothing repeats inside a 4 MB
|
|
20
|
+
* window; at whole-file scope it gets 59x. Compression here has to see the
|
|
21
|
+
* whole file, or it is not worth changing.
|
|
22
|
+
*
|
|
23
|
+
* Built into node:zlib since Node 22.15, so this costs no dependency and no
|
|
24
|
+
* new licence. Nothing decides to *read* zstd from these helpers: the archive's
|
|
25
|
+
* own envelope records which compressor sealed it, and gzip archives written
|
|
26
|
+
* before this existed keep opening through the gzip path, forever.
|
|
27
|
+
*/
|
|
28
|
+
const WINDOW_LOG = 27;
|
|
29
|
+
const COMPRESS = {
|
|
30
|
+
[Z.ZSTD_c_compressionLevel]: 9,
|
|
31
|
+
[Z.ZSTD_c_enableLongDistanceMatching]: 1,
|
|
32
|
+
[Z.ZSTD_c_windowLog]: WINDOW_LOG,
|
|
33
|
+
};
|
|
34
|
+
/** Readers must be told to allocate the same window the writer used. */
|
|
35
|
+
const DECOMPRESS = { [Z.ZSTD_d_windowLogMax]: WINDOW_LOG };
|
|
36
|
+
export const squeezeStream = () => createZstdCompress({ params: { ...COMPRESS } });
|
|
37
|
+
export const unsqueezeStream = () => createZstdDecompress({ params: { ...DECOMPRESS } });
|
|
38
|
+
export const squeezeSync = (input) => zstdCompressSync(input, { params: { ...COMPRESS } });
|
|
39
|
+
export const unsqueezeSync = (input, options = {}) => zstdDecompressSync(input, { ...options, params: { ...DECOMPRESS } });
|
|
@@ -36,6 +36,20 @@ export type StreamEncryptOptions = EncryptOptions & {
|
|
|
36
36
|
scratchDir?: string;
|
|
37
37
|
/** Called as the source is read, with cumulative bytes so far — a person watching a 1.6 GB seal deserves a number that moves. Throttling is the caller's business. */
|
|
38
38
|
onProgress?: (originalBytes: number) => void;
|
|
39
|
+
/** Cancels source reads, compression, sink writes and unpublished output. */
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
/**
|
|
42
|
+
* Cooperative scheduling checkpoint for low-priority callers. It is awaited
|
|
43
|
+
* after each bounded amount of compression/encryption work, so a streaming
|
|
44
|
+
* seal cannot monopolise a core merely because its memory use is bounded.
|
|
45
|
+
*/
|
|
46
|
+
onYield?: () => void | Promise<void>;
|
|
47
|
+
/**
|
|
48
|
+
* Each chunk's plaintext, as it is sealed (chunk-compressed layouts only —
|
|
49
|
+
* a whole-file body has no chunks to report). The caller that indexes what
|
|
50
|
+
* it seals reads the source once, here, instead of decrypting it again.
|
|
51
|
+
*/
|
|
52
|
+
onPlaintext?: (chunkIndex: number, slice: Buffer) => void;
|
|
39
53
|
};
|
|
40
54
|
export type SealResult = {
|
|
41
55
|
envelope: Envelope;
|
|
@@ -58,6 +72,16 @@ export type OpenResult = {
|
|
|
58
72
|
bytes: number;
|
|
59
73
|
sha256: string;
|
|
60
74
|
};
|
|
75
|
+
export type StreamOpenOptions = {
|
|
76
|
+
/** Reject before allocating the reusable ciphertext buffer if any stored chunk exceeds this many bytes. */
|
|
77
|
+
maxStoredChunkBytes?: number;
|
|
78
|
+
/** Reject both a larger declared original and actual decompression output beyond this many bytes. */
|
|
79
|
+
maxOriginalBytes?: number;
|
|
80
|
+
/** Reject one independently-compressed chunk after this many expanded bytes. */
|
|
81
|
+
maxExpandedChunkBytes?: number;
|
|
82
|
+
/** Expand independently-compressed chunks through a stream instead of a whole-chunk sync allocation. */
|
|
83
|
+
streamChunkDecompression?: boolean;
|
|
84
|
+
};
|
|
61
85
|
/**
|
|
62
86
|
* Opens an archive straight to a file, holding one chunk at a time.
|
|
63
87
|
*
|
|
@@ -68,7 +92,16 @@ export type OpenResult = {
|
|
|
68
92
|
* a rejection can leave a partial file behind, which is why `restoreArchive`
|
|
69
93
|
* renames into place rather than writing the destination directly.
|
|
70
94
|
*/
|
|
71
|
-
|
|
95
|
+
/**
|
|
96
|
+
* `destPath` may be a Writable instead of a path. Verification needs to prove an
|
|
97
|
+
* archive decrypts to its recorded bytes, and materialising the plaintext to do
|
|
98
|
+
* so costs as much free disk as the original — on the nearly-full disk that
|
|
99
|
+
* makes someone reach for this product in the first place, that is exactly the
|
|
100
|
+
* space they do not have, so the proof became impossible precisely when it
|
|
101
|
+
* mattered. Every integrity check below runs the same either way; only the
|
|
102
|
+
* destination changes.
|
|
103
|
+
*/
|
|
104
|
+
export declare function openArchiveToFile(envelope: Envelope, ciphertextPath: string, destPath: string | NodeJS.WritableStream, unlock: Unlock, options?: StreamOpenOptions): Promise<OpenResult>;
|
|
72
105
|
/**
|
|
73
106
|
* SHA-256 of several prefixes of a file, in one read and in constant memory.
|
|
74
107
|
*
|
|
@@ -80,7 +113,7 @@ export declare function openArchiveToFile(envelope: Envelope, ciphertextPath: st
|
|
|
80
113
|
*
|
|
81
114
|
* An offset past the end of the file is simply absent from the result.
|
|
82
115
|
*/
|
|
83
|
-
export declare function hashFilePrefixes(path: string, offsets: readonly number[]): Promise<Map<number, string>>;
|
|
116
|
+
export declare function hashFilePrefixes(path: string, offsets: readonly number[], onProgress?: (bytesRead: number) => void, signal?: AbortSignal, onYield?: () => void | Promise<void>): Promise<Map<number, string>>;
|
|
84
117
|
/**
|
|
85
118
|
* SHA-256 of a byte range of a file, read in constant memory.
|
|
86
119
|
*
|
|
@@ -88,7 +121,7 @@ export declare function hashFilePrefixes(path: string, offsets: readonly number[
|
|
|
88
121
|
* live file and comparing it with what an archive recorded is the only evidence
|
|
89
122
|
* that the archive's bytes are still a prefix of it.
|
|
90
123
|
*/
|
|
91
|
-
export declare function hashFileRange(path: string, start?: number, end?: number): Promise<{
|
|
124
|
+
export declare function hashFileRange(path: string, start?: number, end?: number, onProgress?: (bytesRead: number) => void, signal?: AbortSignal, onYield?: () => void | Promise<void>): Promise<{
|
|
92
125
|
sha256: string;
|
|
93
126
|
bytes: number;
|
|
94
127
|
}>;
|