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
package/README.md
CHANGED
|
@@ -15,14 +15,18 @@ sealkeep autopilot
|
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
That is the whole setup. It creates your vault, shows your recovery phrase once, finds the
|
|
18
|
-
sessions already on this machine,
|
|
18
|
+
sessions already on this machine, installs the agent memory hooks, and installs a background
|
|
19
|
+
service that starts at login.
|
|
19
20
|
|
|
20
|
-
From then on you do nothing. New sessions are sealed as they finish
|
|
21
|
-
|
|
21
|
+
From then on you do nothing. New sessions are sealed, indexed, and uploaded as they finish;
|
|
22
|
+
their original files stay on this machine by default. If you explicitly opt into reclaim,
|
|
23
|
+
verified and searchable old local originals can be permanently reclaimed after every idle,
|
|
24
|
+
grace, identity, and liveness check passes.
|
|
22
25
|
|
|
23
26
|
```sh
|
|
27
|
+
sealkeep autopilot --reclaim # explicitly allow safe background disk reclaim on this machine
|
|
24
28
|
sealkeep autopilot status # is it alive, and what has it done
|
|
25
|
-
sealkeep autopilot off # stop it — every archive stays
|
|
29
|
+
sealkeep autopilot off # stop it, remove Sealkeep's hooks — every archive stays
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
It tells you it is working rather than leaving you to wonder. The service writes a
|
|
@@ -63,10 +67,15 @@ a laptop rewraps keys rather than re-encrypting your data.
|
|
|
63
67
|
count and checksum must match what was computed locally, or the archive is not marked
|
|
64
68
|
durable and your local file stays exactly where it is.
|
|
65
69
|
|
|
66
|
-
**4. Reclaim** — only a verified, idle, out-of-grace-period session
|
|
67
|
-
|
|
70
|
+
**4. Reclaim** — only a remotely verified, searchable, idle, out-of-grace-period session
|
|
71
|
+
whose source still exactly matches its archive becomes eligible. With explicit or opt-in
|
|
72
|
+
automatic approval, Sealkeep durably journals the change, leaves Codex a tiny same-session
|
|
73
|
+
resume pointer, and permanently removes the staged full original. Before the purge decision,
|
|
74
|
+
any failure restores the original; after it, restart reconciliation finishes the committed purge.
|
|
68
75
|
|
|
69
|
-
Autopilot runs
|
|
76
|
+
Autopilot always runs Catch, Seal, and Verify on a loop. Reclaim joins that loop only
|
|
77
|
+
after you explicitly opt this machine in with `--reclaim` or the local Settings switch.
|
|
78
|
+
To unlock the vault without a human present, it keeps
|
|
70
79
|
your phrase in the OS keystore — macOS Keychain, Freedesktop Secret Service, or Windows
|
|
71
80
|
DPAPI. That is a real trade-off, stated plainly: anything running as you can read it once
|
|
72
81
|
you are logged in. Opt out with `--no-remember` and supply
|
|
@@ -146,19 +155,20 @@ it stores archives as single objects, and says so.
|
|
|
146
155
|
|
|
147
156
|
| | |
|
|
148
157
|
| --- | --- |
|
|
149
|
-
| `sealkeep autopilot` |
|
|
158
|
+
| `sealkeep autopilot` | Preserve, index, and upload automatically; keep original sessions by default |
|
|
159
|
+
| `sealkeep autopilot --reclaim` | Explicitly allow this machine to reclaim eligible originals |
|
|
150
160
|
| `sealkeep autopilot status` · `off` | Check on it, or stop it |
|
|
151
161
|
| `sealkeep quickstart` | Same setup, no background service |
|
|
152
162
|
| `sealkeep status` · `doctor` | What is archived; whether this machine is healthy |
|
|
153
163
|
| `sealkeep queue run` | Encrypt everything waiting |
|
|
154
|
-
| `sealkeep daemon` | Watch, seal,
|
|
164
|
+
| `sealkeep daemon` | Watch, seal, index, and upload continuously; reclaim only when enabled |
|
|
155
165
|
| `sealkeep search <query>` | Metadata search; `--content` searches inside sessions |
|
|
156
166
|
| `sealkeep recover <id> <dest>` | Restore original bytes; `--native` puts them back where they came from |
|
|
157
167
|
| `sealkeep recover <filename>` | The file an agent's resume just complained about, back in place |
|
|
158
168
|
| `sealkeep archive <path> --stream` | Seal straight to the bucket as a chunk folder, resumable mid-upload |
|
|
159
169
|
| `sealkeep share <id>` | A sealed bundle under a one-time passcode — the phrase never travels |
|
|
160
170
|
| `sealkeep mcp install` | Register the vault as a tool server with Claude Code and Codex |
|
|
161
|
-
| `sealkeep retention apply` | Dry run; `--confirm`
|
|
171
|
+
| `sealkeep retention apply` | Dry run; `--confirm` permanently frees verified local originals |
|
|
162
172
|
| `sealkeep desktop` · `tui` | The dashboard, in a browser or the terminal |
|
|
163
173
|
| `sealkeep audit` | Everything Sealkeep has done to your files |
|
|
164
174
|
|
|
@@ -171,13 +181,86 @@ Codex, so the agent itself can search your sealed history, check vault
|
|
|
171
181
|
status, and restore a session it needs — six tools, all local, nothing new
|
|
172
182
|
exposed to the network.
|
|
173
183
|
|
|
184
|
+
Ordinary work does not depend on the agent remembering to call that tool.
|
|
185
|
+
The installed lifecycle hooks identify the current Git project and inject a
|
|
186
|
+
small, labelled context automatically:
|
|
187
|
+
|
|
188
|
+
- **One developer, several machines.** A finished Mac session is sealed and
|
|
189
|
+
its encrypted index syncs in the background. When the same developer opens
|
|
190
|
+
the project on a VPS, the newest preserved handoffs, commits, and touched
|
|
191
|
+
files are prepared automatically—even if the prompt only says “continue.”
|
|
192
|
+
- **Independent accounts working on one project.** One person enables Team mode
|
|
193
|
+
for the project in the Shared page and invites teammates by account email.
|
|
194
|
+
Each machine receives only its own wrapped copy of the random project feed
|
|
195
|
+
key. The background service automatically holds one authenticated realtime
|
|
196
|
+
connection and multiplexes every bound project over it. Normal Claude Code
|
|
197
|
+
and Codex lifecycle hooks publish encrypted start/progress/finish events;
|
|
198
|
+
push writes them to a bounded local inbox, and the next safe agent boundary
|
|
199
|
+
consumes them without polling. HTTP cursor reads happen only at cold start,
|
|
200
|
+
after reconnect, or while push is unhealthy, so an API contract chosen on
|
|
201
|
+
one machine reaches the other invited accounts without sharing a recovery
|
|
202
|
+
phrase or running a Sealkeep command.
|
|
203
|
+
|
|
204
|
+
Recall and cooperation use two hook lanes. The agent-facing lane only claims
|
|
205
|
+
and decrypts already-prepared local context, with a two-second fail-open limit;
|
|
206
|
+
it never searches an archive or waits for the network. A companion hook runs
|
|
207
|
+
asynchronously after startup, prompts, and meaningful tools: it publishes the
|
|
208
|
+
current task first, reconciles the encrypted team inbox, refreshes/searches the
|
|
209
|
+
memory index, then seals the result for the next safe agent boundary. Usually
|
|
210
|
+
startup preparation is ready for the first prompt. If a provider is slow, the
|
|
211
|
+
agent continues immediately and receives the consistent result at a later safe
|
|
212
|
+
boundary instead of freezing the person's work.
|
|
213
|
+
|
|
214
|
+
The hosted realtime route is itself a 256-bit opaque capability derived from
|
|
215
|
+
the team space. A signed-in client that somehow obtains that id can observe
|
|
216
|
+
traffic timing and ciphertext, but cannot publish through the membership-
|
|
217
|
+
checked endpoint or decrypt the feed without a wrapped project key. Durable
|
|
218
|
+
reads and writes always enforce membership. Every displayed team event also
|
|
219
|
+
shows the server-authenticated account id; its friendly label and text remain
|
|
220
|
+
explicitly untrusted because a legitimate member chooses both.
|
|
221
|
+
|
|
222
|
+
There are no daily `sealkeep search`, `sealkeep team`, or bridge commands in
|
|
223
|
+
that flow. Pairing the machine, choosing whether a project is active, and the
|
|
224
|
+
agent's normal one-time hook trust review are setup/security decisions; after
|
|
225
|
+
that, capture, recall, and exchange are background behavior. Recalled text is
|
|
226
|
+
always labelled as untrusted historical data and current facts are checked
|
|
227
|
+
against Git.
|
|
228
|
+
|
|
229
|
+
Autopilot-owned background integrations upgrade automatically on macOS and
|
|
230
|
+
Linux. Foreground `ui`, `desktop`, and `api` processes must be restarted after
|
|
231
|
+
an upgrade; they are not part of the background-service promise.
|
|
232
|
+
On a systemd Linux host, Autopilot also enables lingering for the current
|
|
233
|
+
numeric user when the host permits it, so the user service starts at boot and
|
|
234
|
+
survives the last SSH logout. Sealkeep records whether it changed that
|
|
235
|
+
user-wide setting and reverses only its own change when Autopilot is turned
|
|
236
|
+
off. If `loginctl` is unavailable or policy refuses the change, preservation
|
|
237
|
+
continues for the current login and resumes at the next one; setup reports the
|
|
238
|
+
limitation instead of blocking or silently claiming reboot persistence.
|
|
239
|
+
On its first start after an upgrade, the background service reconciles only
|
|
240
|
+
lifecycle hooks that Sealkeep already owns;
|
|
241
|
+
unrelated settings and user hooks are retained and the pre-edit file is backed
|
|
242
|
+
up. It never opts a previously unhooked agent into integration. Running
|
|
243
|
+
`sealkeep autopilot off` removes those owned hooks while keeping the vault and
|
|
244
|
+
every archive. A disclosed, package-independent integration manager also stays
|
|
245
|
+
next to the vault while Autopilot is enabled. If the npm package remains absent
|
|
246
|
+
for five minutes after a raw package-manager uninstall, it removes only the
|
|
247
|
+
owned service and hook entries, writes a cleanup receipt, and preserves sealed
|
|
248
|
+
archives, the outbox, recovery material, and remote objects.
|
|
249
|
+
|
|
250
|
+
Recovery material is kept in the strongest backend that proves it can perform
|
|
251
|
+
a real reversible write on that machine: macOS Keychain, Linux Secret Service,
|
|
252
|
+
modern systemd user-scoped encrypted credentials on a headless Linux host, or
|
|
253
|
+
Windows DPAPI. If none is usable, setup names the owner-only file fallback
|
|
254
|
+
honestly; the native reboot rehearsal refuses that fallback.
|
|
255
|
+
|
|
174
256
|
And the failure this whole product could have caused is handled where it
|
|
175
257
|
would happen: resuming a session whose transcript was archived and reclaimed.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<
|
|
180
|
-
|
|
258
|
+
Codex keeps a tiny valid native pointer under the same session id. The resumed
|
|
259
|
+
agent searches relevant preserved memory through MCP without downloading the
|
|
260
|
+
full transcript. For an explicit full restore, run `sealkeep recover
|
|
261
|
+
<archive-id> --native --overwrite backup` and resume the same id again. The
|
|
262
|
+
backup policy preserves anything already at the native path; no SessionStart
|
|
263
|
+
hook automatically replaces an open transcript or downloads gigabytes.
|
|
181
264
|
|
|
182
265
|
## Your storage
|
|
183
266
|
|
|
@@ -204,7 +287,7 @@ Format v2: a random per-archive key encrypts the session in chunks under ChaCha2
|
|
|
204
287
|
authenticated over its own position, the archive's identity, and the total chunk count — so
|
|
205
288
|
reordering, truncation, splicing, and suite downgrade are all detectable rather than silent.
|
|
206
289
|
|
|
207
|
-
The format lives in [`packages/
|
|
290
|
+
The format lives in [`packages/sealkeep-crypto/`](./packages/sealkeep-crypto/README.md)
|
|
208
291
|
with deterministic test vectors and a tamper suite that flips every byte of a stored archive
|
|
209
292
|
in turn. Assumptions and limits are in [`THREAT_MODEL.md`](./THREAT_MODEL.md).
|
|
210
293
|
|
|
@@ -216,20 +299,20 @@ built on published primitives — not an audited end-to-end-encryption product.
|
|
|
216
299
|
| Area | State |
|
|
217
300
|
| --- | --- |
|
|
218
301
|
| Capture and sealing | **Shipping** — durable queue, crash-safe, deduplicating, resumable streaming |
|
|
219
|
-
| Restore | **Shipping** — byte-identical, verified twice,
|
|
220
|
-
| Retention | **Shipping** —
|
|
302
|
+
| Restore | **Shipping** — byte-identical, verified twice, refuses to overwrite unless told; a reclaimed-session pointer supports MCP search and explicit safe full restore |
|
|
303
|
+
| Retention | **Shipping** — opt-in, transaction-staged permanent reclaim; pre-commit failures restore and committed crashes reconcile |
|
|
221
304
|
| Search | **Shipping** — sealed content index, synced across your machines as ciphertext |
|
|
222
|
-
| Agent integration | **Shipping** — MCP server for Claude Code and Codex
|
|
305
|
+
| Agent integration | **Shipping** — MCP server plus automatic project recall and active-team lifecycle hooks for Claude Code and Codex |
|
|
223
306
|
| Encryption format | **Unaudited** — vectors and tamper suite, no third-party audit yet |
|
|
224
|
-
| Cloud providers | **
|
|
307
|
+
| Cloud providers | **R2 live in production** (managed storage); S3, GCS, and Drive verified against signature vectors and emulators, without a long production soak yet |
|
|
225
308
|
| Signed binaries | **Not yet** — npm provenance and a signed manifest; no OS code-signing certificate |
|
|
226
309
|
| Distribution | **Published** — `npm install -g sealkeep` |
|
|
227
310
|
|
|
228
311
|
## Self-hosting the control plane
|
|
229
312
|
|
|
230
313
|
```sh
|
|
231
|
-
docker build -t
|
|
232
|
-
docker run -p 8787:8787 -v
|
|
314
|
+
docker build -t sealkeep-control-plane .
|
|
315
|
+
docker run -p 8787:8787 -v sealkeep-data:/data sealkeep-control-plane
|
|
233
316
|
```
|
|
234
317
|
|
|
235
318
|
Devices authenticate with Ed25519 request signatures, leases are single-use and expiring,
|
|
@@ -241,6 +324,9 @@ and revocation blocks new leases without touching archives. See
|
|
|
241
324
|
```sh
|
|
242
325
|
npm test # the full suite — crypto vectors, a tamper suite, provider emulators, dashboard rendering
|
|
243
326
|
npm run build
|
|
327
|
+
npm run rehearsal:install-upgrade # disposable global install plus real 0.9.0 → current upgrade
|
|
328
|
+
npm run rehearsal:automatic-memory # real Mac/VPS Codex; needs documented QA account + SSH VPS env vars
|
|
329
|
+
npm run rehearsal:team-realtime # four real Codex actors; isolated local control-plane lab
|
|
244
330
|
```
|
|
245
331
|
|
|
246
332
|
Architecture is in [`ARCHITECTURE.md`](./ARCHITECTURE.md); designs and operations live in
|
package/THIRD_PARTY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Third-party components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Sealkeep ships with the following third-party components, each under its own
|
|
4
4
|
licence (see §8 of LICENSE). Nothing else in the published package is
|
|
5
5
|
third-party code.
|
|
6
6
|
|
|
@@ -9,7 +9,7 @@ third-party code.
|
|
|
9
9
|
- **zod** — TypeScript schema validation.
|
|
10
10
|
Licence: MIT. https://github.com/colinhacks/zod
|
|
11
11
|
- **@modelcontextprotocol/sdk** — Model Context Protocol SDK, used by
|
|
12
|
-
`
|
|
12
|
+
`sealkeep mcp`.
|
|
13
13
|
Licence: MIT. https://github.com/modelcontextprotocol/typescript-sdk
|
|
14
14
|
|
|
15
15
|
## Vendored data
|
package/THREAT_MODEL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Sealkeep Threat Model
|
|
2
2
|
|
|
3
|
-
**Scope:** the `
|
|
3
|
+
**Scope:** the `sealkeep-crypto` archive envelope (format v2) and the client that produces it.
|
|
4
4
|
**Status:** written by the implementing engineers. **Not yet independently audited.** See "Open gates".
|
|
5
5
|
|
|
6
6
|
## 1. What the system protects
|
|
@@ -56,7 +56,7 @@ are separately bound to archive id and suite. Consequently these are all detecte
|
|
|
56
56
|
|
|
57
57
|
Out of scope for confidentiality: an attacker running as the user can read the plaintext
|
|
58
58
|
transcripts directly from the agent's own directories. Sealkeep reduces blast radius by
|
|
59
|
-
keeping the recovery phrase out of files (it is
|
|
59
|
+
keeping the recovery phrase out of files (today it is also accepted as a `--recovery-phrase` argument, which lands in shell history and the process list — prefer the keystore or the environment variable; tightening this is an open item), storing
|
|
60
60
|
its own state at mode `600`, and never writing the phrase to logs, queue jobs, or the API.
|
|
61
61
|
|
|
62
62
|
### 3.5 Lost or stolen device
|
|
@@ -79,6 +79,25 @@ The MCP server is local but model-driven. Search returns metadata only. Archive
|
|
|
79
79
|
require the recovery phrase from the local environment, recover refuses to overwrite by default,
|
|
80
80
|
and native restore refuses any path outside the adapter's own transcript roots.
|
|
81
81
|
|
|
82
|
+
### 3.8 One-time Backup Recovery Kit
|
|
83
|
+
|
|
84
|
+
The ordinary escrow record contains only the permanent 24-word phrase wrap. Each of the five
|
|
85
|
+
backup codes has a separate encrypted capsule, indexed by the first 128 bits of SHA-256 over the
|
|
86
|
+
canonical 128-bit code. The service therefore sees an opaque, non-enumerable locator but never
|
|
87
|
+
the code, phrase, archive key, or plaintext vault material.
|
|
88
|
+
|
|
89
|
+
Redeeming a capsule and recording its random per-machine claim happen in one database
|
|
90
|
+
transaction, scoped to the authenticated account. A different claim receives no capsule. If the
|
|
91
|
+
winning response is lost, only that same claim can retrieve the capsule. Replacing the kit
|
|
92
|
+
atomically removes both the active capsules and their consumption rows before installing the new
|
|
93
|
+
set. The client checks that the target has no physical archive records before redemption and does
|
|
94
|
+
not decrypt or persist permanent vault material until Cloud has awarded the capsule.
|
|
95
|
+
|
|
96
|
+
The control plane can still deny recovery or destroy a capsule. It cannot use a stored capsule
|
|
97
|
+
without the printed code, and a database copy cannot replay a spent code through the service.
|
|
98
|
+
As with any bearer recovery secret, anyone who copies an unused printed code and can sign in to
|
|
99
|
+
the same account may race its owner; exactly one claim wins.
|
|
100
|
+
|
|
82
101
|
## 4. Cryptographic design
|
|
83
102
|
|
|
84
103
|
- **Suite:** ChaCha20-Poly1305 (default) or AES-256-GCM, both from OpenSSL via Node. No custom primitive.
|
|
@@ -95,11 +114,11 @@ and native restore refuses any path outside the adapter's own transcript roots.
|
|
|
95
114
|
3. **No forward secrecy for the phrase.** Anyone who learns the recovery phrase can open every archive it was a recipient for, including old ones.
|
|
96
115
|
4. **Zeroization is best effort.** JavaScript may retain copies of buffers the implementation cannot reach.
|
|
97
116
|
5. **scrypt parameters are modest** so that a hook-driven flow stays responsive; they are recorded per archive and can be raised without breaking existing archives.
|
|
98
|
-
6. **
|
|
117
|
+
6. **Streaming.** Archives seal through a chunked stream (64 MB windows); neither sealing nor verification holds a whole archive in memory.
|
|
99
118
|
|
|
100
119
|
## 6. Open gates before an end-to-end-encryption claim
|
|
101
120
|
|
|
102
|
-
- [ ] Independent third-party cryptographic audit of `
|
|
121
|
+
- [ ] Independent third-party cryptographic audit of `sealkeep-crypto` and this document.
|
|
103
122
|
- [ ] Publication of the package under a public repository with the test vectors.
|
|
104
123
|
- [ ] Review of the key lifecycle by someone outside the implementing team.
|
|
105
124
|
|
|
@@ -22,6 +22,26 @@ export type ChunkWindow = {
|
|
|
22
22
|
rawStartInWindow: number;
|
|
23
23
|
rawLengthInWindow: number;
|
|
24
24
|
};
|
|
25
|
+
export type ChunkAccessOptions = {
|
|
26
|
+
/** Stored bytes in this window, including chunks that pad the requested range. */
|
|
27
|
+
maxStoredBytes?: number;
|
|
28
|
+
/** All expanded bytes in this window, including padding, not just returned bytes. */
|
|
29
|
+
maxExpandedBytes?: number;
|
|
30
|
+
maxExpandedChunkBytes?: number;
|
|
31
|
+
/** Checked before/after synchronous decompression and between chunks. */
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
/**
|
|
34
|
+
* The archive key of this envelope, already unwrapped by the caller (see
|
|
35
|
+
* `unwrapWindowKey`). Unwrapping a phrase recipient is an scrypt derivation
|
|
36
|
+
* of ~70 ms; a reader that opens dozens of windows of one sealed file pays
|
|
37
|
+
* it once instead of per window. The caller owns the key's lifetime and
|
|
38
|
+
* zeroization — it is never zeroized here.
|
|
39
|
+
*/
|
|
40
|
+
archiveKey?: Buffer;
|
|
41
|
+
};
|
|
42
|
+
/** The archive key `openChunkWindow` would unwrap for every window; unwrap it
|
|
43
|
+
* once, pass it as `options.archiveKey`, and zeroize it when done. */
|
|
44
|
+
export declare function unwrapWindowKey(envelope: Envelope, unlock: Unlock): Buffer;
|
|
25
45
|
/** True when an envelope supports random access at all. */
|
|
26
46
|
export declare function supportsChunkAccess(envelope: Envelope): boolean;
|
|
27
47
|
/**
|
|
@@ -32,8 +52,10 @@ export declare function supportsChunkAccess(envelope: Envelope): boolean;
|
|
|
32
52
|
export declare function chunkWindowForRange(envelope: Envelope, rawStart: number, rawEnd: number): ChunkWindow;
|
|
33
53
|
/**
|
|
34
54
|
* Opens the chunks inside a fetched stored slice and returns exactly the raw
|
|
35
|
-
* bytes the window was asked for.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
55
|
+
* bytes the window was asked for. The caller MUST authenticate the complete
|
|
56
|
+
* envelope and source-to-range mapping before using this primitive: v2 chunk
|
|
57
|
+
* AAD does not bind rawBytes or the codec, and a partial read cannot verify the
|
|
58
|
+
* whole-source hash or the lengths of chunks outside the window. The checks
|
|
59
|
+
* here prove selected ciphertext integrity and exact expanded chunk lengths.
|
|
38
60
|
*/
|
|
39
|
-
export declare function openChunkWindow(envelope: Envelope, window: ChunkWindow, storedSlice: Buffer, unlock: Unlock): Buffer;
|
|
61
|
+
export declare function openChunkWindow(envelope: Envelope, window: ChunkWindow, storedSlice: Buffer, unlock: Unlock, options?: ChunkAccessOptions): Buffer;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gunzipSync } from "node:zlib";
|
|
3
|
+
import { unsqueezeSync } from "./squeeze.js";
|
|
4
|
+
import { aeadDecipher } from "./aead.js";
|
|
5
|
+
import { validateEnvelope } from "./envelope.js";
|
|
6
|
+
import { chunkAad, TAG_BYTES } from "./format.js";
|
|
7
|
+
import { CryptoError, unwrapArchiveKey, zeroize } from "./recipients.js";
|
|
8
|
+
const sha256 = (input) => createHash("sha256").update(input).digest("hex");
|
|
9
|
+
/** The archive key `openChunkWindow` would unwrap for every window; unwrap it
|
|
10
|
+
* once, pass it as `options.archiveKey`, and zeroize it when done. */
|
|
11
|
+
export function unwrapWindowKey(envelope, unlock) {
|
|
12
|
+
return unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
|
|
13
|
+
}
|
|
14
|
+
const DEFAULT_WINDOW_BYTES = 128 * 1024 * 1024;
|
|
15
|
+
const DEFAULT_EXPANDED_CHUNK_BYTES = 64 * 1024 * 1024;
|
|
16
|
+
const nonnegativeInteger = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
17
|
+
function checkedAdd(left, right) {
|
|
18
|
+
const total = left + right;
|
|
19
|
+
if (!nonnegativeInteger(total))
|
|
20
|
+
throw new CryptoError("envelope_malformed", "Chunk lengths exceed the safe integer range");
|
|
21
|
+
return total;
|
|
22
|
+
}
|
|
23
|
+
/** True when an envelope supports random access at all. */
|
|
24
|
+
export function supportsChunkAccess(envelope) {
|
|
25
|
+
// Both chunk codecs qualify. What makes ranged access possible is that each
|
|
26
|
+
// chunk compresses and encrypts ALONE and records its own raw length — that
|
|
27
|
+
// is true of zstd-chunk exactly as it is of gzip-chunk. Naming only gzip here
|
|
28
|
+
// silently disqualified every zstd-chunk archive: they were built chunk-wise
|
|
29
|
+
// precisely so search could quote a line without decompressing a gigabyte,
|
|
30
|
+
// and then reported as un-rangeable, so search decompressed the gigabyte.
|
|
31
|
+
const codec = envelope?.manifest?.compression;
|
|
32
|
+
return (codec === "gzip-chunk" || codec === "zstd-chunk") && Array.isArray(envelope.chunks)
|
|
33
|
+
&& envelope.chunks.length > 0 && envelope.chunks.every((chunk) => chunk && nonnegativeInteger(chunk.rawBytes));
|
|
34
|
+
}
|
|
35
|
+
/** Structural validation is not authentication of the envelope or its raw offsets. */
|
|
36
|
+
function validateChunkLayout(envelope, signal) {
|
|
37
|
+
if (!supportsChunkAccess(envelope)) {
|
|
38
|
+
throw new CryptoError("envelope_malformed", "This archive has no valid chunk-independent layout; for a legacy archive, restore it whole instead of by range.");
|
|
39
|
+
}
|
|
40
|
+
if (!nonnegativeInteger(envelope.manifest.originalBytes)
|
|
41
|
+
|| !nonnegativeInteger(envelope.manifest.plaintextBytes)) {
|
|
42
|
+
throw new CryptoError("envelope_malformed", "The chunk manifest has invalid byte lengths");
|
|
43
|
+
}
|
|
44
|
+
let rawBytes = 0;
|
|
45
|
+
let storedBytes = 0;
|
|
46
|
+
let plaintextBytes = 0;
|
|
47
|
+
for (const chunk of envelope.chunks) {
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
if (!nonnegativeInteger(chunk.storedBytes) || !nonnegativeInteger(chunk.plaintextBytes)) {
|
|
50
|
+
throw new CryptoError("envelope_malformed", "A chunk has invalid byte lengths");
|
|
51
|
+
}
|
|
52
|
+
rawBytes = checkedAdd(rawBytes, chunk.rawBytes);
|
|
53
|
+
storedBytes = checkedAdd(storedBytes, chunk.storedBytes);
|
|
54
|
+
plaintextBytes = checkedAdd(plaintextBytes, chunk.plaintextBytes);
|
|
55
|
+
}
|
|
56
|
+
if (rawBytes !== envelope.manifest.originalBytes || plaintextBytes !== envelope.manifest.plaintextBytes
|
|
57
|
+
|| envelope.chunks.some((chunk) => chunk.rawBytes === 0 && (rawBytes !== 0 || envelope.chunks.length !== 1))) {
|
|
58
|
+
throw new CryptoError("envelope_malformed", "Chunk lengths do not describe the manifest's original bytes");
|
|
59
|
+
}
|
|
60
|
+
validateEnvelope(envelope, storedBytes);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Maps a raw (original-file) byte range to the chunk window that covers it.
|
|
64
|
+
* Legacy whole-body-gzip archives cannot answer this — a slice of their stream
|
|
65
|
+
* is undecodable alone — and the error says to restore whole instead.
|
|
66
|
+
*/
|
|
67
|
+
export function chunkWindowForRange(envelope, rawStart, rawEnd) {
|
|
68
|
+
validateChunkLayout(envelope);
|
|
69
|
+
return windowForValidatedRange(envelope, rawStart, rawEnd);
|
|
70
|
+
}
|
|
71
|
+
function windowForValidatedRange(envelope, rawStart, rawEnd) {
|
|
72
|
+
const total = envelope.manifest.originalBytes;
|
|
73
|
+
// An empty archive still has one authenticated compressed chunk. Explicitly
|
|
74
|
+
// reading its empty interval must verify that chunk rather than skip AEAD.
|
|
75
|
+
if (total === 0 && rawStart === 0 && rawEnd === 0) {
|
|
76
|
+
return { firstIndex: 0, lastIndex: 0, storedOffset: 0, storedLength: envelope.chunks[0].storedBytes,
|
|
77
|
+
rawStartInWindow: 0, rawLengthInWindow: 0 };
|
|
78
|
+
}
|
|
79
|
+
if (!nonnegativeInteger(rawStart) || !nonnegativeInteger(rawEnd) || rawEnd <= rawStart || rawStart >= total) {
|
|
80
|
+
throw new CryptoError("invalid_range", `Range ${rawStart}..${rawEnd} is not a range of this ${total}-byte archive`);
|
|
81
|
+
}
|
|
82
|
+
const end = Math.min(rawEnd, total);
|
|
83
|
+
let rawCursor = 0;
|
|
84
|
+
let storedCursor = 0;
|
|
85
|
+
let firstIndex = -1;
|
|
86
|
+
let lastIndex = -1;
|
|
87
|
+
let storedOffset = 0;
|
|
88
|
+
let storedLength = 0;
|
|
89
|
+
let rawStartInWindow = 0;
|
|
90
|
+
for (const chunk of envelope.chunks) {
|
|
91
|
+
const rawFrom = rawCursor;
|
|
92
|
+
const rawTo = rawCursor + (chunk.rawBytes ?? 0);
|
|
93
|
+
const overlaps = rawTo > rawStart && rawFrom < end;
|
|
94
|
+
if (overlaps && firstIndex === -1) {
|
|
95
|
+
firstIndex = chunk.index;
|
|
96
|
+
storedOffset = storedCursor;
|
|
97
|
+
rawStartInWindow = rawStart - rawFrom;
|
|
98
|
+
}
|
|
99
|
+
if (overlaps) {
|
|
100
|
+
lastIndex = chunk.index;
|
|
101
|
+
storedLength += chunk.storedBytes;
|
|
102
|
+
}
|
|
103
|
+
rawCursor = rawTo;
|
|
104
|
+
storedCursor += chunk.storedBytes;
|
|
105
|
+
}
|
|
106
|
+
return { firstIndex, lastIndex, storedOffset, storedLength, rawStartInWindow, rawLengthInWindow: end - rawStart };
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Opens the chunks inside a fetched stored slice and returns exactly the raw
|
|
110
|
+
* bytes the window was asked for. The caller MUST authenticate the complete
|
|
111
|
+
* envelope and source-to-range mapping before using this primitive: v2 chunk
|
|
112
|
+
* AAD does not bind rawBytes or the codec, and a partial read cannot verify the
|
|
113
|
+
* whole-source hash or the lengths of chunks outside the window. The checks
|
|
114
|
+
* here prove selected ciphertext integrity and exact expanded chunk lengths.
|
|
115
|
+
*/
|
|
116
|
+
export function openChunkWindow(envelope, window, storedSlice, unlock, options = {}) {
|
|
117
|
+
options.signal?.throwIfAborted();
|
|
118
|
+
validateChunkLayout(envelope, options.signal);
|
|
119
|
+
const maxStoredBytes = options.maxStoredBytes ?? DEFAULT_WINDOW_BYTES;
|
|
120
|
+
const maxExpandedBytes = options.maxExpandedBytes ?? DEFAULT_WINDOW_BYTES;
|
|
121
|
+
const maxExpandedChunkBytes = options.maxExpandedChunkBytes ?? DEFAULT_EXPANDED_CHUNK_BYTES;
|
|
122
|
+
for (const [name, limit] of Object.entries({ maxStoredBytes, maxExpandedBytes, maxExpandedChunkBytes })) {
|
|
123
|
+
if (!nonnegativeInteger(limit))
|
|
124
|
+
throw new CryptoError("resource_limit_invalid", `${name} must be a non-negative safe integer`);
|
|
125
|
+
}
|
|
126
|
+
const fields = ["firstIndex", "lastIndex", "storedOffset", "storedLength", "rawStartInWindow", "rawLengthInWindow"];
|
|
127
|
+
if (!window || fields.some((field) => !nonnegativeInteger(window[field]))
|
|
128
|
+
|| window.firstIndex > window.lastIndex || window.lastIndex >= envelope.chunks.length) {
|
|
129
|
+
throw new CryptoError("invalid_range", "The requested chunk window has invalid bounds");
|
|
130
|
+
}
|
|
131
|
+
let rawBeforeWindow = 0;
|
|
132
|
+
for (let index = 0; index < window.firstIndex; index += 1)
|
|
133
|
+
rawBeforeWindow += envelope.chunks[index].rawBytes;
|
|
134
|
+
const rawStart = rawBeforeWindow + window.rawStartInWindow;
|
|
135
|
+
const rawEnd = rawStart + window.rawLengthInWindow;
|
|
136
|
+
const expected = windowForValidatedRange(envelope, rawStart, rawEnd);
|
|
137
|
+
if (fields.some((field) => window[field] !== expected[field])) {
|
|
138
|
+
throw new CryptoError("invalid_range", "The requested chunk window does not match its declared raw range");
|
|
139
|
+
}
|
|
140
|
+
if (storedSlice.length !== window.storedLength) {
|
|
141
|
+
throw new CryptoError("ciphertext_length_mismatch", `The fetched slice is ${storedSlice.length} bytes; the window needs ${window.storedLength}`);
|
|
142
|
+
}
|
|
143
|
+
if (window.storedLength > maxStoredBytes)
|
|
144
|
+
throw new CryptoError("resource_limit_exceeded", "The chunk window exceeds its stored byte limit");
|
|
145
|
+
let expandedBytes = 0;
|
|
146
|
+
for (let index = window.firstIndex; index <= window.lastIndex; index += 1) {
|
|
147
|
+
const rawBytes = envelope.chunks[index].rawBytes;
|
|
148
|
+
expandedBytes += rawBytes;
|
|
149
|
+
if (rawBytes > maxExpandedChunkBytes || expandedBytes > maxExpandedBytes) {
|
|
150
|
+
throw new CryptoError("resource_limit_exceeded", "The chunk window exceeds its expanded byte limit, including padding");
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
options.signal?.throwIfAborted();
|
|
154
|
+
const ownsKey = options.archiveKey === undefined;
|
|
155
|
+
const archiveKey = options.archiveKey ?? unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
|
|
156
|
+
let result;
|
|
157
|
+
try {
|
|
158
|
+
result = Buffer.allocUnsafe(window.rawLengthInWindow);
|
|
159
|
+
let offset = 0;
|
|
160
|
+
let rawOffset = 0;
|
|
161
|
+
let written = 0;
|
|
162
|
+
for (let index = window.firstIndex; index <= window.lastIndex; index += 1) {
|
|
163
|
+
options.signal?.throwIfAborted();
|
|
164
|
+
const chunk = envelope.chunks[index];
|
|
165
|
+
const stored = storedSlice.subarray(offset, offset + chunk.storedBytes);
|
|
166
|
+
offset += chunk.storedBytes;
|
|
167
|
+
if (sha256(stored) !== chunk.sha256)
|
|
168
|
+
throw new CryptoError("chunk_hash_mismatch", `Chunk ${chunk.index} does not match its recorded hash`);
|
|
169
|
+
const decipher = aeadDecipher(envelope.suite, archiveKey, Buffer.from(chunk.nonce, "base64"));
|
|
170
|
+
decipher.setAAD(chunkAad({ archiveId: envelope.archiveId, suite: envelope.suite, index: chunk.index, totalChunks: envelope.chunks.length, plaintextBytes: chunk.plaintextBytes, final: chunk.final }));
|
|
171
|
+
decipher.setAuthTag(stored.subarray(stored.length - TAG_BYTES));
|
|
172
|
+
let packed;
|
|
173
|
+
try {
|
|
174
|
+
packed = Buffer.concat([decipher.update(stored.subarray(0, stored.length - TAG_BYTES)), decipher.final()]);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
throw new CryptoError("chunk_auth_failed", `Chunk ${chunk.index} failed authentication`);
|
|
178
|
+
}
|
|
179
|
+
let raw;
|
|
180
|
+
try {
|
|
181
|
+
const expand = envelope.manifest.compression === "zstd-chunk" ? unsqueezeSync : gunzipSync;
|
|
182
|
+
// Native output bounds stop expansion during decompression. zlib
|
|
183
|
+
// requires a positive bound even for the one valid empty chunk.
|
|
184
|
+
try {
|
|
185
|
+
raw = expand(packed, { maxOutputLength: Math.max(1, chunk.rawBytes) });
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
if (error.code === "ERR_BUFFER_TOO_LARGE") {
|
|
189
|
+
throw new CryptoError("plaintext_length_mismatch", `Chunk ${chunk.index} exceeds its declared original length`);
|
|
190
|
+
}
|
|
191
|
+
throw new CryptoError("plaintext_hash_mismatch", `Chunk ${chunk.index} failed decompression after authentication`);
|
|
192
|
+
}
|
|
193
|
+
options.signal?.throwIfAborted();
|
|
194
|
+
if (raw.length !== chunk.rawBytes)
|
|
195
|
+
throw new CryptoError("plaintext_length_mismatch", `Chunk ${chunk.index} does not match its declared original length`);
|
|
196
|
+
const from = Math.max(0, window.rawStartInWindow - rawOffset);
|
|
197
|
+
const to = Math.min(raw.length, window.rawStartInWindow + window.rawLengthInWindow - rawOffset);
|
|
198
|
+
if (to > from)
|
|
199
|
+
written += raw.copy(result, written, from, to);
|
|
200
|
+
rawOffset += raw.length;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
packed.fill(0);
|
|
204
|
+
raw?.fill(0);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (written !== result.length)
|
|
208
|
+
throw new CryptoError("plaintext_length_mismatch", "The chunk window did not reproduce the complete requested range");
|
|
209
|
+
return result;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
result?.fill(0);
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
finally {
|
|
216
|
+
if (ownsKey)
|
|
217
|
+
zeroize(archiveKey);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -20,7 +20,7 @@ export type DeterministicSeed = {
|
|
|
20
20
|
export type EncryptOptions = {
|
|
21
21
|
recipients: Recipient[];
|
|
22
22
|
suite?: Suite;
|
|
23
|
-
compression?: "none" | "gzip" | "gzip-chunk";
|
|
23
|
+
compression?: "none" | "gzip" | "gzip-chunk" | "zstd" | "zstd-chunk";
|
|
24
24
|
chunkBytes?: number;
|
|
25
25
|
archiveId?: string;
|
|
26
26
|
createdAt?: string;
|
|
@@ -50,6 +50,16 @@ export declare function encryptArchive(plaintext: Buffer, options: EncryptOption
|
|
|
50
50
|
* holds the ciphertext, is gated by exactly the same rules.
|
|
51
51
|
*/
|
|
52
52
|
export declare function validateEnvelope(envelope: Envelope, ciphertextBytes: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Authenticates and decrypts the stored body without expanding its compression.
|
|
55
|
+
*
|
|
56
|
+
* Keeping the authenticated chunks separate matters for callers that can stream
|
|
57
|
+
* decompression. A highly-compressible search index may be tens of megabytes on
|
|
58
|
+
* disk and more than a gigabyte after gzip; forcing that expanded body through a
|
|
59
|
+
* single Buffer is enough to OOM a long-lived agent process before it can inspect
|
|
60
|
+
* one query term.
|
|
61
|
+
*/
|
|
62
|
+
export declare function decryptArchiveBodyChunks(envelope: Envelope, ciphertext: Buffer, unlock: Unlock): Buffer[];
|
|
53
63
|
/**
|
|
54
64
|
* Decrypts an archive after verifying its structure and every stored chunk hash.
|
|
55
65
|
* Every chunk is authenticated against associated data that pins its position and
|