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