@cello-protocol/daemon 0.0.8 → 0.0.10
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/dist/agent-loader.d.ts +15 -18
- package/dist/agent-loader.d.ts.map +1 -1
- package/dist/agent-loader.js +24 -81
- package/dist/agent-loader.js.map +1 -1
- package/dist/bin/cello-daemon.js +5 -7
- package/dist/bin/cello-daemon.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +153 -96
- package/dist/daemon.js.map +1 -1
- package/dist/db-identity-store.d.ts +97 -0
- package/dist/db-identity-store.d.ts.map +1 -0
- package/dist/db-identity-store.js +235 -0
- package/dist/db-identity-store.js.map +1 -0
- package/dist/identity-migration.d.ts +40 -0
- package/dist/identity-migration.d.ts.map +1 -0
- package/dist/identity-migration.js +455 -0
- package/dist/identity-migration.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/manifest-version-store-db.d.ts +24 -0
- package/dist/manifest-version-store-db.d.ts.map +1 -0
- package/dist/manifest-version-store-db.js +57 -0
- package/dist/manifest-version-store-db.js.map +1 -0
- package/dist/manifest-version-store.d.ts +3 -1
- package/dist/manifest-version-store.d.ts.map +1 -1
- package/dist/manifest-version-store.js +3 -1
- package/dist/manifest-version-store.js.map +1 -1
- package/dist/nonce-dedup.d.ts +2 -2
- package/dist/nonce-dedup.d.ts.map +1 -1
- package/dist/nonce-dedup.js.map +1 -1
- package/dist/registration-manager.d.ts.map +1 -1
- package/dist/registration-manager.js +78 -44
- package/dist/registration-manager.js.map +1 -1
- package/dist/retry-queue.d.ts +2 -3
- package/dist/retry-queue.d.ts.map +1 -1
- package/dist/retry-queue.js +8 -16
- package/dist/retry-queue.js.map +1 -1
- package/dist/seal-upgrade.d.ts +3 -1
- package/dist/seal-upgrade.d.ts.map +1 -1
- package/dist/seal-upgrade.js +1 -2
- package/dist/seal-upgrade.js.map +1 -1
- package/dist/session-ceremony.d.ts +8 -4
- package/dist/session-ceremony.d.ts.map +1 -1
- package/dist/session-ceremony.js +3 -7
- package/dist/session-ceremony.js.map +1 -1
- package/dist/session-node-manager.d.ts +10 -10
- package/dist/session-node-manager.d.ts.map +1 -1
- package/dist/session-node-manager.js +45 -45
- package/dist/session-node-manager.js.map +1 -1
- package/dist/sqlcipher-db.d.ts +87 -0
- package/dist/sqlcipher-db.d.ts.map +1 -0
- package/dist/sqlcipher-db.js +259 -0
- package/dist/sqlcipher-db.js.map +1 -0
- package/package.json +5 -4
- package/dist/manifest-version-store-file.d.ts +0 -18
- package/dist/manifest-version-store-file.d.ts.map +0 -1
- package/dist/manifest-version-store-file.js +0 -40
- package/dist/manifest-version-store-file.js.map +0 -1
- package/dist/transcript-cipher.d.ts +0 -31
- package/dist/transcript-cipher.d.ts.map +0 -1
- package/dist/transcript-cipher.js +0 -74
- package/dist/transcript-cipher.js.map +0 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CELLO-M7-PERSIST-002 — the daemon's single encrypted store (DEC-1).
|
|
3
|
+
*
|
|
4
|
+
* The daemon DB is a SQLCipher database (whole-file AES-256 at rest). This module is the ONE
|
|
5
|
+
* engine site: it loads @signalapp/sqlcipher (prebuilt — DEC-1; no compile), opens the DB with a
|
|
6
|
+
* PRAGMA key, verifies the key, enables WAL, and presents a thin `DaemonDatabase` adapter so the
|
|
7
|
+
* rest of the daemon (SessionNodeManager, RetryQueue, NonceDedupStore) keeps its node:sqlite-style
|
|
8
|
+
* varargs call sites unchanged.
|
|
9
|
+
*
|
|
10
|
+
* Why an adapter: node:sqlite's `DatabaseSync` uses varargs (`stmt.run(a, b, c)`); @signalapp's
|
|
11
|
+
* better-sqlite3-style API takes an array (`stmt.run([a, b, c])`). The `DaemonDatabase` /
|
|
12
|
+
* `DaemonStatement` interfaces expose the varargs surface — node:sqlite's `DatabaseSync` structurally
|
|
13
|
+
* satisfies them too (used for in-memory test handles and for reading a legacy plaintext DB during
|
|
14
|
+
* migration), and `SqlcipherDatabase` implements them by forwarding varargs as an array.
|
|
15
|
+
*
|
|
16
|
+
* Key custody (DEC-2/DEC-4): the SQLCipher key is a standalone random 32-byte 0600 key file beside
|
|
17
|
+
* the DB. It is NOT derived from K_local (chicken-and-egg — K_local now lives inside the DB). It is
|
|
18
|
+
* the single plaintext key file and replaces the old `sessions.db.transcript-key`.
|
|
19
|
+
*
|
|
20
|
+
* Fail closed (SI-002/AC-011): there is no plaintext fallback anywhere. A wrong/missing key on an
|
|
21
|
+
* existing DB throws `db_encryption_key_mismatch`; the daemon never opens, creates, or migrates to a
|
|
22
|
+
* plaintext store as a fallback.
|
|
23
|
+
*
|
|
24
|
+
* Security invariants:
|
|
25
|
+
* SI-001: the db key is NEVER logged, serialized, or placed in an error message — only the raw
|
|
26
|
+
* `PRAGMA key` string uses the hex, and that string is never logged.
|
|
27
|
+
* SI-003 (M6B-005 parity): WAL mode is enabled only AFTER key verification, so WAL files are
|
|
28
|
+
* encrypted at rest.
|
|
29
|
+
*/
|
|
30
|
+
import { createRequire } from "node:module";
|
|
31
|
+
import { existsSync, readFileSync, readSync, openSync, writeSync, fsyncSync, closeSync, renameSync, mkdirSync, unlinkSync, } from "node:fs";
|
|
32
|
+
import { randomBytes } from "node:crypto";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
const DB_KEY_BYTES = 32; // AES-256
|
|
35
|
+
export class DbEncryptionError extends Error {
|
|
36
|
+
code;
|
|
37
|
+
/** Actionable next step surfaced to the operator (never contains key material). */
|
|
38
|
+
guidance;
|
|
39
|
+
constructor(code, message, guidance) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "DbEncryptionError";
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.guidance = guidance;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// ─── The varargs→array adapter ──────────────────────────────────────────────────
|
|
47
|
+
class SqlcipherStatement {
|
|
48
|
+
#inner;
|
|
49
|
+
constructor(inner) {
|
|
50
|
+
this.#inner = inner;
|
|
51
|
+
}
|
|
52
|
+
run(...params) {
|
|
53
|
+
return this.#inner.run(params);
|
|
54
|
+
}
|
|
55
|
+
get(...params) {
|
|
56
|
+
return this.#inner.get(params);
|
|
57
|
+
}
|
|
58
|
+
all(...params) {
|
|
59
|
+
return this.#inner.all(params);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
class SqlcipherDatabase {
|
|
63
|
+
#inner;
|
|
64
|
+
constructor(inner) {
|
|
65
|
+
this.#inner = inner;
|
|
66
|
+
}
|
|
67
|
+
exec(sql) {
|
|
68
|
+
this.#inner.exec(sql);
|
|
69
|
+
}
|
|
70
|
+
prepare(sql) {
|
|
71
|
+
return new SqlcipherStatement(this.#inner.prepare(sql));
|
|
72
|
+
}
|
|
73
|
+
pragma(source, options) {
|
|
74
|
+
return this.#inner.pragma(source, options);
|
|
75
|
+
}
|
|
76
|
+
close() {
|
|
77
|
+
this.#inner.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// ─── The raw SQLite magic — used to distinguish a plaintext DB from an encrypted one ──
|
|
81
|
+
// A plaintext SQLite file begins with "SQLite format 3\0". A SQLCipher database encrypts the
|
|
82
|
+
// header too, so its first bytes are ciphertext and never match. The migration path (PERSIST-002
|
|
83
|
+
// Unit 6) uses this to detect a pre-story plaintext DB that must be migrated.
|
|
84
|
+
const SQLITE_MAGIC = Buffer.concat([Buffer.from("SQLite format 3", "latin1"), Buffer.from([0x00])]);
|
|
85
|
+
/** True when the file at `dbPath` exists and is an UNENCRYPTED node:sqlite database. */
|
|
86
|
+
export function isPlaintextSqliteFile(dbPath) {
|
|
87
|
+
if (!existsSync(dbPath))
|
|
88
|
+
return false;
|
|
89
|
+
const head = Buffer.alloc(SQLITE_MAGIC.length);
|
|
90
|
+
try {
|
|
91
|
+
const fd = openSync(dbPath, "r");
|
|
92
|
+
try {
|
|
93
|
+
// Read exactly the header bytes — never slurp the whole (possibly very large) DB into RAM.
|
|
94
|
+
const bytesRead = readSync(fd, head, 0, SQLITE_MAGIC.length, 0);
|
|
95
|
+
if (bytesRead < SQLITE_MAGIC.length)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
return head.equals(SQLITE_MAGIC);
|
|
106
|
+
}
|
|
107
|
+
// ─── Key custody (DEC-2/DEC-4) ───────────────────────────────────────────────────
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the SQLCipher key, generating it on a genuinely fresh install only.
|
|
110
|
+
*
|
|
111
|
+
* Fail-closed matrix (SI-002/AC-011):
|
|
112
|
+
* key present → load it (must be 32 bytes).
|
|
113
|
+
* key absent + DB absent → generate + persist (0600), proceed (fresh install).
|
|
114
|
+
* key absent + DB present → THROW db_encryption_key_mismatch (never overwrite an
|
|
115
|
+
* existing DB with a new key — that would be data loss; and
|
|
116
|
+
* a pre-story PLAINTEXT DB must be migrated first, not opened).
|
|
117
|
+
*/
|
|
118
|
+
export function resolveDbKey(dbPath, keyPath) {
|
|
119
|
+
if (existsSync(keyPath)) {
|
|
120
|
+
const key = readFileSync(keyPath);
|
|
121
|
+
if (key.length !== DB_KEY_BYTES) {
|
|
122
|
+
throw new DbEncryptionError("db_encryption_key_mismatch", `SQLCipher key file is ${key.length} bytes, expected ${DB_KEY_BYTES}`, `The key file at ${keyPath} is malformed. Restore it from backup or remove it only if the database is also gone.`);
|
|
123
|
+
}
|
|
124
|
+
return new Uint8Array(key);
|
|
125
|
+
}
|
|
126
|
+
if (existsSync(dbPath)) {
|
|
127
|
+
throw new DbEncryptionError("db_encryption_key_mismatch", `database exists at ${dbPath} but its SQLCipher key file is missing`, `The encrypted database cannot be opened without its key file (${keyPath}). Restore the key file from backup. The daemon will not create a new plaintext store.`);
|
|
128
|
+
}
|
|
129
|
+
// Fresh install: generate and persist the key atomically with 0600.
|
|
130
|
+
const key = randomBytes(DB_KEY_BYTES);
|
|
131
|
+
const keyDir = dirname(keyPath);
|
|
132
|
+
mkdirSync(keyDir, { recursive: true, mode: 0o700 });
|
|
133
|
+
const tmp = join(keyDir, `.cello-dbkey-tmp-${process.pid}-${randomBytes(6).toString("hex")}`);
|
|
134
|
+
const fd = openSync(tmp, "wx", 0o600);
|
|
135
|
+
try {
|
|
136
|
+
writeSync(fd, key);
|
|
137
|
+
fsyncSync(fd);
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
// Never leave a stray plaintext key fragment on disk if the write/fsync failed partway
|
|
141
|
+
// (SI-001: the only plaintext key on disk is the one sanctioned key file).
|
|
142
|
+
closeSync(fd);
|
|
143
|
+
try {
|
|
144
|
+
unlinkSync(tmp);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* ignore */
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
try {
|
|
153
|
+
renameSync(tmp, keyPath);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
try {
|
|
157
|
+
unlinkSync(tmp);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* ignore */
|
|
161
|
+
}
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
// Durability of the rename itself (parent-dir fsync) so a crash right after a fresh install can't
|
|
165
|
+
// lose the key while the encrypted DB exists. Best-effort — some platforms reject dir fsync.
|
|
166
|
+
try {
|
|
167
|
+
const dfd = openSync(keyDir, "r");
|
|
168
|
+
try {
|
|
169
|
+
fsyncSync(dfd);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
closeSync(dfd);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
/* directory fsync is best-effort */
|
|
177
|
+
}
|
|
178
|
+
return new Uint8Array(key);
|
|
179
|
+
}
|
|
180
|
+
// ─── Open ────────────────────────────────────────────────────────────────────────
|
|
181
|
+
function loadSignalModule() {
|
|
182
|
+
const require = createRequire(import.meta.url);
|
|
183
|
+
try {
|
|
184
|
+
return require("@signalapp/sqlcipher");
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
188
|
+
throw new DbEncryptionError("db_sqlcipher_unavailable", `SQLCipher native module unavailable: ${reason}`, "The @signalapp/sqlcipher prebuilt binary failed to load for this platform. Reinstall the CELLO client.");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Open `dbPath` as a SQLCipher database keyed by `dbKey`. Verifies the key (reads sqlite_master)
|
|
193
|
+
* and enables WAL after verification. Throws DbEncryptionError on any failure — never returns a
|
|
194
|
+
* plaintext handle (SI-002).
|
|
195
|
+
*/
|
|
196
|
+
export function openEncryptedDatabase(dbPath, dbKey, logger) {
|
|
197
|
+
const mod = loadSignalModule();
|
|
198
|
+
const Ctor = mod.Database ?? mod.default;
|
|
199
|
+
let inner;
|
|
200
|
+
try {
|
|
201
|
+
inner = new Ctor(dbPath);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
205
|
+
throw new DbEncryptionError("db_open_failed", reason, `Could not open the database file at ${dbPath}.`);
|
|
206
|
+
}
|
|
207
|
+
// SI-001: set the key OUTSIDE the message-bearing try below, so the key hex can never reach a
|
|
208
|
+
// thrown/logged error string even if a future SQLCipher build echoed the pragma source on error.
|
|
209
|
+
// A well-formed `key` pragma does not throw; a wrong key surfaces at the verify read instead.
|
|
210
|
+
const keyHex = Buffer.from(dbKey).toString("hex");
|
|
211
|
+
inner.pragma(`key = "x'${keyHex}'"`);
|
|
212
|
+
try {
|
|
213
|
+
// Verify: reading sqlite_master decrypts the header. Wrong key / plaintext file → throws.
|
|
214
|
+
inner.prepare("SELECT count(*) AS c FROM sqlite_master").get([]);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
try {
|
|
218
|
+
inner.close();
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
/* ignore */
|
|
222
|
+
}
|
|
223
|
+
// Deliberately do NOT echo the underlying SQLITE message into guidance verbatim — but DO log
|
|
224
|
+
// nothing here (the caller logs). SI-001: no key material in the message.
|
|
225
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
226
|
+
throw new DbEncryptionError("db_encryption_key_mismatch", `database did not decrypt with the supplied key (${reason})`, "The SQLCipher key does not match this database. Restore the correct key file; the daemon will not fall back to a plaintext store.");
|
|
227
|
+
}
|
|
228
|
+
// SI-003 (M6B-005 parity): WAL only AFTER key verification, so WAL/SHM files are encrypted.
|
|
229
|
+
try {
|
|
230
|
+
const mode = inner.pragma("journal_mode=WAL", { simple: true });
|
|
231
|
+
if (mode !== "wal") {
|
|
232
|
+
// Not fatal — SQLCipher encrypts the rollback journal too, so at-rest integrity holds — but
|
|
233
|
+
// surface it so a permanent non-WAL degradation (e.g. a network filesystem) is not silent.
|
|
234
|
+
logger?.warn("persist.db.wal.unavailable", { mode: String(mode) });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
logger?.warn("persist.db.wal.unavailable", { error: err instanceof Error ? err.message : String(err) });
|
|
239
|
+
}
|
|
240
|
+
return new SqlcipherDatabase(inner);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Convenience used by tests and tooling: resolve the key file beside `dbPath` and open. Throws if
|
|
244
|
+
* the key file is absent (it does NOT generate one) — callers inspecting an existing DB always have
|
|
245
|
+
* the key.
|
|
246
|
+
*/
|
|
247
|
+
export function openEncryptedDatabaseAtPath(dbPath, keyPath) {
|
|
248
|
+
const kp = keyPath ?? `${dbPath}.key`;
|
|
249
|
+
if (!existsSync(kp)) {
|
|
250
|
+
throw new DbEncryptionError("db_encryption_key_mismatch", `SQLCipher key file not found at ${kp}`, "Provide the key file path that was generated beside the database.");
|
|
251
|
+
}
|
|
252
|
+
const key = readFileSync(kp);
|
|
253
|
+
return openEncryptedDatabase(dbPath, new Uint8Array(key));
|
|
254
|
+
}
|
|
255
|
+
/** The key file path the daemon uses for a given DB path (DEC-2: one plaintext key file). */
|
|
256
|
+
export function dbKeyPathFor(dbPath) {
|
|
257
|
+
return `${dbPath}.key`;
|
|
258
|
+
}
|
|
259
|
+
//# sourceMappingURL=sqlcipher-db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlcipher-db.js","sourceRoot":"","sources":["../src/sqlcipher-db.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cello-protocol/daemon",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -27,11 +27,12 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@libp2p/interface": "^3.0.0",
|
|
30
|
+
"@signalapp/sqlcipher": "^3.3.5",
|
|
30
31
|
"cbor-x": "^1.6.0",
|
|
31
32
|
"it-length-prefixed": "^10.0.1",
|
|
32
|
-
"@cello-protocol/crypto": "0.0.
|
|
33
|
-
"@cello-protocol/transport": "0.0.
|
|
34
|
-
"@cello-protocol/protocol-types": "0.0.
|
|
33
|
+
"@cello-protocol/crypto": "0.0.10",
|
|
34
|
+
"@cello-protocol/transport": "0.0.7",
|
|
35
|
+
"@cello-protocol/protocol-types": "0.0.7"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/node": "^25.6.2",
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileManifestVersionStore — file-backed IManifestVersionStore.
|
|
3
|
-
*
|
|
4
|
-
* Persists the last-seen manifest version to a JSON file on disk, enabling
|
|
5
|
-
* version monotonicity enforcement across daemon restarts.
|
|
6
|
-
*
|
|
7
|
-
* Crypto reference: RFC 8032 (this module stores the manifest version number
|
|
8
|
-
* that backs the monotonicity invariant; the version itself is not cryptographic,
|
|
9
|
-
* but its correct persistence is load-bearing for anti-rollback security).
|
|
10
|
-
*/
|
|
11
|
-
import type { IManifestVersionStore } from "@cello-protocol/transport";
|
|
12
|
-
export declare class FileManifestVersionStore implements IManifestVersionStore {
|
|
13
|
-
#private;
|
|
14
|
-
constructor(filePath: string);
|
|
15
|
-
getLastSeenVersion(): Promise<number | null>;
|
|
16
|
-
persistVersion(version: number): Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
//# sourceMappingURL=manifest-version-store-file.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-version-store-file.d.ts","sourceRoot":"","sources":["../src/manifest-version-store-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAMvE,qBAAa,wBAAyB,YAAW,qBAAqB;;gBAGxD,QAAQ,EAAE,MAAM;IAItB,kBAAkB,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAU5C,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAWrD"}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* FileManifestVersionStore — file-backed IManifestVersionStore.
|
|
3
|
-
*
|
|
4
|
-
* Persists the last-seen manifest version to a JSON file on disk, enabling
|
|
5
|
-
* version monotonicity enforcement across daemon restarts.
|
|
6
|
-
*
|
|
7
|
-
* Crypto reference: RFC 8032 (this module stores the manifest version number
|
|
8
|
-
* that backs the monotonicity invariant; the version itself is not cryptographic,
|
|
9
|
-
* but its correct persistence is load-bearing for anti-rollback security).
|
|
10
|
-
*/
|
|
11
|
-
import { readFile, writeFile, rename, mkdir } from "node:fs/promises";
|
|
12
|
-
import { dirname } from "node:path";
|
|
13
|
-
export class FileManifestVersionStore {
|
|
14
|
-
#filePath;
|
|
15
|
-
constructor(filePath) {
|
|
16
|
-
this.#filePath = filePath;
|
|
17
|
-
}
|
|
18
|
-
async getLastSeenVersion() {
|
|
19
|
-
try {
|
|
20
|
-
const raw = await readFile(this.#filePath, "utf-8");
|
|
21
|
-
const parsed = JSON.parse(raw);
|
|
22
|
-
return typeof parsed.lastSeenVersion === "number" ? parsed.lastSeenVersion : null;
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
async persistVersion(version) {
|
|
29
|
-
await mkdir(dirname(this.#filePath), { recursive: true });
|
|
30
|
-
const content = { lastSeenVersion: version };
|
|
31
|
-
// ADV-005: Atomic write using write-to-temp-then-rename pattern.
|
|
32
|
-
// rename() is atomic on POSIX (same filesystem). This prevents a crash
|
|
33
|
-
// during write from corrupting the version file and resetting the
|
|
34
|
-
// monotonicity check (which would enable a rollback attack on next start).
|
|
35
|
-
const tmpPath = this.#filePath + ".tmp";
|
|
36
|
-
await writeFile(tmpPath, JSON.stringify(content), "utf-8");
|
|
37
|
-
await rename(tmpPath, this.#filePath);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
//# sourceMappingURL=manifest-version-store-file.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"manifest-version-store-file.js","sourceRoot":"","sources":["../src/manifest-version-store-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,MAAM,OAAO,wBAAwB;IAC1B,SAAS,CAAS;IAE3B,YAAY,QAAgB;QAC1B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,kBAAkB;QACtB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACpD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAqB,CAAC;YACnD,OAAO,OAAO,MAAM,CAAC,eAAe,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC;QACpF,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,OAAe;QAClC,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAqB,EAAE,eAAe,EAAE,OAAO,EAAE,CAAC;QAC/D,iEAAiE;QACjE,uEAAuE;QACvE,kEAAkE;QAClE,2EAA2E;QAC3E,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QACxC,MAAM,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC;QAC3D,MAAM,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACxC,CAAC;CACF"}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-LOG-1 (PERSIST-LOG-001) — at-rest encryption for the durable transcript store.
|
|
3
|
-
*
|
|
4
|
-
* The daemon uses plain node:sqlite (no whole-DB encryption — SQLCipher is a native dep that
|
|
5
|
-
* compiles from source and is intentionally not dragged into the daemon). So the readable
|
|
6
|
-
* transcript plaintext is envelope-encrypted at the column level before it touches disk: each
|
|
7
|
-
* message blob is AES-256-GCM sealed with a dedicated per-DB transcript key.
|
|
8
|
-
*
|
|
9
|
-
* Key separation by design: the agent's Ed25519 identity key SIGNS (the KeyProvider interface is
|
|
10
|
-
* sign-only and never exposes the seed — good hygiene); a SEPARATE 32-byte symmetric key encrypts
|
|
11
|
-
* the transcript at rest. The key lives in a 0600 file beside the daemon DB and is generated once.
|
|
12
|
-
*
|
|
13
|
-
* Blob layout: iv(12) || ciphertext || authTag(16). GCM authenticates, so a tampered or truncated
|
|
14
|
-
* blob fails to decrypt (returns null) rather than yielding garbage plaintext.
|
|
15
|
-
*/
|
|
16
|
-
export declare class TranscriptCipher {
|
|
17
|
-
#private;
|
|
18
|
-
private constructor();
|
|
19
|
-
/**
|
|
20
|
-
* Load the transcript key from `keyPath`, or generate + persist a fresh one (0600) on first use.
|
|
21
|
-
* Atomic-ish create: write then the file is the durable key for this DB for all time.
|
|
22
|
-
*/
|
|
23
|
-
static loadOrCreate(keyPath: string): TranscriptCipher;
|
|
24
|
-
/** For tests: an in-memory cipher with a supplied (or random) key. */
|
|
25
|
-
static fromKey(key?: Buffer): TranscriptCipher;
|
|
26
|
-
/** Encrypt plaintext → iv || ciphertext || tag. */
|
|
27
|
-
encrypt(plaintext: Uint8Array): Uint8Array;
|
|
28
|
-
/** Decrypt an iv||ciphertext||tag blob. Returns null on any tamper / wrong key / malformed input. */
|
|
29
|
-
decrypt(blob: Uint8Array): Uint8Array | null;
|
|
30
|
-
}
|
|
31
|
-
//# sourceMappingURL=transcript-cipher.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transcript-cipher.d.ts","sourceRoot":"","sources":["../src/transcript-cipher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAUH,qBAAa,gBAAgB;;IAG3B,OAAO;IAIP;;;OAGG;IACH,MAAM,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB;IActD,sEAAsE;IACtE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,gBAAgB;IAI9C,mDAAmD;IACnD,OAAO,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU;IAQ1C,qGAAqG;IACrG,OAAO,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI;CAc7C"}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DOD-LOG-1 (PERSIST-LOG-001) — at-rest encryption for the durable transcript store.
|
|
3
|
-
*
|
|
4
|
-
* The daemon uses plain node:sqlite (no whole-DB encryption — SQLCipher is a native dep that
|
|
5
|
-
* compiles from source and is intentionally not dragged into the daemon). So the readable
|
|
6
|
-
* transcript plaintext is envelope-encrypted at the column level before it touches disk: each
|
|
7
|
-
* message blob is AES-256-GCM sealed with a dedicated per-DB transcript key.
|
|
8
|
-
*
|
|
9
|
-
* Key separation by design: the agent's Ed25519 identity key SIGNS (the KeyProvider interface is
|
|
10
|
-
* sign-only and never exposes the seed — good hygiene); a SEPARATE 32-byte symmetric key encrypts
|
|
11
|
-
* the transcript at rest. The key lives in a 0600 file beside the daemon DB and is generated once.
|
|
12
|
-
*
|
|
13
|
-
* Blob layout: iv(12) || ciphertext || authTag(16). GCM authenticates, so a tampered or truncated
|
|
14
|
-
* blob fails to decrypt (returns null) rather than yielding garbage plaintext.
|
|
15
|
-
*/
|
|
16
|
-
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
17
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
18
|
-
import { dirname } from "node:path";
|
|
19
|
-
const KEY_LEN = 32; // AES-256
|
|
20
|
-
const IV_LEN = 12; // GCM standard nonce
|
|
21
|
-
const TAG_LEN = 16;
|
|
22
|
-
export class TranscriptCipher {
|
|
23
|
-
#key;
|
|
24
|
-
constructor(key) {
|
|
25
|
-
this.#key = key;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Load the transcript key from `keyPath`, or generate + persist a fresh one (0600) on first use.
|
|
29
|
-
* Atomic-ish create: write then the file is the durable key for this DB for all time.
|
|
30
|
-
*/
|
|
31
|
-
static loadOrCreate(keyPath) {
|
|
32
|
-
if (existsSync(keyPath)) {
|
|
33
|
-
const key = readFileSync(keyPath);
|
|
34
|
-
if (key.length !== KEY_LEN) {
|
|
35
|
-
throw new Error(`transcript key at ${keyPath} is ${key.length} bytes, expected ${KEY_LEN}`);
|
|
36
|
-
}
|
|
37
|
-
return new TranscriptCipher(key);
|
|
38
|
-
}
|
|
39
|
-
const key = randomBytes(KEY_LEN);
|
|
40
|
-
mkdirSync(dirname(keyPath), { recursive: true });
|
|
41
|
-
writeFileSync(keyPath, key, { mode: 0o600 });
|
|
42
|
-
return new TranscriptCipher(key);
|
|
43
|
-
}
|
|
44
|
-
/** For tests: an in-memory cipher with a supplied (or random) key. */
|
|
45
|
-
static fromKey(key) {
|
|
46
|
-
return new TranscriptCipher(key ?? randomBytes(KEY_LEN));
|
|
47
|
-
}
|
|
48
|
-
/** Encrypt plaintext → iv || ciphertext || tag. */
|
|
49
|
-
encrypt(plaintext) {
|
|
50
|
-
const iv = randomBytes(IV_LEN);
|
|
51
|
-
const cipher = createCipheriv("aes-256-gcm", this.#key, iv);
|
|
52
|
-
const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
53
|
-
const tag = cipher.getAuthTag();
|
|
54
|
-
return Buffer.concat([iv, ct, tag]);
|
|
55
|
-
}
|
|
56
|
-
/** Decrypt an iv||ciphertext||tag blob. Returns null on any tamper / wrong key / malformed input. */
|
|
57
|
-
decrypt(blob) {
|
|
58
|
-
if (blob.length < IV_LEN + TAG_LEN)
|
|
59
|
-
return null;
|
|
60
|
-
const buf = Buffer.from(blob);
|
|
61
|
-
const iv = buf.subarray(0, IV_LEN);
|
|
62
|
-
const tag = buf.subarray(buf.length - TAG_LEN);
|
|
63
|
-
const ct = buf.subarray(IV_LEN, buf.length - TAG_LEN);
|
|
64
|
-
try {
|
|
65
|
-
const decipher = createDecipheriv("aes-256-gcm", this.#key, iv);
|
|
66
|
-
decipher.setAuthTag(tag);
|
|
67
|
-
return Uint8Array.from(Buffer.concat([decipher.update(ct), decipher.final()]));
|
|
68
|
-
}
|
|
69
|
-
catch {
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
//# sourceMappingURL=transcript-cipher.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"transcript-cipher.js","sourceRoot":"","sources":["../src/transcript-cipher.ts"],"names":[],"mappings":"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"}
|