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
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHA-256 with exportable mid-stream state.
|
|
3
|
+
*
|
|
4
|
+
* Node's `createHash` keeps its running state inside OpenSSL and offers no way
|
|
5
|
+
* to serialize it, which is exactly what a resumable streamed seal needs: an
|
|
6
|
+
* upload interrupted at chunk 300 must resume its whole-object plaintext hash
|
|
7
|
+
* and whole-object ciphertext hash without re-reading 1.2 GB of source. This
|
|
8
|
+
* file is FIPS 180-4 SHA-256 in plain TypeScript so that the entire state —
|
|
9
|
+
* eight hash words, a byte count, and the sub-block tail — can be written into
|
|
10
|
+
* a journal and picked up by a later process.
|
|
11
|
+
*
|
|
12
|
+
* Digesting runs on a copy of the state rather than consuming it, because one
|
|
13
|
+
* instance has to answer twice for the same bytes: the archive record speaks
|
|
14
|
+
* hex (`ciphertextSha256`) while the remote verification speaks base64
|
|
15
|
+
* (`remote.checksum`), and a hasher that died on its first digest would force
|
|
16
|
+
* the seal to run two of them over every chunk.
|
|
17
|
+
*/
|
|
18
|
+
import { CryptoError } from "./recipients.js";
|
|
19
|
+
// The first 32 bits of the fractional parts of the cube roots of the first 64
|
|
20
|
+
// primes — the FIPS 180-4 round constants, in order.
|
|
21
|
+
const K = new Uint32Array([
|
|
22
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
23
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
24
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
25
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
26
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
27
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
28
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
29
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
30
|
+
]);
|
|
31
|
+
// The first 32 bits of the fractional parts of the square roots of the first
|
|
32
|
+
// 8 primes — the initial hash value H(0).
|
|
33
|
+
const INITIAL = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]);
|
|
34
|
+
const BLOCK_BYTES = 64;
|
|
35
|
+
const STATE_VERSION = 1;
|
|
36
|
+
/**
|
|
37
|
+
* One compression round over the 64 bytes at `offset`.
|
|
38
|
+
*
|
|
39
|
+
* All arithmetic is done in doubles and folded back to uint32 with `>>> 0`:
|
|
40
|
+
* every intermediate sum is below 2^35, which a double holds exactly, and
|
|
41
|
+
* `>>> 0` reduces modulo 2^32 — so mixing the signed results of the bitwise
|
|
42
|
+
* operators into the sums is still congruent to the unsigned arithmetic the
|
|
43
|
+
* specification describes.
|
|
44
|
+
*/
|
|
45
|
+
function compressBlock(h, w, block, offset) {
|
|
46
|
+
for (let i = 0; i < 16; i += 1) {
|
|
47
|
+
const at = offset + i * 4;
|
|
48
|
+
w[i] = ((block[at] << 24) | (block[at + 1] << 16) | (block[at + 2] << 8) | block[at + 3]) >>> 0;
|
|
49
|
+
}
|
|
50
|
+
for (let i = 16; i < 64; i += 1) {
|
|
51
|
+
const x = w[i - 15];
|
|
52
|
+
const y = w[i - 2];
|
|
53
|
+
const s0 = ((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3);
|
|
54
|
+
const s1 = ((y >>> 17) | (y << 15)) ^ ((y >>> 19) | (y << 13)) ^ (y >>> 10);
|
|
55
|
+
w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0;
|
|
56
|
+
}
|
|
57
|
+
let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];
|
|
58
|
+
for (let i = 0; i < 64; i += 1) {
|
|
59
|
+
const S1 = ((e >>> 6) | (e << 26)) ^ ((e >>> 11) | (e << 21)) ^ ((e >>> 25) | (e << 7));
|
|
60
|
+
const ch = (e & f) ^ (~e & g);
|
|
61
|
+
const t1 = (hh + S1 + ch + K[i] + w[i]) >>> 0;
|
|
62
|
+
const S0 = ((a >>> 2) | (a << 30)) ^ ((a >>> 13) | (a << 19)) ^ ((a >>> 22) | (a << 10));
|
|
63
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
64
|
+
const t2 = (S0 + maj) >>> 0;
|
|
65
|
+
hh = g;
|
|
66
|
+
g = f;
|
|
67
|
+
f = e;
|
|
68
|
+
e = (d + t1) >>> 0;
|
|
69
|
+
d = c;
|
|
70
|
+
c = b;
|
|
71
|
+
b = a;
|
|
72
|
+
a = (t1 + t2) >>> 0;
|
|
73
|
+
}
|
|
74
|
+
h[0] = (h[0] + a) >>> 0;
|
|
75
|
+
h[1] = (h[1] + b) >>> 0;
|
|
76
|
+
h[2] = (h[2] + c) >>> 0;
|
|
77
|
+
h[3] = (h[3] + d) >>> 0;
|
|
78
|
+
h[4] = (h[4] + e) >>> 0;
|
|
79
|
+
h[5] = (h[5] + f) >>> 0;
|
|
80
|
+
h[6] = (h[6] + g) >>> 0;
|
|
81
|
+
h[7] = (h[7] + hh) >>> 0;
|
|
82
|
+
}
|
|
83
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
84
|
+
function malformed(why) {
|
|
85
|
+
throw new CryptoError("hash_state_malformed", `This is not a usable saved hash state: ${why}. The interrupted seal cannot resume its hashes; start it over.`);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Checksum over the exported fields, computed with this same implementation so
|
|
89
|
+
* the module stays dependency-free. It is not a security boundary — anyone who
|
|
90
|
+
* can rewrite the state can recompute it — it exists so that a journal bit-flip
|
|
91
|
+
* or hand edit is refused at import instead of silently resuming into a
|
|
92
|
+
* whole-object digest that is wrong and only discovered at restore time.
|
|
93
|
+
*/
|
|
94
|
+
function stateCheck(h, bytes, tailB64) {
|
|
95
|
+
return new StreamingSha256()
|
|
96
|
+
.update(Buffer.from(`vaultline-sha256-state:v${STATE_VERSION}|${h.join(",")}|${bytes}|${tailB64}`, "utf8"))
|
|
97
|
+
.digestHex()
|
|
98
|
+
.slice(0, 16);
|
|
99
|
+
}
|
|
100
|
+
export class StreamingSha256 {
|
|
101
|
+
h = Uint32Array.from(INITIAL);
|
|
102
|
+
w = new Uint32Array(64);
|
|
103
|
+
tail = Buffer.alloc(BLOCK_BYTES);
|
|
104
|
+
tailLength = 0;
|
|
105
|
+
bytes = 0;
|
|
106
|
+
update(data) {
|
|
107
|
+
this.bytes += data.length;
|
|
108
|
+
let offset = 0;
|
|
109
|
+
if (this.tailLength > 0) {
|
|
110
|
+
const take = Math.min(BLOCK_BYTES - this.tailLength, data.length);
|
|
111
|
+
data.copy(this.tail, this.tailLength, 0, take);
|
|
112
|
+
this.tailLength += take;
|
|
113
|
+
offset = take;
|
|
114
|
+
if (this.tailLength === BLOCK_BYTES) {
|
|
115
|
+
compressBlock(this.h, this.w, this.tail, 0);
|
|
116
|
+
this.tailLength = 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
while (offset + BLOCK_BYTES <= data.length) {
|
|
120
|
+
compressBlock(this.h, this.w, data, offset);
|
|
121
|
+
offset += BLOCK_BYTES;
|
|
122
|
+
}
|
|
123
|
+
if (offset < data.length) {
|
|
124
|
+
data.copy(this.tail, 0, offset);
|
|
125
|
+
this.tailLength = data.length - offset;
|
|
126
|
+
}
|
|
127
|
+
return this;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Finalizes on a copy of the state: the running stream stays live, so hex and
|
|
131
|
+
* base64 can both be asked of the same instance and updates may continue —
|
|
132
|
+
* unlike `createHash`, which a digest consumes.
|
|
133
|
+
*/
|
|
134
|
+
finalDigest() {
|
|
135
|
+
const h = Uint32Array.from(this.h);
|
|
136
|
+
const w = new Uint32Array(64);
|
|
137
|
+
// Padding: 0x80, zeros to 56 mod 64, then the message length in BITS as a
|
|
138
|
+
// big-endian 64-bit integer. BigInt keeps the multiply exact where
|
|
139
|
+
// `bytes * 8` would still fit a double but `bytes << 3` would not.
|
|
140
|
+
const padded = Buffer.alloc(this.tailLength < 56 ? BLOCK_BYTES : 2 * BLOCK_BYTES);
|
|
141
|
+
this.tail.copy(padded, 0, 0, this.tailLength);
|
|
142
|
+
padded[this.tailLength] = 0x80;
|
|
143
|
+
padded.writeBigUInt64BE(BigInt(this.bytes) * 8n, padded.length - 8);
|
|
144
|
+
compressBlock(h, w, padded, 0);
|
|
145
|
+
if (padded.length === 2 * BLOCK_BYTES)
|
|
146
|
+
compressBlock(h, w, padded, BLOCK_BYTES);
|
|
147
|
+
const digest = Buffer.alloc(32);
|
|
148
|
+
for (let i = 0; i < 8; i += 1)
|
|
149
|
+
digest.writeUInt32BE(h[i], i * 4);
|
|
150
|
+
return digest;
|
|
151
|
+
}
|
|
152
|
+
digestHex() {
|
|
153
|
+
return this.finalDigest().toString("hex");
|
|
154
|
+
}
|
|
155
|
+
digestBase64() {
|
|
156
|
+
return this.finalDigest().toString("base64");
|
|
157
|
+
}
|
|
158
|
+
/** JSON-safe string capturing H[0..7], byte count, and the unprocessed tail. */
|
|
159
|
+
exportState() {
|
|
160
|
+
const tailB64 = this.tail.subarray(0, this.tailLength).toString("base64");
|
|
161
|
+
const h = Array.from(this.h);
|
|
162
|
+
return JSON.stringify({ alg: "sha256", v: STATE_VERSION, h, bytes: this.bytes, tail: tailB64, check: stateCheck(h, this.bytes, tailB64) });
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Rebuilds a stream from `exportState` output. Shape is validated field by
|
|
166
|
+
* field and the checksum recomputed, so garbage throws here rather than
|
|
167
|
+
* continuing into a digest that looks plausible and is wrong.
|
|
168
|
+
*/
|
|
169
|
+
static importState(state) {
|
|
170
|
+
let parsed;
|
|
171
|
+
try {
|
|
172
|
+
parsed = JSON.parse(state);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
malformed("it is not JSON");
|
|
176
|
+
}
|
|
177
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
178
|
+
malformed("it is not an object");
|
|
179
|
+
const record = parsed;
|
|
180
|
+
if (record.alg !== "sha256")
|
|
181
|
+
malformed("it does not describe a SHA-256 stream");
|
|
182
|
+
if (record.v !== STATE_VERSION)
|
|
183
|
+
malformed(`it claims state version ${String(record.v)} and this build reads version ${STATE_VERSION}`);
|
|
184
|
+
const h = record.h;
|
|
185
|
+
if (!Array.isArray(h) || h.length !== 8 || !h.every((word) => Number.isInteger(word) && word >= 0 && word <= 0xffffffff)) {
|
|
186
|
+
malformed("its hash words are not eight 32-bit integers");
|
|
187
|
+
}
|
|
188
|
+
const bytes = record.bytes;
|
|
189
|
+
if (!Number.isSafeInteger(bytes) || bytes < 0)
|
|
190
|
+
malformed("its byte count is not a non-negative integer");
|
|
191
|
+
const tailB64 = record.tail;
|
|
192
|
+
if (typeof tailB64 !== "string" || !BASE64.test(tailB64))
|
|
193
|
+
malformed("its unprocessed tail is not base64");
|
|
194
|
+
const tail = Buffer.from(tailB64, "base64");
|
|
195
|
+
if (tail.length !== bytes % BLOCK_BYTES)
|
|
196
|
+
malformed("its unprocessed tail does not agree with its byte count");
|
|
197
|
+
if (record.check !== stateCheck(h, bytes, tailB64))
|
|
198
|
+
malformed("it does not match its own checksum, so it was corrupted or edited after it was written");
|
|
199
|
+
const hash = new StreamingSha256();
|
|
200
|
+
hash.h.set(h);
|
|
201
|
+
tail.copy(hash.tail);
|
|
202
|
+
hash.tailLength = tail.length;
|
|
203
|
+
hash.bytes = bytes;
|
|
204
|
+
return hash;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-to-file sealing and opening, in constant memory.
|
|
3
|
+
*
|
|
4
|
+
* `envelope.ts` holds the whole plaintext, the whole compressed body and the
|
|
5
|
+
* whole ciphertext at once, so archiving a 4 GB agent rollout needed roughly
|
|
6
|
+
* 14.5 GB of resident memory. The archives these functions produce are the
|
|
7
|
+
* *same* format — a v2 envelope, gzip applied to the whole body before
|
|
8
|
+
* chunking — so anything written here opens with `decryptArchive`, and anything
|
|
9
|
+
* written by `encryptArchive` opens with `openArchiveToFile`. Only the memory
|
|
10
|
+
* profile changes.
|
|
11
|
+
*
|
|
12
|
+
* The one thing that stops a seal being a single streaming pass is the
|
|
13
|
+
* associated data: every chunk commits to the *total* chunk count, which is not
|
|
14
|
+
* known until the compressed body is complete. Rather than weaken that binding,
|
|
15
|
+
* the body is written once to scratch, measured, and then sealed. Two sequential
|
|
16
|
+
* streams, no format change, nothing larger than one chunk ever resident.
|
|
17
|
+
*/
|
|
18
|
+
import { type Unlock } from "./recipients.js";
|
|
19
|
+
import { StreamingSha256 } from "./sha256-stream.js";
|
|
20
|
+
import { type EncryptOptions } from "./envelope.js";
|
|
21
|
+
import { type Envelope, type Suite } from "./format.js";
|
|
22
|
+
/**
|
|
23
|
+
* The byte range of the source this archive covers.
|
|
24
|
+
*
|
|
25
|
+
* `start` is what makes an incremental archive possible: an agent transcript is
|
|
26
|
+
* append-only JSONL, so once a caller has *proved* the first N bytes are
|
|
27
|
+
* unchanged it can seal only `[N, size)` and store just the new portion.
|
|
28
|
+
*/
|
|
29
|
+
export type SealSource = {
|
|
30
|
+
path: string;
|
|
31
|
+
start?: number;
|
|
32
|
+
end?: number;
|
|
33
|
+
};
|
|
34
|
+
export type StreamEncryptOptions = EncryptOptions & {
|
|
35
|
+
/** Where the compressed body is staged. Defaults to the destination's directory. */
|
|
36
|
+
scratchDir?: string;
|
|
37
|
+
/** Called as the source is read, with cumulative bytes so far — a person watching a 1.6 GB seal deserves a number that moves. Throttling is the caller's business. */
|
|
38
|
+
onProgress?: (originalBytes: number) => void;
|
|
39
|
+
};
|
|
40
|
+
export type SealResult = {
|
|
41
|
+
envelope: Envelope;
|
|
42
|
+
/** SHA-256 of the stored ciphertext, so the caller never re-reads it to record one. */
|
|
43
|
+
ciphertextSha256: string;
|
|
44
|
+
storedBytes: number;
|
|
45
|
+
/** The caller's own bytes covered by this archive — the sealed range, not the whole file. */
|
|
46
|
+
originalBytes: number;
|
|
47
|
+
originalSha256: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Seals a byte range of one file into another, holding one chunk at a time.
|
|
51
|
+
*
|
|
52
|
+
* The destination is written but never renamed here: the manifest hashes are
|
|
53
|
+
* only proven at the very end, so publishing the result is the caller's job and
|
|
54
|
+
* must happen after this resolves. On failure the destination is removed.
|
|
55
|
+
*/
|
|
56
|
+
export declare function sealArchiveToFile(source: SealSource, destPath: string, options: StreamEncryptOptions): Promise<SealResult>;
|
|
57
|
+
export type OpenResult = {
|
|
58
|
+
bytes: number;
|
|
59
|
+
sha256: string;
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* Opens an archive straight to a file, holding one chunk at a time.
|
|
63
|
+
*
|
|
64
|
+
* Every chunk is authenticated before its bytes reach the destination, so what
|
|
65
|
+
* is written is never unauthenticated data — but the *whole-archive* manifest
|
|
66
|
+
* hashes can only be checked once the last chunk has gone past. A caller must
|
|
67
|
+
* therefore write to a temporary path and publish it only after this resolves;
|
|
68
|
+
* a rejection can leave a partial file behind, which is why `restoreArchive`
|
|
69
|
+
* renames into place rather than writing the destination directly.
|
|
70
|
+
*/
|
|
71
|
+
export declare function openArchiveToFile(envelope: Envelope, ciphertextPath: string, destPath: string, unlock: Unlock): Promise<OpenResult>;
|
|
72
|
+
/**
|
|
73
|
+
* SHA-256 of several prefixes of a file, in one read and in constant memory.
|
|
74
|
+
*
|
|
75
|
+
* This is how an append is *proved* rather than assumed: hashing `[0, N)` of the
|
|
76
|
+
* live file and comparing it with what an archive recorded is the only evidence
|
|
77
|
+
* that the archive's bytes are still a prefix of it. Several offsets are
|
|
78
|
+
* answered together because a transcript usually carries several archives, and
|
|
79
|
+
* asking one at a time would re-read it once per archive.
|
|
80
|
+
*
|
|
81
|
+
* An offset past the end of the file is simply absent from the result.
|
|
82
|
+
*/
|
|
83
|
+
export declare function hashFilePrefixes(path: string, offsets: readonly number[]): Promise<Map<number, string>>;
|
|
84
|
+
/**
|
|
85
|
+
* SHA-256 of a byte range of a file, read in constant memory.
|
|
86
|
+
*
|
|
87
|
+
* This is how an append is *proved* rather than assumed: hashing `[0, N)` of the
|
|
88
|
+
* live file and comparing it with what an archive recorded is the only evidence
|
|
89
|
+
* that the archive's bytes are still a prefix of it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function hashFileRange(path: string, start?: number, end?: number): Promise<{
|
|
92
|
+
sha256: string;
|
|
93
|
+
bytes: number;
|
|
94
|
+
}>;
|
|
95
|
+
/** Where sealed chunks go: a file today, a resumable upload session tomorrow. */
|
|
96
|
+
export type ChunkSink = {
|
|
97
|
+
write(stored: Buffer): Promise<void>;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* The single-pass gzip-chunk seal, destination-agnostic. This is what makes
|
|
101
|
+
* seal-to-cloud a sink implementation instead of an architecture project: the
|
|
102
|
+
* chunk loop neither knows nor cares whether `write` lands on a disk, a GCS
|
|
103
|
+
* resumable session, or an S3 multipart part buffer. Local cost is one slice
|
|
104
|
+
* in memory, full stop.
|
|
105
|
+
*/
|
|
106
|
+
export declare function sealChunksToSink(source: SealSource, sink: ChunkSink, options: StreamEncryptOptions & {
|
|
107
|
+
suite?: Suite;
|
|
108
|
+
archiveKey?: Buffer;
|
|
109
|
+
noncePrefix?: Buffer;
|
|
110
|
+
/**
|
|
111
|
+
* Externally owned whole-object hashers, for resumable seals: `plaintext`
|
|
112
|
+
* sees every raw source byte this call reads (it becomes the manifest's
|
|
113
|
+
* `originalSha256`), `stored` sees every stored byte (it becomes
|
|
114
|
+
* `SealResult.ciphertextSha256`), both still hex. A resumed seal imports
|
|
115
|
+
* these from its journal so the digests cover the whole object even though
|
|
116
|
+
* this call only reads the remainder. Omitted, fresh hashes are used and
|
|
117
|
+
* nothing changes.
|
|
118
|
+
*/
|
|
119
|
+
hashers?: {
|
|
120
|
+
plaintext?: StreamingSha256;
|
|
121
|
+
stored?: StreamingSha256;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Where chunk numbering starts, for resumable seals. Chunk AAD binds each
|
|
125
|
+
* chunk's index and the archive's total chunk count, so a seal re-entered
|
|
126
|
+
* at a byte offset must keep counting where the interrupted run stopped —
|
|
127
|
+
* restarting at zero would mint chunks whose AAD collides with the ones
|
|
128
|
+
* already uploaded. The caller passes the journal's `nextChunkIndex` along
|
|
129
|
+
* with `source.start` at that chunk's byte offset; the produced AADs then
|
|
130
|
+
* agree with the interrupted run's, including the total.
|
|
131
|
+
*/
|
|
132
|
+
firstChunkIndex?: number;
|
|
133
|
+
/**
|
|
134
|
+
* Observes each RAW source slice as it is read, before compression and
|
|
135
|
+
* encryption — the hook seal-time indexing rides on. The slice is the
|
|
136
|
+
* loop's reused buffer window: consume it synchronously, never keep it.
|
|
137
|
+
*/
|
|
138
|
+
onPlaintext?: (chunkIndex: number, slice: Buffer) => void;
|
|
139
|
+
}): Promise<SealResult>;
|