@vex-chat/libvex 1.1.0 → 4.0.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/README.md +104 -41
- package/dist/Client.d.ts +473 -560
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +1486 -1551
- package/dist/Client.js.map +1 -1
- package/dist/Storage.d.ts +111 -0
- package/dist/Storage.d.ts.map +1 -0
- package/dist/Storage.js +2 -0
- package/dist/Storage.js.map +1 -0
- package/dist/__tests__/harness/memory-storage.d.ts +29 -27
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -0
- package/dist/__tests__/harness/memory-storage.js +120 -109
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/codec.d.ts +44 -0
- package/dist/codec.d.ts.map +1 -0
- package/dist/codec.js +51 -0
- package/dist/codec.js.map +1 -0
- package/dist/codecs.d.ts +201 -0
- package/dist/codecs.d.ts.map +1 -0
- package/dist/codecs.js +67 -0
- package/dist/codecs.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keystore/memory.d.ts +5 -4
- package/dist/keystore/memory.d.ts.map +1 -0
- package/dist/keystore/memory.js +9 -7
- package/dist/keystore/memory.js.map +1 -1
- package/dist/keystore/node.d.ts +8 -6
- package/dist/keystore/node.d.ts.map +1 -0
- package/dist/keystore/node.js +47 -22
- package/dist/keystore/node.js.map +1 -1
- package/dist/preset/common.d.ts +7 -0
- package/dist/preset/common.d.ts.map +1 -0
- package/dist/preset/common.js +2 -0
- package/dist/preset/common.js.map +1 -0
- package/dist/preset/node.d.ts +4 -7
- package/dist/preset/node.d.ts.map +1 -0
- package/dist/preset/node.js +4 -11
- package/dist/preset/node.js.map +1 -1
- package/dist/preset/test.d.ts +4 -5
- package/dist/preset/test.d.ts.map +1 -0
- package/dist/preset/test.js +3 -20
- package/dist/preset/test.js.map +1 -1
- package/dist/storage/node.d.ts +3 -3
- package/dist/storage/node.d.ts.map +1 -0
- package/dist/storage/node.js +4 -10
- package/dist/storage/node.js.map +1 -1
- package/dist/storage/schema.d.ts +55 -54
- package/dist/storage/schema.d.ts.map +1 -0
- package/dist/storage/sqlite.d.ts +41 -28
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +339 -297
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/transport/types.d.ts +17 -16
- package/dist/transport/types.d.ts.map +1 -0
- package/dist/transport/websocket.d.ts +26 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +83 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types/crypto.d.ts +38 -0
- package/dist/types/crypto.d.ts.map +1 -0
- package/dist/types/crypto.js +9 -0
- package/dist/types/crypto.js.map +1 -0
- package/dist/types/identity.d.ts +22 -0
- package/dist/types/identity.d.ts.map +1 -0
- package/dist/types/identity.js +6 -0
- package/dist/types/identity.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/capitalize.d.ts +1 -0
- package/dist/utils/capitalize.d.ts.map +1 -0
- package/dist/utils/formatBytes.d.ts +1 -0
- package/dist/utils/formatBytes.d.ts.map +1 -0
- package/dist/utils/formatBytes.js +3 -1
- package/dist/utils/formatBytes.js.map +1 -1
- package/dist/utils/sqlSessionToCrypto.d.ts +4 -2
- package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -0
- package/dist/utils/sqlSessionToCrypto.js +5 -5
- package/dist/utils/sqlSessionToCrypto.js.map +1 -1
- package/dist/utils/uint8uuid.d.ts +1 -4
- package/dist/utils/uint8uuid.d.ts.map +1 -0
- package/dist/utils/uint8uuid.js +1 -7
- package/dist/utils/uint8uuid.js.map +1 -1
- package/package.json +74 -91
- package/src/Client.ts +3086 -0
- package/{dist/IStorage.d.ts → src/Storage.ts} +70 -62
- package/src/__tests__/codec.test.ts +256 -0
- package/src/__tests__/ghost.png +0 -0
- package/src/__tests__/harness/fixtures.ts +22 -0
- package/src/__tests__/harness/memory-storage.ts +254 -0
- package/src/__tests__/harness/platform-transports.ts +4 -0
- package/src/__tests__/harness/poison-node-imports.ts +107 -0
- package/src/__tests__/harness/shared-suite.ts +426 -0
- package/src/__tests__/platform-browser.test.ts +14 -0
- package/src/__tests__/platform-node.test.ts +9 -0
- package/src/__tests__/triggered.png +0 -0
- package/src/codec.ts +68 -0
- package/src/codecs.ts +101 -0
- package/src/index.ts +40 -0
- package/src/keystore/memory.ts +30 -0
- package/src/keystore/node.ts +102 -0
- package/src/preset/common.ts +7 -0
- package/src/preset/node.ts +18 -0
- package/src/preset/test.ts +20 -0
- package/src/storage/node.ts +22 -0
- package/src/storage/schema.ts +94 -0
- package/src/storage/sqlite.ts +655 -0
- package/src/transport/types.ts +22 -0
- package/src/transport/websocket.ts +106 -0
- package/src/types/crypto.ts +42 -0
- package/src/types/identity.ts +23 -0
- package/src/types/index.ts +9 -0
- package/src/utils/capitalize.ts +6 -0
- package/src/utils/formatBytes.ts +15 -0
- package/src/utils/sqlSessionToCrypto.ts +16 -0
- package/src/utils/uint8uuid.ts +7 -0
- package/dist/IStorage.js +0 -2
- package/dist/IStorage.js.map +0 -1
- package/dist/keystore/types.d.ts +0 -4
- package/dist/keystore/types.js +0 -2
- package/dist/keystore/types.js.map +0 -1
- package/dist/preset/expo.d.ts +0 -2
- package/dist/preset/expo.js +0 -39
- package/dist/preset/expo.js.map +0 -1
- package/dist/preset/tauri.d.ts +0 -2
- package/dist/preset/tauri.js +0 -36
- package/dist/preset/tauri.js.map +0 -1
- package/dist/preset/types.d.ts +0 -14
- package/dist/preset/types.js +0 -2
- package/dist/preset/types.js.map +0 -1
- package/dist/storage/expo.d.ts +0 -3
- package/dist/storage/expo.js +0 -18
- package/dist/storage/expo.js.map +0 -1
- package/dist/storage/tauri.d.ts +0 -3
- package/dist/storage/tauri.js +0 -21
- package/dist/storage/tauri.js.map +0 -1
- package/dist/transport/browser.d.ts +0 -17
- package/dist/transport/browser.js +0 -56
- package/dist/transport/browser.js.map +0 -1
- package/dist/utils/constants.d.ts +0 -8
- package/dist/utils/constants.js +0 -9
- package/dist/utils/constants.js.map +0 -1
- package/dist/utils/createLogger.d.ts +0 -5
- package/dist/utils/createLogger.js +0 -27
- package/dist/utils/createLogger.js.map +0 -1
package/dist/storage/sqlite.js
CHANGED
|
@@ -8,28 +8,192 @@
|
|
|
8
8
|
* This replaces three separate storage classes (Storage.ts, TauriStorage,
|
|
9
9
|
* ExpoStorage) with a single implementation.
|
|
10
10
|
*/
|
|
11
|
-
import { XKeyConvert, XUtils } from "@vex-chat/crypto";
|
|
11
|
+
import { xBoxKeyPairFromSecret, XKeyConvert, xMakeNonce, xSecretbox, xSecretboxOpen, xSignKeyPairFromSecret, XUtils, } from "@vex-chat/crypto";
|
|
12
12
|
import { EventEmitter } from "eventemitter3";
|
|
13
|
-
import nacl from "tweetnacl";
|
|
14
13
|
export class SqliteStorage extends EventEmitter {
|
|
15
14
|
ready = false;
|
|
16
15
|
closing = false;
|
|
17
16
|
db;
|
|
18
|
-
log;
|
|
19
17
|
idKeys;
|
|
20
|
-
constructor(db, SK
|
|
18
|
+
constructor(db, SK) {
|
|
21
19
|
super();
|
|
22
20
|
this.db = db;
|
|
23
|
-
|
|
24
|
-
const idKeys = XKeyConvert.convertKeyPair(nacl.sign.keyPair.fromSecretKey(XUtils.decodeHex(SK)));
|
|
21
|
+
const idKeys = XKeyConvert.convertKeyPair(xSignKeyPairFromSecret(XUtils.decodeHex(SK)));
|
|
25
22
|
if (!idKeys) {
|
|
26
23
|
throw new Error("Can't convert SK!");
|
|
27
24
|
}
|
|
28
25
|
this.idKeys = idKeys;
|
|
29
26
|
}
|
|
30
27
|
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
28
|
+
async close() {
|
|
29
|
+
this.closing = true;
|
|
30
|
+
await this.db.destroy();
|
|
31
|
+
}
|
|
32
|
+
async deleteHistory(channelOrUserID) {
|
|
33
|
+
await this.db
|
|
34
|
+
.deleteFrom("messages")
|
|
35
|
+
.where((eb) => eb.or([
|
|
36
|
+
eb("group", "=", channelOrUserID),
|
|
37
|
+
eb.and([
|
|
38
|
+
eb("group", "is", null),
|
|
39
|
+
eb("authorID", "=", channelOrUserID),
|
|
40
|
+
]),
|
|
41
|
+
eb.and([
|
|
42
|
+
eb("group", "is", null),
|
|
43
|
+
eb("readerID", "=", channelOrUserID),
|
|
44
|
+
]),
|
|
45
|
+
]))
|
|
46
|
+
.execute();
|
|
47
|
+
}
|
|
48
|
+
// ── Messages ─────────────────────────────────────────────────────────────
|
|
49
|
+
async deleteMessage(mailID) {
|
|
50
|
+
if (this.closing) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await this.db
|
|
54
|
+
.deleteFrom("messages")
|
|
55
|
+
.where("mailID", "=", mailID)
|
|
56
|
+
.execute();
|
|
57
|
+
}
|
|
58
|
+
async deleteOneTimeKey(index) {
|
|
59
|
+
if (this.closing) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await this.db
|
|
63
|
+
.deleteFrom("oneTimeKeys")
|
|
64
|
+
.where("index", "=", index)
|
|
65
|
+
.execute();
|
|
66
|
+
}
|
|
67
|
+
async getAllSessions() {
|
|
68
|
+
if (this.closing) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const rows = await this.db
|
|
72
|
+
.selectFrom("sessions")
|
|
73
|
+
.selectAll()
|
|
74
|
+
.orderBy("lastUsed", "desc")
|
|
75
|
+
.execute();
|
|
76
|
+
return rows.map((s) => this.sessionRowToSQL(s));
|
|
77
|
+
}
|
|
78
|
+
async getDevice(deviceID) {
|
|
79
|
+
const rows = await this.db
|
|
80
|
+
.selectFrom("devices")
|
|
81
|
+
.selectAll()
|
|
82
|
+
.where("deviceID", "=", deviceID)
|
|
83
|
+
.execute();
|
|
84
|
+
const row = rows[0];
|
|
85
|
+
if (!row) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return this.deviceRowToDevice(row);
|
|
89
|
+
}
|
|
90
|
+
async getGroupHistory(channelID) {
|
|
91
|
+
if (this.closing) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const messages = await this.db
|
|
95
|
+
.selectFrom("messages")
|
|
96
|
+
.selectAll()
|
|
97
|
+
.where("group", "=", channelID)
|
|
98
|
+
.orderBy("timestamp", "asc")
|
|
99
|
+
.execute();
|
|
100
|
+
return this.decryptMessages(messages);
|
|
101
|
+
}
|
|
102
|
+
async getMessageHistory(userID) {
|
|
103
|
+
if (this.closing) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const messages = await this.db
|
|
107
|
+
.selectFrom("messages")
|
|
108
|
+
.selectAll()
|
|
109
|
+
.where((eb) => eb.or([
|
|
110
|
+
eb.and([
|
|
111
|
+
eb("direction", "=", "incoming"),
|
|
112
|
+
eb("authorID", "=", userID),
|
|
113
|
+
eb("group", "is", null),
|
|
114
|
+
]),
|
|
115
|
+
eb.and([
|
|
116
|
+
eb("direction", "=", "outgoing"),
|
|
117
|
+
eb("readerID", "=", userID),
|
|
118
|
+
eb("group", "is", null),
|
|
119
|
+
]),
|
|
120
|
+
]))
|
|
121
|
+
.orderBy("timestamp", "asc")
|
|
122
|
+
.execute();
|
|
123
|
+
return this.decryptMessages(messages);
|
|
124
|
+
}
|
|
125
|
+
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
126
|
+
async getOneTimeKey(index) {
|
|
127
|
+
await this.untilReady();
|
|
128
|
+
if (this.closing) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const rows = await this.db
|
|
132
|
+
.selectFrom("oneTimeKeys")
|
|
133
|
+
.selectAll()
|
|
134
|
+
.where("index", "=", index)
|
|
135
|
+
.execute();
|
|
136
|
+
const otkInfo = rows[0];
|
|
137
|
+
if (!otkInfo) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
index: otkInfo.index,
|
|
142
|
+
keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(this.unsealHex(otkInfo.privateKey))),
|
|
143
|
+
signature: XUtils.decodeHex(otkInfo.signature),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
async getPreKeys() {
|
|
147
|
+
await this.untilReady();
|
|
148
|
+
if (this.closing) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const rows = await this.db.selectFrom("preKeys").selectAll().execute();
|
|
152
|
+
const preKeyInfo = rows[0];
|
|
153
|
+
if (!preKeyInfo) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
index: preKeyInfo.index,
|
|
158
|
+
keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(this.unsealHex(preKeyInfo.privateKey))),
|
|
159
|
+
signature: XUtils.decodeHex(preKeyInfo.signature),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async getSessionByDeviceID(deviceID) {
|
|
163
|
+
if (this.closing) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const rows = await this.db
|
|
167
|
+
.selectFrom("sessions")
|
|
168
|
+
.selectAll()
|
|
169
|
+
.where("deviceID", "=", deviceID)
|
|
170
|
+
.orderBy("lastUsed", "desc")
|
|
171
|
+
.limit(1)
|
|
172
|
+
.execute();
|
|
173
|
+
const sessionRow = rows[0];
|
|
174
|
+
if (!sessionRow) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
return this.sqlToCrypto(this.sessionRowToSQL(sessionRow));
|
|
178
|
+
}
|
|
179
|
+
async getSessionByPublicKey(publicKey) {
|
|
180
|
+
if (this.closing) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
const hex = XUtils.encodeHex(publicKey);
|
|
184
|
+
const rows = await this.db
|
|
185
|
+
.selectFrom("sessions")
|
|
186
|
+
.selectAll()
|
|
187
|
+
.where("publicKey", "=", hex)
|
|
188
|
+
.limit(1)
|
|
189
|
+
.execute();
|
|
190
|
+
const sessionRow = rows[0];
|
|
191
|
+
if (!sessionRow) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return this.sqlToCrypto(this.sessionRowToSQL(sessionRow));
|
|
195
|
+
}
|
|
31
196
|
async init() {
|
|
32
|
-
this.log.info("Initializing database tables.");
|
|
33
197
|
try {
|
|
34
198
|
await this.db.schema
|
|
35
199
|
.createTable("messages")
|
|
@@ -96,124 +260,22 @@ export class SqliteStorage extends EventEmitter {
|
|
|
96
260
|
this.emit("ready");
|
|
97
261
|
}
|
|
98
262
|
catch (err) {
|
|
99
|
-
this.emit("error", err);
|
|
263
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
100
264
|
}
|
|
101
265
|
}
|
|
102
|
-
async
|
|
103
|
-
this.closing = true;
|
|
104
|
-
this.log.info("Closing database.");
|
|
105
|
-
await this.db.destroy();
|
|
106
|
-
}
|
|
107
|
-
// ── Messages ─────────────────────────────────────────────────────────────
|
|
108
|
-
async saveMessage(message) {
|
|
109
|
-
if (this.closing) {
|
|
110
|
-
this.log.warn("Database is closing, saveMessage() will not complete.");
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
// Encrypt plaintext with our idkey before saving to disk
|
|
114
|
-
const encryptedMessage = XUtils.encodeHex(nacl.secretbox(XUtils.decodeUTF8(message.message), XUtils.decodeHex(message.nonce), this.idKeys.secretKey));
|
|
115
|
-
try {
|
|
116
|
-
await this.db
|
|
117
|
-
.insertInto("messages")
|
|
118
|
-
.values({
|
|
119
|
-
nonce: message.nonce,
|
|
120
|
-
sender: message.sender,
|
|
121
|
-
recipient: message.recipient,
|
|
122
|
-
group: message.group ?? null,
|
|
123
|
-
mailID: message.mailID,
|
|
124
|
-
message: encryptedMessage,
|
|
125
|
-
direction: message.direction,
|
|
126
|
-
timestamp: message.timestamp instanceof Date
|
|
127
|
-
? message.timestamp.toISOString()
|
|
128
|
-
: String(message.timestamp),
|
|
129
|
-
decrypted: message.decrypted ? 1 : 0,
|
|
130
|
-
forward: message.forward ? 1 : 0,
|
|
131
|
-
authorID: message.authorID,
|
|
132
|
-
readerID: message.readerID,
|
|
133
|
-
})
|
|
134
|
-
.execute();
|
|
135
|
-
}
|
|
136
|
-
catch (err) {
|
|
137
|
-
if (this.closing)
|
|
138
|
-
return;
|
|
139
|
-
if (err?.errno === 19 || err?.message?.includes("UNIQUE")) {
|
|
140
|
-
this.log.warn("Duplicate nonce in message table.");
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
throw err;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
async deleteMessage(mailID) {
|
|
266
|
+
async markSessionUsed(sessionID) {
|
|
148
267
|
if (this.closing) {
|
|
149
|
-
this.log.warn("Database is closing, deleteMessage() will not complete.");
|
|
150
268
|
return;
|
|
151
269
|
}
|
|
152
270
|
await this.db
|
|
153
|
-
.
|
|
154
|
-
.
|
|
155
|
-
.
|
|
156
|
-
}
|
|
157
|
-
async getMessageHistory(userID) {
|
|
158
|
-
if (this.closing) {
|
|
159
|
-
this.log.warn("Database is closing, getMessageHistory() will not complete.");
|
|
160
|
-
return [];
|
|
161
|
-
}
|
|
162
|
-
const messages = await this.db
|
|
163
|
-
.selectFrom("messages")
|
|
164
|
-
.selectAll()
|
|
165
|
-
.where((eb) => eb.or([
|
|
166
|
-
eb.and([
|
|
167
|
-
eb("direction", "=", "incoming"),
|
|
168
|
-
eb("authorID", "=", userID),
|
|
169
|
-
eb("group", "is", null),
|
|
170
|
-
]),
|
|
171
|
-
eb.and([
|
|
172
|
-
eb("direction", "=", "outgoing"),
|
|
173
|
-
eb("readerID", "=", userID),
|
|
174
|
-
eb("group", "is", null),
|
|
175
|
-
]),
|
|
176
|
-
]))
|
|
177
|
-
.orderBy("timestamp", "asc")
|
|
178
|
-
.execute();
|
|
179
|
-
return this.decryptMessages(messages);
|
|
180
|
-
}
|
|
181
|
-
async getGroupHistory(channelID) {
|
|
182
|
-
if (this.closing) {
|
|
183
|
-
this.log.warn("Database is closing, getGroupHistory() will not complete.");
|
|
184
|
-
return [];
|
|
185
|
-
}
|
|
186
|
-
const messages = await this.db
|
|
187
|
-
.selectFrom("messages")
|
|
188
|
-
.selectAll()
|
|
189
|
-
.where("group", "=", channelID)
|
|
190
|
-
.orderBy("timestamp", "asc")
|
|
191
|
-
.execute();
|
|
192
|
-
return this.decryptMessages(messages);
|
|
193
|
-
}
|
|
194
|
-
async deleteHistory(channelOrUserID, _olderThan) {
|
|
195
|
-
await this.db
|
|
196
|
-
.deleteFrom("messages")
|
|
197
|
-
.where((eb) => eb.or([
|
|
198
|
-
eb("group", "=", channelOrUserID),
|
|
199
|
-
eb.and([
|
|
200
|
-
eb("group", "is", null),
|
|
201
|
-
eb("authorID", "=", channelOrUserID),
|
|
202
|
-
]),
|
|
203
|
-
eb.and([
|
|
204
|
-
eb("group", "is", null),
|
|
205
|
-
eb("readerID", "=", channelOrUserID),
|
|
206
|
-
]),
|
|
207
|
-
]))
|
|
271
|
+
.updateTable("sessions")
|
|
272
|
+
.set({ lastUsed: new Date(Date.now()).toISOString() })
|
|
273
|
+
.where("sessionID", "=", sessionID)
|
|
208
274
|
.execute();
|
|
209
275
|
}
|
|
210
|
-
|
|
211
|
-
await this.db.deleteFrom("messages").execute();
|
|
212
|
-
}
|
|
213
|
-
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
276
|
+
// ── PreKeys / OneTimeKeys ────────────────────────────────────────────────
|
|
214
277
|
async markSessionVerified(sessionID) {
|
|
215
278
|
if (this.closing) {
|
|
216
|
-
this.log.warn("Database is closing, markSessionVerified() will not complete.");
|
|
217
279
|
return;
|
|
218
280
|
}
|
|
219
281
|
await this.db
|
|
@@ -222,263 +284,243 @@ export class SqliteStorage extends EventEmitter {
|
|
|
222
284
|
.where("sessionID", "=", sessionID)
|
|
223
285
|
.execute();
|
|
224
286
|
}
|
|
225
|
-
async
|
|
226
|
-
|
|
227
|
-
this.log.warn("Database is closing, markSessionUsed() will not complete.");
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
await this.db
|
|
231
|
-
.updateTable("sessions")
|
|
232
|
-
.set({ lastUsed: new Date(Date.now()).toISOString() })
|
|
233
|
-
.where("sessionID", "=", sessionID)
|
|
234
|
-
.execute();
|
|
287
|
+
async purgeHistory() {
|
|
288
|
+
await this.db.deleteFrom("messages").execute();
|
|
235
289
|
}
|
|
236
|
-
async
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const hex = XUtils.encodeHex(publicKey);
|
|
242
|
-
const rows = await this.db
|
|
243
|
-
.selectFrom("sessions")
|
|
244
|
-
.selectAll()
|
|
245
|
-
.where("publicKey", "=", hex)
|
|
246
|
-
.limit(1)
|
|
247
|
-
.execute();
|
|
248
|
-
if (rows.length === 0) {
|
|
249
|
-
this.log.warn(`getSessionByPublicKey(${hex}) => ${JSON.stringify(null)}`);
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
return this.sqlToCrypto(rows[0]);
|
|
290
|
+
async purgeKeyData() {
|
|
291
|
+
await this.db.deleteFrom("sessions").execute();
|
|
292
|
+
await this.db.deleteFrom("oneTimeKeys").execute();
|
|
293
|
+
await this.db.deleteFrom("preKeys").execute();
|
|
294
|
+
await this.db.deleteFrom("messages").execute();
|
|
253
295
|
}
|
|
254
|
-
async
|
|
296
|
+
async saveDevice(device) {
|
|
255
297
|
if (this.closing) {
|
|
256
|
-
|
|
257
|
-
return [];
|
|
298
|
+
return;
|
|
258
299
|
}
|
|
259
|
-
|
|
260
|
-
.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
this.log.warn("Database is closing, getSessionByDeviceID() will not complete.");
|
|
272
|
-
return null;
|
|
300
|
+
try {
|
|
301
|
+
await this.db
|
|
302
|
+
.insertInto("devices")
|
|
303
|
+
.values({
|
|
304
|
+
deleted: device.deleted ? 1 : 0,
|
|
305
|
+
deviceID: device.deviceID,
|
|
306
|
+
lastLogin: device.lastLogin,
|
|
307
|
+
name: device.name,
|
|
308
|
+
owner: device.owner,
|
|
309
|
+
signKey: device.signKey,
|
|
310
|
+
})
|
|
311
|
+
.execute();
|
|
273
312
|
}
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (rows.length === 0) {
|
|
282
|
-
this.log.debug("getSession() => " + JSON.stringify(null));
|
|
283
|
-
return null;
|
|
313
|
+
catch (err) {
|
|
314
|
+
if (this.isDuplicateError(err)) {
|
|
315
|
+
// duplicate deviceID — ignore
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
throw err;
|
|
319
|
+
}
|
|
284
320
|
}
|
|
285
|
-
return this.sqlToCrypto(rows[0]);
|
|
286
321
|
}
|
|
287
|
-
|
|
322
|
+
// ── Devices ──────────────────────────────────────────────────────────────
|
|
323
|
+
async saveMessage(message) {
|
|
288
324
|
if (this.closing) {
|
|
289
|
-
this.log.warn("Database is closing, saveSession() will not complete.");
|
|
290
325
|
return;
|
|
291
326
|
}
|
|
327
|
+
// Encrypt plaintext with our idkey before saving to disk
|
|
328
|
+
const encryptedMessage = XUtils.encodeHex(xSecretbox(XUtils.decodeUTF8(message.message), XUtils.decodeHex(message.nonce), this.idKeys.secretKey));
|
|
292
329
|
try {
|
|
293
330
|
await this.db
|
|
294
|
-
.insertInto("
|
|
331
|
+
.insertInto("messages")
|
|
295
332
|
.values({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
333
|
+
authorID: message.authorID,
|
|
334
|
+
decrypted: message.decrypted ? 1 : 0,
|
|
335
|
+
direction: message.direction,
|
|
336
|
+
forward: message.forward ? 1 : 0,
|
|
337
|
+
group: message.group ?? null,
|
|
338
|
+
mailID: message.mailID,
|
|
339
|
+
message: encryptedMessage,
|
|
340
|
+
nonce: message.nonce,
|
|
341
|
+
readerID: message.readerID,
|
|
342
|
+
recipient: message.recipient,
|
|
343
|
+
sender: message.sender,
|
|
344
|
+
timestamp: message.timestamp,
|
|
307
345
|
})
|
|
308
346
|
.execute();
|
|
309
347
|
}
|
|
310
348
|
catch (err) {
|
|
311
|
-
if (err
|
|
312
|
-
|
|
349
|
+
if (this.isDuplicateError(err)) {
|
|
350
|
+
// duplicate nonce — ignore
|
|
313
351
|
}
|
|
314
352
|
else {
|
|
315
353
|
throw err;
|
|
316
354
|
}
|
|
317
355
|
}
|
|
318
356
|
}
|
|
319
|
-
// ── PreKeys / OneTimeKeys ────────────────────────────────────────────────
|
|
320
357
|
async savePreKeys(preKeys, oneTime) {
|
|
321
358
|
await this.untilReady();
|
|
322
359
|
if (this.closing) {
|
|
323
|
-
this.log.warn("Database is closing, savePreKeys() will not complete.");
|
|
324
360
|
return [];
|
|
325
361
|
}
|
|
326
362
|
const table = oneTime ? "oneTimeKeys" : "preKeys";
|
|
327
|
-
const
|
|
363
|
+
const saved = [];
|
|
328
364
|
for (const preKey of preKeys) {
|
|
329
|
-
const
|
|
365
|
+
const row = await this.db
|
|
330
366
|
.insertInto(table)
|
|
331
367
|
.values({
|
|
332
|
-
privateKey: XUtils.encodeHex(preKey.keyPair.secretKey),
|
|
368
|
+
privateKey: this.sealHex(XUtils.encodeHex(preKey.keyPair.secretKey)),
|
|
333
369
|
publicKey: XUtils.encodeHex(preKey.keyPair.publicKey),
|
|
334
370
|
signature: XUtils.encodeHex(preKey.signature),
|
|
335
371
|
})
|
|
336
|
-
.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.
|
|
346
|
-
return rows.map((key) => {
|
|
347
|
-
delete key.privateKey;
|
|
348
|
-
return key;
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
async getPreKeys() {
|
|
352
|
-
await this.untilReady();
|
|
353
|
-
if (this.closing) {
|
|
354
|
-
this.log.warn("Database is closing, getPreKeys() will not complete.");
|
|
355
|
-
return null;
|
|
356
|
-
}
|
|
357
|
-
const rows = await this.db.selectFrom("preKeys").selectAll().execute();
|
|
358
|
-
if (rows.length === 0) {
|
|
359
|
-
this.log.debug("getPreKeys() => " + JSON.stringify(null));
|
|
360
|
-
return null;
|
|
361
|
-
}
|
|
362
|
-
const preKeyInfo = rows[0];
|
|
363
|
-
return {
|
|
364
|
-
keyPair: nacl.box.keyPair.fromSecretKey(XUtils.decodeHex(preKeyInfo.privateKey)),
|
|
365
|
-
signature: XUtils.decodeHex(preKeyInfo.signature),
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
async getOneTimeKey(index) {
|
|
369
|
-
await this.untilReady();
|
|
370
|
-
if (this.closing) {
|
|
371
|
-
this.log.warn("Database is closing, getOneTimeKey() will not complete.");
|
|
372
|
-
return null;
|
|
373
|
-
}
|
|
374
|
-
const rows = await this.db
|
|
375
|
-
.selectFrom("oneTimeKeys")
|
|
376
|
-
.selectAll()
|
|
377
|
-
.where("index", "=", index)
|
|
378
|
-
.execute();
|
|
379
|
-
if (rows.length === 0) {
|
|
380
|
-
this.log.debug("getOneTimeKey() => " + JSON.stringify(null));
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
const otkInfo = rows[0];
|
|
384
|
-
return {
|
|
385
|
-
keyPair: nacl.box.keyPair.fromSecretKey(XUtils.decodeHex(otkInfo.privateKey)),
|
|
386
|
-
signature: XUtils.decodeHex(otkInfo.signature),
|
|
387
|
-
index: otkInfo.index,
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
async deleteOneTimeKey(index) {
|
|
391
|
-
if (this.closing) {
|
|
392
|
-
this.log.warn("Database is closing, deleteOneTimeKey() will not complete.");
|
|
393
|
-
return;
|
|
372
|
+
.returning([
|
|
373
|
+
"deviceID",
|
|
374
|
+
"index",
|
|
375
|
+
"keyID",
|
|
376
|
+
"publicKey",
|
|
377
|
+
"signature",
|
|
378
|
+
"userID",
|
|
379
|
+
])
|
|
380
|
+
.executeTakeFirstOrThrow();
|
|
381
|
+
saved.push(row);
|
|
394
382
|
}
|
|
395
|
-
|
|
396
|
-
.deleteFrom("oneTimeKeys")
|
|
397
|
-
.where("index", "=", index)
|
|
398
|
-
.execute();
|
|
383
|
+
return saved;
|
|
399
384
|
}
|
|
400
|
-
// ──
|
|
401
|
-
async
|
|
402
|
-
const rows = await this.db
|
|
403
|
-
.selectFrom("devices")
|
|
404
|
-
.selectAll()
|
|
405
|
-
.where("deviceID", "=", deviceID)
|
|
406
|
-
.execute();
|
|
407
|
-
if (rows.length === 0) {
|
|
408
|
-
return null;
|
|
409
|
-
}
|
|
410
|
-
return rows[0];
|
|
411
|
-
}
|
|
412
|
-
async saveDevice(device) {
|
|
385
|
+
// ── Purge ────────────────────────────────────────────────────────────────
|
|
386
|
+
async saveSession(session) {
|
|
413
387
|
if (this.closing) {
|
|
414
|
-
this.log.warn("Database is closing, saveDevice() will not complete.");
|
|
415
388
|
return;
|
|
416
389
|
}
|
|
417
390
|
try {
|
|
418
391
|
await this.db
|
|
419
|
-
.insertInto("
|
|
392
|
+
.insertInto("sessions")
|
|
420
393
|
.values({
|
|
421
|
-
deviceID:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
394
|
+
deviceID: session.deviceID,
|
|
395
|
+
fingerprint: session.fingerprint,
|
|
396
|
+
lastUsed: session.lastUsed,
|
|
397
|
+
mode: session.mode,
|
|
398
|
+
publicKey: session.publicKey,
|
|
399
|
+
sessionID: session.sessionID,
|
|
400
|
+
SK: this.sealHex(session.SK),
|
|
401
|
+
userID: session.userID,
|
|
402
|
+
verified: session.verified ? 1 : 0,
|
|
427
403
|
})
|
|
428
404
|
.execute();
|
|
429
405
|
}
|
|
430
406
|
catch (err) {
|
|
431
|
-
if (err
|
|
432
|
-
|
|
407
|
+
if (this.isDuplicateError(err)) {
|
|
408
|
+
// duplicate SK — ignore
|
|
433
409
|
}
|
|
434
410
|
else {
|
|
435
411
|
throw err;
|
|
436
412
|
}
|
|
437
413
|
}
|
|
438
414
|
}
|
|
439
|
-
// ── Purge ────────────────────────────────────────────────────────────────
|
|
440
|
-
async purgeKeyData() {
|
|
441
|
-
await this.db.deleteFrom("sessions").execute();
|
|
442
|
-
await this.db.deleteFrom("oneTimeKeys").execute();
|
|
443
|
-
await this.db.deleteFrom("preKeys").execute();
|
|
444
|
-
await this.db.deleteFrom("messages").execute();
|
|
445
|
-
}
|
|
446
415
|
// ── Private helpers ──────────────────────────────────────────────────────
|
|
447
416
|
decryptMessages(messages) {
|
|
448
417
|
return messages.map((msg) => {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
const decrypted = nacl.secretbox.open(XUtils.decodeHex(msg.message), XUtils.decodeHex(msg.nonce), this.idKeys.secretKey);
|
|
418
|
+
const decryptedFlag = msg.decrypted !== 0;
|
|
419
|
+
let plaintext = msg.message;
|
|
420
|
+
if (decryptedFlag) {
|
|
421
|
+
const decrypted = xSecretboxOpen(XUtils.decodeHex(msg.message), XUtils.decodeHex(msg.nonce), this.idKeys.secretKey);
|
|
454
422
|
if (decrypted) {
|
|
455
|
-
|
|
423
|
+
plaintext = XUtils.encodeUTF8(decrypted);
|
|
456
424
|
}
|
|
457
425
|
else {
|
|
458
426
|
throw new Error("Couldn't decrypt messages on disk!");
|
|
459
427
|
}
|
|
460
428
|
}
|
|
461
|
-
|
|
429
|
+
const direction = msg.direction === "incoming" ? "incoming" : "outgoing";
|
|
430
|
+
return {
|
|
431
|
+
authorID: msg.authorID,
|
|
432
|
+
decrypted: decryptedFlag,
|
|
433
|
+
direction,
|
|
434
|
+
forward: msg.forward !== 0,
|
|
435
|
+
group: msg.group,
|
|
436
|
+
mailID: msg.mailID,
|
|
437
|
+
message: plaintext,
|
|
438
|
+
nonce: msg.nonce,
|
|
439
|
+
readerID: msg.readerID,
|
|
440
|
+
recipient: msg.recipient,
|
|
441
|
+
sender: msg.sender,
|
|
442
|
+
timestamp: msg.timestamp,
|
|
443
|
+
};
|
|
462
444
|
});
|
|
463
445
|
}
|
|
446
|
+
deviceRowToDevice(row) {
|
|
447
|
+
return {
|
|
448
|
+
deleted: row.deleted !== 0,
|
|
449
|
+
deviceID: row.deviceID,
|
|
450
|
+
lastLogin: row.lastLogin,
|
|
451
|
+
name: row.name,
|
|
452
|
+
owner: row.owner,
|
|
453
|
+
signKey: row.signKey,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
isDuplicateError(err) {
|
|
457
|
+
if (err instanceof Error) {
|
|
458
|
+
return err.message.includes("UNIQUE");
|
|
459
|
+
}
|
|
460
|
+
if (typeof err === "object" && err !== null && "errno" in err) {
|
|
461
|
+
return err.errno === 19;
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Encrypt a hex-encoded secret for at-rest storage.
|
|
467
|
+
* Returns hex(nonce || ciphertext) where nonce is 24 random bytes.
|
|
468
|
+
*/
|
|
469
|
+
sealHex(plainHex) {
|
|
470
|
+
const nonce = xMakeNonce();
|
|
471
|
+
const ct = xSecretbox(XUtils.decodeHex(plainHex), nonce, this.idKeys.secretKey);
|
|
472
|
+
const sealed = new Uint8Array(nonce.length + ct.length);
|
|
473
|
+
sealed.set(nonce);
|
|
474
|
+
sealed.set(ct, nonce.length);
|
|
475
|
+
return XUtils.encodeHex(sealed);
|
|
476
|
+
}
|
|
477
|
+
sessionRowToSQL(row) {
|
|
478
|
+
return {
|
|
479
|
+
deviceID: row.deviceID,
|
|
480
|
+
fingerprint: row.fingerprint,
|
|
481
|
+
lastUsed: row.lastUsed,
|
|
482
|
+
mode: row.mode === "initiator" ? "initiator" : "receiver",
|
|
483
|
+
publicKey: row.publicKey,
|
|
484
|
+
sessionID: row.sessionID,
|
|
485
|
+
SK: this.unsealHex(row.SK),
|
|
486
|
+
userID: row.userID,
|
|
487
|
+
verified: row.verified !== 0,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
464
490
|
sqlToCrypto(session) {
|
|
465
491
|
return {
|
|
466
|
-
|
|
467
|
-
|
|
492
|
+
fingerprint: XUtils.decodeHex(session.fingerprint),
|
|
493
|
+
lastUsed: session.lastUsed,
|
|
468
494
|
mode: session.mode,
|
|
469
|
-
SK: XUtils.decodeHex(session.SK),
|
|
470
495
|
publicKey: XUtils.decodeHex(session.publicKey),
|
|
471
|
-
|
|
472
|
-
|
|
496
|
+
sessionID: session.sessionID,
|
|
497
|
+
SK: XUtils.decodeHex(session.SK),
|
|
498
|
+
userID: session.userID,
|
|
473
499
|
};
|
|
474
500
|
}
|
|
501
|
+
/**
|
|
502
|
+
* Decrypt a value produced by sealHex().
|
|
503
|
+
* Expects hex(nonce || ciphertext), returns the original hex string.
|
|
504
|
+
*/
|
|
505
|
+
unsealHex(sealed) {
|
|
506
|
+
const bytes = XUtils.decodeHex(sealed);
|
|
507
|
+
const nonce = bytes.slice(0, 24);
|
|
508
|
+
const ct = bytes.slice(24);
|
|
509
|
+
const plain = xSecretboxOpen(ct, nonce, this.idKeys.secretKey);
|
|
510
|
+
if (!plain) {
|
|
511
|
+
throw new Error("Failed to decrypt sealed column value.");
|
|
512
|
+
}
|
|
513
|
+
return XUtils.encodeHex(plain);
|
|
514
|
+
}
|
|
475
515
|
async untilReady() {
|
|
476
516
|
if (this.ready)
|
|
477
517
|
return;
|
|
478
518
|
return new Promise((resolve) => {
|
|
479
519
|
const check = () => {
|
|
480
|
-
if (this.ready)
|
|
481
|
-
|
|
520
|
+
if (this.ready) {
|
|
521
|
+
resolve();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
482
524
|
setTimeout(check, 10);
|
|
483
525
|
};
|
|
484
526
|
check();
|