@vex-chat/libvex 1.1.0 → 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 +440 -551
- package/dist/Client.d.ts.map +1 -0
- package/dist/Client.js +1566 -1502
- 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 -8
- 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 -11
- 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 -54
- 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 -291
- 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 +3 -3
- 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 -83
- 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 -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/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,121 +277,21 @@ export class SqliteStorage extends EventEmitter {
|
|
|
96
277
|
this.emit("ready");
|
|
97
278
|
}
|
|
98
279
|
catch (err) {
|
|
99
|
-
this.emit("error", err);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
async close() {
|
|
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
|
-
}
|
|
280
|
+
this.emit("error", err instanceof Error ? err : new Error(String(err)));
|
|
145
281
|
}
|
|
146
282
|
}
|
|
147
|
-
async
|
|
283
|
+
async markSessionUsed(sessionID) {
|
|
148
284
|
if (this.closing) {
|
|
149
|
-
this.log.warn("Database is closing,
|
|
285
|
+
this.log.warn("Database is closing, markSessionUsed() will not complete.");
|
|
150
286
|
return;
|
|
151
287
|
}
|
|
152
288
|
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
|
-
]))
|
|
289
|
+
.updateTable("sessions")
|
|
290
|
+
.set({ lastUsed: new Date(Date.now()).toISOString() })
|
|
291
|
+
.where("sessionID", "=", sessionID)
|
|
208
292
|
.execute();
|
|
209
293
|
}
|
|
210
|
-
|
|
211
|
-
await this.db.deleteFrom("messages").execute();
|
|
212
|
-
}
|
|
213
|
-
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
294
|
+
// ── PreKeys / OneTimeKeys ────────────────────────────────────────────────
|
|
214
295
|
async markSessionVerified(sessionID) {
|
|
215
296
|
if (this.closing) {
|
|
216
297
|
this.log.warn("Database is closing, markSessionVerified() will not complete.");
|
|
@@ -222,101 +303,78 @@ export class SqliteStorage extends EventEmitter {
|
|
|
222
303
|
.where("sessionID", "=", sessionID)
|
|
223
304
|
.execute();
|
|
224
305
|
}
|
|
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();
|
|
306
|
+
async purgeHistory() {
|
|
307
|
+
await this.db.deleteFrom("messages").execute();
|
|
235
308
|
}
|
|
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]);
|
|
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();
|
|
253
314
|
}
|
|
254
|
-
async
|
|
315
|
+
async saveDevice(device) {
|
|
255
316
|
if (this.closing) {
|
|
256
|
-
this.log.warn("Database is closing,
|
|
257
|
-
return
|
|
317
|
+
this.log.warn("Database is closing, saveDevice() will not complete.");
|
|
318
|
+
return;
|
|
258
319
|
}
|
|
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;
|
|
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();
|
|
273
332
|
}
|
|
274
|
-
|
|
275
|
-
.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (rows.length === 0) {
|
|
282
|
-
this.log.debug("getSession() => " + JSON.stringify(null));
|
|
283
|
-
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
|
+
}
|
|
284
340
|
}
|
|
285
|
-
return this.sqlToCrypto(rows[0]);
|
|
286
341
|
}
|
|
287
|
-
|
|
342
|
+
// ── Devices ──────────────────────────────────────────────────────────────
|
|
343
|
+
async saveMessage(message) {
|
|
288
344
|
if (this.closing) {
|
|
289
|
-
this.log.warn("Database is closing,
|
|
345
|
+
this.log.warn("Database is closing, saveMessage() will not complete.");
|
|
290
346
|
return;
|
|
291
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));
|
|
292
350
|
try {
|
|
293
351
|
await this.db
|
|
294
|
-
.insertInto("
|
|
352
|
+
.insertInto("messages")
|
|
295
353
|
.values({
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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,
|
|
307
366
|
})
|
|
308
367
|
.execute();
|
|
309
368
|
}
|
|
310
369
|
catch (err) {
|
|
311
|
-
if (err
|
|
312
|
-
this.log.warn("
|
|
370
|
+
if (this.isDuplicateError(err)) {
|
|
371
|
+
this.log.warn("Duplicate nonce in message table.");
|
|
313
372
|
}
|
|
314
373
|
else {
|
|
315
374
|
throw err;
|
|
316
375
|
}
|
|
317
376
|
}
|
|
318
377
|
}
|
|
319
|
-
// ── PreKeys / OneTimeKeys ────────────────────────────────────────────────
|
|
320
378
|
async savePreKeys(preKeys, oneTime) {
|
|
321
379
|
await this.untilReady();
|
|
322
380
|
if (this.closing) {
|
|
@@ -324,152 +382,131 @@ export class SqliteStorage extends EventEmitter {
|
|
|
324
382
|
return [];
|
|
325
383
|
}
|
|
326
384
|
const table = oneTime ? "oneTimeKeys" : "preKeys";
|
|
327
|
-
const
|
|
385
|
+
const saved = [];
|
|
328
386
|
for (const preKey of preKeys) {
|
|
329
|
-
const
|
|
387
|
+
const row = await this.db
|
|
330
388
|
.insertInto(table)
|
|
331
389
|
.values({
|
|
332
390
|
privateKey: XUtils.encodeHex(preKey.keyPair.secretKey),
|
|
333
391
|
publicKey: XUtils.encodeHex(preKey.keyPair.publicKey),
|
|
334
392
|
signature: XUtils.encodeHex(preKey.signature),
|
|
335
393
|
})
|
|
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;
|
|
394
|
-
}
|
|
395
|
-
await this.db
|
|
396
|
-
.deleteFrom("oneTimeKeys")
|
|
397
|
-
.where("index", "=", index)
|
|
398
|
-
.execute();
|
|
399
|
-
}
|
|
400
|
-
// ── Devices ──────────────────────────────────────────────────────────────
|
|
401
|
-
async getDevice(deviceID) {
|
|
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;
|
|
394
|
+
.returning([
|
|
395
|
+
"deviceID",
|
|
396
|
+
"index",
|
|
397
|
+
"keyID",
|
|
398
|
+
"publicKey",
|
|
399
|
+
"signature",
|
|
400
|
+
"userID",
|
|
401
|
+
])
|
|
402
|
+
.executeTakeFirstOrThrow();
|
|
403
|
+
saved.push(row);
|
|
409
404
|
}
|
|
410
|
-
return
|
|
405
|
+
return saved;
|
|
411
406
|
}
|
|
412
|
-
|
|
407
|
+
// ── Purge ────────────────────────────────────────────────────────────────
|
|
408
|
+
async saveSession(session) {
|
|
413
409
|
if (this.closing) {
|
|
414
|
-
this.log.warn("Database is closing,
|
|
410
|
+
this.log.warn("Database is closing, saveSession() will not complete.");
|
|
415
411
|
return;
|
|
416
412
|
}
|
|
417
413
|
try {
|
|
418
414
|
await this.db
|
|
419
|
-
.insertInto("
|
|
415
|
+
.insertInto("sessions")
|
|
420
416
|
.values({
|
|
421
|
-
deviceID:
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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,
|
|
427
426
|
})
|
|
428
427
|
.execute();
|
|
429
428
|
}
|
|
430
429
|
catch (err) {
|
|
431
|
-
if (err
|
|
432
|
-
this.log.warn("Attempted to insert duplicate
|
|
430
|
+
if (this.isDuplicateError(err)) {
|
|
431
|
+
this.log.warn("Attempted to insert duplicate SK");
|
|
433
432
|
}
|
|
434
433
|
else {
|
|
435
434
|
throw err;
|
|
436
435
|
}
|
|
437
436
|
}
|
|
438
437
|
}
|
|
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
438
|
// ── Private helpers ──────────────────────────────────────────────────────
|
|
447
439
|
decryptMessages(messages) {
|
|
448
440
|
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);
|
|
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);
|
|
454
445
|
if (decrypted) {
|
|
455
|
-
|
|
446
|
+
plaintext = XUtils.encodeUTF8(decrypted);
|
|
456
447
|
}
|
|
457
448
|
else {
|
|
458
449
|
throw new Error("Couldn't decrypt messages on disk!");
|
|
459
450
|
}
|
|
460
451
|
}
|
|
461
|
-
|
|
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
|
+
};
|
|
462
467
|
});
|
|
463
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
|
+
}
|
|
464
501
|
sqlToCrypto(session) {
|
|
465
502
|
return {
|
|
466
|
-
|
|
467
|
-
|
|
503
|
+
fingerprint: XUtils.decodeHex(session.fingerprint),
|
|
504
|
+
lastUsed: session.lastUsed,
|
|
468
505
|
mode: session.mode,
|
|
469
|
-
SK: XUtils.decodeHex(session.SK),
|
|
470
506
|
publicKey: XUtils.decodeHex(session.publicKey),
|
|
471
|
-
|
|
472
|
-
|
|
507
|
+
sessionID: session.sessionID,
|
|
508
|
+
SK: XUtils.decodeHex(session.SK),
|
|
509
|
+
userID: session.userID,
|
|
473
510
|
};
|
|
474
511
|
}
|
|
475
512
|
async untilReady() {
|
|
@@ -477,8 +514,10 @@ export class SqliteStorage extends EventEmitter {
|
|
|
477
514
|
return;
|
|
478
515
|
return new Promise((resolve) => {
|
|
479
516
|
const check = () => {
|
|
480
|
-
if (this.ready)
|
|
481
|
-
|
|
517
|
+
if (this.ready) {
|
|
518
|
+
resolve();
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
482
521
|
setTimeout(check, 10);
|
|
483
522
|
};
|
|
484
523
|
check();
|