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