@vex-chat/libvex 0.27.1 → 1.0.0-rc.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Client.js CHANGED
@@ -1,61 +1,33 @@
1
- "use strict";
2
1
  // tslint:disable: no-empty-interface
3
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
- if (k2 === undefined) k2 = k;
5
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
6
- }) : (function(o, m, k, k2) {
7
- if (k2 === undefined) k2 = k;
8
- o[k2] = m[k];
9
- }));
10
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
11
- Object.defineProperty(o, "default", { enumerable: true, value: v });
12
- }) : function(o, v) {
13
- o["default"] = v;
14
- });
15
- var __importStar = (this && this.__importStar) || function (mod) {
16
- if (mod && mod.__esModule) return mod;
17
- var result = {};
18
- if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
19
- __setModuleDefault(result, mod);
20
- return result;
21
- };
22
- var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
23
- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
24
- return new (P || (P = Promise))(function (resolve, reject) {
25
- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
26
- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
27
- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
28
- step((generator = generator.apply(thisArg, _arguments || [])).next());
29
- });
30
- };
31
- var __importDefault = (this && this.__importDefault) || function (mod) {
32
- return (mod && mod.__esModule) ? mod : { "default": mod };
33
- };
34
- Object.defineProperty(exports, "__esModule", { value: true });
35
- exports.Client = void 0;
36
- const sleep_1 = require("@extrahash/sleep");
37
- const crypto_1 = require("@vex-chat/crypto");
38
- const types_1 = require("@vex-chat/types");
39
- const axios_1 = __importDefault(require("axios"));
40
- const browser_or_node_1 = require("browser-or-node");
41
- const btoa_1 = __importDefault(require("btoa"));
42
- const chalk_1 = __importDefault(require("chalk"));
43
- const events_1 = require("events");
44
- const msgpack_lite_1 = __importDefault(require("msgpack-lite"));
45
- const object_hash_1 = __importDefault(require("object-hash"));
46
- const os_1 = __importDefault(require("os"));
47
- const perf_hooks_1 = require("perf_hooks");
48
- const tweetnacl_1 = __importDefault(require("tweetnacl"));
49
- const uuid = __importStar(require("uuid"));
50
- const ws_1 = __importDefault(require("ws"));
51
- const Storage_1 = require("./Storage");
52
- const capitalize_1 = require("./utils/capitalize");
53
- const createLogger_1 = require("./utils/createLogger");
54
- const formatBytes_1 = require("./utils/formatBytes");
55
- const sqlSessionToCrypto_1 = require("./utils/sqlSessionToCrypto");
56
- const uint8uuid_1 = require("./utils/uint8uuid");
57
- axios_1.default.defaults.withCredentials = true;
58
- axios_1.default.defaults.responseType = "arraybuffer";
2
+ import { sleep } from "@extrahash/sleep";
3
+ import { xConcat, xConstants, xDH, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMnemonic, XUtils, } from "@vex-chat/crypto";
4
+ import { MailType } from "@vex-chat/types";
5
+ import ax, { AxiosError } from "axios";
6
+ import { isBrowser, isNode } from "browser-or-node";
7
+ import btoa from "btoa";
8
+ import chalk from "chalk";
9
+ import { EventEmitter } from "events";
10
+ import { Packr } from "msgpackr";
11
+ // useRecords:false emits standard msgpack (no nonstandard record extension).
12
+ // moreTypes:false keeps the extension set to what every other decoder understands.
13
+ // Packr.pack() returns Node Buffer, which axios sends correctly (plain Uint8Array
14
+ // would have its pool buffer sent in full — see axios issue #4068).
15
+ const msgpack = new Packr({ useRecords: false, moreTypes: false });
16
+ import objectHash from "object-hash";
17
+ import * as os from "node:os";
18
+ import { performance } from "node:perf_hooks";
19
+ import nacl from "tweetnacl";
20
+ import * as uuid from "uuid";
21
+ import winston from "winston";
22
+ import WebSocket from "ws";
23
+ import { Storage } from "./Storage.js";
24
+ import { capitalize } from "./utils/capitalize.js";
25
+ import { createLogger } from "./utils/createLogger.js";
26
+ import { formatBytes } from "./utils/formatBytes.js";
27
+ import { sqlSessionToCrypto } from "./utils/sqlSessionToCrypto.js";
28
+ import { uuidToUint8 } from "./utils/uint8uuid.js";
29
+ ax.defaults.withCredentials = true;
30
+ ax.defaults.responseType = "arraybuffer";
59
31
  const protocolMsgRegex = /��\w+:\w+��/g;
60
32
  /**
61
33
  * Client provides an interface for you to use a vex chat server and
@@ -103,256 +75,378 @@ const protocolMsgRegex = /��\w+:\w+��/g;
103
75
  *
104
76
  * main();
105
77
  * ```
106
- *
107
- * @noInheritDoc
108
78
  */
109
- class Client extends events_1.EventEmitter {
110
- constructor(privateKey, options, storage) {
111
- super();
79
+ export class Client extends EventEmitter {
80
+ /**
81
+ * Loads a key file from disk.
82
+ *
83
+ * Pass-through utility from `@vex-chat/crypto`.
84
+ */
85
+ static loadKeyFile = XUtils.loadKeyFile;
86
+ /**
87
+ * Saves a key file to disk.
88
+ *
89
+ * Pass-through utility from `@vex-chat/crypto`.
90
+ */
91
+ static saveKeyFile = XUtils.saveKeyFile;
92
+ /**
93
+ * Creates and initializes a client in one step.
94
+ *
95
+ * @param privateKey Optional hex secret key. When omitted, a fresh key is generated.
96
+ * @param options Runtime options.
97
+ * @param storage Optional custom storage backend implementing `IStorage`.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * const client = await Client.create(privateKey, { host: "api.vex.wtf" });
102
+ * ```
103
+ */
104
+ static create = async (privateKey, options, storage) => {
105
+ const client = new Client(privateKey, options, storage);
106
+ await client.init();
107
+ return client;
108
+ };
109
+ /**
110
+ * Generates an ed25519 secret key as a hex string.
111
+ *
112
+ * @returns - A secret key to use for the client. Save it permanently somewhere safe.
113
+ */
114
+ static generateSecretKey() {
115
+ return XUtils.encodeHex(nacl.sign.keyPair().secretKey);
116
+ }
117
+ /**
118
+ * Generates a random username using bip39.
119
+ *
120
+ * @returns - The username.
121
+ */
122
+ static randomUsername() {
123
+ const IKM = XUtils.decodeHex(XUtils.encodeHex(nacl.randomBytes(16)));
124
+ const mnemonic = xMnemonic(IKM).split(" ");
125
+ const addendum = XUtils.uint8ArrToNumber(nacl.randomBytes(1));
126
+ return (capitalize(mnemonic[0]) +
127
+ capitalize(mnemonic[1]) +
128
+ addendum.toString());
129
+ }
130
+ static getMnemonic(session) {
131
+ return xMnemonic(xKDF(XUtils.decodeHex(session.fingerprint)));
132
+ }
133
+ static deserializeExtra(type, extra) {
134
+ switch (type) {
135
+ case MailType.initial:
136
+ /* 32 bytes for signkey, 32 bytes for ephemeral key,
137
+ 68 bytes for AD, 6 bytes for otk index (empty for no otk) */
138
+ const signKey = extra.slice(0, 32);
139
+ const ephKey = extra.slice(32, 64);
140
+ const ad = extra.slice(96, 164);
141
+ const index = extra.slice(164, 170);
142
+ return [signKey, ephKey, ad, index];
143
+ case MailType.subsequent:
144
+ const publicKey = extra;
145
+ return [publicKey];
146
+ default:
147
+ return [];
148
+ }
149
+ }
150
+ /**
151
+ * User operations.
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * const [user] = await client.users.retrieve("alice");
156
+ * const familiarUsers = await client.users.familiars();
157
+ * ```
158
+ */
159
+ users = {
112
160
  /**
113
- * The IUsers interface contains methods for dealing with users.
161
+ * Retrieves a user's information by a string identifier.
162
+ * @param identifier: A userID, hex string public key, or a username.
163
+ *
164
+ * @returns - The user's IUser object, or null if the user does not exist.
114
165
  */
115
- this.users = {
116
- /**
117
- * Retrieves a user's information by a string identifier.
118
- * @param identifier: A userID, hex string public key, or a username.
119
- *
120
- * @returns - The user's IUser object, or null if the user does not exist.
121
- */
122
- retrieve: this.retrieveUserDBEntry.bind(this),
123
- /**
124
- * Retrieves the list of users you can currently access, or are already familiar with.
125
- *
126
- * @returns - The list of IUser objects.
127
- */
128
- familiars: this.getFamiliars.bind(this),
129
- };
130
- this.emoji = {
131
- create: this.uploadEmoji.bind(this),
132
- retrieveList: this.retrieveEmojiList.bind(this),
133
- retrieve: this.retrieveEmojiByID.bind(this),
134
- };
135
- this.me = {
136
- /**
137
- * Retrieves your user information
138
- *
139
- * @returns - The logged in user's IUser object.
140
- */
141
- user: this.getUser.bind(this),
142
- /**
143
- * Retrieves current device details
144
- *
145
- * @returns - The logged in device's IDevice object.
146
- */
147
- device: this.getDevice.bind(this),
148
- /**
149
- * Changes your avatar.
150
- */
151
- setAvatar: this.uploadAvatar.bind(this),
152
- };
153
- this.devices = {
154
- retrieve: this.getDeviceByID.bind(this),
155
- register: this.registerDevice.bind(this),
156
- delete: this.deleteDevice.bind(this),
157
- };
166
+ retrieve: this.retrieveUserDBEntry.bind(this),
158
167
  /**
159
- * The IMessages interface contains methods for dealing with messages.
168
+ * Retrieves the list of users you can currently access, or are already familiar with.
169
+ *
170
+ * @returns - The list of IUser objects.
160
171
  */
161
- this.files = {
162
- /**
163
- * Uploads an encrypted file and returns the details and the secret key.
164
- * @param file: The file as a Buffer.
165
- *
166
- * @returns Details of the file uploaded and the key to encrypt in the form [details, key].
167
- */
168
- create: this.createFile.bind(this),
169
- retrieve: this.retrieveFile.bind(this),
170
- };
172
+ familiars: this.getFamiliars.bind(this),
173
+ };
174
+ /**
175
+ * Emoji operations.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const emoji = await client.emoji.create(imageBuffer, "party", serverID);
180
+ * const list = await client.emoji.retrieveList(serverID);
181
+ * ```
182
+ */
183
+ emoji = {
184
+ create: this.uploadEmoji.bind(this),
185
+ retrieveList: this.retrieveEmojiList.bind(this),
186
+ retrieve: this.retrieveEmojiByID.bind(this),
187
+ };
188
+ /**
189
+ * Helpers for information/actions related to the currently authenticated account.
190
+ */
191
+ me = {
171
192
  /**
172
- * The IPermissions object contains all methods for dealing with permissions.
193
+ * Retrieves your user information
194
+ *
195
+ * @returns - The logged in user's IUser object.
173
196
  */
174
- this.permissions = {
175
- retrieve: this.getPermissions.bind(this),
176
- delete: this.deletePermission.bind(this),
177
- };
197
+ user: this.getUser.bind(this),
178
198
  /**
179
- * The IModeration object contains all methods for dealing with permissions.
199
+ * Retrieves current device details
200
+ *
201
+ * @returns - The logged in device's IDevice object.
180
202
  */
181
- this.moderation = {
182
- kick: this.kickUser.bind(this),
183
- fetchPermissionList: this.fetchPermissionList.bind(this),
184
- };
203
+ device: this.getDevice.bind(this),
185
204
  /**
186
- * The IInvites interface contains methods for dealing with invites.
205
+ * Changes your avatar.
187
206
  */
188
- this.invites = {
189
- create: this.createInvite.bind(this),
190
- redeem: this.redeemInvite.bind(this),
191
- retrieve: this.retrieveInvites.bind(this),
192
- };
207
+ setAvatar: this.uploadAvatar.bind(this),
208
+ };
209
+ /**
210
+ * Device management methods.
211
+ */
212
+ devices = {
213
+ retrieve: this.getDeviceByID.bind(this),
214
+ register: this.registerDevice.bind(this),
215
+ delete: this.deleteDevice.bind(this),
216
+ };
217
+ /** File upload/download methods. */
218
+ files = {
193
219
  /**
194
- * The IMessages interface contains methods for dealing with messages.
220
+ * Uploads an encrypted file and returns the details and the secret key.
221
+ * @param file: The file as a Buffer.
222
+ *
223
+ * @returns Details of the file uploaded and the key to encrypt in the form [details, key].
195
224
  */
196
- this.messages = {
197
- /**
198
- * Send a direct message.
199
- * @param userID: The userID of the user to send a message to.
200
- * @param message: The message to send.
201
- */
202
- send: this.sendMessage.bind(this),
203
- /**
204
- * Send a group message to a channel.
205
- * @param channelID: The channelID of the channel to send a message to.
206
- * @param message: The message to send.
207
- */
208
- group: this.sendGroupMessage.bind(this),
209
- /**
210
- * Gets the message history with a specific userID.
211
- * @param userID: The userID of the user to retrieve message history for.
212
- *
213
- * @returns - The list of IMessage objects.
214
- */
215
- retrieve: this.getMessageHistory.bind(this),
216
- /**
217
- * Gets the group message history with a specific channelID.
218
- * @param chqnnelID: The channelID of the channel to retrieve message history for.
219
- *
220
- * @returns - The list of IMessage objects.
221
- */
222
- retrieveGroup: this.getGroupHistory.bind(this),
223
- delete: this.deleteHistory.bind(this),
224
- purge: this.purgeHistory.bind(this),
225
- };
225
+ create: this.createFile.bind(this),
226
+ retrieve: this.retrieveFile.bind(this),
227
+ };
228
+ /**
229
+ * Permission-management methods for the current user.
230
+ */
231
+ permissions = {
232
+ retrieve: this.getPermissions.bind(this),
233
+ delete: this.deletePermission.bind(this),
234
+ };
235
+ /**
236
+ * Server moderation helper methods.
237
+ */
238
+ moderation = {
239
+ kick: this.kickUser.bind(this),
240
+ fetchPermissionList: this.fetchPermissionList.bind(this),
241
+ };
242
+ /**
243
+ * Invite-management methods.
244
+ */
245
+ invites = {
246
+ create: this.createInvite.bind(this),
247
+ redeem: this.redeemInvite.bind(this),
248
+ retrieve: this.retrieveInvites.bind(this),
249
+ };
250
+ /**
251
+ * Message operations (direct and group).
252
+ *
253
+ * @example
254
+ * ```ts
255
+ * await client.messages.send(userID, "Hello!");
256
+ * await client.messages.group(channelID, "Hello channel!");
257
+ * const dmHistory = await client.messages.retrieve(userID);
258
+ * ```
259
+ */
260
+ messages = {
226
261
  /**
227
- * The ISessions interface contains methods for dealing with encryption sessions.
262
+ * Send a direct message.
263
+ * @param userID: The userID of the user to send a message to.
264
+ * @param message: The message to send.
228
265
  */
229
- this.sessions = {
230
- /**
231
- * Gets all encryption sessions.
232
- *
233
- * @returns - The list of ISession encryption sessions.
234
- */
235
- retrieve: this.getSessionList.bind(this),
236
- /**
237
- * Returns a mnemonic for the session, to verify with the other user.
238
- * @param session the ISession object to get the mnemonic for.
239
- *
240
- * @returns - The mnemonic representation of the session.
241
- */
242
- verify: Client.getMnemonic,
243
- /**
244
- * Marks a mnemonic verified, implying that the the user has confirmed
245
- * that the session mnemonic matches with the other user.
246
- * @param sessionID the sessionID of the session to mark.
247
- * @param status Optionally, what to mark it as. Defaults to true.
248
- */
249
- markVerified: this.markSessionVerified.bind(this),
250
- };
251
- this.servers = {
252
- /**
253
- * Retrieves all servers the logged in user has access to.
254
- *
255
- * @returns - The list of IServer objects.
256
- */
257
- retrieve: this.getServerList.bind(this),
258
- /**
259
- * Retrieves server details by its unique serverID.
260
- *
261
- * @returns - The requested IServer object, or null if the id does not exist.
262
- */
263
- retrieveByID: this.getServerByID.bind(this),
264
- /**
265
- * Creates a new server.
266
- * @param name: The server name.
267
- *
268
- * @returns - The created IServer object.
269
- */
270
- create: this.createServer.bind(this),
271
- /**
272
- * Deletes a server.
273
- * @param serverID: The unique serverID to delete.
274
- */
275
- delete: this.deleteServer.bind(this),
276
- leave: this.leaveServer.bind(this),
277
- };
278
- this.channels = {
279
- /**
280
- * Retrieves all channels in a server.
281
- *
282
- * @returns - The list of IChannel objects.
283
- */
284
- retrieve: this.getChannelList.bind(this),
285
- /**
286
- * Retrieves channel details by its unique channelID.
287
- *
288
- * @returns - The list of IChannel objects.
289
- */
290
- retrieveByID: this.getChannelByID.bind(this),
291
- /**
292
- * Creates a new channel in a server.
293
- * @param name: The channel name.
294
- * @param serverID: The unique serverID to create the channel in.
295
- *
296
- * @returns - The created IChannel object.
297
- */
298
- create: this.createChannel.bind(this),
299
- /**
300
- * Deletes a channel.
301
- * @param channelID: The unique channelID to delete.
302
- */
303
- delete: this.deleteChannel.bind(this),
304
- /**
305
- * Retrieves a channel's userlist.
306
- * @param channelID: The channelID to retrieve userlist for.
307
- */
308
- userList: this.getUserList.bind(this),
309
- };
266
+ send: this.sendMessage.bind(this),
310
267
  /**
311
- * This is true if the client has ever been initialized. You can only initialize
312
- * a client once.
268
+ * Send a group message to a channel.
269
+ * @param channelID: The channelID of the channel to send a message to.
270
+ * @param message: The message to send.
313
271
  */
314
- this.hasInit = false;
272
+ group: this.sendGroupMessage.bind(this),
315
273
  /**
316
- * This is true if the client has ever logged in before. You can only login a client once.
274
+ * Gets the message history with a specific userID.
275
+ * @param userID: The userID of the user to retrieve message history for.
276
+ *
277
+ * @returns - The list of IMessage objects.
317
278
  */
318
- this.hasLoggedIn = false;
319
- this.sending = {};
320
- this.firstMailFetch = true;
321
- this.userRecords = {};
322
- this.deviceRecords = {};
323
- this.sessionRecords = {};
324
- this.isAlive = true;
325
- this.reading = false;
326
- this.fetchingMail = false;
327
- this.cookies = [];
328
- this.pingInterval = null;
329
- this.manuallyClosing = false;
330
- this.token = null;
331
- this.forwarded = [];
332
- this.log = createLogger_1.createLogger("client", options === null || options === void 0 ? void 0 : options.logLevel);
333
- this.prefixes = (options === null || options === void 0 ? void 0 : options.unsafeHttp) ? { HTTP: "http://", WS: "ws://" }
279
+ retrieve: this.getMessageHistory.bind(this),
280
+ /**
281
+ * Gets the group message history with a specific channelID.
282
+ * @param chqnnelID: The channelID of the channel to retrieve message history for.
283
+ *
284
+ * @returns - The list of IMessage objects.
285
+ */
286
+ retrieveGroup: this.getGroupHistory.bind(this),
287
+ delete: this.deleteHistory.bind(this),
288
+ purge: this.purgeHistory.bind(this),
289
+ };
290
+ /**
291
+ * Encryption-session helpers.
292
+ */
293
+ sessions = {
294
+ /**
295
+ * Gets all encryption sessions.
296
+ *
297
+ * @returns - The list of ISession encryption sessions.
298
+ */
299
+ retrieve: this.getSessionList.bind(this),
300
+ /**
301
+ * Returns a mnemonic for the session, to verify with the other user.
302
+ * @param session the ISession object to get the mnemonic for.
303
+ *
304
+ * @returns - The mnemonic representation of the session.
305
+ */
306
+ verify: Client.getMnemonic,
307
+ /**
308
+ * Marks a mnemonic verified, implying that the the user has confirmed
309
+ * that the session mnemonic matches with the other user.
310
+ * @param sessionID the sessionID of the session to mark.
311
+ * @param status Optionally, what to mark it as. Defaults to true.
312
+ */
313
+ markVerified: this.markSessionVerified.bind(this),
314
+ };
315
+ /**
316
+ * Server operations.
317
+ *
318
+ * @example
319
+ * ```ts
320
+ * const servers = await client.servers.retrieve();
321
+ * const created = await client.servers.create("Team Space");
322
+ * ```
323
+ */
324
+ servers = {
325
+ /**
326
+ * Retrieves all servers the logged in user has access to.
327
+ *
328
+ * @returns - The list of IServer objects.
329
+ */
330
+ retrieve: this.getServerList.bind(this),
331
+ /**
332
+ * Retrieves server details by its unique serverID.
333
+ *
334
+ * @returns - The requested IServer object, or null if the id does not exist.
335
+ */
336
+ retrieveByID: this.getServerByID.bind(this),
337
+ /**
338
+ * Creates a new server.
339
+ * @param name: The server name.
340
+ *
341
+ * @returns - The created IServer object.
342
+ */
343
+ create: this.createServer.bind(this),
344
+ /**
345
+ * Deletes a server.
346
+ * @param serverID: The unique serverID to delete.
347
+ */
348
+ delete: this.deleteServer.bind(this),
349
+ leave: this.leaveServer.bind(this),
350
+ };
351
+ /**
352
+ * Channel operations.
353
+ */
354
+ channels = {
355
+ /**
356
+ * Retrieves all channels in a server.
357
+ *
358
+ * @returns - The list of IChannel objects.
359
+ */
360
+ retrieve: this.getChannelList.bind(this),
361
+ /**
362
+ * Retrieves channel details by its unique channelID.
363
+ *
364
+ * @returns - The list of IChannel objects.
365
+ */
366
+ retrieveByID: this.getChannelByID.bind(this),
367
+ /**
368
+ * Creates a new channel in a server.
369
+ * @param name: The channel name.
370
+ * @param serverID: The unique serverID to create the channel in.
371
+ *
372
+ * @returns - The created IChannel object.
373
+ */
374
+ create: this.createChannel.bind(this),
375
+ /**
376
+ * Deletes a channel.
377
+ * @param channelID: The unique channelID to delete.
378
+ */
379
+ delete: this.deleteChannel.bind(this),
380
+ /**
381
+ * Retrieves a channel's userlist.
382
+ * @param channelID: The channelID to retrieve userlist for.
383
+ */
384
+ userList: this.getUserList.bind(this),
385
+ };
386
+ /**
387
+ * This is true if the client has ever been initialized. You can only initialize
388
+ * a client once.
389
+ */
390
+ hasInit = false;
391
+ /**
392
+ * This is true if the client has ever logged in before. You can only login a client once.
393
+ */
394
+ hasLoggedIn = false;
395
+ sending = {};
396
+ database;
397
+ dbPath;
398
+ conn;
399
+ host;
400
+ firstMailFetch = true;
401
+ // these are created from one set of sign keys
402
+ signKeys;
403
+ idKeys;
404
+ xKeyRing;
405
+ user;
406
+ device;
407
+ userRecords = {};
408
+ deviceRecords = {};
409
+ sessionRecords = {};
410
+ isAlive = true;
411
+ reading = false;
412
+ fetchingMail = false;
413
+ cookies = [];
414
+ log;
415
+ pingInterval = null;
416
+ mailInterval;
417
+ manuallyClosing = false;
418
+ token = null;
419
+ forwarded = [];
420
+ prefixes;
421
+ constructor(privateKey, options, storage) {
422
+ super();
423
+ this.log = createLogger("client", options?.logLevel);
424
+ this.prefixes = options?.unsafeHttp
425
+ ? { HTTP: "http://", WS: "ws://" }
334
426
  : { HTTP: "https://", WS: "wss://" };
335
427
  this.signKeys = privateKey
336
- ? tweetnacl_1.default.sign.keyPair.fromSecretKey(crypto_1.XUtils.decodeHex(privateKey))
337
- : tweetnacl_1.default.sign.keyPair();
338
- this.idKeys = crypto_1.XKeyConvert.convertKeyPair(this.signKeys);
428
+ ? nacl.sign.keyPair.fromSecretKey(XUtils.decodeHex(privateKey))
429
+ : nacl.sign.keyPair();
430
+ this.idKeys = XKeyConvert.convertKeyPair(this.signKeys);
339
431
  if (!this.idKeys) {
340
432
  throw new Error("Could not convert key to X25519!");
341
433
  }
342
- this.host = (options === null || options === void 0 ? void 0 : options.host) || "api.vex.wtf";
343
- const dbFileName = (options === null || options === void 0 ? void 0 : options.inMemoryDb) ? ":memory:"
344
- : crypto_1.XUtils.encodeHex(this.signKeys.publicKey) + ".sqlite";
345
- this.dbPath = (options === null || options === void 0 ? void 0 : options.dbFolder) ? (options === null || options === void 0 ? void 0 : options.dbFolder) + "/" + dbFileName
434
+ this.host = options?.host || "api.vex.wtf";
435
+ const dbFileName = options?.inMemoryDb
436
+ ? ":memory:"
437
+ : XUtils.encodeHex(this.signKeys.publicKey) + ".sqlite";
438
+ this.dbPath = options?.dbFolder
439
+ ? options?.dbFolder + "/" + dbFileName
346
440
  : dbFileName;
347
441
  this.database = storage
348
442
  ? storage
349
- : new Storage_1.Storage(this.dbPath, crypto_1.XUtils.encodeHex(this.signKeys.secretKey), options);
443
+ : new Storage(this.dbPath, XUtils.encodeHex(this.signKeys.secretKey), options);
350
444
  this.database.on("error", (error) => {
351
445
  this.log.error(error.toString());
352
446
  this.close(true);
353
447
  });
354
448
  // we want to initialize this later with login()
355
- this.conn = new ws_1.default("ws://localhost:1234");
449
+ this.conn = new WebSocket("ws://localhost:1234");
356
450
  // silence the error for connecting to junk ws
357
451
  // tslint:disable-next-line: no-empty
358
452
  this.conn.onerror = () => { };
@@ -362,165 +456,149 @@ class Client extends events_1.EventEmitter {
362
456
  host: this.getHost(),
363
457
  dbPath: this.dbPath,
364
458
  environment: {
365
- isBrowser: browser_or_node_1.isBrowser,
366
- isNode: browser_or_node_1.isNode,
459
+ isBrowser,
460
+ isNode,
367
461
  },
368
462
  options,
369
463
  }, null, 4));
370
464
  }
371
465
  /**
372
- * Generates an ed25519 secret key as a hex string.
373
- *
374
- * @returns - A secret key to use for the client. Save it permanently somewhere safe.
375
- */
376
- static generateSecretKey() {
377
- return crypto_1.XUtils.encodeHex(tweetnacl_1.default.sign.keyPair().secretKey);
378
- }
379
- /**
380
- * Generates a random username using bip39.
466
+ * Returns the current HTTP API origin with protocol.
381
467
  *
382
- * @returns - The username.
468
+ * @example
469
+ * ```ts
470
+ * console.log(client.getHost()); // "https://api.vex.wtf"
471
+ * ```
383
472
  */
384
- static randomUsername() {
385
- const IKM = crypto_1.XUtils.decodeHex(crypto_1.XUtils.encodeHex(tweetnacl_1.default.randomBytes(16)));
386
- const mnemonic = crypto_1.xMnemonic(IKM).split(" ");
387
- const addendum = crypto_1.XUtils.uint8ArrToNumber(tweetnacl_1.default.randomBytes(1));
388
- return (capitalize_1.capitalize(mnemonic[0]) +
389
- capitalize_1.capitalize(mnemonic[1]) +
390
- addendum.toString());
391
- }
392
- static getMnemonic(session) {
393
- return crypto_1.xMnemonic(crypto_1.xKDF(crypto_1.XUtils.decodeHex(session.fingerprint)));
394
- }
395
- static deserializeExtra(type, extra) {
396
- switch (type) {
397
- case types_1.XTypes.WS.MailType.initial:
398
- /* 32 bytes for signkey, 32 bytes for ephemeral key,
399
- 68 bytes for AD, 6 bytes for otk index (empty for no otk) */
400
- const signKey = extra.slice(0, 32);
401
- const ephKey = extra.slice(32, 64);
402
- const ad = extra.slice(96, 164);
403
- const index = extra.slice(164, 170);
404
- return [signKey, ephKey, ad, index];
405
- case types_1.XTypes.WS.MailType.subsequent:
406
- const publicKey = extra;
407
- return [publicKey];
408
- default:
409
- return [];
410
- }
411
- }
412
473
  getHost() {
413
474
  return this.prefixes.HTTP + this.host;
414
475
  }
415
476
  /**
416
477
  * Manually closes the client. Emits the closed event on successful shutdown.
417
478
  */
418
- close(muteEvent = false) {
419
- return __awaiter(this, void 0, void 0, function* () {
420
- this.manuallyClosing = true;
421
- this.log.info("Manually closing client.");
422
- this.conn.close();
423
- yield this.database.close();
424
- if (this.pingInterval) {
425
- clearInterval(this.pingInterval);
426
- }
427
- if (this.mailInterval) {
428
- clearInterval(this.mailInterval);
429
- }
430
- delete this.xKeyRing;
431
- if (!muteEvent) {
432
- this.emit("closed");
433
- }
434
- return;
435
- });
479
+ async close(muteEvent = false) {
480
+ this.manuallyClosing = true;
481
+ this.log.info("Manually closing client.");
482
+ this.conn.close();
483
+ await this.database.close();
484
+ if (this.pingInterval) {
485
+ clearInterval(this.pingInterval);
486
+ }
487
+ if (this.mailInterval) {
488
+ clearInterval(this.mailInterval);
489
+ }
490
+ delete this.xKeyRing;
491
+ if (!muteEvent) {
492
+ this.emit("closed");
493
+ }
494
+ return;
436
495
  }
437
496
  /**
438
497
  * Gets the hex string representations of the public and private keys.
439
498
  */
440
499
  getKeys() {
441
500
  return {
442
- public: crypto_1.XUtils.encodeHex(this.signKeys.publicKey),
443
- private: crypto_1.XUtils.encodeHex(this.signKeys.secretKey),
501
+ public: XUtils.encodeHex(this.signKeys.publicKey),
502
+ private: XUtils.encodeHex(this.signKeys.secretKey),
444
503
  };
445
504
  }
446
- login(username, password) {
447
- return __awaiter(this, void 0, void 0, function* () {
448
- try {
449
- const res = yield axios_1.default.post(this.getHost() + "/auth", msgpack_lite_1.default.encode({
450
- username,
451
- password,
452
- }), {
453
- headers: { "Content-Type": "application/msgpack" },
454
- });
455
- const { user, token, } = msgpack_lite_1.default.decode(Buffer.from(res.data));
456
- const cookies = res.headers["set-cookie"];
457
- if (cookies) {
458
- for (const cookie of cookies) {
459
- if (cookie.includes("auth")) {
460
- this.addCookie(cookie);
461
- }
505
+ /**
506
+ * Authenticates with username/password and stores the auth token/cookie.
507
+ *
508
+ * @param username Account username.
509
+ * @param password Account password.
510
+ * @returns `null` on success, or the thrown error object on failure.
511
+ *
512
+ * @example
513
+ * ```ts
514
+ * const err = await client.login("alice", "correct horse battery staple");
515
+ * if (err) console.error(err);
516
+ * ```
517
+ */
518
+ async login(username, password) {
519
+ try {
520
+ const res = await ax.post(this.getHost() + "/auth", msgpack.encode({
521
+ username,
522
+ password,
523
+ }), {
524
+ headers: { "Content-Type": "application/msgpack" },
525
+ });
526
+ const { user, token } = msgpack.decode(Buffer.from(res.data));
527
+ const cookies = res.headers["set-cookie"];
528
+ if (cookies) {
529
+ for (const cookie of cookies) {
530
+ if (cookie.includes("auth")) {
531
+ this.addCookie(cookie);
462
532
  }
463
533
  }
464
- this.setUser(user);
465
- this.token = token;
466
- }
467
- catch (err) {
468
- console.error(err.toString());
469
- return err;
470
534
  }
471
- return null;
472
- });
535
+ this.setUser(user);
536
+ this.token = token;
537
+ }
538
+ catch (err) {
539
+ console.error(err.toString());
540
+ return err;
541
+ }
542
+ return null;
473
543
  }
474
544
  /**
475
545
  * Returns the authorization cookie details. Throws if you don't have a
476
546
  * valid authorization cookie.
477
547
  */
478
- whoami() {
479
- return __awaiter(this, void 0, void 0, function* () {
480
- const res = yield axios_1.default.post(this.getHost() + "/whoami", null, {
481
- withCredentials: true,
482
- responseType: "arraybuffer",
483
- });
484
- const whoami = msgpack_lite_1.default.decode(Buffer.from(res.data));
485
- return whoami;
548
+ /**
549
+ * Returns details about the currently authenticated session.
550
+ *
551
+ * @returns The authenticated user, token expiry, and active token.
552
+ *
553
+ * @example
554
+ * ```ts
555
+ * const auth = await client.whoami();
556
+ * console.log(auth.user.username, new Date(auth.exp));
557
+ * ```
558
+ */
559
+ async whoami() {
560
+ const res = await ax.post(this.getHost() + "/whoami", null, {
561
+ withCredentials: true,
562
+ responseType: "arraybuffer",
486
563
  });
564
+ const whoami = msgpack.decode(Buffer.from(res.data));
565
+ return whoami;
487
566
  }
488
- logout() {
489
- return __awaiter(this, void 0, void 0, function* () {
490
- yield axios_1.default.post(this.getHost() + "/goodbye");
491
- });
567
+ /**
568
+ * Logs out the current authenticated session from the server.
569
+ */
570
+ async logout() {
571
+ await ax.post(this.getHost() + "/goodbye");
492
572
  }
493
573
  /**
494
574
  * Connects your device to the chat. You must have an valid authorization cookie.
495
575
  * You can check whoami() to see before calling connect().
496
576
  */
497
- connect() {
498
- return __awaiter(this, void 0, void 0, function* () {
499
- const { user, token } = yield this.whoami();
500
- this.token = token;
501
- if (!user || !token) {
502
- throw new Error("Auth cookie missing or expired. Log in again.");
503
- }
504
- this.setUser(user);
505
- this.device = yield this.retrieveOrCreateDevice();
506
- const connectToken = yield this.getToken("connect");
507
- if (!connectToken) {
508
- throw new Error("Couldn't get connect token.");
509
- }
510
- const signed = tweetnacl_1.default.sign(Uint8Array.from(uuid.parse(connectToken.key)), this.signKeys.secretKey);
511
- const res = yield axios_1.default.post(this.getHost() + "/device/" + this.device.deviceID + "/connect", msgpack_lite_1.default.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
512
- const cookies = res.headers["set-cookie"];
513
- if (cookies) {
514
- for (const cookie of cookies) {
515
- if (cookie.includes("device")) {
516
- this.addCookie(cookie);
517
- }
577
+ async connect() {
578
+ const { user, token } = await this.whoami();
579
+ this.token = token;
580
+ if (!user || !token) {
581
+ throw new Error("Auth cookie missing or expired. Log in again.");
582
+ }
583
+ this.setUser(user);
584
+ this.device = await this.retrieveOrCreateDevice();
585
+ const connectToken = await this.getToken("connect");
586
+ if (!connectToken) {
587
+ throw new Error("Couldn't get connect token.");
588
+ }
589
+ const signed = nacl.sign(Uint8Array.from(uuid.parse(connectToken.key)), this.signKeys.secretKey);
590
+ const res = await ax.post(this.getHost() + "/device/" + this.device.deviceID + "/connect", msgpack.encode({ signed }), { headers: { "Content-Type": "application/msgpack" } });
591
+ const cookies = res.headers["set-cookie"];
592
+ if (cookies) {
593
+ for (const cookie of cookies) {
594
+ if (cookie.includes("device")) {
595
+ this.addCookie(cookie);
518
596
  }
519
597
  }
520
- this.log.info("Starting websocket.");
521
- this.initSocket();
522
- yield this.negotiateOTK();
523
- });
598
+ }
599
+ this.log.info("Starting websocket.");
600
+ this.initSocket();
601
+ await this.negotiateOTK();
524
602
  }
525
603
  /**
526
604
  * Registers a new account on the server.
@@ -530,519 +608,432 @@ class Client extends events_1.EventEmitter {
530
608
  *
531
609
  * @example [user, err] = await client.register("MyUsername");
532
610
  */
533
- register(username, password) {
534
- return __awaiter(this, void 0, void 0, function* () {
535
- while (!this.xKeyRing) {
536
- yield sleep_1.sleep(100);
611
+ async register(username, password) {
612
+ while (!this.xKeyRing) {
613
+ await sleep(100);
614
+ }
615
+ const regKey = await this.getToken("register");
616
+ if (regKey) {
617
+ const signKey = XUtils.encodeHex(this.signKeys.publicKey);
618
+ const signed = XUtils.encodeHex(nacl.sign(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
619
+ const regMsg = {
620
+ username,
621
+ signKey,
622
+ signed,
623
+ preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
624
+ preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
625
+ preKeyIndex: this.xKeyRing.preKeys.index,
626
+ password,
627
+ deviceName: `${os.platform()}`,
628
+ };
629
+ try {
630
+ const res = await ax.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
631
+ this.setUser(msgpack.decode(Buffer.from(res.data)));
632
+ return [this.getUser(), null];
537
633
  }
538
- const regKey = yield this.getToken("register");
539
- if (regKey) {
540
- const signKey = crypto_1.XUtils.encodeHex(this.signKeys.publicKey);
541
- const signed = crypto_1.XUtils.encodeHex(tweetnacl_1.default.sign(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
542
- const regMsg = {
543
- username,
544
- signKey,
545
- signed,
546
- preKey: crypto_1.XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
547
- preKeySignature: crypto_1.XUtils.encodeHex(this.xKeyRing.preKeys.signature),
548
- preKeyIndex: this.xKeyRing.preKeys.index,
549
- password,
550
- deviceName: `${os_1.default.platform()}`,
551
- };
552
- try {
553
- const res = yield axios_1.default.post(this.getHost() + "/register", msgpack_lite_1.default.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
554
- this.setUser(msgpack_lite_1.default.decode(Buffer.from(res.data)));
555
- return [this.getUser(), null];
634
+ catch (err) {
635
+ if (err.response) {
636
+ return [null, new Error(err.response.data.error)];
556
637
  }
557
- catch (err) {
558
- if (err.response) {
559
- return [null, new Error(err.response.data.error)];
560
- }
561
- else {
562
- return [null, err];
563
- }
638
+ else {
639
+ return [null, err];
564
640
  }
565
641
  }
566
- else {
567
- return [null, new Error("Couldn't get regkey from server.")];
568
- }
569
- });
642
+ }
643
+ else {
644
+ return [null, new Error("Couldn't get regkey from server.")];
645
+ }
570
646
  }
647
+ /**
648
+ * Returns a compact `<username><deviceID>` debug label.
649
+ */
571
650
  toString() {
572
- var _a, _b;
573
- return ((_a = this.user) === null || _a === void 0 ? void 0 : _a.username) + "<" + ((_b = this.device) === null || _b === void 0 ? void 0 : _b.deviceID) + ">";
651
+ return this.user?.username + "<" + this.device?.deviceID + ">";
574
652
  }
575
- redeemInvite(inviteID) {
576
- return __awaiter(this, void 0, void 0, function* () {
577
- const res = yield axios_1.default.patch(this.getHost() + "/invite/" + inviteID);
578
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
579
- });
653
+ async redeemInvite(inviteID) {
654
+ const res = await ax.patch(this.getHost() + "/invite/" + inviteID);
655
+ return msgpack.decode(Buffer.from(res.data));
580
656
  }
581
- retrieveInvites(serverID) {
582
- return __awaiter(this, void 0, void 0, function* () {
583
- const res = yield axios_1.default.get(this.getHost() + "/server/" + serverID + "/invites");
584
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
585
- });
657
+ async retrieveInvites(serverID) {
658
+ const res = await ax.get(this.getHost() + "/server/" + serverID + "/invites");
659
+ return msgpack.decode(Buffer.from(res.data));
586
660
  }
587
- createInvite(serverID, duration) {
588
- return __awaiter(this, void 0, void 0, function* () {
589
- const payload = {
590
- serverID,
591
- duration,
592
- };
593
- const res = yield axios_1.default.post(this.getHost() + "/server/" + serverID + "/invites", payload);
594
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
595
- });
661
+ async createInvite(serverID, duration) {
662
+ const payload = {
663
+ serverID,
664
+ duration,
665
+ };
666
+ const res = await ax.post(this.getHost() + "/server/" + serverID + "/invites", payload);
667
+ return msgpack.decode(Buffer.from(res.data));
596
668
  }
597
- retrieveEmojiList(serverID) {
598
- return __awaiter(this, void 0, void 0, function* () {
599
- const res = yield axios_1.default.get(this.getHost() + "/server/" + serverID + "/emoji");
600
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
601
- });
669
+ async retrieveEmojiList(serverID) {
670
+ const res = await ax.get(this.getHost() + "/server/" + serverID + "/emoji");
671
+ return msgpack.decode(Buffer.from(res.data));
602
672
  }
603
- retrieveEmojiByID(emojiID) {
604
- return __awaiter(this, void 0, void 0, function* () {
605
- const res = yield axios_1.default.get(this.getHost() + "/emoji/" + emojiID + "/details");
606
- // this is actually empty string
607
- if (!res.data) {
608
- return null;
609
- }
610
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
611
- });
673
+ async retrieveEmojiByID(emojiID) {
674
+ const res = await ax.get(this.getHost() + "/emoji/" + emojiID + "/details");
675
+ // this is actually empty string
676
+ if (!res.data) {
677
+ return null;
678
+ }
679
+ return msgpack.decode(Buffer.from(res.data));
612
680
  }
613
- leaveServer(serverID) {
614
- return __awaiter(this, void 0, void 0, function* () {
615
- const permissionList = yield this.permissions.retrieve();
616
- for (const permission of permissionList) {
617
- if (permission.resourceID === serverID) {
618
- yield this.deletePermission(permission.permissionID);
619
- }
681
+ async leaveServer(serverID) {
682
+ const permissionList = await this.permissions.retrieve();
683
+ for (const permission of permissionList) {
684
+ if (permission.resourceID === serverID) {
685
+ await this.deletePermission(permission.permissionID);
620
686
  }
621
- });
687
+ }
622
688
  }
623
- kickUser(userID, serverID) {
624
- return __awaiter(this, void 0, void 0, function* () {
625
- const permissionList = yield this.fetchPermissionList(serverID);
626
- for (const permission of permissionList) {
627
- if (userID === permission.userID) {
628
- yield this.deletePermission(permission.permissionID);
629
- return;
630
- }
689
+ async kickUser(userID, serverID) {
690
+ const permissionList = await this.fetchPermissionList(serverID);
691
+ for (const permission of permissionList) {
692
+ if (userID === permission.userID) {
693
+ await this.deletePermission(permission.permissionID);
694
+ return;
631
695
  }
632
- throw new Error("Couldn't kick user.");
633
- });
696
+ }
697
+ throw new Error("Couldn't kick user.");
634
698
  }
635
699
  addCookie(cookie) {
636
700
  if (!this.cookies.includes(cookie)) {
637
701
  this.cookies.push(cookie);
638
702
  this.log.info("cookies changed", this.getCookies());
639
- if (browser_or_node_1.isNode) {
640
- axios_1.default.defaults.headers.cookie = this.cookies.join(";");
703
+ if (isNode) {
704
+ ax.defaults.headers.cookie = this.cookies.join(";");
641
705
  }
642
706
  }
643
707
  }
644
708
  getCookies() {
645
709
  return this.cookies.join(";");
646
710
  }
647
- uploadEmoji(emoji, name, serverID) {
648
- return __awaiter(this, void 0, void 0, function* () {
649
- if (typeof FormData !== "undefined") {
650
- const fpayload = new FormData();
651
- fpayload.set("emoji", new Blob([emoji]));
652
- fpayload.set("name", name);
653
- try {
654
- const res = yield axios_1.default.post(this.getHost() + "/emoji/" + serverID, fpayload, {
655
- headers: { "Content-Type": "multipart/form-data" },
656
- onUploadProgress: (progressEvent) => {
657
- const percentCompleted = Math.round((progressEvent.loaded * 100) /
658
- progressEvent.total);
659
- const { loaded, total } = progressEvent;
660
- const progress = {
661
- direction: "upload",
662
- token: name,
663
- progress: percentCompleted,
664
- loaded,
665
- total,
666
- };
667
- this.emit("fileProgress", progress);
668
- },
669
- });
670
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
671
- }
672
- catch (err) {
673
- return null;
674
- }
675
- }
676
- const payload = {
677
- file: crypto_1.XUtils.encodeBase64(emoji),
678
- name,
679
- };
711
+ async uploadEmoji(emoji, name, serverID) {
712
+ if (typeof FormData !== "undefined") {
713
+ const fpayload = new FormData();
714
+ fpayload.set("emoji", new Blob([new Uint8Array(emoji)]));
715
+ fpayload.set("name", name);
680
716
  try {
681
- const res = yield axios_1.default.post(this.getHost() + "/emoji/" + serverID + "/json", msgpack_lite_1.default.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
682
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
717
+ const res = await ax.post(this.getHost() + "/emoji/" + serverID, fpayload, {
718
+ headers: { "Content-Type": "multipart/form-data" },
719
+ onUploadProgress: (progressEvent) => {
720
+ const percentCompleted = Math.round((progressEvent.loaded * 100) /
721
+ (progressEvent.total ?? 1));
722
+ const { loaded, total = 0 } = progressEvent;
723
+ const progress = {
724
+ direction: "upload",
725
+ token: name,
726
+ progress: percentCompleted,
727
+ loaded,
728
+ total,
729
+ };
730
+ this.emit("fileProgress", progress);
731
+ },
732
+ });
733
+ return msgpack.decode(Buffer.from(res.data));
683
734
  }
684
735
  catch (err) {
685
736
  return null;
686
737
  }
687
- });
738
+ }
739
+ const payload = {
740
+ file: XUtils.encodeBase64(emoji),
741
+ name,
742
+ };
743
+ try {
744
+ const res = await ax.post(this.getHost() + "/emoji/" + serverID + "/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
745
+ return msgpack.decode(Buffer.from(res.data));
746
+ }
747
+ catch (err) {
748
+ return null;
749
+ }
688
750
  }
689
- retrieveOrCreateDevice() {
690
- var _a;
691
- return __awaiter(this, void 0, void 0, function* () {
692
- let device;
693
- try {
694
- const res = yield axios_1.default.get(this.prefixes.HTTP +
695
- this.host +
696
- "/device/" +
697
- crypto_1.XUtils.encodeHex(this.signKeys.publicKey));
698
- device = msgpack_lite_1.default.decode(Buffer.from(res.data));
699
- }
700
- catch (err) {
701
- this.log.error(err.toString());
702
- if (((_a = err.response) === null || _a === void 0 ? void 0 : _a.status) === 404) {
703
- // just in case
704
- yield this.database.purgeKeyData();
705
- yield this.populateKeyRing();
706
- this.log.info("Attempting to register device.");
707
- const newDevice = yield this.registerDevice();
708
- if (newDevice) {
709
- device = newDevice;
710
- }
711
- else {
712
- throw new Error("Error registering device.");
713
- }
751
+ async retrieveOrCreateDevice() {
752
+ let device;
753
+ try {
754
+ const res = await ax.get(this.prefixes.HTTP +
755
+ this.host +
756
+ "/device/" +
757
+ XUtils.encodeHex(this.signKeys.publicKey));
758
+ device = msgpack.decode(Buffer.from(res.data));
759
+ }
760
+ catch (err) {
761
+ this.log.error(err.toString());
762
+ if (err.response?.status === 404) {
763
+ // just in case
764
+ await this.database.purgeKeyData();
765
+ await this.populateKeyRing();
766
+ this.log.info("Attempting to register device.");
767
+ const newDevice = await this.registerDevice();
768
+ if (newDevice) {
769
+ device = newDevice;
714
770
  }
715
771
  else {
716
- throw err;
772
+ throw new Error("Error registering device.");
717
773
  }
718
774
  }
719
- this.log.info("Got device " + JSON.stringify(device, null, 4));
720
- return device;
721
- });
722
- }
723
- registerDevice() {
724
- return __awaiter(this, void 0, void 0, function* () {
725
- while (!this.xKeyRing) {
726
- yield sleep_1.sleep(100);
727
- }
728
- const token = yield this.getToken("device");
729
- const [userDetails, err] = yield this.retrieveUserDBEntry(this.user.username);
730
- if (!userDetails) {
731
- throw new Error("Username not found " + this.user.username);
732
- }
733
- if (err) {
734
- throw err;
735
- }
736
- if (!token) {
737
- throw new Error("Couldn't fetch token.");
738
- }
739
- const signKey = this.getKeys().public;
740
- const signed = crypto_1.XUtils.encodeHex(tweetnacl_1.default.sign(Uint8Array.from(uuid.parse(token.key)), this.signKeys.secretKey));
741
- const devMsg = {
742
- username: userDetails.username,
743
- signKey,
744
- signed,
745
- preKey: crypto_1.XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
746
- preKeySignature: crypto_1.XUtils.encodeHex(this.xKeyRing.preKeys.signature),
747
- preKeyIndex: this.xKeyRing.preKeys.index,
748
- deviceName: `${os_1.default.platform()}`,
749
- };
750
- try {
751
- const res = yield axios_1.default.post(this.prefixes.HTTP +
752
- this.host +
753
- "/user/" +
754
- userDetails.userID +
755
- "/devices", msgpack_lite_1.default.encode(devMsg), { headers: { "Content-Type": "application/msgpack" } });
756
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
757
- }
758
- catch (err) {
775
+ else {
759
776
  throw err;
760
777
  }
761
- });
778
+ }
779
+ this.log.info("Got device " + JSON.stringify(device, null, 4));
780
+ return device;
762
781
  }
763
- getToken(type) {
764
- return __awaiter(this, void 0, void 0, function* () {
765
- try {
766
- const res = yield axios_1.default.get(this.getHost() + "/token/" + type, {
767
- responseType: "arraybuffer",
768
- });
769
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
770
- }
771
- catch (err) {
772
- this.log.warn(err.toString());
773
- return null;
774
- }
775
- });
782
+ async registerDevice() {
783
+ while (!this.xKeyRing) {
784
+ await sleep(100);
785
+ }
786
+ const token = await this.getToken("device");
787
+ const [userDetails, err] = await this.retrieveUserDBEntry(this.user.username);
788
+ if (!userDetails) {
789
+ throw new Error("Username not found " + this.user.username);
790
+ }
791
+ if (err) {
792
+ throw err;
793
+ }
794
+ if (!token) {
795
+ throw new Error("Couldn't fetch token.");
796
+ }
797
+ const signKey = this.getKeys().public;
798
+ const signed = XUtils.encodeHex(nacl.sign(Uint8Array.from(uuid.parse(token.key)), this.signKeys.secretKey));
799
+ const devMsg = {
800
+ username: userDetails.username,
801
+ signKey,
802
+ signed,
803
+ preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
804
+ preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
805
+ preKeyIndex: this.xKeyRing.preKeys.index,
806
+ deviceName: `${os.platform()}`,
807
+ };
808
+ try {
809
+ const res = await ax.post(this.prefixes.HTTP +
810
+ this.host +
811
+ "/user/" +
812
+ userDetails.userID +
813
+ "/devices", msgpack.encode(devMsg), { headers: { "Content-Type": "application/msgpack" } });
814
+ return msgpack.decode(Buffer.from(res.data));
815
+ }
816
+ catch (err) {
817
+ throw err;
818
+ }
776
819
  }
777
- uploadAvatar(avatar) {
778
- return __awaiter(this, void 0, void 0, function* () {
779
- if (typeof FormData !== "undefined") {
780
- const fpayload = new FormData();
781
- fpayload.set("avatar", new Blob([avatar]));
782
- yield axios_1.default.post(this.prefixes.HTTP +
783
- this.host +
784
- "/avatar/" +
785
- this.me.user().userID, fpayload, {
786
- headers: { "Content-Type": "multipart/form-data" },
787
- onUploadProgress: (progressEvent) => {
788
- const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
789
- const { loaded, total } = progressEvent;
790
- const progress = {
791
- direction: "upload",
792
- token: this.getUser().userID,
793
- progress: percentCompleted,
794
- loaded,
795
- total,
796
- };
797
- this.emit("fileProgress", progress);
798
- },
799
- });
800
- return;
801
- }
802
- const payload = {
803
- file: crypto_1.XUtils.encodeBase64(avatar),
804
- };
805
- yield axios_1.default.post(this.prefixes.HTTP +
820
+ async getToken(type) {
821
+ try {
822
+ const res = await ax.get(this.getHost() + "/token/" + type, {
823
+ responseType: "arraybuffer",
824
+ });
825
+ return msgpack.decode(Buffer.from(res.data));
826
+ }
827
+ catch (err) {
828
+ this.log.warn(err.toString());
829
+ return null;
830
+ }
831
+ }
832
+ async uploadAvatar(avatar) {
833
+ if (typeof FormData !== "undefined") {
834
+ const fpayload = new FormData();
835
+ fpayload.set("avatar", new Blob([new Uint8Array(avatar)]));
836
+ await ax.post(this.prefixes.HTTP +
806
837
  this.host +
807
838
  "/avatar/" +
808
- this.me.user().userID +
809
- "/json", msgpack_lite_1.default.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
810
- });
839
+ this.me.user().userID, fpayload, {
840
+ headers: { "Content-Type": "multipart/form-data" },
841
+ onUploadProgress: (progressEvent) => {
842
+ const percentCompleted = Math.round((progressEvent.loaded * 100) /
843
+ (progressEvent.total ?? 1));
844
+ const { loaded, total = 0 } = progressEvent;
845
+ const progress = {
846
+ direction: "upload",
847
+ token: this.getUser().userID,
848
+ progress: percentCompleted,
849
+ loaded,
850
+ total,
851
+ };
852
+ this.emit("fileProgress", progress);
853
+ },
854
+ });
855
+ return;
856
+ }
857
+ const payload = {
858
+ file: XUtils.encodeBase64(avatar),
859
+ };
860
+ await ax.post(this.prefixes.HTTP +
861
+ this.host +
862
+ "/avatar/" +
863
+ this.me.user().userID +
864
+ "/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
811
865
  }
812
866
  /**
813
867
  * Gets a list of permissions for a server.
814
868
  *
815
869
  * @returns - The list of IPermissions objects.
816
870
  */
817
- fetchPermissionList(serverID) {
818
- return __awaiter(this, void 0, void 0, function* () {
819
- const res = yield axios_1.default.get(this.prefixes.HTTP +
820
- this.host +
821
- "/server/" +
822
- serverID +
823
- "/permissions");
824
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
825
- });
871
+ async fetchPermissionList(serverID) {
872
+ const res = await ax.get(this.prefixes.HTTP +
873
+ this.host +
874
+ "/server/" +
875
+ serverID +
876
+ "/permissions");
877
+ return msgpack.decode(Buffer.from(res.data));
826
878
  }
827
879
  /**
828
880
  * Gets all permissions for the logged in user.
829
881
  *
830
882
  * @returns - The list of IPermissions objects.
831
883
  */
832
- getPermissions() {
833
- return __awaiter(this, void 0, void 0, function* () {
834
- const res = yield axios_1.default.get(this.getHost() + "/user/" + this.getUser().userID + "/permissions");
835
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
836
- });
884
+ async getPermissions() {
885
+ const res = await ax.get(this.getHost() + "/user/" + this.getUser().userID + "/permissions");
886
+ return msgpack.decode(Buffer.from(res.data));
837
887
  }
838
- deletePermission(permissionID) {
839
- return __awaiter(this, void 0, void 0, function* () {
840
- yield axios_1.default.delete(this.getHost() + "/permission/" + permissionID);
841
- });
888
+ async deletePermission(permissionID) {
889
+ await ax.delete(this.getHost() + "/permission/" + permissionID);
842
890
  }
843
- retrieveFile(fileID, key) {
844
- return __awaiter(this, void 0, void 0, function* () {
845
- try {
846
- const detailsRes = yield axios_1.default.get(this.getHost() + "/file/" + fileID + "/details");
847
- const details = msgpack_lite_1.default.decode(Buffer.from(detailsRes.data));
848
- const res = yield axios_1.default.get(this.getHost() + "/file/" + fileID, {
849
- onDownloadProgress: (progressEvent) => {
850
- const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
851
- const { loaded, total } = progressEvent;
852
- const progress = {
853
- direction: "download",
854
- token: fileID,
855
- progress: percentCompleted,
856
- loaded,
857
- total,
858
- };
859
- this.emit("fileProgress", progress);
860
- },
861
- });
862
- const fileData = res.data;
863
- const decrypted = tweetnacl_1.default.secretbox.open(Uint8Array.from(Buffer.from(fileData)), crypto_1.XUtils.decodeHex(details.nonce), crypto_1.XUtils.decodeHex(key));
864
- if (decrypted) {
865
- const resp = {
866
- details,
867
- data: Buffer.from(decrypted),
891
+ async retrieveFile(fileID, key) {
892
+ try {
893
+ const detailsRes = await ax.get(this.getHost() + "/file/" + fileID + "/details");
894
+ const details = msgpack.decode(Buffer.from(detailsRes.data));
895
+ const res = await ax.get(this.getHost() + "/file/" + fileID, {
896
+ onDownloadProgress: (progressEvent) => {
897
+ const percentCompleted = Math.round((progressEvent.loaded * 100) /
898
+ (progressEvent.total ?? 1));
899
+ const { loaded, total = 0 } = progressEvent;
900
+ const progress = {
901
+ direction: "download",
902
+ token: fileID,
903
+ progress: percentCompleted,
904
+ loaded,
905
+ total,
868
906
  };
869
- return resp;
870
- }
871
- throw new Error("Decryption failed.");
872
- }
873
- catch (err) {
874
- throw err;
907
+ this.emit("fileProgress", progress);
908
+ },
909
+ });
910
+ const fileData = res.data;
911
+ const decrypted = nacl.secretbox.open(Uint8Array.from(Buffer.from(fileData)), XUtils.decodeHex(details.nonce), XUtils.decodeHex(key));
912
+ if (decrypted) {
913
+ const resp = {
914
+ details,
915
+ data: Buffer.from(decrypted),
916
+ };
917
+ return resp;
875
918
  }
876
- });
919
+ throw new Error("Decryption failed.");
920
+ }
921
+ catch (err) {
922
+ throw err;
923
+ }
877
924
  }
878
- deleteServer(serverID) {
879
- return __awaiter(this, void 0, void 0, function* () {
880
- yield axios_1.default.delete(this.getHost() + "/server/" + serverID);
881
- });
925
+ async deleteServer(serverID) {
926
+ await ax.delete(this.getHost() + "/server/" + serverID);
882
927
  }
883
928
  /**
884
929
  * Initializes the keyring. This must be called before anything else.
885
930
  */
886
- init() {
887
- return __awaiter(this, void 0, void 0, function* () {
888
- if (this.hasInit) {
889
- return new Error("You should only call init() once.");
931
+ async init() {
932
+ if (this.hasInit) {
933
+ return new Error("You should only call init() once.");
934
+ }
935
+ this.hasInit = true;
936
+ await this.populateKeyRing();
937
+ this.on("message", async (message) => {
938
+ if (message.direction === "outgoing" && !message.forward) {
939
+ this.forward(message);
940
+ }
941
+ if (message.direction === "incoming" &&
942
+ message.recipient === message.sender) {
943
+ return;
890
944
  }
891
- this.hasInit = true;
892
- yield this.populateKeyRing();
893
- this.on("message", (message) => __awaiter(this, void 0, void 0, function* () {
894
- if (message.direction === "outgoing" && !message.forward) {
895
- this.forward(message);
896
- }
897
- if (message.direction === "incoming" &&
898
- message.recipient === message.sender) {
899
- return;
900
- }
901
- yield this.database.saveMessage(message);
902
- }));
903
- this.emit("ready");
945
+ await this.database.saveMessage(message);
904
946
  });
947
+ this.emit("ready");
905
948
  }
906
- deleteChannel(channelID) {
907
- return __awaiter(this, void 0, void 0, function* () {
908
- yield axios_1.default.delete(this.getHost() + "/channel/" + channelID);
909
- });
949
+ async deleteChannel(channelID) {
950
+ await ax.delete(this.getHost() + "/channel/" + channelID);
910
951
  }
911
952
  // returns the file details and the encryption key
912
- createFile(file) {
913
- return __awaiter(this, void 0, void 0, function* () {
914
- this.log.info("Creating file, size: " + formatBytes_1.formatBytes(Buffer.byteLength(file)));
915
- const nonce = crypto_1.xMakeNonce();
916
- const key = tweetnacl_1.default.box.keyPair();
917
- const box = tweetnacl_1.default.secretbox(Uint8Array.from(file), nonce, key.secretKey);
918
- this.log.info("Encrypted size: " + formatBytes_1.formatBytes(Buffer.byteLength(box)));
919
- if (typeof FormData !== "undefined") {
920
- const fpayload = new FormData();
921
- fpayload.set("owner", this.getDevice().deviceID);
922
- fpayload.set("nonce", crypto_1.XUtils.encodeHex(nonce));
923
- fpayload.set("file", new Blob([box]));
924
- const fres = yield axios_1.default.post(this.getHost() + "/file", fpayload, {
925
- headers: { "Content-Type": "multipart/form-data" },
926
- onUploadProgress: (progressEvent) => {
927
- const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
928
- const { loaded, total } = progressEvent;
929
- const progress = {
930
- direction: "upload",
931
- token: crypto_1.XUtils.encodeHex(nonce),
932
- progress: percentCompleted,
933
- loaded,
934
- total,
935
- };
936
- this.emit("fileProgress", progress);
937
- },
938
- });
939
- const fcreatedFile = msgpack_lite_1.default.decode(Buffer.from(fres.data));
940
- return [fcreatedFile, crypto_1.XUtils.encodeHex(key.secretKey)];
941
- }
942
- const payload = {
943
- owner: this.getDevice().deviceID,
944
- nonce: crypto_1.XUtils.encodeHex(nonce),
945
- file: crypto_1.XUtils.encodeBase64(box),
946
- };
947
- const res = yield axios_1.default.post(this.getHost() + "/file/json", msgpack_lite_1.default.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
948
- const createdFile = msgpack_lite_1.default.decode(Buffer.from(res.data));
949
- return [createdFile, crypto_1.XUtils.encodeHex(key.secretKey)];
950
- });
953
+ async createFile(file) {
954
+ this.log.info("Creating file, size: " + formatBytes(Buffer.byteLength(file)));
955
+ const nonce = xMakeNonce();
956
+ const key = nacl.box.keyPair();
957
+ const box = nacl.secretbox(Uint8Array.from(file), nonce, key.secretKey);
958
+ this.log.info("Encrypted size: " + formatBytes(Buffer.byteLength(box)));
959
+ if (typeof FormData !== "undefined") {
960
+ const fpayload = new FormData();
961
+ fpayload.set("owner", this.getDevice().deviceID);
962
+ fpayload.set("nonce", XUtils.encodeHex(nonce));
963
+ fpayload.set("file", new Blob([new Uint8Array(box)]));
964
+ const fres = await ax.post(this.getHost() + "/file", fpayload, {
965
+ headers: { "Content-Type": "multipart/form-data" },
966
+ onUploadProgress: (progressEvent) => {
967
+ const percentCompleted = Math.round((progressEvent.loaded * 100) /
968
+ (progressEvent.total ?? 1));
969
+ const { loaded, total = 0 } = progressEvent;
970
+ const progress = {
971
+ direction: "upload",
972
+ token: XUtils.encodeHex(nonce),
973
+ progress: percentCompleted,
974
+ loaded,
975
+ total,
976
+ };
977
+ this.emit("fileProgress", progress);
978
+ },
979
+ });
980
+ const fcreatedFile = msgpack.decode(Buffer.from(fres.data));
981
+ return [fcreatedFile, XUtils.encodeHex(key.secretKey)];
982
+ }
983
+ const payload = {
984
+ owner: this.getDevice().deviceID,
985
+ nonce: XUtils.encodeHex(nonce),
986
+ file: XUtils.encodeBase64(box),
987
+ };
988
+ const res = await ax.post(this.getHost() + "/file/json", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
989
+ const createdFile = msgpack.decode(Buffer.from(res.data));
990
+ return [createdFile, XUtils.encodeHex(key.secretKey)];
951
991
  }
952
- getUserList(channelID) {
953
- return __awaiter(this, void 0, void 0, function* () {
954
- const res = yield axios_1.default.post(this.getHost() + "/userList/" + channelID);
955
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
956
- });
992
+ async getUserList(channelID) {
993
+ const res = await ax.post(this.getHost() + "/userList/" + channelID);
994
+ return msgpack.decode(Buffer.from(res.data));
957
995
  }
958
- markSessionVerified(sessionID) {
959
- return __awaiter(this, void 0, void 0, function* () {
960
- return this.database.markSessionVerified(sessionID);
961
- });
996
+ async markSessionVerified(sessionID) {
997
+ return this.database.markSessionVerified(sessionID);
962
998
  }
963
- getGroupHistory(channelID) {
964
- return __awaiter(this, void 0, void 0, function* () {
965
- const messages = yield this.database.getGroupHistory(channelID);
966
- return messages;
967
- });
999
+ async getGroupHistory(channelID) {
1000
+ const messages = await this.database.getGroupHistory(channelID);
1001
+ return messages;
968
1002
  }
969
- deleteHistory(channelOrUserID, olderThan) {
970
- return __awaiter(this, void 0, void 0, function* () {
971
- yield this.database.deleteHistory(channelOrUserID, olderThan);
972
- });
1003
+ async deleteHistory(channelOrUserID, olderThan) {
1004
+ await this.database.deleteHistory(channelOrUserID, olderThan);
973
1005
  }
974
- purgeHistory() {
975
- return __awaiter(this, void 0, void 0, function* () {
976
- yield this.database.purgeHistory();
977
- });
1006
+ async purgeHistory() {
1007
+ await this.database.purgeHistory();
978
1008
  }
979
- getMessageHistory(userID) {
980
- return __awaiter(this, void 0, void 0, function* () {
981
- const messages = yield this.database.getMessageHistory(userID);
982
- return messages;
983
- });
1009
+ async getMessageHistory(userID) {
1010
+ const messages = await this.database.getMessageHistory(userID);
1011
+ return messages;
984
1012
  }
985
- sendMessage(userID, message) {
986
- var _a, _b;
987
- return __awaiter(this, void 0, void 0, function* () {
988
- try {
989
- const [userEntry, err] = yield this.retrieveUserDBEntry(userID);
990
- if (err) {
991
- throw err;
992
- }
993
- if (!userEntry) {
994
- throw new Error("Couldn't get user entry.");
995
- }
996
- let deviceList = yield this.getUserDeviceList(userID);
997
- if (!deviceList) {
998
- let retries = 0;
999
- while (!deviceList) {
1000
- deviceList = yield this.getUserDeviceList(userID);
1001
- retries++;
1002
- if (retries > 3) {
1003
- throw new Error("Couldn't get device list.");
1004
- }
1005
- }
1006
- }
1007
- const mailID = uuid.v4();
1008
- const promises = [];
1009
- for (const device of deviceList) {
1010
- promises.push(this.sendMail(device, userEntry, crypto_1.XUtils.decodeUTF8(message), null, mailID, false));
1011
- }
1012
- Promise.allSettled(promises).then((results) => {
1013
- for (const result of results) {
1014
- const { status } = result;
1015
- if (status === "rejected") {
1016
- this.log.warn("Message failed.");
1017
- this.log.warn(result);
1018
- }
1019
- }
1020
- });
1021
- }
1022
- catch (err) {
1023
- this.log.error("Message " + (((_a = err.message) === null || _a === void 0 ? void 0 : _a.mailID) || "") + " threw exception.");
1024
- this.log.error(err.toString());
1025
- if ((_b = err.message) === null || _b === void 0 ? void 0 : _b.mailID) {
1026
- yield this.database.deleteMessage(err.message.mailID);
1027
- }
1013
+ async sendMessage(userID, message) {
1014
+ try {
1015
+ const [userEntry, err] = await this.retrieveUserDBEntry(userID);
1016
+ if (err) {
1028
1017
  throw err;
1029
1018
  }
1030
- });
1031
- }
1032
- sendGroupMessage(channelID, message) {
1033
- return __awaiter(this, void 0, void 0, function* () {
1034
- const userList = yield this.getUserList(channelID);
1035
- for (const user of userList) {
1036
- this.userRecords[user.userID] = user;
1019
+ if (!userEntry) {
1020
+ throw new Error("Couldn't get user entry.");
1021
+ }
1022
+ let deviceList = await this.getUserDeviceList(userID);
1023
+ if (!deviceList) {
1024
+ let retries = 0;
1025
+ while (!deviceList) {
1026
+ deviceList = await this.getUserDeviceList(userID);
1027
+ retries++;
1028
+ if (retries > 3) {
1029
+ throw new Error("Couldn't get device list.");
1030
+ }
1031
+ }
1037
1032
  }
1038
- this.log.info("Sending to userlist:\n" + JSON.stringify(userList, null, 4));
1039
1033
  const mailID = uuid.v4();
1040
1034
  const promises = [];
1041
- const userIDs = [...new Set(userList.map((user) => user.userID))];
1042
- const devices = yield this.getMultiUserDeviceList(userIDs);
1043
- this.log.info("Retrieved devicelist:\n" + JSON.stringify(devices, null, 4));
1044
- for (const device of devices) {
1045
- promises.push(this.sendMail(device, this.userRecords[device.owner], crypto_1.XUtils.decodeUTF8(message), uint8uuid_1.uuidToUint8(channelID), mailID, false));
1035
+ for (const device of deviceList) {
1036
+ promises.push(this.sendMail(device, userEntry, XUtils.decodeUTF8(message), null, mailID, false));
1046
1037
  }
1047
1038
  Promise.allSettled(promises).then((results) => {
1048
1039
  for (const result of results) {
@@ -1053,102 +1044,131 @@ class Client extends events_1.EventEmitter {
1053
1044
  }
1054
1045
  }
1055
1046
  });
1056
- });
1047
+ }
1048
+ catch (err) {
1049
+ this.log.error("Message " + (err.message?.mailID || "") + " threw exception.");
1050
+ this.log.error(err.toString());
1051
+ if (err.message?.mailID) {
1052
+ await this.database.deleteMessage(err.message.mailID);
1053
+ }
1054
+ throw err;
1055
+ }
1057
1056
  }
1058
- createServer(name) {
1059
- return __awaiter(this, void 0, void 0, function* () {
1060
- const res = yield axios_1.default.post(this.getHost() + "/server/" + btoa_1.default(name));
1061
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1057
+ async sendGroupMessage(channelID, message) {
1058
+ const userList = await this.getUserList(channelID);
1059
+ for (const user of userList) {
1060
+ this.userRecords[user.userID] = user;
1061
+ }
1062
+ this.log.info("Sending to userlist:\n" + JSON.stringify(userList, null, 4));
1063
+ const mailID = uuid.v4();
1064
+ const promises = [];
1065
+ const userIDs = [...new Set(userList.map((user) => user.userID))];
1066
+ const devices = await this.getMultiUserDeviceList(userIDs);
1067
+ this.log.info("Retrieved devicelist:\n" + JSON.stringify(devices, null, 4));
1068
+ for (const device of devices) {
1069
+ promises.push(this.sendMail(device, this.userRecords[device.owner], XUtils.decodeUTF8(message), uuidToUint8(channelID), mailID, false));
1070
+ }
1071
+ Promise.allSettled(promises).then((results) => {
1072
+ for (const result of results) {
1073
+ const { status } = result;
1074
+ if (status === "rejected") {
1075
+ this.log.warn("Message failed.");
1076
+ this.log.warn(result);
1077
+ }
1078
+ }
1062
1079
  });
1063
1080
  }
1064
- forward(message) {
1065
- return __awaiter(this, void 0, void 0, function* () {
1066
- const copy = Object.assign({}, message);
1067
- if (this.forwarded.includes(copy.mailID)) {
1068
- return;
1069
- }
1070
- this.forwarded.push(copy.mailID);
1071
- if (this.forwarded.length > 1000) {
1072
- this.forwarded.shift();
1073
- }
1074
- const msgBytes = Uint8Array.from(msgpack_lite_1.default.encode(copy));
1075
- const devices = yield this.getUserDeviceList(this.getUser().userID);
1076
- this.log.info("Forwarding to my other devices, deviceList length is " + (devices === null || devices === void 0 ? void 0 : devices.length));
1077
- if (!devices) {
1078
- throw new Error("Couldn't get own devices.");
1079
- }
1080
- const promises = [];
1081
- for (const device of devices) {
1082
- if (device.deviceID !== this.getDevice().deviceID) {
1083
- promises.push(this.sendMail(device, this.getUser(), msgBytes, null, copy.mailID, true));
1084
- }
1081
+ async createServer(name) {
1082
+ const res = await ax.post(this.getHost() + "/server/" + btoa(name));
1083
+ return msgpack.decode(Buffer.from(res.data));
1084
+ }
1085
+ async forward(message) {
1086
+ const copy = { ...message };
1087
+ if (this.forwarded.includes(copy.mailID)) {
1088
+ return;
1089
+ }
1090
+ this.forwarded.push(copy.mailID);
1091
+ if (this.forwarded.length > 1000) {
1092
+ this.forwarded.shift();
1093
+ }
1094
+ const msgBytes = Uint8Array.from(msgpack.encode(copy));
1095
+ const devices = await this.getUserDeviceList(this.getUser().userID);
1096
+ this.log.info("Forwarding to my other devices, deviceList length is " +
1097
+ devices?.length);
1098
+ if (!devices) {
1099
+ throw new Error("Couldn't get own devices.");
1100
+ }
1101
+ const promises = [];
1102
+ for (const device of devices) {
1103
+ if (device.deviceID !== this.getDevice().deviceID) {
1104
+ promises.push(this.sendMail(device, this.getUser(), msgBytes, null, copy.mailID, true));
1085
1105
  }
1086
- Promise.allSettled(promises).then((results) => {
1087
- for (const result of results) {
1088
- const { status } = result;
1089
- if (status === "rejected") {
1090
- this.log.warn("Message failed.");
1091
- this.log.warn(result);
1092
- }
1106
+ }
1107
+ Promise.allSettled(promises).then((results) => {
1108
+ for (const result of results) {
1109
+ const { status } = result;
1110
+ if (status === "rejected") {
1111
+ this.log.warn("Message failed.");
1112
+ this.log.warn(result);
1093
1113
  }
1094
- });
1114
+ }
1095
1115
  });
1096
1116
  }
1097
1117
  /* Sends encrypted mail to a user. */
1098
- sendMail(device, user, msg, group, mailID, forward, retry = false) {
1099
- return __awaiter(this, void 0, void 0, function* () {
1100
- while (this.sending[device.deviceID] !== undefined) {
1101
- this.log.warn("Sending in progress to device ID " +
1102
- device.deviceID +
1103
- ", waiting.");
1104
- yield sleep_1.sleep(100);
1105
- }
1106
- this.log.info("Sending mail to user: \n" + JSON.stringify(user, null, 4));
1107
- this.log.info("Sending mail to device:\n " +
1108
- JSON.stringify(device.deviceID, null, 4));
1109
- this.sending[device.deviceID] = device;
1110
- const session = yield this.database.getSessionByDeviceID(device.deviceID);
1111
- if (!session || retry) {
1112
- this.log.info("Creating new session for " + device.deviceID);
1113
- yield this.createSession(device, user, msg, group, mailID, forward);
1114
- return;
1115
- }
1116
- else {
1117
- this.log.info("Found existing session for " + device.deviceID);
1118
- }
1119
- const nonce = crypto_1.xMakeNonce();
1120
- const cipher = tweetnacl_1.default.secretbox(msg, nonce, session.SK);
1121
- const extra = session.publicKey;
1122
- const mail = {
1123
- mailType: types_1.XTypes.WS.MailType.subsequent,
1124
- mailID: mailID || uuid.v4(),
1125
- recipient: device.deviceID,
1126
- cipher,
1127
- nonce,
1128
- extra,
1129
- sender: this.getDevice().deviceID,
1130
- group,
1131
- forward,
1132
- authorID: this.getUser().userID,
1133
- readerID: session.userID,
1134
- };
1135
- const msgb = {
1136
- transmissionID: uuid.v4(),
1137
- type: "resource",
1138
- resourceType: "mail",
1139
- action: "CREATE",
1140
- data: mail,
1141
- };
1142
- const hmac = crypto_1.xHMAC(mail, session.SK);
1143
- this.log.info("Mail hash: " + object_hash_1.default(mail));
1144
- this.log.info("Calculated hmac: " + crypto_1.XUtils.encodeHex(hmac));
1145
- const outMsg = forward
1146
- ? Object.assign(Object.assign({}, msgpack_lite_1.default.decode(msg)), { forward: true }) : {
1118
+ async sendMail(device, user, msg, group, mailID, forward, retry = false) {
1119
+ while (this.sending[device.deviceID] !== undefined) {
1120
+ this.log.warn("Sending in progress to device ID " +
1121
+ device.deviceID +
1122
+ ", waiting.");
1123
+ await sleep(100);
1124
+ }
1125
+ this.log.info("Sending mail to user: \n" + JSON.stringify(user, null, 4));
1126
+ this.log.info("Sending mail to device:\n " +
1127
+ JSON.stringify(device.deviceID, null, 4));
1128
+ this.sending[device.deviceID] = device;
1129
+ const session = await this.database.getSessionByDeviceID(device.deviceID);
1130
+ if (!session || retry) {
1131
+ this.log.info("Creating new session for " + device.deviceID);
1132
+ await this.createSession(device, user, msg, group, mailID, forward);
1133
+ return;
1134
+ }
1135
+ else {
1136
+ this.log.info("Found existing session for " + device.deviceID);
1137
+ }
1138
+ const nonce = xMakeNonce();
1139
+ const cipher = nacl.secretbox(msg, nonce, session.SK);
1140
+ const extra = session.publicKey;
1141
+ const mail = {
1142
+ mailType: MailType.subsequent,
1143
+ mailID: mailID || uuid.v4(),
1144
+ recipient: device.deviceID,
1145
+ cipher,
1146
+ nonce,
1147
+ extra,
1148
+ sender: this.getDevice().deviceID,
1149
+ group,
1150
+ forward,
1151
+ authorID: this.getUser().userID,
1152
+ readerID: session.userID,
1153
+ };
1154
+ const msgb = {
1155
+ transmissionID: uuid.v4(),
1156
+ type: "resource",
1157
+ resourceType: "mail",
1158
+ action: "CREATE",
1159
+ data: mail,
1160
+ };
1161
+ const hmac = xHMAC(mail, session.SK);
1162
+ this.log.info("Mail hash: " + objectHash(mail));
1163
+ this.log.info("Calculated hmac: " + XUtils.encodeHex(hmac));
1164
+ const outMsg = forward
1165
+ ? { ...msgpack.decode(msg), forward: true }
1166
+ : {
1147
1167
  mailID: mail.mailID,
1148
1168
  sender: mail.sender,
1149
1169
  recipient: mail.recipient,
1150
- nonce: crypto_1.XUtils.encodeHex(mail.nonce),
1151
- message: crypto_1.XUtils.encodeUTF8(msg),
1170
+ nonce: XUtils.encodeHex(mail.nonce),
1171
+ message: XUtils.encodeUTF8(msg),
1152
1172
  direction: "outgoing",
1153
1173
  timestamp: new Date(Date.now()),
1154
1174
  decrypted: true,
@@ -1157,142 +1177,121 @@ class Client extends events_1.EventEmitter {
1157
1177
  authorID: mail.authorID,
1158
1178
  readerID: mail.readerID,
1159
1179
  };
1160
- this.emit("message", outMsg);
1161
- yield new Promise((res, rej) => {
1162
- const callback = (packedMsg) => __awaiter(this, void 0, void 0, function* () {
1163
- const [header, receivedMsg] = crypto_1.XUtils.unpackMessage(packedMsg);
1164
- if (receivedMsg.transmissionID === msgb.transmissionID) {
1165
- this.conn.off("message", callback);
1166
- if (receivedMsg.type === "success") {
1167
- res(receivedMsg.data);
1168
- }
1169
- else {
1170
- rej({
1171
- error: receivedMsg,
1172
- message: outMsg,
1173
- });
1174
- }
1180
+ this.emit("message", outMsg);
1181
+ await new Promise((res, rej) => {
1182
+ const callback = async (packedMsg) => {
1183
+ const [header, receivedMsg] = XUtils.unpackMessage(packedMsg);
1184
+ if (receivedMsg.transmissionID === msgb.transmissionID) {
1185
+ this.conn.off("message", callback);
1186
+ if (receivedMsg.type === "success") {
1187
+ res(receivedMsg.data);
1175
1188
  }
1176
- });
1177
- this.conn.on("message", callback);
1178
- this.send(msgb, hmac);
1179
- });
1180
- delete this.sending[device.deviceID];
1181
- });
1182
- }
1183
- getSessionList() {
1184
- return __awaiter(this, void 0, void 0, function* () {
1185
- return this.database.getAllSessions();
1189
+ else {
1190
+ rej({
1191
+ error: receivedMsg,
1192
+ message: outMsg,
1193
+ });
1194
+ }
1195
+ }
1196
+ };
1197
+ this.conn.on("message", callback);
1198
+ this.send(msgb, hmac);
1186
1199
  });
1200
+ delete this.sending[device.deviceID];
1187
1201
  }
1188
- getServerList() {
1189
- return __awaiter(this, void 0, void 0, function* () {
1190
- const res = yield axios_1.default.get(this.getHost() + "/user/" + this.getUser().userID + "/servers");
1191
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1192
- });
1202
+ async getSessionList() {
1203
+ return this.database.getAllSessions();
1193
1204
  }
1194
- createChannel(name, serverID) {
1195
- return __awaiter(this, void 0, void 0, function* () {
1196
- const body = { name };
1197
- const res = yield axios_1.default.post(this.getHost() + "/server/" + serverID + "/channels", msgpack_lite_1.default.encode(body), { headers: { "Content-Type": "application/msgpack" } });
1198
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1199
- });
1205
+ async getServerList() {
1206
+ const res = await ax.get(this.getHost() + "/user/" + this.getUser().userID + "/servers");
1207
+ return msgpack.decode(Buffer.from(res.data));
1200
1208
  }
1201
- getDeviceByID(deviceID) {
1202
- return __awaiter(this, void 0, void 0, function* () {
1203
- if (this.deviceRecords[deviceID]) {
1204
- this.log.info("Found device in local cache.");
1205
- return this.deviceRecords[deviceID];
1206
- }
1207
- const device = yield this.database.getDevice(deviceID);
1208
- if (device) {
1209
- this.log.info("Found device in local db.");
1210
- this.deviceRecords[deviceID] = device;
1211
- return device;
1212
- }
1213
- try {
1214
- const res = yield axios_1.default.get(this.getHost() + "/device/" + deviceID);
1215
- this.log.info("Retrieved device from server.");
1216
- const fetchedDevice = msgpack_lite_1.default.decode(Buffer.from(res.data));
1217
- this.deviceRecords[deviceID] = fetchedDevice;
1218
- yield this.database.saveDevice(fetchedDevice);
1219
- return fetchedDevice;
1220
- }
1221
- catch (err) {
1222
- return null;
1223
- }
1224
- });
1209
+ async createChannel(name, serverID) {
1210
+ const body = { name };
1211
+ const res = await ax.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
1212
+ return msgpack.decode(Buffer.from(res.data));
1225
1213
  }
1226
- deleteDevice(deviceID) {
1227
- return __awaiter(this, void 0, void 0, function* () {
1228
- if (deviceID === this.getDevice().deviceID) {
1229
- throw new Error("You can't delete the device you're logged in to.");
1230
- }
1231
- yield axios_1.default.delete(this.prefixes.HTTP +
1232
- this.host +
1233
- "/user/" +
1234
- this.getUser().userID +
1235
- "/devices/" +
1236
- deviceID);
1237
- });
1214
+ async getDeviceByID(deviceID) {
1215
+ if (this.deviceRecords[deviceID]) {
1216
+ this.log.info("Found device in local cache.");
1217
+ return this.deviceRecords[deviceID];
1218
+ }
1219
+ const device = await this.database.getDevice(deviceID);
1220
+ if (device) {
1221
+ this.log.info("Found device in local db.");
1222
+ this.deviceRecords[deviceID] = device;
1223
+ return device;
1224
+ }
1225
+ try {
1226
+ const res = await ax.get(this.getHost() + "/device/" + deviceID);
1227
+ this.log.info("Retrieved device from server.");
1228
+ const fetchedDevice = msgpack.decode(Buffer.from(res.data));
1229
+ this.deviceRecords[deviceID] = fetchedDevice;
1230
+ await this.database.saveDevice(fetchedDevice);
1231
+ return fetchedDevice;
1232
+ }
1233
+ catch (err) {
1234
+ return null;
1235
+ }
1238
1236
  }
1239
- getMultiUserDeviceList(userIDs) {
1240
- return __awaiter(this, void 0, void 0, function* () {
1241
- try {
1242
- const res = yield axios_1.default.post(this.getHost() + "/deviceList", msgpack_lite_1.default.encode(userIDs), { headers: { "Content-Type": "application/msgpack" } });
1243
- const devices = msgpack_lite_1.default.decode(Buffer.from(res.data));
1244
- for (const device of devices) {
1245
- this.deviceRecords[device.deviceID] = device;
1246
- }
1247
- return devices;
1248
- }
1249
- catch (err) {
1250
- return [];
1237
+ async deleteDevice(deviceID) {
1238
+ if (deviceID === this.getDevice().deviceID) {
1239
+ throw new Error("You can't delete the device you're logged in to.");
1240
+ }
1241
+ await ax.delete(this.prefixes.HTTP +
1242
+ this.host +
1243
+ "/user/" +
1244
+ this.getUser().userID +
1245
+ "/devices/" +
1246
+ deviceID);
1247
+ }
1248
+ async getMultiUserDeviceList(userIDs) {
1249
+ try {
1250
+ const res = await ax.post(this.getHost() + "/deviceList", msgpack.encode(userIDs), { headers: { "Content-Type": "application/msgpack" } });
1251
+ const devices = msgpack.decode(Buffer.from(res.data));
1252
+ for (const device of devices) {
1253
+ this.deviceRecords[device.deviceID] = device;
1251
1254
  }
1252
- });
1255
+ return devices;
1256
+ }
1257
+ catch (err) {
1258
+ return [];
1259
+ }
1253
1260
  }
1254
- getUserDeviceList(userID) {
1255
- return __awaiter(this, void 0, void 0, function* () {
1256
- try {
1257
- const res = yield axios_1.default.get(this.getHost() + "/user/" + userID + "/devices");
1258
- const devices = msgpack_lite_1.default.decode(Buffer.from(res.data));
1259
- for (const device of devices) {
1260
- this.deviceRecords[device.deviceID] = device;
1261
- }
1262
- return devices;
1263
- }
1264
- catch (err) {
1265
- return null;
1261
+ async getUserDeviceList(userID) {
1262
+ try {
1263
+ const res = await ax.get(this.getHost() + "/user/" + userID + "/devices");
1264
+ const devices = msgpack.decode(Buffer.from(res.data));
1265
+ for (const device of devices) {
1266
+ this.deviceRecords[device.deviceID] = device;
1266
1267
  }
1267
- });
1268
+ return devices;
1269
+ }
1270
+ catch (err) {
1271
+ return null;
1272
+ }
1268
1273
  }
1269
- getServerByID(serverID) {
1270
- return __awaiter(this, void 0, void 0, function* () {
1271
- try {
1272
- const res = yield axios_1.default.get(this.getHost() + "/server/" + serverID);
1273
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1274
- }
1275
- catch (err) {
1276
- return null;
1277
- }
1278
- });
1274
+ async getServerByID(serverID) {
1275
+ try {
1276
+ const res = await ax.get(this.getHost() + "/server/" + serverID);
1277
+ return msgpack.decode(Buffer.from(res.data));
1278
+ }
1279
+ catch (err) {
1280
+ return null;
1281
+ }
1279
1282
  }
1280
- getChannelByID(channelID) {
1281
- return __awaiter(this, void 0, void 0, function* () {
1282
- try {
1283
- const res = yield axios_1.default.get(this.getHost() + "/channel/" + channelID);
1284
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1285
- }
1286
- catch (err) {
1287
- return null;
1288
- }
1289
- });
1283
+ async getChannelByID(channelID) {
1284
+ try {
1285
+ const res = await ax.get(this.getHost() + "/channel/" + channelID);
1286
+ return msgpack.decode(Buffer.from(res.data));
1287
+ }
1288
+ catch (err) {
1289
+ return null;
1290
+ }
1290
1291
  }
1291
- getChannelList(serverID) {
1292
- return __awaiter(this, void 0, void 0, function* () {
1293
- const res = yield axios_1.default.get(this.getHost() + "/server/" + serverID + "/channels");
1294
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1295
- });
1292
+ async getChannelList(serverID) {
1293
+ const res = await ax.get(this.getHost() + "/server/" + serverID + "/channels");
1294
+ return msgpack.decode(Buffer.from(res.data));
1296
1295
  }
1297
1296
  /* Get the currently logged in user. You cannot call this until
1298
1297
  after the auth event is emitted. */
@@ -1314,139 +1313,135 @@ class Client extends events_1.EventEmitter {
1314
1313
  /* Retrieves the userID with the user identifier.
1315
1314
  user identifier is checked for userID, then signkey,
1316
1315
  and finally falls back to username. */
1317
- retrieveUserDBEntry(userIdentifier) {
1318
- return __awaiter(this, void 0, void 0, function* () {
1319
- if (this.userRecords[userIdentifier]) {
1320
- return [this.userRecords[userIdentifier], null];
1321
- }
1322
- try {
1323
- const res = yield axios_1.default.get(this.getHost() + "/user/" + userIdentifier);
1324
- const userRecord = msgpack_lite_1.default.decode(Buffer.from(res.data));
1325
- this.userRecords[userIdentifier] = userRecord;
1326
- return [userRecord, null];
1327
- }
1328
- catch (err) {
1329
- return [null, err];
1330
- }
1331
- });
1316
+ async retrieveUserDBEntry(userIdentifier) {
1317
+ if (this.userRecords[userIdentifier]) {
1318
+ return [this.userRecords[userIdentifier], null];
1319
+ }
1320
+ try {
1321
+ const res = await ax.get(this.getHost() + "/user/" + userIdentifier);
1322
+ const userRecord = msgpack.decode(Buffer.from(res.data));
1323
+ this.userRecords[userIdentifier] = userRecord;
1324
+ return [userRecord, null];
1325
+ }
1326
+ catch (err) {
1327
+ return [null, err];
1328
+ }
1332
1329
  }
1333
1330
  /* Retrieves the current list of users you have sessions with. */
1334
- getFamiliars() {
1335
- return __awaiter(this, void 0, void 0, function* () {
1336
- const sessions = yield this.database.getAllSessions();
1337
- const familiars = [];
1338
- for (const session of sessions) {
1339
- const [user, err] = yield this.retrieveUserDBEntry(session.userID);
1340
- if (user) {
1341
- familiars.push(user);
1342
- }
1331
+ async getFamiliars() {
1332
+ const sessions = await this.database.getAllSessions();
1333
+ const familiars = [];
1334
+ for (const session of sessions) {
1335
+ const [user, err] = await this.retrieveUserDBEntry(session.userID);
1336
+ if (user) {
1337
+ familiars.push(user);
1343
1338
  }
1344
- return familiars;
1345
- });
1339
+ }
1340
+ return familiars;
1346
1341
  }
1347
- createSession(device, user, message, group,
1342
+ async createSession(device, user, message, group,
1348
1343
  /* this is passed through if the first message is
1349
1344
  part of a group message */
1350
1345
  mailID, forward) {
1351
- var _a;
1352
- return __awaiter(this, void 0, void 0, function* () {
1353
- let keyBundle;
1354
- this.log.info("Requesting key bundle for device: " +
1355
- JSON.stringify(device, null, 4));
1356
- try {
1357
- keyBundle = yield this.retrieveKeyBundle(device.deviceID);
1358
- }
1359
- catch (err) {
1360
- this.log.warn("Couldn't get key bundle:", err);
1361
- return;
1362
- }
1363
- this.log.warn(this.toString() +
1364
- " retrieved keybundle #" + ((_a = keyBundle.otk) === null || _a === void 0 ? void 0 : _a.index.toString()) +
1365
- " for " +
1366
- device.deviceID);
1367
- // my keys
1368
- const IK_A = this.xKeyRing.identityKeys.secretKey;
1369
- const IK_AP = this.xKeyRing.identityKeys.publicKey;
1370
- const EK_A = this.xKeyRing.ephemeralKeys.secretKey;
1371
- // their keys
1372
- const IK_B = crypto_1.XKeyConvert.convertPublicKey(keyBundle.signKey);
1373
- const SPK_B = keyBundle.preKey.publicKey;
1374
- const OPK_B = keyBundle.otk ? keyBundle.otk.publicKey : null;
1375
- // diffie hellman functions
1376
- const DH1 = crypto_1.xDH(IK_A, SPK_B);
1377
- const DH2 = crypto_1.xDH(EK_A, IK_B);
1378
- const DH3 = crypto_1.xDH(EK_A, SPK_B);
1379
- const DH4 = OPK_B ? crypto_1.xDH(EK_A, OPK_B) : null;
1380
- // initial key material
1381
- const IKM = DH4 ? crypto_1.xConcat(DH1, DH2, DH3, DH4) : crypto_1.xConcat(DH1, DH2, DH3);
1382
- // one time key index
1383
- const IDX = keyBundle.otk
1384
- ? crypto_1.XUtils.numberToUint8Arr(keyBundle.otk.index)
1385
- : crypto_1.XUtils.numberToUint8Arr(0);
1386
- // shared secret key
1387
- const SK = crypto_1.xKDF(IKM);
1388
- this.log.info("Obtained SK, " + crypto_1.XUtils.encodeHex(SK));
1389
- const PK = tweetnacl_1.default.box.keyPair.fromSecretKey(SK).publicKey;
1390
- this.log.info(this.toString() +
1391
- " Obtained PK for " +
1392
- device.deviceID +
1393
- " " +
1394
- crypto_1.XUtils.encodeHex(PK));
1395
- const AD = crypto_1.xConcat(crypto_1.xEncode(crypto_1.xConstants.CURVE, IK_AP), crypto_1.xEncode(crypto_1.xConstants.CURVE, IK_B));
1396
- const nonce = crypto_1.xMakeNonce();
1397
- const cipher = tweetnacl_1.default.secretbox(message, nonce, SK);
1398
- this.log.info("Encrypted ciphertext.");
1399
- /* 32 bytes for signkey, 32 bytes for ephemeral key,
1400
- 68 bytes for AD, 6 bytes for otk index (empty for no otk) */
1401
- const extra = crypto_1.xConcat(this.signKeys.publicKey, this.xKeyRing.ephemeralKeys.publicKey, PK, AD, IDX);
1402
- const mail = {
1403
- mailType: types_1.XTypes.WS.MailType.initial,
1404
- mailID: mailID || uuid.v4(),
1405
- recipient: device.deviceID,
1406
- cipher,
1407
- nonce,
1408
- extra,
1409
- sender: this.getDevice().deviceID,
1410
- group,
1411
- forward,
1412
- authorID: this.getUser().userID,
1413
- readerID: user.userID,
1414
- };
1415
- const hmac = crypto_1.xHMAC(mail, SK);
1416
- this.log.info("Mail hash: " + object_hash_1.default(mail));
1417
- this.log.info("Generated hmac: " + crypto_1.XUtils.encodeHex(hmac));
1418
- const msg = {
1419
- transmissionID: uuid.v4(),
1420
- type: "resource",
1421
- resourceType: "mail",
1422
- action: "CREATE",
1423
- data: mail,
1424
- };
1425
- // discard the ephemeral keys
1426
- this.newEphemeralKeys();
1427
- // save the encryption session
1428
- this.log.info("Saving new session.");
1429
- const sessionEntry = {
1430
- verified: false,
1431
- sessionID: uuid.v4(),
1432
- userID: user.userID,
1433
- mode: "initiator",
1434
- SK: crypto_1.XUtils.encodeHex(SK),
1435
- publicKey: crypto_1.XUtils.encodeHex(PK),
1436
- lastUsed: new Date(Date.now()),
1437
- fingerprint: crypto_1.XUtils.encodeHex(AD),
1438
- deviceID: device.deviceID,
1439
- };
1440
- yield this.database.saveSession(sessionEntry);
1441
- this.emit("session", sessionEntry, user);
1442
- // emit the message
1443
- const emitMsg = forward
1444
- ? Object.assign(Object.assign({}, msgpack_lite_1.default.decode(message)), { forward: true }) : {
1445
- nonce: crypto_1.XUtils.encodeHex(mail.nonce),
1346
+ let keyBundle;
1347
+ this.log.info("Requesting key bundle for device: " +
1348
+ JSON.stringify(device, null, 4));
1349
+ try {
1350
+ keyBundle = await this.retrieveKeyBundle(device.deviceID);
1351
+ }
1352
+ catch (err) {
1353
+ this.log.warn("Couldn't get key bundle:", err);
1354
+ return;
1355
+ }
1356
+ this.log.warn(this.toString() +
1357
+ " retrieved keybundle #" +
1358
+ keyBundle.otk?.index.toString() +
1359
+ " for " +
1360
+ device.deviceID);
1361
+ // my keys
1362
+ const IK_A = this.xKeyRing.identityKeys.secretKey;
1363
+ const IK_AP = this.xKeyRing.identityKeys.publicKey;
1364
+ const EK_A = this.xKeyRing.ephemeralKeys.secretKey;
1365
+ // their keys
1366
+ const IK_B = XKeyConvert.convertPublicKey(keyBundle.signKey);
1367
+ const SPK_B = keyBundle.preKey.publicKey;
1368
+ const OPK_B = keyBundle.otk ? keyBundle.otk.publicKey : null;
1369
+ // diffie hellman functions
1370
+ const DH1 = xDH(IK_A, SPK_B);
1371
+ const DH2 = xDH(EK_A, IK_B);
1372
+ const DH3 = xDH(EK_A, SPK_B);
1373
+ const DH4 = OPK_B ? xDH(EK_A, OPK_B) : null;
1374
+ // initial key material
1375
+ const IKM = DH4 ? xConcat(DH1, DH2, DH3, DH4) : xConcat(DH1, DH2, DH3);
1376
+ // one time key index
1377
+ const IDX = keyBundle.otk
1378
+ ? XUtils.numberToUint8Arr(keyBundle.otk.index)
1379
+ : XUtils.numberToUint8Arr(0);
1380
+ // shared secret key
1381
+ const SK = xKDF(IKM);
1382
+ this.log.info("Obtained SK, " + XUtils.encodeHex(SK));
1383
+ const PK = nacl.box.keyPair.fromSecretKey(SK).publicKey;
1384
+ this.log.info(this.toString() +
1385
+ " Obtained PK for " +
1386
+ device.deviceID +
1387
+ " " +
1388
+ XUtils.encodeHex(PK));
1389
+ const AD = xConcat(xEncode(xConstants.CURVE, IK_AP), xEncode(xConstants.CURVE, IK_B));
1390
+ const nonce = xMakeNonce();
1391
+ const cipher = nacl.secretbox(message, nonce, SK);
1392
+ this.log.info("Encrypted ciphertext.");
1393
+ /* 32 bytes for signkey, 32 bytes for ephemeral key,
1394
+ 68 bytes for AD, 6 bytes for otk index (empty for no otk) */
1395
+ const extra = xConcat(this.signKeys.publicKey, this.xKeyRing.ephemeralKeys.publicKey, PK, AD, IDX);
1396
+ const mail = {
1397
+ mailType: MailType.initial,
1398
+ mailID: mailID || uuid.v4(),
1399
+ recipient: device.deviceID,
1400
+ cipher,
1401
+ nonce,
1402
+ extra,
1403
+ sender: this.getDevice().deviceID,
1404
+ group,
1405
+ forward,
1406
+ authorID: this.getUser().userID,
1407
+ readerID: user.userID,
1408
+ };
1409
+ const hmac = xHMAC(mail, SK);
1410
+ this.log.info("Mail hash: " + objectHash(mail));
1411
+ this.log.info("Generated hmac: " + XUtils.encodeHex(hmac));
1412
+ const msg = {
1413
+ transmissionID: uuid.v4(),
1414
+ type: "resource",
1415
+ resourceType: "mail",
1416
+ action: "CREATE",
1417
+ data: mail,
1418
+ };
1419
+ // discard the ephemeral keys
1420
+ this.newEphemeralKeys();
1421
+ // save the encryption session
1422
+ this.log.info("Saving new session.");
1423
+ const sessionEntry = {
1424
+ verified: false,
1425
+ sessionID: uuid.v4(),
1426
+ userID: user.userID,
1427
+ mode: "initiator",
1428
+ SK: XUtils.encodeHex(SK),
1429
+ publicKey: XUtils.encodeHex(PK),
1430
+ lastUsed: new Date(Date.now()),
1431
+ fingerprint: XUtils.encodeHex(AD),
1432
+ deviceID: device.deviceID,
1433
+ };
1434
+ await this.database.saveSession(sessionEntry);
1435
+ this.emit("session", sessionEntry, user);
1436
+ // emit the message
1437
+ const emitMsg = forward
1438
+ ? { ...msgpack.decode(message), forward: true }
1439
+ : {
1440
+ nonce: XUtils.encodeHex(mail.nonce),
1446
1441
  mailID: mail.mailID,
1447
1442
  sender: mail.sender,
1448
1443
  recipient: mail.recipient,
1449
- message: crypto_1.XUtils.encodeUTF8(message),
1444
+ message: XUtils.encodeUTF8(message),
1450
1445
  direction: "outgoing",
1451
1446
  timestamp: new Date(Date.now()),
1452
1447
  decrypted: true,
@@ -1455,30 +1450,29 @@ class Client extends events_1.EventEmitter {
1455
1450
  authorID: mail.authorID,
1456
1451
  readerID: mail.readerID,
1457
1452
  };
1458
- this.emit("message", emitMsg);
1459
- // send mail and wait for response
1460
- yield new Promise((res, rej) => {
1461
- const callback = (packedMsg) => {
1462
- const [header, receivedMsg] = crypto_1.XUtils.unpackMessage(packedMsg);
1463
- if (receivedMsg.transmissionID === msg.transmissionID) {
1464
- this.conn.off("message", callback);
1465
- if (receivedMsg.type === "success") {
1466
- res(receivedMsg.data);
1467
- }
1468
- else {
1469
- rej({
1470
- error: receivedMsg,
1471
- message: emitMsg,
1472
- });
1473
- }
1453
+ this.emit("message", emitMsg);
1454
+ // send mail and wait for response
1455
+ await new Promise((res, rej) => {
1456
+ const callback = (packedMsg) => {
1457
+ const [header, receivedMsg] = XUtils.unpackMessage(packedMsg);
1458
+ if (receivedMsg.transmissionID === msg.transmissionID) {
1459
+ this.conn.off("message", callback);
1460
+ if (receivedMsg.type === "success") {
1461
+ res(receivedMsg.data);
1474
1462
  }
1475
- };
1476
- this.conn.on("message", callback);
1477
- this.send(msg, hmac);
1478
- this.log.info("Mail sent.");
1479
- });
1480
- delete this.sending[device.deviceID];
1463
+ else {
1464
+ rej({
1465
+ error: receivedMsg,
1466
+ message: emitMsg,
1467
+ });
1468
+ }
1469
+ }
1470
+ };
1471
+ this.conn.on("message", callback);
1472
+ this.send(msg, hmac);
1473
+ this.log.info("Mail sent.");
1481
1474
  });
1475
+ delete this.sending[device.deviceID];
1482
1476
  }
1483
1477
  sendReceipt(nonce) {
1484
1478
  const receipt = {
@@ -1488,91 +1482,93 @@ class Client extends events_1.EventEmitter {
1488
1482
  };
1489
1483
  this.send(receipt);
1490
1484
  }
1491
- getSessionByPubkey(publicKey) {
1492
- return __awaiter(this, void 0, void 0, function* () {
1493
- const strPubKey = crypto_1.XUtils.encodeHex(publicKey);
1494
- if (this.sessionRecords[strPubKey]) {
1495
- return this.sessionRecords[strPubKey];
1496
- }
1497
- const session = yield this.database.getSessionByPublicKey(publicKey);
1498
- if (session) {
1499
- this.sessionRecords[strPubKey] = session;
1500
- }
1501
- return session;
1502
- });
1503
- }
1504
- readMail(header, mail, timestamp) {
1505
- return __awaiter(this, void 0, void 0, function* () {
1506
- this.sendReceipt(mail.nonce);
1507
- let timeout = 1;
1508
- while (this.reading) {
1509
- yield sleep_1.sleep(timeout);
1510
- timeout *= 2;
1511
- }
1512
- this.reading = true;
1513
- const healSession = () => __awaiter(this, void 0, void 0, function* () {
1485
+ async getSessionByPubkey(publicKey) {
1486
+ const strPubKey = XUtils.encodeHex(publicKey);
1487
+ if (this.sessionRecords[strPubKey]) {
1488
+ return this.sessionRecords[strPubKey];
1489
+ }
1490
+ const session = await this.database.getSessionByPublicKey(publicKey);
1491
+ if (session) {
1492
+ this.sessionRecords[strPubKey] = session;
1493
+ }
1494
+ return session;
1495
+ }
1496
+ async readMail(header, mail, timestamp) {
1497
+ this.sendReceipt(mail.nonce);
1498
+ let timeout = 1;
1499
+ while (this.reading) {
1500
+ await sleep(timeout);
1501
+ timeout *= 2;
1502
+ }
1503
+ this.reading = true;
1504
+ try {
1505
+ const healSession = async () => {
1514
1506
  this.log.info("Requesting retry of " + mail.mailID);
1515
- const deviceEntry = yield this.getDeviceByID(mail.sender);
1516
- const [user, err] = yield this.retrieveUserDBEntry(mail.authorID);
1507
+ const deviceEntry = await this.getDeviceByID(mail.sender);
1508
+ const [user, err] = await this.retrieveUserDBEntry(mail.authorID);
1517
1509
  if (deviceEntry && user) {
1518
- this.createSession(deviceEntry, user, crypto_1.XUtils.decodeUTF8(`��RETRY_REQUEST:${mail.mailID}��`), mail.group, uuid.v4(), false);
1510
+ this.createSession(deviceEntry, user, XUtils.decodeUTF8(`��RETRY_REQUEST:${mail.mailID}��`), mail.group, uuid.v4(), false);
1519
1511
  }
1520
- });
1512
+ };
1521
1513
  this.log.info("Received mail from " + mail.sender);
1522
1514
  switch (mail.mailType) {
1523
- case types_1.XTypes.WS.MailType.subsequent:
1515
+ case MailType.subsequent:
1524
1516
  const [publicKey] = Client.deserializeExtra(mail.mailType, mail.extra);
1525
- let session = yield this.getSessionByPubkey(publicKey);
1517
+ let session = await this.getSessionByPubkey(publicKey);
1526
1518
  let retries = 0;
1527
1519
  while (!session) {
1528
1520
  if (retries > 3) {
1529
1521
  break;
1530
1522
  }
1531
- session = yield this.getSessionByPubkey(publicKey);
1523
+ session = await this.getSessionByPubkey(publicKey);
1532
1524
  retries++;
1533
1525
  return;
1534
1526
  }
1535
1527
  if (!session) {
1536
1528
  this.log.warn("Couldn't find session public key " +
1537
- crypto_1.XUtils.encodeHex(publicKey));
1529
+ XUtils.encodeHex(publicKey));
1538
1530
  healSession();
1539
1531
  return;
1540
1532
  }
1541
1533
  this.log.info("Session found for " + mail.sender);
1542
- this.log.info("Mail nonce " + crypto_1.XUtils.encodeHex(mail.nonce));
1543
- const HMAC = crypto_1.xHMAC(mail, session.SK);
1544
- this.log.info("Mail hash: " + object_hash_1.default(mail));
1545
- this.log.info("Calculated hmac: " + crypto_1.XUtils.encodeHex(HMAC));
1546
- if (!crypto_1.XUtils.bytesEqual(HMAC, header)) {
1534
+ this.log.info("Mail nonce " + XUtils.encodeHex(mail.nonce));
1535
+ const HMAC = xHMAC(mail, session.SK);
1536
+ this.log.info("Mail hash: " + objectHash(mail));
1537
+ this.log.info("Calculated hmac: " + XUtils.encodeHex(HMAC));
1538
+ if (!XUtils.bytesEqual(HMAC, header)) {
1547
1539
  this.log.warn("Message authentication failed (HMAC does not match).");
1548
1540
  healSession();
1549
1541
  return;
1550
1542
  }
1551
- const decrypted = tweetnacl_1.default.secretbox.open(mail.cipher, mail.nonce, session.SK);
1543
+ const decrypted = nacl.secretbox.open(mail.cipher, mail.nonce, session.SK);
1552
1544
  if (decrypted) {
1553
1545
  this.log.info("Decryption successful.");
1554
1546
  let plaintext = "";
1555
1547
  if (!mail.forward) {
1556
- plaintext = crypto_1.XUtils.encodeUTF8(decrypted);
1548
+ plaintext = XUtils.encodeUTF8(decrypted);
1557
1549
  }
1558
1550
  // emit the message
1559
1551
  const message = mail.forward
1560
- ? Object.assign(Object.assign({}, msgpack_lite_1.default.decode(decrypted)), { forward: true }) : {
1561
- nonce: crypto_1.XUtils.encodeHex(mail.nonce),
1562
- mailID: mail.mailID,
1563
- sender: mail.sender,
1564
- recipient: mail.recipient,
1565
- message: crypto_1.XUtils.encodeUTF8(decrypted),
1566
- direction: "incoming",
1567
- timestamp: new Date(timestamp),
1568
- decrypted: true,
1569
- group: mail.group
1570
- ? uuid.stringify(mail.group)
1571
- : null,
1572
- forward: mail.forward,
1573
- authorID: mail.authorID,
1574
- readerID: mail.readerID,
1575
- };
1552
+ ? {
1553
+ ...msgpack.decode(decrypted),
1554
+ forward: true,
1555
+ }
1556
+ : {
1557
+ nonce: XUtils.encodeHex(mail.nonce),
1558
+ mailID: mail.mailID,
1559
+ sender: mail.sender,
1560
+ recipient: mail.recipient,
1561
+ message: XUtils.encodeUTF8(decrypted),
1562
+ direction: "incoming",
1563
+ timestamp: new Date(timestamp),
1564
+ decrypted: true,
1565
+ group: mail.group
1566
+ ? uuid.stringify(mail.group)
1567
+ : null,
1568
+ forward: mail.forward,
1569
+ authorID: mail.authorID,
1570
+ readerID: mail.readerID,
1571
+ };
1576
1572
  this.emit("message", message);
1577
1573
  this.database.markSessionUsed(session.sessionID);
1578
1574
  }
@@ -1581,7 +1577,7 @@ class Client extends events_1.EventEmitter {
1581
1577
  healSession();
1582
1578
  // emit the message
1583
1579
  const message = {
1584
- nonce: crypto_1.XUtils.encodeHex(mail.nonce),
1580
+ nonce: XUtils.encodeHex(mail.nonce),
1585
1581
  mailID: mail.mailID,
1586
1582
  sender: mail.sender,
1587
1583
  recipient: mail.recipient,
@@ -1589,7 +1585,9 @@ class Client extends events_1.EventEmitter {
1589
1585
  direction: "incoming",
1590
1586
  timestamp: new Date(timestamp),
1591
1587
  decrypted: false,
1592
- group: mail.group ? uuid.stringify(mail.group) : null,
1588
+ group: mail.group
1589
+ ? uuid.stringify(mail.group)
1590
+ : null,
1593
1591
  forward: mail.forward,
1594
1592
  authorID: mail.authorID,
1595
1593
  readerID: mail.readerID,
@@ -1597,33 +1595,33 @@ class Client extends events_1.EventEmitter {
1597
1595
  this.emit("message", message);
1598
1596
  }
1599
1597
  break;
1600
- case types_1.XTypes.WS.MailType.initial:
1598
+ case MailType.initial:
1601
1599
  this.log.info("Initiating new session.");
1602
- const [signKey, ephKey, assocData, indexBytes,] = Client.deserializeExtra(types_1.XTypes.WS.MailType.initial, mail.extra);
1603
- const preKeyIndex = crypto_1.XUtils.uint8ArrToNumber(indexBytes);
1600
+ const [signKey, ephKey, assocData, indexBytes] = Client.deserializeExtra(MailType.initial, mail.extra);
1601
+ const preKeyIndex = XUtils.uint8ArrToNumber(indexBytes);
1604
1602
  this.log.info(this.toString() + " otk #" + preKeyIndex + " indicated");
1605
1603
  const otk = preKeyIndex === 0
1606
1604
  ? null
1607
- : yield this.database.getOneTimeKey(preKeyIndex);
1605
+ : await this.database.getOneTimeKey(preKeyIndex);
1608
1606
  if (otk) {
1609
1607
  this.log.info("otk #" +
1610
- JSON.stringify(otk === null || otk === void 0 ? void 0 : otk.index) +
1608
+ JSON.stringify(otk?.index) +
1611
1609
  " retrieved from database.");
1612
1610
  }
1613
- this.log.info("signKey: " + crypto_1.XUtils.encodeHex(signKey));
1614
- this.log.info("preKey: " + crypto_1.XUtils.encodeHex(ephKey));
1611
+ this.log.info("signKey: " + XUtils.encodeHex(signKey));
1612
+ this.log.info("preKey: " + XUtils.encodeHex(ephKey));
1615
1613
  if (otk) {
1616
- this.log.info("OTK: " + crypto_1.XUtils.encodeHex(otk.keyPair.publicKey));
1614
+ this.log.info("OTK: " + XUtils.encodeHex(otk.keyPair.publicKey));
1617
1615
  }
1618
- if ((otk === null || otk === void 0 ? void 0 : otk.index) !== preKeyIndex && preKeyIndex !== 0) {
1616
+ if (otk?.index !== preKeyIndex && preKeyIndex !== 0) {
1619
1617
  this.log.warn("OTK index mismatch, received " +
1620
- JSON.stringify(otk === null || otk === void 0 ? void 0 : otk.index) +
1618
+ JSON.stringify(otk?.index) +
1621
1619
  ", expected " +
1622
1620
  preKeyIndex.toString());
1623
1621
  return;
1624
1622
  }
1625
1623
  // their public keys
1626
- const IK_A = crypto_1.XKeyConvert.convertPublicKey(signKey);
1624
+ const IK_A = XKeyConvert.convertPublicKey(signKey);
1627
1625
  const EK_A = ephKey;
1628
1626
  // my private keys
1629
1627
  const IK_B = this.xKeyRing.identityKeys.secretKey;
@@ -1631,71 +1629,72 @@ class Client extends events_1.EventEmitter {
1631
1629
  const SPK_B = this.xKeyRing.preKeys.keyPair.secretKey;
1632
1630
  const OPK_B = otk ? otk.keyPair.secretKey : null;
1633
1631
  // diffie hellman functions
1634
- const DH1 = crypto_1.xDH(SPK_B, IK_A);
1635
- const DH2 = crypto_1.xDH(IK_B, EK_A);
1636
- const DH3 = crypto_1.xDH(SPK_B, EK_A);
1637
- const DH4 = OPK_B ? crypto_1.xDH(OPK_B, EK_A) : null;
1632
+ const DH1 = xDH(SPK_B, IK_A);
1633
+ const DH2 = xDH(IK_B, EK_A);
1634
+ const DH3 = xDH(SPK_B, EK_A);
1635
+ const DH4 = OPK_B ? xDH(OPK_B, EK_A) : null;
1638
1636
  // initial key material
1639
1637
  const IKM = DH4
1640
- ? crypto_1.xConcat(DH1, DH2, DH3, DH4)
1641
- : crypto_1.xConcat(DH1, DH2, DH3);
1638
+ ? xConcat(DH1, DH2, DH3, DH4)
1639
+ : xConcat(DH1, DH2, DH3);
1642
1640
  // shared secret key
1643
- const SK = crypto_1.xKDF(IKM);
1641
+ const SK = xKDF(IKM);
1644
1642
  this.log.info("Obtained SK for " +
1645
1643
  mail.sender +
1646
1644
  ", " +
1647
- crypto_1.XUtils.encodeHex(SK));
1645
+ XUtils.encodeHex(SK));
1648
1646
  // shared public key
1649
- const PK = tweetnacl_1.default.box.keyPair.fromSecretKey(SK).publicKey;
1647
+ const PK = nacl.box.keyPair.fromSecretKey(SK).publicKey;
1650
1648
  this.log.info(this.toString() +
1651
1649
  "Obtained PK for " +
1652
1650
  mail.sender +
1653
1651
  " " +
1654
- crypto_1.XUtils.encodeHex(PK));
1655
- const hmac = crypto_1.xHMAC(mail, SK);
1656
- this.log.info("Mail hash: " + object_hash_1.default(mail));
1657
- this.log.info("Calculated hmac: " + crypto_1.XUtils.encodeHex(hmac));
1652
+ XUtils.encodeHex(PK));
1653
+ const hmac = xHMAC(mail, SK);
1654
+ this.log.info("Mail hash: " + objectHash(mail));
1655
+ this.log.info("Calculated hmac: " + XUtils.encodeHex(hmac));
1658
1656
  // associated data
1659
- const AD = crypto_1.xConcat(crypto_1.xEncode(crypto_1.xConstants.CURVE, IK_A), crypto_1.xEncode(crypto_1.xConstants.CURVE, IK_BP));
1660
- if (!crypto_1.XUtils.bytesEqual(hmac, header)) {
1657
+ const AD = xConcat(xEncode(xConstants.CURVE, IK_A), xEncode(xConstants.CURVE, IK_BP));
1658
+ if (!XUtils.bytesEqual(hmac, header)) {
1661
1659
  console.warn("Mail authentication failed (HMAC did not match).");
1662
1660
  console.warn(mail);
1663
1661
  return;
1664
1662
  }
1665
1663
  this.log.info("Mail authenticated successfully.");
1666
- const unsealed = tweetnacl_1.default.secretbox.open(mail.cipher, mail.nonce, SK);
1664
+ const unsealed = nacl.secretbox.open(mail.cipher, mail.nonce, SK);
1667
1665
  if (unsealed) {
1668
1666
  this.log.info("Decryption successful.");
1669
1667
  let plaintext = "";
1670
1668
  if (!mail.forward) {
1671
- plaintext = crypto_1.XUtils.encodeUTF8(unsealed);
1669
+ plaintext = XUtils.encodeUTF8(unsealed);
1672
1670
  }
1673
1671
  // emit the message
1674
1672
  const message = mail.forward
1675
- ? Object.assign(Object.assign({}, msgpack_lite_1.default.decode(unsealed)), { forward: true }) : {
1676
- nonce: crypto_1.XUtils.encodeHex(mail.nonce),
1677
- mailID: mail.mailID,
1678
- sender: mail.sender,
1679
- recipient: mail.recipient,
1680
- message: plaintext,
1681
- direction: "incoming",
1682
- timestamp: new Date(timestamp),
1683
- decrypted: true,
1684
- group: mail.group
1685
- ? uuid.stringify(mail.group)
1686
- : null,
1687
- forward: mail.forward,
1688
- authorID: mail.authorID,
1689
- readerID: mail.readerID,
1690
- };
1673
+ ? { ...msgpack.decode(unsealed), forward: true }
1674
+ : {
1675
+ nonce: XUtils.encodeHex(mail.nonce),
1676
+ mailID: mail.mailID,
1677
+ sender: mail.sender,
1678
+ recipient: mail.recipient,
1679
+ message: plaintext,
1680
+ direction: "incoming",
1681
+ timestamp: new Date(timestamp),
1682
+ decrypted: true,
1683
+ group: mail.group
1684
+ ? uuid.stringify(mail.group)
1685
+ : null,
1686
+ forward: mail.forward,
1687
+ authorID: mail.authorID,
1688
+ readerID: mail.readerID,
1689
+ };
1691
1690
  this.emit("message", message);
1692
1691
  // discard onetimekey
1693
- yield this.database.deleteOneTimeKey(preKeyIndex);
1694
- const deviceEntry = yield this.getDeviceByID(mail.sender);
1692
+ await this.database.deleteOneTimeKey(preKeyIndex);
1693
+ const deviceEntry = await this.getDeviceByID(mail.sender);
1695
1694
  if (!deviceEntry) {
1696
1695
  throw new Error("Couldn't get device entry.");
1697
1696
  }
1698
- const [userEntry, userErr] = yield this.retrieveUserDBEntry(deviceEntry.owner);
1697
+ const [userEntry, userErr] = await this.retrieveUserDBEntry(deviceEntry.owner);
1699
1698
  if (!userEntry) {
1700
1699
  throw new Error("Couldn't get user entry.");
1701
1700
  }
@@ -1707,14 +1706,14 @@ class Client extends events_1.EventEmitter {
1707
1706
  sessionID: uuid.v4(),
1708
1707
  userID: userEntry.userID,
1709
1708
  mode: "receiver",
1710
- SK: crypto_1.XUtils.encodeHex(SK),
1711
- publicKey: crypto_1.XUtils.encodeHex(PK),
1709
+ SK: XUtils.encodeHex(SK),
1710
+ publicKey: XUtils.encodeHex(PK),
1712
1711
  lastUsed: new Date(Date.now()),
1713
- fingerprint: crypto_1.XUtils.encodeHex(AD),
1712
+ fingerprint: XUtils.encodeHex(AD),
1714
1713
  deviceID: mail.sender,
1715
1714
  };
1716
- yield this.database.saveSession(newSession);
1717
- let [user, err] = yield this.retrieveUserDBEntry(newSession.userID);
1715
+ await this.database.saveSession(newSession);
1716
+ let [user, err] = await this.retrieveUserDBEntry(newSession.userID);
1718
1717
  if (user) {
1719
1718
  this.emit("session", newSession, user);
1720
1719
  }
@@ -1722,7 +1721,7 @@ class Client extends events_1.EventEmitter {
1722
1721
  let failed = 1;
1723
1722
  // retry a couple times
1724
1723
  while (!user) {
1725
- [user, err] = yield this.retrieveUserDBEntry(newSession.userID);
1724
+ [user, err] = await this.retrieveUserDBEntry(newSession.userID);
1726
1725
  failed++;
1727
1726
  if (failed > 3) {
1728
1727
  this.log.warn("Couldn't retrieve user entry.");
@@ -1739,74 +1738,73 @@ class Client extends events_1.EventEmitter {
1739
1738
  this.log.warn("Unsupported MailType:", mail.mailType);
1740
1739
  break;
1741
1740
  }
1741
+ }
1742
+ finally {
1742
1743
  this.reading = false;
1743
- });
1744
+ }
1744
1745
  }
1745
1746
  newEphemeralKeys() {
1746
- this.xKeyRing.ephemeralKeys = tweetnacl_1.default.box.keyPair();
1747
+ this.xKeyRing.ephemeralKeys = nacl.box.keyPair();
1747
1748
  }
1748
1749
  createPreKey() {
1749
- const preKeyPair = tweetnacl_1.default.box.keyPair();
1750
+ const preKeyPair = nacl.box.keyPair();
1750
1751
  const preKeys = {
1751
1752
  keyPair: preKeyPair,
1752
- signature: tweetnacl_1.default.sign(crypto_1.xEncode(crypto_1.xConstants.CURVE, preKeyPair.publicKey), this.signKeys.secretKey),
1753
+ signature: nacl.sign(xEncode(xConstants.CURVE, preKeyPair.publicKey), this.signKeys.secretKey),
1753
1754
  };
1754
1755
  return preKeys;
1755
1756
  }
1756
- handleNotify(msg) {
1757
- return __awaiter(this, void 0, void 0, function* () {
1758
- switch (msg.event) {
1759
- case "mail":
1760
- this.log.info("Server has informed us of new mail.");
1761
- yield this.getMail();
1762
- this.fetchingMail = false;
1763
- break;
1764
- case "permission":
1765
- this.emit("permission", msg.data);
1766
- break;
1767
- case "retryRequest":
1768
- const messageID = msg.data;
1769
- break;
1770
- default:
1771
- this.log.info("Unsupported notification event " + msg.event);
1772
- break;
1773
- }
1774
- });
1757
+ async handleNotify(msg) {
1758
+ switch (msg.event) {
1759
+ case "mail":
1760
+ this.log.info("Server has informed us of new mail.");
1761
+ await this.getMail();
1762
+ this.fetchingMail = false;
1763
+ break;
1764
+ case "permission":
1765
+ this.emit("permission", msg.data);
1766
+ break;
1767
+ case "retryRequest":
1768
+ const messageID = msg.data;
1769
+ break;
1770
+ default:
1771
+ this.log.info("Unsupported notification event " + msg.event);
1772
+ break;
1773
+ }
1775
1774
  }
1776
- populateKeyRing() {
1777
- return __awaiter(this, void 0, void 0, function* () {
1778
- // we've checked in the constructor that these exist
1779
- const identityKeys = this.idKeys;
1780
- let preKeys = yield this.database.getPreKeys();
1781
- if (!preKeys) {
1782
- this.log.warn("No prekeys found in database, creating a new one.");
1783
- preKeys = this.createPreKey();
1784
- yield this.database.savePreKeys([preKeys], false);
1785
- }
1786
- const sessions = yield this.database.getAllSessions();
1787
- for (const session of sessions) {
1788
- this.sessionRecords[session.publicKey] = sqlSessionToCrypto_1.sqlSessionToCrypto(session);
1789
- }
1790
- const ephemeralKeys = tweetnacl_1.default.box.keyPair();
1791
- this.xKeyRing = {
1792
- identityKeys,
1793
- preKeys,
1794
- ephemeralKeys,
1795
- };
1796
- this.log.info("Keyring populated:\n" +
1797
- JSON.stringify({
1798
- signKey: crypto_1.XUtils.encodeHex(this.signKeys.publicKey),
1799
- preKey: crypto_1.XUtils.encodeHex(preKeys.keyPair.publicKey),
1800
- ephemeralKey: crypto_1.XUtils.encodeHex(ephemeralKeys.publicKey),
1801
- }, null, 4));
1802
- });
1775
+ async populateKeyRing() {
1776
+ // we've checked in the constructor that these exist
1777
+ const identityKeys = this.idKeys;
1778
+ let preKeys = await this.database.getPreKeys();
1779
+ if (!preKeys) {
1780
+ this.log.warn("No prekeys found in database, creating a new one.");
1781
+ preKeys = this.createPreKey();
1782
+ await this.database.savePreKeys([preKeys], false);
1783
+ }
1784
+ const sessions = await this.database.getAllSessions();
1785
+ for (const session of sessions) {
1786
+ this.sessionRecords[session.publicKey] =
1787
+ sqlSessionToCrypto(session);
1788
+ }
1789
+ const ephemeralKeys = nacl.box.keyPair();
1790
+ this.xKeyRing = {
1791
+ identityKeys,
1792
+ preKeys,
1793
+ ephemeralKeys,
1794
+ };
1795
+ this.log.info("Keyring populated:\n" +
1796
+ JSON.stringify({
1797
+ signKey: XUtils.encodeHex(this.signKeys.publicKey),
1798
+ preKey: XUtils.encodeHex(preKeys.keyPair.publicKey),
1799
+ ephemeralKey: XUtils.encodeHex(ephemeralKeys.publicKey),
1800
+ }, null, 4));
1803
1801
  }
1804
1802
  initSocket() {
1805
1803
  try {
1806
1804
  if (!this.token) {
1807
1805
  throw new Error("No token found, did you call login()?");
1808
1806
  }
1809
- this.conn = new ws_1.default(this.prefixes.WS + this.host + "/socket", { headers: { Cookie: "auth=" + this.token } });
1807
+ this.conn = new WebSocket(this.prefixes.WS + this.host + "/socket", { headers: { Cookie: "auth=" + this.token } });
1810
1808
  this.conn.on("open", () => {
1811
1809
  this.log.info("Connection opened.");
1812
1810
  this.pingInterval = setInterval(this.ping.bind(this), 15000);
@@ -1820,10 +1818,10 @@ class Client extends events_1.EventEmitter {
1820
1818
  this.conn.on("error", (error) => {
1821
1819
  throw error;
1822
1820
  });
1823
- this.conn.on("message", (message) => __awaiter(this, void 0, void 0, function* () {
1824
- const [header, msg] = crypto_1.XUtils.unpackMessage(message);
1825
- this.log.debug(chalk_1.default.red.bold("INH ") + crypto_1.XUtils.encodeHex(header));
1826
- this.log.debug(chalk_1.default.red.bold("IN ") + JSON.stringify(msg, null, 4));
1821
+ this.conn.on("message", async (message) => {
1822
+ const [header, msg] = XUtils.unpackMessage(message);
1823
+ this.log.debug(chalk.red.bold("INH ") + XUtils.encodeHex(header));
1824
+ this.log.debug(chalk.red.bold("IN ") + JSON.stringify(msg, null, 4));
1827
1825
  switch (msg.type) {
1828
1826
  case "ping":
1829
1827
  this.pong(msg.transmissionID);
@@ -1854,7 +1852,7 @@ class Client extends events_1.EventEmitter {
1854
1852
  this.log.info("Unsupported message " + msg.type);
1855
1853
  break;
1856
1854
  }
1857
- }));
1855
+ });
1858
1856
  }
1859
1857
  catch (err) {
1860
1858
  throw new Error("Error initiating websocket connection " + err.toString());
@@ -1863,161 +1861,137 @@ class Client extends events_1.EventEmitter {
1863
1861
  setAlive(status) {
1864
1862
  this.isAlive = status;
1865
1863
  }
1866
- postAuth() {
1867
- return __awaiter(this, void 0, void 0, function* () {
1868
- let count = 0;
1869
- while (true) {
1870
- try {
1871
- yield this.getMail();
1872
- count++;
1873
- this.fetchingMail = false;
1874
- if (count > 10) {
1875
- this.negotiateOTK();
1876
- count = 0;
1877
- }
1878
- }
1879
- catch (err) {
1880
- this.log.warn("Problem fetching mail" + err.toString());
1881
- }
1882
- yield sleep_1.sleep(1000 * 60);
1883
- }
1884
- });
1885
- }
1886
- getMail() {
1887
- return __awaiter(this, void 0, void 0, function* () {
1888
- while (this.fetchingMail) {
1889
- yield sleep_1.sleep(500);
1890
- }
1891
- this.fetchingMail = true;
1892
- let firstFetch = false;
1893
- if (this.firstMailFetch) {
1894
- firstFetch = true;
1895
- this.firstMailFetch = false;
1896
- }
1897
- if (firstFetch) {
1898
- this.emit("decryptingMail");
1899
- }
1900
- this.log.info("fetching mail for device " + this.getDevice().deviceID);
1864
+ async postAuth() {
1865
+ let count = 0;
1866
+ while (true) {
1901
1867
  try {
1902
- const res = yield axios_1.default.post(this.getHost() +
1903
- "/device/" +
1904
- this.getDevice().deviceID +
1905
- "/mail");
1906
- const inbox = msgpack_lite_1.default
1907
- .decode(Buffer.from(res.data))
1908
- .sort((a, b) => b[2].getTime() - a[2].getTime());
1909
- for (const mailDetails of inbox) {
1910
- const [mailHeader, mailBody, timestamp] = mailDetails;
1911
- try {
1912
- yield this.readMail(mailHeader, mailBody, timestamp.toString());
1913
- }
1914
- catch (err) {
1915
- console.warn(err.toString());
1916
- }
1868
+ await this.getMail();
1869
+ count++;
1870
+ this.fetchingMail = false;
1871
+ if (count > 10) {
1872
+ this.negotiateOTK();
1873
+ count = 0;
1917
1874
  }
1918
1875
  }
1919
1876
  catch (err) {
1920
- console.warn(err.toString());
1877
+ this.log.warn("Problem fetching mail" + err.toString());
1921
1878
  }
1922
- this.fetchingMail = false;
1923
- });
1924
- }
1925
- /* header is 32 bytes and is either empty
1926
- or contains an HMAC of the message with
1927
- a derived SK */
1928
- send(msg, header) {
1929
- return __awaiter(this, void 0, void 0, function* () {
1930
- let i = 0;
1931
- while (this.conn.readyState !== 1) {
1932
- yield sleep_1.sleep(i);
1933
- i *= 2;
1934
- }
1935
- this.log.debug(chalk_1.default.red.bold("OUTH ") +
1936
- crypto_1.XUtils.encodeHex(header || crypto_1.XUtils.emptyHeader()));
1937
- this.log.debug(chalk_1.default.red.bold("OUT ") + JSON.stringify(msg, null, 4));
1938
- this.conn.send(crypto_1.XUtils.packMessage(msg, header));
1939
- });
1940
- }
1941
- retrieveKeyBundle(deviceID) {
1942
- return __awaiter(this, void 0, void 0, function* () {
1943
- const res = yield axios_1.default.post(this.getHost() + "/device/" + deviceID + "/keyBundle");
1944
- return msgpack_lite_1.default.decode(Buffer.from(res.data));
1945
- });
1879
+ await sleep(1000 * 60);
1880
+ }
1946
1881
  }
1947
- getOTKCount() {
1948
- return __awaiter(this, void 0, void 0, function* () {
1949
- const res = yield axios_1.default.get(this.getHost() +
1882
+ async getMail() {
1883
+ while (this.fetchingMail) {
1884
+ await sleep(500);
1885
+ }
1886
+ this.fetchingMail = true;
1887
+ let firstFetch = false;
1888
+ if (this.firstMailFetch) {
1889
+ firstFetch = true;
1890
+ this.firstMailFetch = false;
1891
+ }
1892
+ if (firstFetch) {
1893
+ this.emit("decryptingMail");
1894
+ }
1895
+ this.log.info("fetching mail for device " + this.getDevice().deviceID);
1896
+ try {
1897
+ const res = await ax.post(this.getHost() +
1950
1898
  "/device/" +
1951
1899
  this.getDevice().deviceID +
1952
- "/otk/count");
1953
- return msgpack_lite_1.default.decode(Buffer.from(res.data)).count;
1954
- });
1955
- }
1956
- submitOTK(amount) {
1957
- return __awaiter(this, void 0, void 0, function* () {
1958
- const otks = [];
1959
- const t0 = perf_hooks_1.performance.now();
1960
- for (let i = 0; i < amount; i++) {
1961
- otks[i] = this.createPreKey();
1900
+ "/mail");
1901
+ const inbox = msgpack
1902
+ .decode(Buffer.from(res.data))
1903
+ .sort((a, b) => b[2].getTime() - a[2].getTime());
1904
+ for (const mailDetails of inbox) {
1905
+ const [mailHeader, mailBody, timestamp] = mailDetails;
1906
+ try {
1907
+ await this.readMail(mailHeader, mailBody, timestamp.toString());
1908
+ }
1909
+ catch (err) {
1910
+ console.warn(err.toString());
1911
+ }
1962
1912
  }
1963
- const t1 = perf_hooks_1.performance.now();
1964
- this.log.info("Generated " + amount + " one time keys in " + (t1 - t0) + " ms.");
1965
- const savedKeys = yield this.database.savePreKeys(otks, true);
1966
- yield axios_1.default.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack_lite_1.default.encode(savedKeys.map((key) => this.censorPreKey(key))), {
1967
- headers: { "Content-Type": "application/msgpack" },
1968
- });
1969
- });
1913
+ }
1914
+ catch (err) {
1915
+ console.warn(err.toString());
1916
+ }
1917
+ this.fetchingMail = false;
1970
1918
  }
1971
- negotiateOTK() {
1972
- return __awaiter(this, void 0, void 0, function* () {
1973
- const otkCount = yield this.getOTKCount();
1974
- this.log.info("Server reported OTK: " + otkCount.toString());
1975
- const needs = crypto_1.xConstants.MIN_OTK_SUPPLY - otkCount;
1976
- if (needs === 0) {
1977
- this.log.info("Server otk supply full.");
1978
- return;
1979
- }
1980
- yield this.submitOTK(needs);
1919
+ /* header is 32 bytes and is either empty
1920
+ or contains an HMAC of the message with
1921
+ a derived SK */
1922
+ async send(msg, header) {
1923
+ let i = 0;
1924
+ while (this.conn.readyState !== 1) {
1925
+ await sleep(i);
1926
+ i *= 2;
1927
+ }
1928
+ this.log.debug(chalk.red.bold("OUTH ") +
1929
+ XUtils.encodeHex(header || XUtils.emptyHeader()));
1930
+ this.log.debug(chalk.red.bold("OUT ") + JSON.stringify(msg, null, 4));
1931
+ this.conn.send(XUtils.packMessage(msg, header));
1932
+ }
1933
+ async retrieveKeyBundle(deviceID) {
1934
+ const res = await ax.post(this.getHost() + "/device/" + deviceID + "/keyBundle");
1935
+ return msgpack.decode(Buffer.from(res.data));
1936
+ }
1937
+ async getOTKCount() {
1938
+ const res = await ax.get(this.getHost() +
1939
+ "/device/" +
1940
+ this.getDevice().deviceID +
1941
+ "/otk/count");
1942
+ return msgpack.decode(Buffer.from(res.data)).count;
1943
+ }
1944
+ async submitOTK(amount) {
1945
+ const otks = [];
1946
+ const t0 = performance.now();
1947
+ for (let i = 0; i < amount; i++) {
1948
+ otks[i] = this.createPreKey();
1949
+ }
1950
+ const t1 = performance.now();
1951
+ this.log.info("Generated " + amount + " one time keys in " + (t1 - t0) + " ms.");
1952
+ const savedKeys = await this.database.savePreKeys(otks, true);
1953
+ await ax.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {
1954
+ headers: { "Content-Type": "application/msgpack" },
1981
1955
  });
1982
1956
  }
1957
+ async negotiateOTK() {
1958
+ const otkCount = await this.getOTKCount();
1959
+ this.log.info("Server reported OTK: " + otkCount.toString());
1960
+ const needs = xConstants.MIN_OTK_SUPPLY - otkCount;
1961
+ if (needs === 0) {
1962
+ this.log.info("Server otk supply full.");
1963
+ return;
1964
+ }
1965
+ await this.submitOTK(needs);
1966
+ }
1983
1967
  respond(msg) {
1984
1968
  const response = {
1985
1969
  transmissionID: msg.transmissionID,
1986
1970
  type: "response",
1987
- signed: tweetnacl_1.default.sign(msg.challenge, this.signKeys.secretKey),
1971
+ signed: nacl.sign(msg.challenge, this.signKeys.secretKey),
1988
1972
  };
1989
1973
  this.send(response);
1990
1974
  }
1991
1975
  pong(transmissionID) {
1992
1976
  this.send({ transmissionID, type: "pong" });
1993
1977
  }
1994
- ping() {
1995
- return __awaiter(this, void 0, void 0, function* () {
1996
- if (!this.isAlive) {
1997
- this.log.warn("Ping failed.");
1998
- }
1999
- this.setAlive(false);
2000
- this.send({ transmissionID: uuid.v4(), type: "ping" });
2001
- });
1978
+ async ping() {
1979
+ if (!this.isAlive) {
1980
+ this.log.warn("Ping failed.");
1981
+ }
1982
+ this.setAlive(false);
1983
+ this.send({ transmissionID: uuid.v4(), type: "ping" });
2002
1984
  }
2003
1985
  censorPreKey(preKey) {
2004
1986
  if (!preKey.index) {
2005
1987
  throw new Error("Key index is required.");
2006
1988
  }
2007
1989
  return {
2008
- publicKey: crypto_1.XUtils.decodeHex(preKey.publicKey),
2009
- signature: crypto_1.XUtils.decodeHex(preKey.signature),
1990
+ publicKey: XUtils.decodeHex(preKey.publicKey),
1991
+ signature: XUtils.decodeHex(preKey.signature),
2010
1992
  index: preKey.index,
2011
1993
  deviceID: this.getDevice().deviceID,
2012
1994
  };
2013
1995
  }
2014
1996
  }
2015
- exports.Client = Client;
2016
- Client.loadKeyFile = crypto_1.XUtils.loadKeyFile;
2017
- Client.saveKeyFile = crypto_1.XUtils.saveKeyFile;
2018
- Client.create = (privateKey, options, storage) => __awaiter(void 0, void 0, void 0, function* () {
2019
- const client = new Client(privateKey, options, storage);
2020
- yield client.init();
2021
- return client;
2022
- });
2023
1997
  //# sourceMappingURL=Client.js.map