sealkeep 0.5.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 +201 -0
- package/CHANGELOG.md +218 -0
- package/CONTROL_PLANE.md +86 -0
- package/LICENSE +34 -0
- package/README.md +249 -0
- package/THIRD_PARTY.md +22 -0
- package/THREAT_MODEL.md +107 -0
- package/dist/packages/vaultline-crypto/src/aead.d.ts +12 -0
- package/dist/packages/vaultline-crypto/src/aead.js +24 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.d.ts +39 -0
- package/dist/packages/vaultline-crypto/src/chunk-access.js +93 -0
- package/dist/packages/vaultline-crypto/src/envelope.d.ts +71 -0
- package/dist/packages/vaultline-crypto/src/envelope.js +188 -0
- package/dist/packages/vaultline-crypto/src/format.d.ts +106 -0
- package/dist/packages/vaultline-crypto/src/format.js +43 -0
- package/dist/packages/vaultline-crypto/src/index.d.ts +5 -0
- package/dist/packages/vaultline-crypto/src/index.js +5 -0
- package/dist/packages/vaultline-crypto/src/recipients.d.ts +42 -0
- package/dist/packages/vaultline-crypto/src/recipients.js +129 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.d.ts +41 -0
- package/dist/packages/vaultline-crypto/src/sha256-stream.js +206 -0
- package/dist/packages/vaultline-crypto/src/stream.d.ts +139 -0
- package/dist/packages/vaultline-crypto/src/stream.js +477 -0
- package/dist/site/index.html +1542 -0
- package/dist/site.zip +0 -0
- package/dist/src/activity.d.ts +22 -0
- package/dist/src/activity.js +52 -0
- package/dist/src/adapters.d.ts +212 -0
- package/dist/src/adapters.js +533 -0
- package/dist/src/audit.d.ts +24 -0
- package/dist/src/audit.js +41 -0
- package/dist/src/autopilot.d.ts +77 -0
- package/dist/src/autopilot.js +148 -0
- package/dist/src/bip39-wordlist.d.ts +15 -0
- package/dist/src/bip39-wordlist.js +272 -0
- package/dist/src/branding.d.ts +31 -0
- package/dist/src/branding.js +31 -0
- package/dist/src/chunk-store.d.ts +142 -0
- package/dist/src/chunk-store.js +502 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +2035 -0
- package/dist/src/cloud.d.ts +434 -0
- package/dist/src/cloud.js +851 -0
- package/dist/src/control-plane/auth.d.ts +62 -0
- package/dist/src/control-plane/auth.js +123 -0
- package/dist/src/control-plane/server.d.ts +31 -0
- package/dist/src/control-plane/server.js +263 -0
- package/dist/src/control-plane/store.d.ts +101 -0
- package/dist/src/control-plane/store.js +82 -0
- package/dist/src/control-plane-cli.d.ts +2 -0
- package/dist/src/control-plane-cli.js +37 -0
- package/dist/src/control-plane-server.d.ts +10 -0
- package/dist/src/control-plane-server.js +11 -0
- package/dist/src/control-plane.d.ts +78 -0
- package/dist/src/control-plane.js +61 -0
- package/dist/src/crypto.d.ts +56 -0
- package/dist/src/crypto.js +132 -0
- package/dist/src/daemon.d.ts +52 -0
- package/dist/src/daemon.js +142 -0
- package/dist/src/dashboard-cli.d.ts +2 -0
- package/dist/src/dashboard-cli.js +20 -0
- package/dist/src/disk.d.ts +110 -0
- package/dist/src/disk.js +169 -0
- package/dist/src/doctor.d.ts +11 -0
- package/dist/src/doctor.js +198 -0
- package/dist/src/enroll.d.ts +27 -0
- package/dist/src/enroll.js +136 -0
- package/dist/src/errors.d.ts +26 -0
- package/dist/src/errors.js +23 -0
- package/dist/src/heartbeat.d.ts +89 -0
- package/dist/src/heartbeat.js +120 -0
- package/dist/src/index-sync.d.ts +53 -0
- package/dist/src/index-sync.js +147 -0
- package/dist/src/leakscan.d.ts +48 -0
- package/dist/src/leakscan.js +222 -0
- package/dist/src/local-api.d.ts +132 -0
- package/dist/src/local-api.js +1757 -0
- package/dist/src/managed-chunks.d.ts +55 -0
- package/dist/src/managed-chunks.js +108 -0
- package/dist/src/mcp-install.d.ts +52 -0
- package/dist/src/mcp-install.js +140 -0
- package/dist/src/mcp.d.ts +1 -0
- package/dist/src/mcp.js +59 -0
- package/dist/src/migrate.d.ts +35 -0
- package/dist/src/migrate.js +88 -0
- package/dist/src/mnemonic.d.ts +60 -0
- package/dist/src/mnemonic.js +134 -0
- package/dist/src/net.d.ts +2 -0
- package/dist/src/net.js +16 -0
- package/dist/src/notify.d.ts +46 -0
- package/dist/src/notify.js +84 -0
- package/dist/src/offload.d.ts +117 -0
- package/dist/src/offload.js +331 -0
- package/dist/src/onboarding.d.ts +10 -0
- package/dist/src/onboarding.js +44 -0
- package/dist/src/packages.d.ts +126 -0
- package/dist/src/packages.js +114 -0
- package/dist/src/passkey.d.ts +26 -0
- package/dist/src/passkey.js +54 -0
- package/dist/src/password-lock.d.ts +19 -0
- package/dist/src/password-lock.js +156 -0
- package/dist/src/paths.d.ts +9 -0
- package/dist/src/paths.js +24 -0
- package/dist/src/providers/gcs.d.ts +133 -0
- package/dist/src/providers/gcs.js +235 -0
- package/dist/src/providers/gdrive.d.ts +156 -0
- package/dist/src/providers/gdrive.js +335 -0
- package/dist/src/providers/index.d.ts +45 -0
- package/dist/src/providers/index.js +74 -0
- package/dist/src/providers/s3.d.ts +174 -0
- package/dist/src/providers/s3.js +345 -0
- package/dist/src/providers/sigv4.d.ts +78 -0
- package/dist/src/providers/sigv4.js +112 -0
- package/dist/src/queue.d.ts +185 -0
- package/dist/src/queue.js +286 -0
- package/dist/src/recovery.d.ts +40 -0
- package/dist/src/recovery.js +132 -0
- package/dist/src/rehydrate.d.ts +43 -0
- package/dist/src/rehydrate.js +66 -0
- package/dist/src/restore.d.ts +34 -0
- package/dist/src/restore.js +80 -0
- package/dist/src/retention.d.ts +251 -0
- package/dist/src/retention.js +446 -0
- package/dist/src/rotate.d.ts +47 -0
- package/dist/src/rotate.js +95 -0
- package/dist/src/search.d.ts +147 -0
- package/dist/src/search.js +677 -0
- package/dist/src/secrets.d.ts +86 -0
- package/dist/src/secrets.js +220 -0
- package/dist/src/service.d.ts +73 -0
- package/dist/src/service.js +197 -0
- package/dist/src/share.d.ts +34 -0
- package/dist/src/share.js +68 -0
- package/dist/src/spool.d.ts +97 -0
- package/dist/src/spool.js +213 -0
- package/dist/src/start-tui.d.ts +17 -0
- package/dist/src/start-tui.js +113 -0
- package/dist/src/start.d.ts +75 -0
- package/dist/src/start.js +101 -0
- package/dist/src/storage-setup.d.ts +49 -0
- package/dist/src/storage-setup.js +222 -0
- package/dist/src/storage-targets.d.ts +40 -0
- package/dist/src/storage-targets.js +147 -0
- package/dist/src/stream-to-cloud.d.ts +76 -0
- package/dist/src/stream-to-cloud.js +820 -0
- package/dist/src/sync-rules.d.ts +85 -0
- package/dist/src/sync-rules.js +125 -0
- package/dist/src/trash.d.ts +15 -0
- package/dist/src/trash.js +63 -0
- package/dist/src/tui.d.ts +18 -0
- package/dist/src/tui.js +179 -0
- package/dist/src/types.d.ts +191 -0
- package/dist/src/types.js +3 -0
- package/dist/src/ui-server.d.ts +187 -0
- package/dist/src/ui-server.js +293 -0
- package/dist/src/ui.d.ts +41 -0
- package/dist/src/ui.js +102 -0
- package/dist/src/update.d.ts +30 -0
- package/dist/src/update.js +56 -0
- package/dist/src/upload.d.ts +46 -0
- package/dist/src/upload.js +80 -0
- package/dist/src/vault.d.ts +208 -0
- package/dist/src/vault.js +812 -0
- package/dist/src/watcher.d.ts +34 -0
- package/dist/src/watcher.js +121 -0
- package/dist/src/worker.d.ts +52 -0
- package/dist/src/worker.js +190 -0
- package/package.json +65 -0
- package/web/app.js +1372 -0
- package/web/index.html +476 -0
- package/web/rail.js +308 -0
- package/web/retention.html +17 -0
- package/web/rules-view.js +249 -0
- package/web/sessions-view.js +448 -0
- package/web/sessions.html +17 -0
- package/web/setup-api.js +181 -0
- package/web/setup-logic.js +394 -0
- package/web/setup.html +419 -0
- package/web/setup.js +697 -0
- package/web/style.css +990 -0
package/README.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ⌑ Sealkeep
|
|
4
|
+
|
|
5
|
+
**Keep your agent history, not the disk usage.**
|
|
6
|
+
|
|
7
|
+
Codex and Claude Code write down every session you have. Sealkeep seals them on your
|
|
8
|
+
machine, verifies a copy landed in your own bucket, and only then lets the local file go.
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install -g sealkeep
|
|
14
|
+
sealkeep autopilot
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
That is the whole setup. It creates your vault, shows your recovery phrase once, finds the
|
|
18
|
+
sessions already on this machine, and installs a background service that starts at login.
|
|
19
|
+
|
|
20
|
+
From then on you do nothing. New sessions are sealed as they finish. Once you point it at a
|
|
21
|
+
bucket, verified copies go up and old local files move to your trash on their own.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
sealkeep autopilot status # is it alive, and what has it done
|
|
25
|
+
sealkeep autopilot off # stop it — every archive stays
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
It tells you it is working rather than leaving you to wonder. The service writes a
|
|
29
|
+
heartbeat every cycle, so `status`, the dashboard, and the local API all answer
|
|
30
|
+
"is it running, when did it last check, and what has it sealed" from the same fact.
|
|
31
|
+
It also raises a native notification when disk is reclaimed or something needs a
|
|
32
|
+
decision — never for routine sealing, which would just train you to ignore it.
|
|
33
|
+
Silence it entirely with `VAULTLINE_NOTIFICATIONS=off`.
|
|
34
|
+
|
|
35
|
+
Prefer to drive it yourself? `sealkeep quickstart` does the same setup without installing
|
|
36
|
+
a service, and you run `sealkeep queue run` when you feel like it.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Why
|
|
41
|
+
|
|
42
|
+
Transcripts are the only record of *how* a change happened — the dead ends, the constraint
|
|
43
|
+
you found on the third attempt, the reason that function is strange. They also contain your
|
|
44
|
+
source, your prompts, and whatever you pasted at 2am.
|
|
45
|
+
|
|
46
|
+
So the two obvious options are both bad: let them fill the disk, or `rm -rf` the reasoning
|
|
47
|
+
behind six months of work. Sealkeep is the third option.
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
The order is the safety property. Nothing is reclaimed before it is verified, and nothing is
|
|
52
|
+
verified before it is sealed.
|
|
53
|
+
|
|
54
|
+
**1. Catch** — your agent's session hook records that a transcript is ready, then exits. It
|
|
55
|
+
holds no secret, makes no network call, and finishes in milliseconds, so it can never stall
|
|
56
|
+
your agent.
|
|
57
|
+
|
|
58
|
+
**2. Seal** — a worker encrypts the session with a key generated for that archive alone.
|
|
59
|
+
That key is wrapped once for your recovery phrase and once per device you trust, so revoking
|
|
60
|
+
a laptop rewraps keys rather than re-encrypting your data.
|
|
61
|
+
|
|
62
|
+
**3. Verify** — the sealed archive goes to your bucket, and Sealkeep reads it back. Byte
|
|
63
|
+
count and checksum must match what was computed locally, or the archive is not marked
|
|
64
|
+
durable and your local file stays exactly where it is.
|
|
65
|
+
|
|
66
|
+
**4. Reclaim** — only a verified, idle, out-of-grace-period session becomes eligible, and
|
|
67
|
+
then it moves to your trash. There is no code path in Sealkeep that deletes a file.
|
|
68
|
+
|
|
69
|
+
Autopilot runs all four on a loop. To unlock the vault without a human present, it keeps
|
|
70
|
+
your phrase in the OS keystore — macOS Keychain, Freedesktop Secret Service, or Windows
|
|
71
|
+
DPAPI. That is a real trade-off, stated plainly: anything running as you can read it once
|
|
72
|
+
you are logged in. Opt out with `--no-remember` and supply
|
|
73
|
+
`VAULTLINE_RECOVERY_PHRASE` to the service yourself. Either way, the phrase you wrote down
|
|
74
|
+
stays your disaster-recovery path.
|
|
75
|
+
|
|
76
|
+
## Your recovery phrase is 24 words
|
|
77
|
+
|
|
78
|
+
Not a base64 string. Words survive bad handwriting, can be read aloud, and carry a
|
|
79
|
+
checksum — so a misread word is caught and corrected rather than silently locking you out.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
acorn oxide lynx atom voyage wool candle umber steel radar east clock
|
|
83
|
+
iris brass stork maple ridge ember quilt fable onyx cedar dune cricket
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```sh
|
|
87
|
+
sealkeep recovery kit --out ~/sealkeep-kit.txt # printable sheet, blanks for the words
|
|
88
|
+
sealkeep recovery verify # type it back to prove your copy works
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Case and spacing do not matter when you type it back. A wrong word is named with
|
|
92
|
+
suggestions: *Word 1 "acron" is not in the list. Did you mean acorn, arrow?*
|
|
93
|
+
|
|
94
|
+
## Setting up a bucket, without the usual afternoon
|
|
95
|
+
|
|
96
|
+
The step people give up on is cloud permissions. So Sealkeep writes it out — the console
|
|
97
|
+
link, the commands with your own bucket and prefix already filled in, and a policy that
|
|
98
|
+
grants the least it can.
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
sealkeep storage setup --provider s3 --bucket my-archives --prefix accounts/me
|
|
102
|
+
sealkeep storage setup --provider r2 --bucket sessions --prefix me --account-id <id>
|
|
103
|
+
sealkeep storage setup --provider gcs --bucket sessions --prefix me --project <project>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
For S3 that is an IAM policy scoped to `bucket/prefix/*` with no delete permission and no
|
|
107
|
+
wildcard resource. For GCS it is a service account with an IAM condition pinned to your
|
|
108
|
+
prefix. Nothing runs automatically — you read each step and paste it yourself, which is the
|
|
109
|
+
only honest way to hand someone an IAM policy.
|
|
110
|
+
|
|
111
|
+
## What we can see
|
|
112
|
+
|
|
113
|
+
The control plane coordinates devices, quotas, and upload permissions.
|
|
114
|
+
|
|
115
|
+
| It holds | It never receives |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| Account and device identifiers | Transcript content — no endpoint accepts it |
|
|
118
|
+
| Byte counts and timestamps | Archive keys — generated and wrapped on your machine |
|
|
119
|
+
| Checksums of sealed archives | Your recovery phrase — shown once, stored nowhere |
|
|
120
|
+
| Encrypted manifests it cannot read | Device private keys — only public halves are registered |
|
|
121
|
+
|
|
122
|
+
> If you lose your recovery kit and every device you registered, your archives are
|
|
123
|
+
> unreadable. Support cannot get them back. That is not a gap in the product — it is the product.
|
|
124
|
+
|
|
125
|
+
## What your bucket sees
|
|
126
|
+
|
|
127
|
+
One archive is one folder of fixed-size encrypted chunks plus a small sealed
|
|
128
|
+
envelope. By default every path segment — project, date, session — is an HMAC
|
|
129
|
+
under a key derived from your phrase, so the bucket's file listing is noise:
|
|
130
|
+
no project names, no dates, no session ids. A sealed sidecar in each folder
|
|
131
|
+
carries the readable identity, so any machine with the phrase rebuilds the
|
|
132
|
+
friendly `project/date/session` view from the bucket alone. Prefer legible
|
|
133
|
+
paths in your own bucket? Set `remoteNaming: "readable"` and the same layout
|
|
134
|
+
is written in plain words.
|
|
135
|
+
|
|
136
|
+
Search works the same way: the content index is sealed with the archive
|
|
137
|
+
machinery and synced beside your data, so every machine of the vault searches
|
|
138
|
+
your whole history — and the provider never sees a token of it.
|
|
139
|
+
|
|
140
|
+
The layout also answers the worst day: a transcript too large to seal on a
|
|
141
|
+
nearly-full disk streams straight into its chunk folder — peak local cost is
|
|
142
|
+
one chunk — instead of waiting for space. Google Drive is the one exception:
|
|
143
|
+
it stores archives as single objects, and says so.
|
|
144
|
+
|
|
145
|
+
## Commands
|
|
146
|
+
|
|
147
|
+
| | |
|
|
148
|
+
| --- | --- |
|
|
149
|
+
| `sealkeep autopilot` | Set it up once and let it run itself |
|
|
150
|
+
| `sealkeep autopilot status` · `off` | Check on it, or stop it |
|
|
151
|
+
| `sealkeep quickstart` | Same setup, no background service |
|
|
152
|
+
| `sealkeep status` · `doctor` | What is archived; whether this machine is healthy |
|
|
153
|
+
| `sealkeep queue run` | Encrypt everything waiting |
|
|
154
|
+
| `sealkeep daemon` | Watch, seal, upload, and reclaim continuously |
|
|
155
|
+
| `sealkeep search <query>` | Metadata search; `--content` searches inside sessions |
|
|
156
|
+
| `sealkeep recover <id> <dest>` | Restore original bytes; `--native` puts them back where they came from |
|
|
157
|
+
| `sealkeep recover <filename>` | The file an agent's resume just complained about, back in place |
|
|
158
|
+
| `sealkeep archive <path> --stream` | Seal straight to the bucket as a chunk folder, resumable mid-upload |
|
|
159
|
+
| `sealkeep share <id>` | A sealed bundle under a one-time passcode — the phrase never travels |
|
|
160
|
+
| `sealkeep mcp install` | Register the vault as a tool server with Claude Code and Codex |
|
|
161
|
+
| `sealkeep retention apply` | Dry run; `--confirm` moves sources to the trash |
|
|
162
|
+
| `sealkeep desktop` · `tui` | The dashboard, in a browser or the terminal |
|
|
163
|
+
| `sealkeep audit` | Everything Sealkeep has done to your files |
|
|
164
|
+
|
|
165
|
+
Every command takes `--json`. Run `sealkeep help` for the full list.
|
|
166
|
+
|
|
167
|
+
## Your agents know the vault is there
|
|
168
|
+
|
|
169
|
+
`sealkeep mcp install` registers a local MCP server with Claude Code and
|
|
170
|
+
Codex, so the agent itself can search your sealed history, check vault
|
|
171
|
+
status, and restore a session it needs — six tools, all local, nothing new
|
|
172
|
+
exposed to the network.
|
|
173
|
+
|
|
174
|
+
And the failure this whole product could have caused is handled where it
|
|
175
|
+
would happen: resuming a session whose transcript was archived and reclaimed.
|
|
176
|
+
A `SessionStart` hook notices the missing file and restores it — exact path,
|
|
177
|
+
exact bytes — before the agent reads it, so the resume proceeds as if the
|
|
178
|
+
file had never left. If anything is ever missing anyway, `sealkeep recover
|
|
179
|
+
<filename>` with the filename from the agent's error puts it back. Archiving
|
|
180
|
+
must never cost you a resume.
|
|
181
|
+
|
|
182
|
+
## Your storage
|
|
183
|
+
|
|
184
|
+
S3, Cloudflare R2, Google Cloud Storage, Google Drive, or anything
|
|
185
|
+
S3-compatible like MinIO. Credentials go into your OS keychain — macOS
|
|
186
|
+
Keychain, Freedesktop Secret Service, or Windows DPAPI — never into a config
|
|
187
|
+
file in your home directory. On the paid tier, Sealkeep runs the storage
|
|
188
|
+
instead: quotas and uploads are brokered by the control plane, and your keys
|
|
189
|
+
and phrase still never leave your machine.
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
sealkeep storage configure --provider r2 --bucket encrypted-sessions --prefix accounts/me
|
|
193
|
+
echo '{"accessKeyId":"…","secretAccessKey":"…"}' | sealkeep storage credentials set
|
|
194
|
+
VAULTLINE_ENABLE_SIGNER=1 sealkeep upload --all
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Or skip storage entirely and keep everything local. Sealkeep still seals every archive; it
|
|
198
|
+
just has nowhere to copy it.
|
|
199
|
+
|
|
200
|
+
## Encryption
|
|
201
|
+
|
|
202
|
+
Format v2: a random per-archive key encrypts the session in chunks under ChaCha20-Poly1305
|
|
203
|
+
(or AES-256-GCM), and that key is wrapped separately for each recipient. Every chunk is
|
|
204
|
+
authenticated over its own position, the archive's identity, and the total chunk count — so
|
|
205
|
+
reordering, truncation, splicing, and suite downgrade are all detectable rather than silent.
|
|
206
|
+
|
|
207
|
+
The format lives in [`packages/vaultline-crypto/`](./packages/vaultline-crypto/README.md)
|
|
208
|
+
with deterministic test vectors and a tamper suite that flips every byte of a stored archive
|
|
209
|
+
in turn. Assumptions and limits are in [`THREAT_MODEL.md`](./THREAT_MODEL.md).
|
|
210
|
+
|
|
211
|
+
**It has not been independently audited.** Until it is, this is authenticated encryption
|
|
212
|
+
built on published primitives — not an audited end-to-end-encryption product.
|
|
213
|
+
|
|
214
|
+
## Status
|
|
215
|
+
|
|
216
|
+
| Area | State |
|
|
217
|
+
| --- | --- |
|
|
218
|
+
| Capture and sealing | **Shipping** — durable queue, crash-safe, deduplicating, resumable streaming |
|
|
219
|
+
| Restore | **Shipping** — byte-identical, verified twice, never overwrites; rehydrates on agent resume |
|
|
220
|
+
| Retention | **Shipping** — trash-staged, never deletes |
|
|
221
|
+
| Search | **Shipping** — sealed content index, synced across your machines as ciphertext |
|
|
222
|
+
| Agent integration | **Shipping** — MCP server for Claude Code and Codex, session hooks |
|
|
223
|
+
| Encryption format | **Unaudited** — vectors and tamper suite, no third-party audit yet |
|
|
224
|
+
| Cloud providers | **GCS live in production**; S3, R2, and Drive verified against signature vectors and emulators, without a long production soak yet |
|
|
225
|
+
| Signed binaries | **Not yet** — npm provenance and a signed manifest; no OS code-signing certificate |
|
|
226
|
+
| Distribution | **Published** — `npm install -g sealkeep` |
|
|
227
|
+
|
|
228
|
+
## Self-hosting the control plane
|
|
229
|
+
|
|
230
|
+
```sh
|
|
231
|
+
docker build -t vaultline-control-plane .
|
|
232
|
+
docker run -p 8787:8787 -v vaultline-data:/data vaultline-control-plane
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Devices authenticate with Ed25519 request signatures, leases are single-use and expiring,
|
|
236
|
+
and revocation blocks new leases without touching archives. See
|
|
237
|
+
[`CONTROL_PLANE.md`](./CONTROL_PLANE.md) and [`docs/RUNBOOK.md`](./docs/RUNBOOK.md).
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```sh
|
|
242
|
+
npm test # the full suite — crypto vectors, a tamper suite, provider emulators, dashboard rendering
|
|
243
|
+
npm run build
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Architecture is in [`ARCHITECTURE.md`](./ARCHITECTURE.md); designs and operations live in
|
|
247
|
+
[`docs/`](./docs/README.md), including the not-yet-built
|
|
248
|
+
[sharing and spaces design](./docs/sharing-and-spaces.md). The marketing page in
|
|
249
|
+
[`site/`](./site/index.html) is a single self-contained file.
|
package/THIRD_PARTY.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Third-party components
|
|
2
|
+
|
|
3
|
+
Vaultline ships with the following third-party components, each under its own
|
|
4
|
+
licence (see §8 of LICENSE). Nothing else in the published package is
|
|
5
|
+
third-party code.
|
|
6
|
+
|
|
7
|
+
## Runtime dependencies
|
|
8
|
+
|
|
9
|
+
- **zod** — TypeScript schema validation.
|
|
10
|
+
Licence: MIT. https://github.com/colinhacks/zod
|
|
11
|
+
- **@modelcontextprotocol/sdk** — Model Context Protocol SDK, used by
|
|
12
|
+
`vaultline mcp`.
|
|
13
|
+
Licence: MIT. https://github.com/modelcontextprotocol/typescript-sdk
|
|
14
|
+
|
|
15
|
+
## Vendored data
|
|
16
|
+
|
|
17
|
+
- **BIP-39 English word list** (`src/bip39-wordlist.ts`) — the 2048-word list
|
|
18
|
+
exactly as frozen by the BIP-39 specification, vendored from
|
|
19
|
+
https://github.com/bitcoin/bips/blob/master/bip-0039/english.txt so the
|
|
20
|
+
supply chain carries no extra dependency. The build verifies it against the
|
|
21
|
+
specification's published SHA-256 digest. The list is distributed under the
|
|
22
|
+
terms stated in the bitcoin/bips repository for BIP-0039.
|
package/THREAT_MODEL.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Sealkeep Threat Model
|
|
2
|
+
|
|
3
|
+
**Scope:** the `vaultline-crypto` archive envelope (format v2) and the client that produces it.
|
|
4
|
+
**Status:** written by the implementing engineers. **Not yet independently audited.** See "Open gates".
|
|
5
|
+
|
|
6
|
+
## 1. What the system protects
|
|
7
|
+
|
|
8
|
+
Agent transcripts contain prompts, source code, credentials pasted into sessions, and
|
|
9
|
+
business context. The asset is the *plaintext* of an archived transcript, plus the
|
|
10
|
+
secrets that unlock it: the recovery phrase and device private keys.
|
|
11
|
+
|
|
12
|
+
## 2. Trust assumptions
|
|
13
|
+
|
|
14
|
+
| Party | Trusted for | Not trusted for |
|
|
15
|
+
| --- | --- | --- |
|
|
16
|
+
| Local client process | Plaintext handling, key generation | — |
|
|
17
|
+
| Local OS user account | Reading its own files | — |
|
|
18
|
+
| Control plane | Availability, metadata integrity, access control | Confidentiality of transcripts |
|
|
19
|
+
| Object store (S3/R2/GCS) | Durability | Confidentiality, integrity |
|
|
20
|
+
| Network | Nothing | Confidentiality, integrity |
|
|
21
|
+
|
|
22
|
+
The client is the only component that ever holds plaintext or an archive key.
|
|
23
|
+
|
|
24
|
+
## 3. Adversaries and outcomes
|
|
25
|
+
|
|
26
|
+
### 3.1 Malicious or breached control plane
|
|
27
|
+
|
|
28
|
+
Sees: account and device identifiers, archive ids, byte counts, timestamps, ciphertext hashes,
|
|
29
|
+
object keys, and opaque encrypted manifests. Cannot decrypt: it never receives an archive key,
|
|
30
|
+
recovery phrase, or device private key. It *can* deny service and it *can* lie about metadata,
|
|
31
|
+
so the client verifies checksums locally and refuses reclamation on any mismatch.
|
|
32
|
+
|
|
33
|
+
### 3.2 Stolen object from the bucket
|
|
34
|
+
|
|
35
|
+
An attacker with the ciphertext holds AEAD output under a random 256-bit per-archive key.
|
|
36
|
+
Recovering plaintext requires breaking ChaCha20-Poly1305 or AES-256-GCM, or obtaining a
|
|
37
|
+
recipient secret. Chunk headers leak archive size and chunk count; the source path is in the
|
|
38
|
+
local record, not in the stored object.
|
|
39
|
+
|
|
40
|
+
### 3.3 Archive tampering
|
|
41
|
+
|
|
42
|
+
Every chunk is authenticated with associated data binding format version, suite, archive id,
|
|
43
|
+
chunk index, total chunk count, chunk plaintext length, and a final-chunk flag. Wrapped keys
|
|
44
|
+
are separately bound to archive id and suite. Consequently these are all detected:
|
|
45
|
+
|
|
46
|
+
| Attack | Detected by | Test |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| Bit flip in ciphertext or tag | AEAD tag | `detects a flipped bit anywhere in the stored ciphertext` |
|
|
49
|
+
| Chunk reordering | Index in chunk AAD | `rejects a reordered archive even when the headers are rewritten` |
|
|
50
|
+
| Truncation | Final flag plus total chunk count | `rejects truncation of the final chunk` |
|
|
51
|
+
| Splicing chunks between archives | Archive id in chunk AAD | `rejects a chunk spliced in from another archive` |
|
|
52
|
+
| Suite downgrade | Suite in wrap AAD and chunk AAD | `rejects a suite downgrade` |
|
|
53
|
+
| Rewriting a chunk hash to match damage | AEAD tag behind the hash check | `rejects a chunk whose recorded hash was rewritten` |
|
|
54
|
+
|
|
55
|
+
### 3.4 Compromised local user account
|
|
56
|
+
|
|
57
|
+
Out of scope for confidentiality: an attacker running as the user can read the plaintext
|
|
58
|
+
transcripts directly from the agent's own directories. Sealkeep reduces blast radius by
|
|
59
|
+
keeping the recovery phrase out of files (it is passed by environment to one command), storing
|
|
60
|
+
its own state at mode `600`, and never writing the phrase to logs, queue jobs, or the API.
|
|
61
|
+
|
|
62
|
+
### 3.5 Lost or stolen device
|
|
63
|
+
|
|
64
|
+
Each device is a separate X25519 recipient. Removing the recipient and running `sealkeep rewrap`
|
|
65
|
+
re-wraps every archive key without that device, without re-encrypting data. Revocation is
|
|
66
|
+
therefore fast, but it is **not retroactive**: an attacker who copied both the ciphertext and the
|
|
67
|
+
device key before revocation retains access to that copy. Control-plane revocation additionally
|
|
68
|
+
blocks new leases.
|
|
69
|
+
|
|
70
|
+
### 3.6 Replayed or stolen upload lease
|
|
71
|
+
|
|
72
|
+
Leases are short-lived, single-object, and checked by `assertLeaseUsable`, which tests expiry
|
|
73
|
+
before anything else. A stolen lease can at worst write one object the client already intended
|
|
74
|
+
to write; it cannot read, and it cannot choose the object key, which the server allocates.
|
|
75
|
+
|
|
76
|
+
### 3.7 Malicious MCP prompt or tool invocation
|
|
77
|
+
|
|
78
|
+
The MCP server is local but model-driven. Search returns metadata only. Archive and recover
|
|
79
|
+
require the recovery phrase from the local environment, recover refuses to overwrite by default,
|
|
80
|
+
and native restore refuses any path outside the adapter's own transcript roots.
|
|
81
|
+
|
|
82
|
+
## 4. Cryptographic design
|
|
83
|
+
|
|
84
|
+
- **Suite:** ChaCha20-Poly1305 (default) or AES-256-GCM, both from OpenSSL via Node. No custom primitive.
|
|
85
|
+
- **Archive key:** 32 random bytes per archive, never reused, zeroized after use where the runtime allows.
|
|
86
|
+
- **Nonces:** 4-byte per-archive random prefix plus a 64-bit big-endian chunk counter. Unique per chunk by construction; unique across archives because the key is fresh per archive.
|
|
87
|
+
- **Phrase recipients:** scrypt (N=2^15, r=8, p=1 by default; recorded per archive so parameters can be raised later), then AEAD key wrap.
|
|
88
|
+
- **Key recipients:** ephemeral-static X25519, HKDF-SHA256 over the shared secret with both public keys as salt, then AEAD key wrap.
|
|
89
|
+
- **Integrity:** per-chunk SHA-256 recorded for cheap corruption detection, AEAD tags for authenticity, whole-plaintext SHA-256 in the manifest checked after decryption.
|
|
90
|
+
|
|
91
|
+
## 5. Known limitations
|
|
92
|
+
|
|
93
|
+
1. **Length and timing leak.** Ciphertext size reveals plaintext size to within one chunk; no padding is applied.
|
|
94
|
+
2. **Metadata in the local record.** Source path, agent, and byte count are stored in cleartext in the local vault directory. Private-metadata mode is not implemented.
|
|
95
|
+
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
|
+
4. **Zeroization is best effort.** JavaScript may retain copies of buffers the implementation cannot reach.
|
|
97
|
+
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. **No streaming.** Archives are encrypted from an in-memory buffer, which bounds practical archive size to available memory.
|
|
99
|
+
|
|
100
|
+
## 6. Open gates before an end-to-end-encryption claim
|
|
101
|
+
|
|
102
|
+
- [ ] Independent third-party cryptographic audit of `vaultline-crypto` and this document.
|
|
103
|
+
- [ ] Publication of the package under a public repository with the test vectors.
|
|
104
|
+
- [ ] Review of the key lifecycle by someone outside the implementing team.
|
|
105
|
+
|
|
106
|
+
Until all three are met, Sealkeep documentation must describe the envelope as
|
|
107
|
+
"authenticated encryption implemented against published primitives", not as audited E2EE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type CipherGCM, type DecipherGCM } from "node:crypto";
|
|
2
|
+
import { type Suite } from "./format.js";
|
|
3
|
+
/**
|
|
4
|
+
* Both suites take a 12-byte nonce, a 16-byte tag, and plain associated data.
|
|
5
|
+
*
|
|
6
|
+
* `@types/node` groups `chacha20-poly1305` with the CCM ciphers, whose typed
|
|
7
|
+
* `setAAD` demands a plaintext length that ChaCha20-Poly1305 does not use. The
|
|
8
|
+
* literal suite is narrowed here so the right runtime overload is selected, and the
|
|
9
|
+
* result is presented with the GCM-shaped interface both ciphers actually have.
|
|
10
|
+
*/
|
|
11
|
+
export declare function aeadCipher(suite: Suite, key: Buffer, nonce: Buffer): CipherGCM;
|
|
12
|
+
export declare function aeadDecipher(suite: Suite, key: Buffer, nonce: Buffer): DecipherGCM;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv } from "node:crypto";
|
|
2
|
+
import { TAG_BYTES } from "./format.js";
|
|
3
|
+
/**
|
|
4
|
+
* Both suites take a 12-byte nonce, a 16-byte tag, and plain associated data.
|
|
5
|
+
*
|
|
6
|
+
* `@types/node` groups `chacha20-poly1305` with the CCM ciphers, whose typed
|
|
7
|
+
* `setAAD` demands a plaintext length that ChaCha20-Poly1305 does not use. The
|
|
8
|
+
* literal suite is narrowed here so the right runtime overload is selected, and the
|
|
9
|
+
* result is presented with the GCM-shaped interface both ciphers actually have.
|
|
10
|
+
*/
|
|
11
|
+
export function aeadCipher(suite, key, nonce) {
|
|
12
|
+
const options = { authTagLength: TAG_BYTES };
|
|
13
|
+
const cipher = suite === "chacha20-poly1305"
|
|
14
|
+
? createCipheriv("chacha20-poly1305", key, nonce, options)
|
|
15
|
+
: createCipheriv("aes-256-gcm", key, nonce, options);
|
|
16
|
+
return cipher;
|
|
17
|
+
}
|
|
18
|
+
export function aeadDecipher(suite, key, nonce) {
|
|
19
|
+
const options = { authTagLength: TAG_BYTES };
|
|
20
|
+
const decipher = suite === "chacha20-poly1305"
|
|
21
|
+
? createDecipheriv("chacha20-poly1305", key, nonce, options)
|
|
22
|
+
: createDecipheriv("aes-256-gcm", key, nonce, options);
|
|
23
|
+
return decipher;
|
|
24
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type Envelope } from "./format.js";
|
|
2
|
+
import { type Unlock } from "./recipients.js";
|
|
3
|
+
/**
|
|
4
|
+
* Random access into a gzip-chunk archive: the primitive behind "fetch the
|
|
5
|
+
* chunks, not the archive".
|
|
6
|
+
*
|
|
7
|
+
* Under gzip-chunk every chunk compresses and encrypts on its own and records
|
|
8
|
+
* the RAW byte range it covers, so a question like "bytes 40–60 MB of that
|
|
9
|
+
* session" resolves to chunk indices and a single stored byte range — without
|
|
10
|
+
* downloading, decrypting, or decompressing anything else. A ranged GET plus
|
|
11
|
+
* this module is a partial restore; a loop over all chunks is a full one, in
|
|
12
|
+
* one chunk of memory.
|
|
13
|
+
*/
|
|
14
|
+
export type ChunkWindow = {
|
|
15
|
+
/** First and last chunk index covering the requested raw range. */
|
|
16
|
+
firstIndex: number;
|
|
17
|
+
lastIndex: number;
|
|
18
|
+
/** The byte range of the STORED object holding those chunks — what a ranged GET fetches. */
|
|
19
|
+
storedOffset: number;
|
|
20
|
+
storedLength: number;
|
|
21
|
+
/** Where the requested raw range sits inside the decompressed window. */
|
|
22
|
+
rawStartInWindow: number;
|
|
23
|
+
rawLengthInWindow: number;
|
|
24
|
+
};
|
|
25
|
+
/** True when an envelope supports random access at all. */
|
|
26
|
+
export declare function supportsChunkAccess(envelope: Envelope): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Maps a raw (original-file) byte range to the chunk window that covers it.
|
|
29
|
+
* Legacy whole-body-gzip archives cannot answer this — a slice of their stream
|
|
30
|
+
* is undecodable alone — and the error says to restore whole instead.
|
|
31
|
+
*/
|
|
32
|
+
export declare function chunkWindowForRange(envelope: Envelope, rawStart: number, rawEnd: number): ChunkWindow;
|
|
33
|
+
/**
|
|
34
|
+
* Opens the chunks inside a fetched stored slice and returns exactly the raw
|
|
35
|
+
* bytes the window was asked for. Every chunk is hash-checked and
|
|
36
|
+
* authenticated before a byte of it is trusted — a ranged read keeps the
|
|
37
|
+
* whole-archive guarantees, chunk by chunk.
|
|
38
|
+
*/
|
|
39
|
+
export declare function openChunkWindow(envelope: Envelope, window: ChunkWindow, storedSlice: Buffer, unlock: Unlock): Buffer;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { gunzipSync } from "node:zlib";
|
|
3
|
+
import { aeadDecipher } from "./aead.js";
|
|
4
|
+
import { chunkAad, TAG_BYTES } from "./format.js";
|
|
5
|
+
import { CryptoError, unwrapArchiveKey, zeroize } from "./recipients.js";
|
|
6
|
+
const sha256 = (input) => createHash("sha256").update(input).digest("hex");
|
|
7
|
+
/** True when an envelope supports random access at all. */
|
|
8
|
+
export function supportsChunkAccess(envelope) {
|
|
9
|
+
return envelope.manifest.compression === "gzip-chunk" && envelope.chunks.every((chunk) => typeof chunk.rawBytes === "number");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Maps a raw (original-file) byte range to the chunk window that covers it.
|
|
13
|
+
* Legacy whole-body-gzip archives cannot answer this — a slice of their stream
|
|
14
|
+
* is undecodable alone — and the error says to restore whole instead.
|
|
15
|
+
*/
|
|
16
|
+
export function chunkWindowForRange(envelope, rawStart, rawEnd) {
|
|
17
|
+
if (!supportsChunkAccess(envelope)) {
|
|
18
|
+
throw new CryptoError("envelope_malformed", "This archive predates chunk-independent compression; restore it whole instead of by range.");
|
|
19
|
+
}
|
|
20
|
+
const total = envelope.manifest.originalBytes ?? 0;
|
|
21
|
+
if (rawStart < 0 || rawEnd <= rawStart || rawStart >= Math.max(total, 1)) {
|
|
22
|
+
throw new CryptoError("invalid_range", `Range ${rawStart}..${rawEnd} is not a range of this ${total}-byte archive`);
|
|
23
|
+
}
|
|
24
|
+
const end = Math.min(rawEnd, total);
|
|
25
|
+
let rawCursor = 0;
|
|
26
|
+
let storedCursor = 0;
|
|
27
|
+
let firstIndex = -1;
|
|
28
|
+
let lastIndex = -1;
|
|
29
|
+
let storedOffset = 0;
|
|
30
|
+
let storedLength = 0;
|
|
31
|
+
let rawStartInWindow = 0;
|
|
32
|
+
for (const chunk of envelope.chunks) {
|
|
33
|
+
const rawFrom = rawCursor;
|
|
34
|
+
const rawTo = rawCursor + (chunk.rawBytes ?? 0);
|
|
35
|
+
const overlaps = rawTo > rawStart && rawFrom < end;
|
|
36
|
+
if (overlaps && firstIndex === -1) {
|
|
37
|
+
firstIndex = chunk.index;
|
|
38
|
+
storedOffset = storedCursor;
|
|
39
|
+
rawStartInWindow = rawStart - rawFrom;
|
|
40
|
+
}
|
|
41
|
+
if (overlaps) {
|
|
42
|
+
lastIndex = chunk.index;
|
|
43
|
+
storedLength += chunk.storedBytes;
|
|
44
|
+
}
|
|
45
|
+
rawCursor = rawTo;
|
|
46
|
+
storedCursor += chunk.storedBytes;
|
|
47
|
+
}
|
|
48
|
+
return { firstIndex, lastIndex, storedOffset, storedLength, rawStartInWindow, rawLengthInWindow: end - rawStart };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Opens the chunks inside a fetched stored slice and returns exactly the raw
|
|
52
|
+
* bytes the window was asked for. Every chunk is hash-checked and
|
|
53
|
+
* authenticated before a byte of it is trusted — a ranged read keeps the
|
|
54
|
+
* whole-archive guarantees, chunk by chunk.
|
|
55
|
+
*/
|
|
56
|
+
export function openChunkWindow(envelope, window, storedSlice, unlock) {
|
|
57
|
+
if (storedSlice.length !== window.storedLength) {
|
|
58
|
+
throw new CryptoError("ciphertext_length_mismatch", `The fetched slice is ${storedSlice.length} bytes; the window needs ${window.storedLength}`);
|
|
59
|
+
}
|
|
60
|
+
const archiveKey = unwrapArchiveKey(envelope.wrappedKeys, envelope.suite, envelope.archiveId, unlock);
|
|
61
|
+
try {
|
|
62
|
+
const parts = [];
|
|
63
|
+
let offset = 0;
|
|
64
|
+
for (let index = window.firstIndex; index <= window.lastIndex; index += 1) {
|
|
65
|
+
const chunk = envelope.chunks[index];
|
|
66
|
+
const stored = storedSlice.subarray(offset, offset + chunk.storedBytes);
|
|
67
|
+
offset += chunk.storedBytes;
|
|
68
|
+
if (sha256(stored) !== chunk.sha256)
|
|
69
|
+
throw new CryptoError("chunk_hash_mismatch", `Chunk ${chunk.index} does not match its recorded hash`);
|
|
70
|
+
const decipher = aeadDecipher(envelope.suite, archiveKey, Buffer.from(chunk.nonce, "base64"));
|
|
71
|
+
decipher.setAAD(chunkAad({ archiveId: envelope.archiveId, suite: envelope.suite, index: chunk.index, totalChunks: envelope.chunks.length, plaintextBytes: chunk.plaintextBytes, final: chunk.final }));
|
|
72
|
+
decipher.setAuthTag(stored.subarray(stored.length - TAG_BYTES));
|
|
73
|
+
let packed;
|
|
74
|
+
try {
|
|
75
|
+
packed = Buffer.concat([decipher.update(stored.subarray(0, stored.length - TAG_BYTES)), decipher.final()]);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
throw new CryptoError("chunk_auth_failed", `Chunk ${chunk.index} failed authentication`);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
parts.push(gunzipSync(packed));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw new CryptoError("plaintext_hash_mismatch", `Chunk ${chunk.index} failed decompression after authentication`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const raw = Buffer.concat(parts);
|
|
88
|
+
return raw.subarray(window.rawStartInWindow, window.rawStartInWindow + window.rawLengthInWindow);
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
zeroize(archiveKey);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type KeyObject } from "node:crypto";
|
|
2
|
+
import { type Unlock } from "./recipients.js";
|
|
3
|
+
import { type Envelope, type ScryptParams, type Suite, type WrappedKey } from "./format.js";
|
|
4
|
+
export type Recipient = {
|
|
5
|
+
kind: "phrase";
|
|
6
|
+
phrase: string;
|
|
7
|
+
scrypt?: ScryptParams;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "x25519";
|
|
10
|
+
publicKey: KeyObject | Buffer;
|
|
11
|
+
};
|
|
12
|
+
/** Supplied only by the test-vector generator so a randomized format can have known answers. */
|
|
13
|
+
export type DeterministicSeed = {
|
|
14
|
+
archiveKey: Buffer;
|
|
15
|
+
noncePrefix: Buffer;
|
|
16
|
+
wrapSalts: Buffer[];
|
|
17
|
+
wrapNonces: Buffer[];
|
|
18
|
+
ephemeralPrivateKeys?: KeyObject[];
|
|
19
|
+
};
|
|
20
|
+
export type EncryptOptions = {
|
|
21
|
+
recipients: Recipient[];
|
|
22
|
+
suite?: Suite;
|
|
23
|
+
compression?: "none" | "gzip" | "gzip-chunk";
|
|
24
|
+
chunkBytes?: number;
|
|
25
|
+
archiveId?: string;
|
|
26
|
+
createdAt?: string;
|
|
27
|
+
adapter?: {
|
|
28
|
+
agent: string;
|
|
29
|
+
version: string;
|
|
30
|
+
};
|
|
31
|
+
deterministic?: DeterministicSeed;
|
|
32
|
+
};
|
|
33
|
+
/** Exported so `stream.ts` produces byte-for-byte the same wraps as the buffered seal. */
|
|
34
|
+
export declare function wrapAll(archiveKey: Buffer, recipients: readonly Recipient[], suite: Suite, archiveId: string, seed?: DeterministicSeed): WrappedKey[];
|
|
35
|
+
/**
|
|
36
|
+
* Encrypts one archive.
|
|
37
|
+
*
|
|
38
|
+
* A random per-archive key encrypts the plaintext in chunks; that key is then
|
|
39
|
+
* wrapped separately for each recipient. Adding or removing a recipient therefore
|
|
40
|
+
* never requires re-encrypting the data.
|
|
41
|
+
*/
|
|
42
|
+
export declare function encryptArchive(plaintext: Buffer, options: EncryptOptions): {
|
|
43
|
+
envelope: Envelope;
|
|
44
|
+
ciphertext: Buffer;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Structural checks that must pass before any key is used.
|
|
48
|
+
*
|
|
49
|
+
* Takes a length rather than the bytes so the streaming reader, which never
|
|
50
|
+
* holds the ciphertext, is gated by exactly the same rules.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateEnvelope(envelope: Envelope, ciphertextBytes: number): void;
|
|
53
|
+
/**
|
|
54
|
+
* Decrypts an archive after verifying its structure and every stored chunk hash.
|
|
55
|
+
* Every chunk is authenticated against associated data that pins its position and
|
|
56
|
+
* the total chunk count, so a reordered or truncated archive fails rather than
|
|
57
|
+
* returning partial plaintext.
|
|
58
|
+
*/
|
|
59
|
+
export declare function decryptArchive(envelope: Envelope, ciphertext: Buffer, unlock: Unlock): Buffer;
|
|
60
|
+
/**
|
|
61
|
+
* Rotates the recipient set without touching ciphertext. `replace` is how a lost
|
|
62
|
+
* device is cut off; `add` is how a new device or extra recovery key is granted.
|
|
63
|
+
*/
|
|
64
|
+
export declare function rewrapArchive(envelope: Envelope, unlock: Unlock, recipients: readonly Recipient[], options?: {
|
|
65
|
+
mode?: "replace" | "add";
|
|
66
|
+
}): Envelope;
|
|
67
|
+
/** Recipient identifiers present on an archive. Useful for auditing who can open it. */
|
|
68
|
+
export declare function recipientIds(envelope: Envelope): {
|
|
69
|
+
phrase: string[];
|
|
70
|
+
keys: string[];
|
|
71
|
+
};
|