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