@vex-chat/libvex 5.1.0 → 5.3.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/CLA.md +38 -0
- package/LICENSE-COMMERCIAL +10 -0
- package/LICENSING.md +15 -0
- package/README.md +8 -2
- package/dist/Client.d.ts +47 -3
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +998 -496
- package/dist/Client.js.map +1 -1
- package/dist/Storage.d.ts +5 -0
- package/dist/Storage.d.ts.map +1 -1
- package/dist/Storage.js +5 -0
- package/dist/Storage.js.map +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts +7 -2
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
- package/dist/__tests__/harness/memory-storage.js +44 -29
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/codec.d.ts +9 -9
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +17 -19
- package/dist/codec.js.map +1 -1
- package/dist/codecs.d.ts +5 -0
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +5 -0
- package/dist/codecs.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/keystore/memory.d.ts +5 -0
- package/dist/keystore/memory.d.ts.map +1 -1
- package/dist/keystore/memory.js +5 -0
- package/dist/keystore/memory.js.map +1 -1
- package/dist/keystore/node.d.ts +5 -0
- package/dist/keystore/node.d.ts.map +1 -1
- package/dist/keystore/node.js +16 -8
- package/dist/keystore/node.js.map +1 -1
- package/dist/preset/common.d.ts +5 -0
- package/dist/preset/common.d.ts.map +1 -1
- package/dist/preset/common.js +5 -0
- package/dist/preset/common.js.map +1 -1
- package/dist/preset/node.d.ts +5 -0
- package/dist/preset/node.d.ts.map +1 -1
- package/dist/preset/node.js +9 -1
- package/dist/preset/node.js.map +1 -1
- package/dist/preset/test.d.ts +5 -0
- package/dist/preset/test.d.ts.map +1 -1
- package/dist/preset/test.js +9 -1
- package/dist/preset/test.js.map +1 -1
- package/dist/storage/node/http-agents.d.ts +5 -0
- package/dist/storage/node/http-agents.d.ts.map +1 -1
- package/dist/storage/node/http-agents.js +5 -0
- package/dist/storage/node/http-agents.js.map +1 -1
- package/dist/storage/node.d.ts +6 -1
- package/dist/storage/node.d.ts.map +1 -1
- package/dist/storage/node.js +7 -4
- package/dist/storage/node.js.map +1 -1
- package/dist/storage/schema.d.ts +5 -0
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/schema.js +5 -0
- package/dist/storage/schema.js.map +1 -1
- package/dist/storage/sqlite.d.ts +22 -4
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +172 -98
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transport/types.d.ts +5 -0
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/transport/types.js +5 -0
- package/dist/transport/types.js.map +1 -1
- package/dist/transport/websocket.d.ts +5 -0
- package/dist/transport/websocket.d.ts.map +1 -1
- package/dist/transport/websocket.js +5 -0
- package/dist/transport/websocket.js.map +1 -1
- package/dist/types/crypto.d.ts +5 -0
- package/dist/types/crypto.d.ts.map +1 -1
- package/dist/types/crypto.js +3 -5
- package/dist/types/crypto.js.map +1 -1
- package/dist/types/identity.d.ts +5 -0
- package/dist/types/identity.d.ts.map +1 -1
- package/dist/types/identity.js +3 -2
- package/dist/types/identity.js.map +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +5 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/capitalize.d.ts +5 -0
- package/dist/utils/capitalize.d.ts.map +1 -1
- package/dist/utils/capitalize.js +5 -0
- package/dist/utils/capitalize.js.map +1 -1
- package/dist/utils/fipsMailExtra.d.ts +30 -0
- package/dist/utils/fipsMailExtra.d.ts.map +1 -0
- package/dist/utils/fipsMailExtra.js +114 -0
- package/dist/utils/fipsMailExtra.js.map +1 -0
- package/dist/utils/formatBytes.d.ts +5 -0
- package/dist/utils/formatBytes.d.ts.map +1 -1
- package/dist/utils/formatBytes.js +5 -0
- package/dist/utils/formatBytes.js.map +1 -1
- package/dist/utils/resolveAtRestAesKey.d.ts +13 -0
- package/dist/utils/resolveAtRestAesKey.d.ts.map +1 -0
- package/dist/utils/resolveAtRestAesKey.js +26 -0
- package/dist/utils/resolveAtRestAesKey.js.map +1 -0
- package/dist/utils/sqlSessionToCrypto.d.ts +5 -0
- package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -1
- package/dist/utils/sqlSessionToCrypto.js +5 -0
- package/dist/utils/sqlSessionToCrypto.js.map +1 -1
- package/dist/utils/uint8uuid.d.ts +5 -0
- package/dist/utils/uint8uuid.d.ts.map +1 -1
- package/dist/utils/uint8uuid.js +5 -0
- package/dist/utils/uint8uuid.js.map +1 -1
- package/package.json +10 -3
- package/src/Client.ts +1281 -642
- package/src/Storage.ts +6 -0
- package/src/__tests__/codec.test.ts +6 -0
- package/src/__tests__/harness/fixtures.ts +6 -0
- package/src/__tests__/harness/memory-storage.ts +72 -52
- package/src/__tests__/harness/platform-transports.ts +6 -0
- package/src/__tests__/harness/poison-node-imports.ts +6 -0
- package/src/__tests__/harness/shared-suite.ts +288 -124
- package/src/__tests__/platform-browser.test.ts +15 -1
- package/src/__tests__/platform-node.test.ts +17 -3
- package/src/codec.ts +21 -8
- package/src/codecs.ts +6 -0
- package/src/index.ts +6 -0
- package/src/keystore/memory.ts +6 -0
- package/src/keystore/node.ts +27 -13
- package/src/preset/common.ts +6 -0
- package/src/preset/node.ts +14 -1
- package/src/preset/test.ts +14 -1
- package/src/storage/node/http-agents.ts +6 -0
- package/src/storage/node.ts +11 -4
- package/src/storage/schema.ts +6 -0
- package/src/storage/sqlite.ts +208 -135
- package/src/transport/types.ts +6 -0
- package/src/transport/websocket.ts +6 -0
- package/src/types/crypto.ts +6 -0
- package/src/types/identity.ts +6 -0
- package/src/types/index.ts +6 -0
- package/src/utils/capitalize.ts +6 -0
- package/src/utils/fipsMailExtra.ts +164 -0
- package/src/utils/formatBytes.ts +6 -0
- package/src/utils/resolveAtRestAesKey.ts +39 -0
- package/src/utils/sqlSessionToCrypto.ts +6 -0
- package/src/utils/uint8uuid.ts +6 -0
package/src/storage/sqlite.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
import type { Message } from "../index.js";
|
|
2
8
|
import type { Storage } from "../Storage.js";
|
|
3
9
|
import type {
|
|
@@ -25,13 +31,14 @@ import type { Kysely } from "kysely";
|
|
|
25
31
|
* ExpoStorage) with a single implementation.
|
|
26
32
|
*/
|
|
27
33
|
import {
|
|
28
|
-
|
|
34
|
+
getCryptoProfile,
|
|
29
35
|
xBoxKeyPairFromSecret,
|
|
30
|
-
|
|
36
|
+
xBoxKeyPairFromSecretAsync,
|
|
31
37
|
xMakeNonce,
|
|
32
38
|
xSecretbox,
|
|
39
|
+
xSecretboxAsync,
|
|
33
40
|
xSecretboxOpen,
|
|
34
|
-
|
|
41
|
+
xSecretboxOpenAsync,
|
|
35
42
|
XUtils,
|
|
36
43
|
} from "@vex-chat/crypto";
|
|
37
44
|
|
|
@@ -40,26 +47,41 @@ import { EventEmitter } from "eventemitter3";
|
|
|
40
47
|
export class SqliteStorage extends EventEmitter implements Storage {
|
|
41
48
|
public ready = false;
|
|
42
49
|
private closing = false;
|
|
50
|
+
/** Shared across concurrent `init()` callers; `close()` awaits it before `destroy()`. */
|
|
51
|
+
private initInFlight: Promise<void> | null = null;
|
|
43
52
|
private readonly db: Kysely<ClientDatabase>;
|
|
44
|
-
|
|
53
|
+
/** 32-byte AES-256 (or nacl) key for local at-rest `secretbox` (see `XUtils.deriveLocalAtRestAesKey`). */
|
|
54
|
+
private readonly atRestAesKey: Uint8Array;
|
|
45
55
|
|
|
46
|
-
constructor(db: Kysely<ClientDatabase>,
|
|
56
|
+
constructor(db: Kysely<ClientDatabase>, atRestAesKey: Uint8Array) {
|
|
47
57
|
super();
|
|
48
58
|
this.db = db;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
xSignKeyPairFromSecret(XUtils.decodeHex(SK)),
|
|
52
|
-
);
|
|
53
|
-
if (!idKeys) {
|
|
54
|
-
throw new Error("Can't convert SK!");
|
|
59
|
+
if (atRestAesKey.length !== 32) {
|
|
60
|
+
throw new Error("SqliteStorage requires a 32-byte atRestAes key.");
|
|
55
61
|
}
|
|
56
|
-
this.
|
|
62
|
+
this.atRestAesKey = atRestAesKey;
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
60
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Read `closing` where TypeScript would incorrectly assume it cannot
|
|
69
|
+
* become true after an earlier guard (e.g. across `await`).
|
|
70
|
+
*/
|
|
71
|
+
private isClosingNow(): boolean {
|
|
72
|
+
return this.closing;
|
|
73
|
+
}
|
|
74
|
+
|
|
61
75
|
async close(): Promise<void> {
|
|
62
76
|
this.closing = true;
|
|
77
|
+
const pending = this.initInFlight;
|
|
78
|
+
if (pending) {
|
|
79
|
+
try {
|
|
80
|
+
await pending;
|
|
81
|
+
} catch {
|
|
82
|
+
// Schema init may have failed; still tear down the driver.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
63
85
|
await this.db.destroy();
|
|
64
86
|
}
|
|
65
87
|
|
|
@@ -114,7 +136,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
114
136
|
.orderBy("lastUsed", "desc")
|
|
115
137
|
.execute();
|
|
116
138
|
|
|
117
|
-
return rows.map((s) => this.
|
|
139
|
+
return await Promise.all(rows.map((s) => this.sessionRowToSQLAsync(s)));
|
|
118
140
|
}
|
|
119
141
|
|
|
120
142
|
async getDevice(deviceID: string): Promise<Device | null> {
|
|
@@ -143,7 +165,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
143
165
|
.orderBy("timestamp", "asc")
|
|
144
166
|
.execute();
|
|
145
167
|
|
|
146
|
-
return this.
|
|
168
|
+
return this.decryptMessagesAsync(messages);
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
async getMessageHistory(userID: string): Promise<Message[]> {
|
|
@@ -171,7 +193,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
171
193
|
.orderBy("timestamp", "asc")
|
|
172
194
|
.execute();
|
|
173
195
|
|
|
174
|
-
return this.
|
|
196
|
+
return this.decryptMessagesAsync(messages);
|
|
175
197
|
}
|
|
176
198
|
|
|
177
199
|
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
@@ -192,11 +214,13 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
192
214
|
if (!otkInfo) {
|
|
193
215
|
return null;
|
|
194
216
|
}
|
|
217
|
+
const rawSk = await this.unsealHex(otkInfo.privateKey);
|
|
195
218
|
return {
|
|
196
219
|
index: otkInfo.index,
|
|
197
|
-
keyPair:
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
keyPair:
|
|
221
|
+
getCryptoProfile() === "fips"
|
|
222
|
+
? await xBoxKeyPairFromSecretAsync(XUtils.decodeHex(rawSk))
|
|
223
|
+
: xBoxKeyPairFromSecret(XUtils.decodeHex(rawSk)),
|
|
200
224
|
signature: XUtils.decodeHex(otkInfo.signature),
|
|
201
225
|
};
|
|
202
226
|
}
|
|
@@ -213,11 +237,13 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
213
237
|
if (!preKeyInfo) {
|
|
214
238
|
return null;
|
|
215
239
|
}
|
|
240
|
+
const rawPk = await this.unsealHex(preKeyInfo.privateKey);
|
|
216
241
|
return {
|
|
217
242
|
index: preKeyInfo.index,
|
|
218
|
-
keyPair:
|
|
219
|
-
|
|
220
|
-
|
|
243
|
+
keyPair:
|
|
244
|
+
getCryptoProfile() === "fips"
|
|
245
|
+
? await xBoxKeyPairFromSecretAsync(XUtils.decodeHex(rawPk))
|
|
246
|
+
: xBoxKeyPairFromSecret(XUtils.decodeHex(rawPk)),
|
|
221
247
|
signature: XUtils.decodeHex(preKeyInfo.signature),
|
|
222
248
|
};
|
|
223
249
|
}
|
|
@@ -241,7 +267,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
241
267
|
return null;
|
|
242
268
|
}
|
|
243
269
|
|
|
244
|
-
return this.sqlToCrypto(this.
|
|
270
|
+
return this.sqlToCrypto(await this.sessionRowToSQLAsync(sessionRow));
|
|
245
271
|
}
|
|
246
272
|
|
|
247
273
|
async getSessionByPublicKey(
|
|
@@ -264,89 +290,97 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
264
290
|
return null;
|
|
265
291
|
}
|
|
266
292
|
|
|
267
|
-
return this.sqlToCrypto(this.
|
|
293
|
+
return this.sqlToCrypto(await this.sessionRowToSQLAsync(sessionRow));
|
|
268
294
|
}
|
|
269
295
|
|
|
270
296
|
async init(): Promise<void> {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
.createTable("messages")
|
|
274
|
-
.ifNotExists()
|
|
275
|
-
.addColumn("nonce", "text", (col) => col.primaryKey())
|
|
276
|
-
.addColumn("sender", "text")
|
|
277
|
-
.addColumn("recipient", "text")
|
|
278
|
-
.addColumn("group", "text")
|
|
279
|
-
.addColumn("mailID", "text")
|
|
280
|
-
.addColumn("message", "text")
|
|
281
|
-
.addColumn("direction", "text")
|
|
282
|
-
.addColumn("timestamp", "text")
|
|
283
|
-
.addColumn("decrypted", "integer")
|
|
284
|
-
.addColumn("forward", "integer")
|
|
285
|
-
.addColumn("authorID", "text")
|
|
286
|
-
.addColumn("readerID", "text")
|
|
287
|
-
.execute();
|
|
288
|
-
|
|
289
|
-
await this.db.schema
|
|
290
|
-
.createTable("devices")
|
|
291
|
-
.ifNotExists()
|
|
292
|
-
.addColumn("deviceID", "text", (col) => col.primaryKey())
|
|
293
|
-
.addColumn("owner", "text")
|
|
294
|
-
.addColumn("signKey", "text")
|
|
295
|
-
.addColumn("name", "text")
|
|
296
|
-
.addColumn("lastLogin", "text")
|
|
297
|
-
.addColumn("deleted", "integer")
|
|
298
|
-
.execute();
|
|
299
|
-
|
|
300
|
-
await this.db.schema
|
|
301
|
-
.createTable("sessions")
|
|
302
|
-
.ifNotExists()
|
|
303
|
-
.addColumn("sessionID", "text", (col) => col.primaryKey())
|
|
304
|
-
.addColumn("userID", "text")
|
|
305
|
-
.addColumn("deviceID", "text")
|
|
306
|
-
.addColumn("SK", "text", (col) => col.unique())
|
|
307
|
-
.addColumn("publicKey", "text")
|
|
308
|
-
.addColumn("fingerprint", "text")
|
|
309
|
-
.addColumn("mode", "text")
|
|
310
|
-
.addColumn("lastUsed", "text")
|
|
311
|
-
.addColumn("verified", "integer")
|
|
312
|
-
.execute();
|
|
313
|
-
|
|
314
|
-
await this.db.schema
|
|
315
|
-
.createTable("preKeys")
|
|
316
|
-
.ifNotExists()
|
|
317
|
-
.addColumn("index", "integer", (col) =>
|
|
318
|
-
col.primaryKey().autoIncrement(),
|
|
319
|
-
)
|
|
320
|
-
.addColumn("keyID", "text", (col) => col.unique())
|
|
321
|
-
.addColumn("userID", "text")
|
|
322
|
-
.addColumn("deviceID", "text")
|
|
323
|
-
.addColumn("privateKey", "text")
|
|
324
|
-
.addColumn("publicKey", "text")
|
|
325
|
-
.addColumn("signature", "text")
|
|
326
|
-
.execute();
|
|
327
|
-
|
|
328
|
-
await this.db.schema
|
|
329
|
-
.createTable("oneTimeKeys")
|
|
330
|
-
.ifNotExists()
|
|
331
|
-
.addColumn("index", "integer", (col) =>
|
|
332
|
-
col.primaryKey().autoIncrement(),
|
|
333
|
-
)
|
|
334
|
-
.addColumn("keyID", "text", (col) => col.unique())
|
|
335
|
-
.addColumn("userID", "text")
|
|
336
|
-
.addColumn("deviceID", "text")
|
|
337
|
-
.addColumn("privateKey", "text")
|
|
338
|
-
.addColumn("publicKey", "text")
|
|
339
|
-
.addColumn("signature", "text")
|
|
340
|
-
.execute();
|
|
341
|
-
|
|
342
|
-
this.ready = true;
|
|
343
|
-
this.emit("ready");
|
|
344
|
-
} catch (err: unknown) {
|
|
345
|
-
this.emit(
|
|
346
|
-
"error",
|
|
347
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
348
|
-
);
|
|
297
|
+
if (this.ready || this.closing) {
|
|
298
|
+
return;
|
|
349
299
|
}
|
|
300
|
+
this.initInFlight ??= (async () => {
|
|
301
|
+
try {
|
|
302
|
+
await this.db.schema
|
|
303
|
+
.createTable("messages")
|
|
304
|
+
.ifNotExists()
|
|
305
|
+
.addColumn("nonce", "text", (col) => col.primaryKey())
|
|
306
|
+
.addColumn("sender", "text")
|
|
307
|
+
.addColumn("recipient", "text")
|
|
308
|
+
.addColumn("group", "text")
|
|
309
|
+
.addColumn("mailID", "text")
|
|
310
|
+
.addColumn("message", "text")
|
|
311
|
+
.addColumn("direction", "text")
|
|
312
|
+
.addColumn("timestamp", "text")
|
|
313
|
+
.addColumn("decrypted", "integer")
|
|
314
|
+
.addColumn("forward", "integer")
|
|
315
|
+
.addColumn("authorID", "text")
|
|
316
|
+
.addColumn("readerID", "text")
|
|
317
|
+
.execute();
|
|
318
|
+
|
|
319
|
+
await this.db.schema
|
|
320
|
+
.createTable("devices")
|
|
321
|
+
.ifNotExists()
|
|
322
|
+
.addColumn("deviceID", "text", (col) => col.primaryKey())
|
|
323
|
+
.addColumn("owner", "text")
|
|
324
|
+
.addColumn("signKey", "text")
|
|
325
|
+
.addColumn("name", "text")
|
|
326
|
+
.addColumn("lastLogin", "text")
|
|
327
|
+
.addColumn("deleted", "integer")
|
|
328
|
+
.execute();
|
|
329
|
+
|
|
330
|
+
await this.db.schema
|
|
331
|
+
.createTable("sessions")
|
|
332
|
+
.ifNotExists()
|
|
333
|
+
.addColumn("sessionID", "text", (col) => col.primaryKey())
|
|
334
|
+
.addColumn("userID", "text")
|
|
335
|
+
.addColumn("deviceID", "text")
|
|
336
|
+
.addColumn("SK", "text", (col) => col.unique())
|
|
337
|
+
.addColumn("publicKey", "text")
|
|
338
|
+
.addColumn("fingerprint", "text")
|
|
339
|
+
.addColumn("mode", "text")
|
|
340
|
+
.addColumn("lastUsed", "text")
|
|
341
|
+
.addColumn("verified", "integer")
|
|
342
|
+
.execute();
|
|
343
|
+
|
|
344
|
+
await this.db.schema
|
|
345
|
+
.createTable("preKeys")
|
|
346
|
+
.ifNotExists()
|
|
347
|
+
.addColumn("index", "integer", (col) =>
|
|
348
|
+
col.primaryKey().autoIncrement(),
|
|
349
|
+
)
|
|
350
|
+
.addColumn("keyID", "text", (col) => col.unique())
|
|
351
|
+
.addColumn("userID", "text")
|
|
352
|
+
.addColumn("deviceID", "text")
|
|
353
|
+
.addColumn("privateKey", "text")
|
|
354
|
+
.addColumn("publicKey", "text")
|
|
355
|
+
.addColumn("signature", "text")
|
|
356
|
+
.execute();
|
|
357
|
+
|
|
358
|
+
await this.db.schema
|
|
359
|
+
.createTable("oneTimeKeys")
|
|
360
|
+
.ifNotExists()
|
|
361
|
+
.addColumn("index", "integer", (col) =>
|
|
362
|
+
col.primaryKey().autoIncrement(),
|
|
363
|
+
)
|
|
364
|
+
.addColumn("keyID", "text", (col) => col.unique())
|
|
365
|
+
.addColumn("userID", "text")
|
|
366
|
+
.addColumn("deviceID", "text")
|
|
367
|
+
.addColumn("privateKey", "text")
|
|
368
|
+
.addColumn("publicKey", "text")
|
|
369
|
+
.addColumn("signature", "text")
|
|
370
|
+
.execute();
|
|
371
|
+
|
|
372
|
+
this.ready = true;
|
|
373
|
+
this.emit("ready");
|
|
374
|
+
} catch (err: unknown) {
|
|
375
|
+
this.emit(
|
|
376
|
+
"error",
|
|
377
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
378
|
+
);
|
|
379
|
+
} finally {
|
|
380
|
+
this.initInFlight = null;
|
|
381
|
+
}
|
|
382
|
+
})();
|
|
383
|
+
await this.initInFlight;
|
|
350
384
|
}
|
|
351
385
|
|
|
352
386
|
async markSessionUsed(sessionID: string): Promise<void> {
|
|
@@ -412,18 +446,27 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
412
446
|
// ── Devices ──────────────────────────────────────────────────────────────
|
|
413
447
|
|
|
414
448
|
async saveMessage(message: Message): Promise<void> {
|
|
415
|
-
if (this.
|
|
449
|
+
if (this.isClosingNow()) {
|
|
416
450
|
return;
|
|
417
451
|
}
|
|
418
452
|
|
|
419
|
-
// Encrypt plaintext with
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
453
|
+
// Encrypt plaintext with at-rest key before saving to disk
|
|
454
|
+
const fips = getCryptoProfile() === "fips";
|
|
455
|
+
const ct = fips
|
|
456
|
+
? await xSecretboxAsync(
|
|
457
|
+
XUtils.decodeUTF8(message.message),
|
|
458
|
+
XUtils.decodeHex(message.nonce),
|
|
459
|
+
this.atRestAesKey,
|
|
460
|
+
)
|
|
461
|
+
: xSecretbox(
|
|
462
|
+
XUtils.decodeUTF8(message.message),
|
|
463
|
+
XUtils.decodeHex(message.nonce),
|
|
464
|
+
this.atRestAesKey,
|
|
465
|
+
);
|
|
466
|
+
if (this.isClosingNow()) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const encryptedMessage = XUtils.encodeHex(ct);
|
|
427
470
|
|
|
428
471
|
try {
|
|
429
472
|
await this.db
|
|
@@ -446,6 +489,8 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
446
489
|
} catch (err: unknown) {
|
|
447
490
|
if (this.isDuplicateError(err)) {
|
|
448
491
|
// duplicate nonce — ignore
|
|
492
|
+
} else if (this.isClosingNow() || this.isTornDownError(err)) {
|
|
493
|
+
// e.g. WS/mail still saving after `close()` destroyed the driver
|
|
449
494
|
} else {
|
|
450
495
|
throw err;
|
|
451
496
|
}
|
|
@@ -468,7 +513,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
468
513
|
const row = await this.db
|
|
469
514
|
.insertInto(table)
|
|
470
515
|
.values({
|
|
471
|
-
privateKey: this.sealHex(
|
|
516
|
+
privateKey: await this.sealHex(
|
|
472
517
|
XUtils.encodeHex(preKey.keyPair.secretKey),
|
|
473
518
|
),
|
|
474
519
|
publicKey: XUtils.encodeHex(preKey.keyPair.publicKey),
|
|
@@ -506,7 +551,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
506
551
|
mode: session.mode,
|
|
507
552
|
publicKey: session.publicKey,
|
|
508
553
|
sessionID: session.sessionID,
|
|
509
|
-
SK: this.sealHex(session.SK),
|
|
554
|
+
SK: await this.sealHex(session.SK),
|
|
510
555
|
userID: session.userID,
|
|
511
556
|
verified: session.verified ? 1 : 0,
|
|
512
557
|
})
|
|
@@ -522,28 +567,33 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
522
567
|
|
|
523
568
|
// ── Private helpers ──────────────────────────────────────────────────────
|
|
524
569
|
|
|
525
|
-
private
|
|
526
|
-
|
|
570
|
+
private async decryptMessagesAsync(
|
|
571
|
+
messages: MessageRow[],
|
|
572
|
+
): Promise<Message[]> {
|
|
573
|
+
const fips = getCryptoProfile() === "fips";
|
|
574
|
+
const out: Message[] = [];
|
|
575
|
+
for (const msg of messages) {
|
|
527
576
|
const decryptedFlag = msg.decrypted !== 0;
|
|
528
577
|
let plaintext = msg.message;
|
|
529
|
-
|
|
530
578
|
if (decryptedFlag) {
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
579
|
+
const cipher = XUtils.decodeHex(msg.message);
|
|
580
|
+
const nonce = XUtils.decodeHex(msg.nonce);
|
|
581
|
+
const decrypted = fips
|
|
582
|
+
? await xSecretboxOpenAsync(
|
|
583
|
+
cipher,
|
|
584
|
+
nonce,
|
|
585
|
+
this.atRestAesKey,
|
|
586
|
+
)
|
|
587
|
+
: xSecretboxOpen(cipher, nonce, this.atRestAesKey);
|
|
536
588
|
if (decrypted) {
|
|
537
589
|
plaintext = XUtils.encodeUTF8(decrypted);
|
|
538
590
|
} else {
|
|
539
591
|
throw new Error("Couldn't decrypt messages on disk!");
|
|
540
592
|
}
|
|
541
593
|
}
|
|
542
|
-
|
|
543
594
|
const direction =
|
|
544
595
|
msg.direction === "incoming" ? "incoming" : "outgoing";
|
|
545
|
-
|
|
546
|
-
return {
|
|
596
|
+
out.push({
|
|
547
597
|
authorID: msg.authorID,
|
|
548
598
|
decrypted: decryptedFlag,
|
|
549
599
|
direction,
|
|
@@ -556,8 +606,9 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
556
606
|
recipient: msg.recipient,
|
|
557
607
|
sender: msg.sender,
|
|
558
608
|
timestamp: msg.timestamp,
|
|
559
|
-
};
|
|
560
|
-
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
return out;
|
|
561
612
|
}
|
|
562
613
|
|
|
563
614
|
private deviceRowToDevice(row: DeviceRow): Device {
|
|
@@ -581,24 +632,43 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
581
632
|
return false;
|
|
582
633
|
}
|
|
583
634
|
|
|
635
|
+
/**
|
|
636
|
+
* After `close` runs, Kysely / better-sqlite3 can reject with this if a
|
|
637
|
+
* message handler is still in flight.
|
|
638
|
+
*/
|
|
639
|
+
private isTornDownError(err: unknown): boolean {
|
|
640
|
+
if (err instanceof Error) {
|
|
641
|
+
const m = err.message.toLowerCase();
|
|
642
|
+
return (
|
|
643
|
+
m.includes("driver has already been destroyed") ||
|
|
644
|
+
m.includes("connection is not open") ||
|
|
645
|
+
m.includes("database is closed")
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
|
|
584
651
|
/**
|
|
585
652
|
* Encrypt a hex-encoded secret for at-rest storage.
|
|
586
653
|
* Returns hex(nonce || ciphertext) where nonce is 24 random bytes.
|
|
587
654
|
*/
|
|
588
|
-
private sealHex(plainHex: string): string {
|
|
655
|
+
private async sealHex(plainHex: string): Promise<string> {
|
|
589
656
|
const nonce = xMakeNonce();
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
657
|
+
const fips = getCryptoProfile() === "fips";
|
|
658
|
+
const ct = fips
|
|
659
|
+
? await xSecretboxAsync(
|
|
660
|
+
XUtils.decodeHex(plainHex),
|
|
661
|
+
nonce,
|
|
662
|
+
this.atRestAesKey,
|
|
663
|
+
)
|
|
664
|
+
: xSecretbox(XUtils.decodeHex(plainHex), nonce, this.atRestAesKey);
|
|
595
665
|
const sealed = new Uint8Array(nonce.length + ct.length);
|
|
596
666
|
sealed.set(nonce);
|
|
597
667
|
sealed.set(ct, nonce.length);
|
|
598
668
|
return XUtils.encodeHex(sealed);
|
|
599
669
|
}
|
|
600
670
|
|
|
601
|
-
private
|
|
671
|
+
private async sessionRowToSQLAsync(row: SessionRow): Promise<SessionSQL> {
|
|
602
672
|
return {
|
|
603
673
|
deviceID: row.deviceID,
|
|
604
674
|
fingerprint: row.fingerprint,
|
|
@@ -606,7 +676,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
606
676
|
mode: row.mode === "initiator" ? "initiator" : "receiver",
|
|
607
677
|
publicKey: row.publicKey,
|
|
608
678
|
sessionID: row.sessionID,
|
|
609
|
-
SK: this.unsealHex(row.SK),
|
|
679
|
+
SK: await this.unsealHex(row.SK),
|
|
610
680
|
userID: row.userID,
|
|
611
681
|
verified: row.verified !== 0,
|
|
612
682
|
};
|
|
@@ -628,11 +698,14 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
628
698
|
* Decrypt a value produced by sealHex().
|
|
629
699
|
* Expects hex(nonce || ciphertext), returns the original hex string.
|
|
630
700
|
*/
|
|
631
|
-
private unsealHex(sealed: string): string {
|
|
701
|
+
private async unsealHex(sealed: string): Promise<string> {
|
|
632
702
|
const bytes = XUtils.decodeHex(sealed);
|
|
633
703
|
const nonce = bytes.slice(0, 24);
|
|
634
704
|
const ct = bytes.slice(24);
|
|
635
|
-
const
|
|
705
|
+
const fips = getCryptoProfile() === "fips";
|
|
706
|
+
const plain = fips
|
|
707
|
+
? await xSecretboxOpenAsync(ct, nonce, this.atRestAesKey)
|
|
708
|
+
: xSecretboxOpen(ct, nonce, this.atRestAesKey);
|
|
636
709
|
if (!plain) {
|
|
637
710
|
throw new Error("Failed to decrypt sealed column value.");
|
|
638
711
|
}
|
package/src/transport/types.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* Adapts the standard WebSocket API (addEventListener/MessageEvent) to the
|
|
3
9
|
* EventEmitter-style .on()/.off() interface used internally by Client.
|
package/src/types/crypto.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* SDK-internal crypto types. These were moved from `@vex-chat/types`
|
|
3
9
|
* because they are only used by the SDK, never by the server.
|
package/src/types/identity.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
1
7
|
/**
|
|
2
8
|
* SDK credential storage types. Moved from `@vex-chat/types`
|
|
3
9
|
* because only the SDK and app consumers use them.
|
package/src/types/index.ts
CHANGED