@vex-chat/libvex 7.4.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Client.d.ts +39 -24
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +194 -124
- package/dist/Client.js.map +1 -1
- package/dist/codecs.d.ts +8 -4
- package/dist/codecs.d.ts.map +1 -1
- package/dist/codecs.js +2 -5
- package/dist/codecs.js.map +1 -1
- package/dist/http.js +15 -1
- package/dist/http.js.map +1 -1
- package/dist/keystore/node.d.ts.map +1 -1
- package/dist/keystore/node.js +9 -11
- package/dist/keystore/node.js.map +1 -1
- package/dist/storage/sqlite.d.ts +6 -0
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +89 -56
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/utils/verifyKeyBundle.d.ts.map +1 -1
- package/dist/utils/verifyKeyBundle.js +7 -10
- package/dist/utils/verifyKeyBundle.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +310 -173
- package/src/__tests__/harness/shared-suite.ts +82 -11
- package/src/__tests__/http.test.ts +22 -0
- package/src/__tests__/node-keystore.test.ts +69 -0
- package/src/__tests__/prekey-reuse.test.ts +13 -13
- package/src/__tests__/storage-sqlite.test.ts +90 -1
- package/src/__tests__/verifyKeyBundle.test.ts +11 -8
- package/src/codecs.ts +2 -5
- package/src/http.ts +16 -1
- package/src/keystore/node.ts +17 -19
- package/src/storage/sqlite.ts +109 -84
- package/src/utils/verifyKeyBundle.ts +13 -13
package/dist/Client.js
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
4
|
* Commercial licenses available at vex.wtf
|
|
5
5
|
*/
|
|
6
|
-
import { enterCryptoProfileScope, getCryptoProfile, leaveCryptoProfileScope, setCryptoProfile, xBoxKeyPairAsync, xBoxKeyPairFromSecretAsync, xConcat, xConstants, xDHAsync, xEcdhKeyPairFromEcdsaKeyPairAsync, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMnemonic, xRandomBytes, xSecretboxAsync, xSecretboxOpenAsync, xSignAsync, xSignKeyPair, xSignKeyPairAsync, xSignKeyPairFromSecret, xSignKeyPairFromSecretAsync, xSignOpenAsync, XUtils, } from "@vex-chat/crypto";
|
|
7
|
-
import { CallEventSchema, CallSessionSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, WSMessageSchema, } from "@vex-chat/types";
|
|
6
|
+
import { enterCryptoProfileScope, getCryptoProfile, leaveCryptoProfileScope, setCryptoProfile, xBoxKeyPairAsync, xBoxKeyPairFromSecretAsync, xConcat, xConstants, xDHAsync, xEcdhKeyPairFromEcdsaKeyPairAsync, xEncode, xHMAC, xKDF, XKeyConvert, xMakeNonce, xMessageKeySubkeys, xMnemonic, xPreKeySignaturePayload, xRandomBytes, xSecretboxAsync, xSecretboxOpenAsync, xSignAsync, xSignKeyPair, xSignKeyPairAsync, xSignKeyPairFromSecret, xSignKeyPairFromSecretAsync, xSignOpenAsync, XUtils, } from "@vex-chat/crypto";
|
|
7
|
+
import { ACCOUNT_PASSWORD_MAX_LENGTH, ACCOUNT_PASSWORD_MIN_LENGTH, CallEventSchema, CallSessionSchema, IceServerConfigSchema, MailType, MailWSSchema, PermissionSchema, WSMessageSchema, } from "@vex-chat/types";
|
|
8
8
|
import { EventEmitter } from "eventemitter3";
|
|
9
9
|
import * as uuid from "uuid";
|
|
10
10
|
import { z } from "zod/v4";
|
|
11
11
|
import { createFetchHttpClient, isHttpError, } from "./http.js";
|
|
12
12
|
import { clampLocalMessageRetentionDays, formatVexRetentionEnvelope, stripVexRetentionEnvelope, } from "./retention.js";
|
|
13
13
|
import { WebSocketAdapter, WebSocketNotOpenError, } from "./transport/websocket.js";
|
|
14
|
-
import { decodeFipsInitialExtraV1, encodeFipsInitialExtraV1, fipsP256AdFromIdentityPubs,
|
|
14
|
+
import { decodeFipsInitialExtraV1, encodeFipsInitialExtraV1, fipsP256AdFromIdentityPubs, isFipsInitialExtraV1, } from "./utils/fipsMailExtra.js";
|
|
15
15
|
import { decodeRatchetHeader, deriveBootstrapSendChain, encodeRatchetHeader, hasRemoteDhChanged, initRatchetSession, ratchetStepReceive, ratchetStepSend, sessionToSqlPatch, takeReceiveMessageKey, takeSendMessageKey, } from "./utils/ratchet.js";
|
|
16
16
|
import { verifyKeyBundleSignatures } from "./utils/verifyKeyBundle.js";
|
|
17
17
|
/**
|
|
@@ -148,6 +148,16 @@ function libvexDebugLevel() {
|
|
|
148
148
|
return "debug";
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
|
+
function registrationPasswordError(password) {
|
|
152
|
+
if (password.trim().length === 0 ||
|
|
153
|
+
password.length < ACCOUNT_PASSWORD_MIN_LENGTH) {
|
|
154
|
+
return new Error(`Password must be at least ${String(ACCOUNT_PASSWORD_MIN_LENGTH)} characters.`);
|
|
155
|
+
}
|
|
156
|
+
if (password.length > ACCOUNT_PASSWORD_MAX_LENGTH) {
|
|
157
|
+
return new Error(`Password must be at most ${String(ACCOUNT_PASSWORD_MAX_LENGTH)} characters.`);
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
151
161
|
function sleep(ms) {
|
|
152
162
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
153
163
|
}
|
|
@@ -529,6 +539,7 @@ export class Client {
|
|
|
529
539
|
* Helpers for information/actions related to the currently authenticated account.
|
|
530
540
|
*/
|
|
531
541
|
me = {
|
|
542
|
+
changePassword: this.changePassword.bind(this),
|
|
532
543
|
/**
|
|
533
544
|
* Retrieves current device details.
|
|
534
545
|
*
|
|
@@ -614,6 +625,7 @@ export class Client {
|
|
|
614
625
|
listDevices: this.passkeyListDevices.bind(this),
|
|
615
626
|
recoverDeviceRequest: this.passkeyRecoverDeviceRequest.bind(this),
|
|
616
627
|
rejectDeviceRequest: this.passkeyRejectDeviceRequest.bind(this),
|
|
628
|
+
resetPassword: this.resetPasswordWithPasskey.bind(this),
|
|
617
629
|
};
|
|
618
630
|
/**
|
|
619
631
|
* Permission-management methods for the current user.
|
|
@@ -849,14 +861,23 @@ export class Client {
|
|
|
849
861
|
const atRestAes = XUtils.deriveLocalAtRestAesKey(idKeys.secretKey, profile);
|
|
850
862
|
let resolvedStorage = storage;
|
|
851
863
|
if (!resolvedStorage) {
|
|
852
|
-
const
|
|
864
|
+
const nodeStorageModule = "./storage/node.js";
|
|
865
|
+
const isNodeStorageModule = (value) => typeof value === "object" &&
|
|
866
|
+
value !== null &&
|
|
867
|
+
"createNodeStorage" in value &&
|
|
868
|
+
typeof value.createNodeStorage === "function";
|
|
869
|
+
const nodeStorage = await import(
|
|
870
|
+
/* @vite-ignore */ nodeStorageModule);
|
|
871
|
+
if (!isNodeStorageModule(nodeStorage)) {
|
|
872
|
+
throw new Error("Node storage adapter is unavailable.");
|
|
873
|
+
}
|
|
853
874
|
const dbFileName = options?.inMemoryDb
|
|
854
875
|
? ":memory:"
|
|
855
876
|
: XUtils.encodeHex(signKeys.publicKey) + ".sqlite";
|
|
856
877
|
const dbPath = options?.dbFolder
|
|
857
878
|
? options.dbFolder + "/" + dbFileName
|
|
858
879
|
: dbFileName;
|
|
859
|
-
resolvedStorage = createNodeStorage(dbPath, atRestAes);
|
|
880
|
+
resolvedStorage = nodeStorage.createNodeStorage(dbPath, atRestAes);
|
|
860
881
|
}
|
|
861
882
|
await resolvedStorage.init();
|
|
862
883
|
const client = new Client({
|
|
@@ -1147,10 +1168,37 @@ export class Client {
|
|
|
1147
1168
|
return null;
|
|
1148
1169
|
}
|
|
1149
1170
|
/**
|
|
1150
|
-
*
|
|
1171
|
+
* Ends the local authenticated session and disconnects realtime transport.
|
|
1172
|
+
* Spire access tokens are stateless, so local credential removal is the
|
|
1173
|
+
* authoritative logout boundary.
|
|
1151
1174
|
*/
|
|
1152
1175
|
async logout() {
|
|
1153
|
-
|
|
1176
|
+
try {
|
|
1177
|
+
if (this.token) {
|
|
1178
|
+
await this.http.post(this.getHost() + "/goodbye");
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
catch {
|
|
1182
|
+
// Logout must still complete locally when the server is unreachable.
|
|
1183
|
+
}
|
|
1184
|
+
finally {
|
|
1185
|
+
this.token = null;
|
|
1186
|
+
this.user = undefined;
|
|
1187
|
+
this.device = undefined;
|
|
1188
|
+
delete this.http.defaults.headers.common.Authorization;
|
|
1189
|
+
delete this.http.defaults.headers.common["X-Device-Token"];
|
|
1190
|
+
this.autoReconnectEnabled = false;
|
|
1191
|
+
this.postAuthVersion++;
|
|
1192
|
+
if (this.reconnectTimer) {
|
|
1193
|
+
clearTimeout(this.reconnectTimer);
|
|
1194
|
+
this.reconnectTimer = null;
|
|
1195
|
+
}
|
|
1196
|
+
if (this.pingInterval) {
|
|
1197
|
+
clearInterval(this.pingInterval);
|
|
1198
|
+
this.pingInterval = null;
|
|
1199
|
+
}
|
|
1200
|
+
this.socket.close();
|
|
1201
|
+
}
|
|
1154
1202
|
}
|
|
1155
1203
|
/** Removes an event listener. See {@link ClientEvents} for available events. */
|
|
1156
1204
|
off(event, fn, context) {
|
|
@@ -1202,115 +1250,45 @@ export class Client {
|
|
|
1202
1250
|
/**
|
|
1203
1251
|
* Registers a new account on the server.
|
|
1204
1252
|
*
|
|
1205
|
-
* @param username -
|
|
1206
|
-
* @param password -
|
|
1253
|
+
* @param username - Username to register.
|
|
1254
|
+
* @param password - Password for the account and device approval request.
|
|
1207
1255
|
* @returns `[user, null]` on success, `[null, error]` on failure.
|
|
1208
1256
|
*
|
|
1209
1257
|
* @example
|
|
1210
1258
|
* ```ts
|
|
1211
|
-
* const [user, err] = await client.register("MyUsername");
|
|
1259
|
+
* const [user, err] = await client.register("MyUsername", "correct horse battery staple");
|
|
1212
1260
|
* ```
|
|
1213
1261
|
*/
|
|
1214
1262
|
async register(username, password) {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
const regKey = await this.getToken("register");
|
|
1219
|
-
if (regKey) {
|
|
1220
|
-
// Usernames are case-insensitive at the protocol level;
|
|
1221
|
-
// lowercase before sending so the local SDK view matches
|
|
1222
|
-
// what the server canonicalizes and persists.
|
|
1223
|
-
const resolvedUsername = username?.trim().length !== 0 && username !== undefined
|
|
1224
|
-
? username.trim().toLowerCase()
|
|
1225
|
-
: Client.randomUsername();
|
|
1226
|
-
const resolvedPassword = password?.trim().length !== 0 && password !== undefined
|
|
1227
|
-
? password
|
|
1228
|
-
: uuid.v4();
|
|
1229
|
-
const signKey = XUtils.encodeHex(this.signKeys.publicKey);
|
|
1230
|
-
const signed = XUtils.encodeHex(await xSignAsync(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
|
|
1231
|
-
const preKeyIndex = this.xKeyRing.preKeys.index;
|
|
1232
|
-
const regMsg = {
|
|
1233
|
-
deviceName: this.options?.deviceName ?? "unknown",
|
|
1234
|
-
password: resolvedPassword,
|
|
1235
|
-
preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
|
|
1236
|
-
preKeyIndex,
|
|
1237
|
-
preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
|
|
1238
|
-
signed,
|
|
1239
|
-
signKey,
|
|
1240
|
-
username: resolvedUsername,
|
|
1241
|
-
};
|
|
1242
|
-
try {
|
|
1243
|
-
const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
1244
|
-
// New key-cluster server response: { device, token, user }.
|
|
1245
|
-
// Legacy response (still deployed in some environments): user only.
|
|
1246
|
-
let didDecodeRegisterResponse = false;
|
|
1247
|
-
let pendingApproval = null;
|
|
1248
|
-
try {
|
|
1249
|
-
const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
|
|
1250
|
-
this.device = device;
|
|
1251
|
-
this.setUser(user);
|
|
1252
|
-
this.token = token;
|
|
1253
|
-
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
1254
|
-
didDecodeRegisterResponse = true;
|
|
1255
|
-
}
|
|
1256
|
-
catch {
|
|
1257
|
-
// fall through to legacy decode path
|
|
1258
|
-
}
|
|
1259
|
-
if (!didDecodeRegisterResponse) {
|
|
1260
|
-
try {
|
|
1261
|
-
pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
|
|
1262
|
-
}
|
|
1263
|
-
catch {
|
|
1264
|
-
// fall through to legacy decode path
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
if (!didDecodeRegisterResponse) {
|
|
1268
|
-
if (pendingApproval !== null) {
|
|
1269
|
-
return [
|
|
1270
|
-
null,
|
|
1271
|
-
new DeviceApprovalRequiredError({
|
|
1272
|
-
challenge: pendingApproval.challenge,
|
|
1273
|
-
expiresAt: pendingApproval.expiresAt,
|
|
1274
|
-
requestID: pendingApproval.requestID,
|
|
1275
|
-
userID: pendingApproval.userID ?? null,
|
|
1276
|
-
}),
|
|
1277
|
-
];
|
|
1278
|
-
}
|
|
1279
|
-
const legacyUser = decodeHttpResponse(UserCodec, res.data);
|
|
1280
|
-
this.setUser(legacyUser);
|
|
1281
|
-
// Legacy servers require /auth after /register to get a JWT.
|
|
1282
|
-
const loginResult = await this.login(resolvedUsername, resolvedPassword);
|
|
1283
|
-
if (!loginResult.ok) {
|
|
1284
|
-
return [
|
|
1285
|
-
null,
|
|
1286
|
-
new Error(loginResult.error ??
|
|
1287
|
-
"Legacy register succeeded but login failed."),
|
|
1288
|
-
];
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return [this.getUser(), null];
|
|
1292
|
-
}
|
|
1293
|
-
catch (err) {
|
|
1294
|
-
if (isHttpError(err) && err.response) {
|
|
1295
|
-
return [
|
|
1296
|
-
null,
|
|
1297
|
-
new Error(spireErrorBodyMessage(err.response.data)),
|
|
1298
|
-
];
|
|
1299
|
-
}
|
|
1300
|
-
return [
|
|
1301
|
-
null,
|
|
1302
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
1303
|
-
];
|
|
1304
|
-
}
|
|
1305
|
-
}
|
|
1306
|
-
else {
|
|
1307
|
-
return [null, new Error("Couldn't get regkey from server.")];
|
|
1263
|
+
const passwordError = registrationPasswordError(password);
|
|
1264
|
+
if (passwordError) {
|
|
1265
|
+
return [null, passwordError];
|
|
1308
1266
|
}
|
|
1267
|
+
return this.registerAccountOrEnrollment(username, "create-account", password);
|
|
1309
1268
|
}
|
|
1310
1269
|
removeAllListeners(event) {
|
|
1311
1270
|
this.emitter.removeAllListeners(event);
|
|
1312
1271
|
return this;
|
|
1313
1272
|
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Requests approval for a new device using the account password. This
|
|
1275
|
+
* enrollment flow never creates an account when the username is unknown.
|
|
1276
|
+
*/
|
|
1277
|
+
async requestDeviceEnrollment(username, password) {
|
|
1278
|
+
const passwordError = registrationPasswordError(password);
|
|
1279
|
+
if (passwordError) {
|
|
1280
|
+
return [null, passwordError];
|
|
1281
|
+
}
|
|
1282
|
+
return this.registerAccountOrEnrollment(username, "enroll-device", password);
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Requests enrollment for this device after a successful passkey ceremony.
|
|
1286
|
+
* This is not account registration: the passkey bearer must identify an
|
|
1287
|
+
* existing account, and the server returns a pending device request.
|
|
1288
|
+
*/
|
|
1289
|
+
async requestDeviceEnrollmentWithPasskey(username) {
|
|
1290
|
+
return this.registerAccountOrEnrollment(username, "enroll-device");
|
|
1291
|
+
}
|
|
1314
1292
|
/**
|
|
1315
1293
|
* Updates the local retention cap (1–30 days) and prunes immediately.
|
|
1316
1294
|
* Does not affect server-side storage.
|
|
@@ -1448,6 +1426,16 @@ export class Client {
|
|
|
1448
1426
|
signature: XUtils.decodeHex(preKey.signature),
|
|
1449
1427
|
};
|
|
1450
1428
|
}
|
|
1429
|
+
async changePassword(currentPassword, newPassword) {
|
|
1430
|
+
const passwordError = registrationPasswordError(newPassword);
|
|
1431
|
+
if (passwordError)
|
|
1432
|
+
throw passwordError;
|
|
1433
|
+
if (currentPassword.length === 0 ||
|
|
1434
|
+
currentPassword.length > ACCOUNT_PASSWORD_MAX_LENGTH) {
|
|
1435
|
+
throw new Error("Current password is invalid.");
|
|
1436
|
+
}
|
|
1437
|
+
await this.replaceAccountPassword({ currentPassword, newPassword });
|
|
1438
|
+
}
|
|
1451
1439
|
async createChannel(name, serverID) {
|
|
1452
1440
|
const body = { name };
|
|
1453
1441
|
const res = await this.http.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
|
|
@@ -1515,12 +1503,10 @@ export class Client {
|
|
|
1515
1503
|
const res = await this.http.post(this.getHost() + "/server/" + serverID + "/invites", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
|
|
1516
1504
|
return decodeHttpResponse(InviteCodec, res.data);
|
|
1517
1505
|
}
|
|
1518
|
-
async createPreKey() {
|
|
1506
|
+
async createPreKey(kind) {
|
|
1519
1507
|
return this.runWithThisCryptoProfile(async () => {
|
|
1520
1508
|
const preKeyPair = await xBoxKeyPairAsync();
|
|
1521
|
-
const toSign = this.cryptoProfile
|
|
1522
|
-
? fipsP256PreKeySignPayload(preKeyPair.publicKey)
|
|
1523
|
-
: xEncode(xConstants.CURVE, preKeyPair.publicKey);
|
|
1509
|
+
const toSign = xPreKeySignaturePayload(preKeyPair.publicKey, kind, this.cryptoProfile);
|
|
1524
1510
|
return {
|
|
1525
1511
|
keyPair: preKeyPair,
|
|
1526
1512
|
signature: await xSignAsync(toSign, this.signKeys.secretKey),
|
|
@@ -1596,12 +1582,13 @@ export class Client {
|
|
|
1596
1582
|
: XUtils.numberToUint8Arr(0);
|
|
1597
1583
|
// shared secret key
|
|
1598
1584
|
const SK = xKDF(IKM);
|
|
1585
|
+
const initialSubkeys = xMessageKeySubkeys(SK);
|
|
1599
1586
|
const PK = (await xBoxKeyPairFromSecretAsync(SK)).publicKey;
|
|
1600
1587
|
const AD = fips
|
|
1601
1588
|
? fipsP256AdFromIdentityPubs(IK_AP, new Uint8Array(keyBundle.signKey))
|
|
1602
1589
|
: xConcat(xEncode(xConstants.CURVE, IK_AP), xEncode(xConstants.CURVE, IK_B));
|
|
1603
1590
|
const nonce = xMakeNonce();
|
|
1604
|
-
const cipher = await xSecretboxAsync(message, nonce,
|
|
1591
|
+
const cipher = await xSecretboxAsync(message, nonce, initialSubkeys.encryptionKey);
|
|
1605
1592
|
const signKeyWire = fips ? IK_AP : this.signKeys.publicKey;
|
|
1606
1593
|
const ephKeyWire = ephemeralKeys.publicKey;
|
|
1607
1594
|
const extra = fips
|
|
@@ -1620,7 +1607,7 @@ export class Client {
|
|
|
1620
1607
|
recipient: device.deviceID,
|
|
1621
1608
|
sender: this.getDevice().deviceID,
|
|
1622
1609
|
};
|
|
1623
|
-
const hmac = xHMAC(mail,
|
|
1610
|
+
const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
|
|
1624
1611
|
const msg = {
|
|
1625
1612
|
action: "CREATE",
|
|
1626
1613
|
data: mail,
|
|
@@ -2114,6 +2101,9 @@ export class Client {
|
|
|
2114
2101
|
"/device/" +
|
|
2115
2102
|
this.getDevice().deviceID +
|
|
2116
2103
|
"/mail");
|
|
2104
|
+
if (!this.token || this.isManualCloseInFlight()) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2117
2107
|
const mailBuffer = new Uint8Array(res.data);
|
|
2118
2108
|
const rawInbox = z
|
|
2119
2109
|
.array(mailInboxEntry)
|
|
@@ -2445,9 +2435,7 @@ export class Client {
|
|
|
2445
2435
|
}
|
|
2446
2436
|
async isPreKeySignedByCurrentDevice(preKey) {
|
|
2447
2437
|
return this.runWithThisCryptoProfile(async () => {
|
|
2448
|
-
const payload =
|
|
2449
|
-
? fipsP256PreKeySignPayload(preKey.keyPair.publicKey)
|
|
2450
|
-
: xEncode(xConstants.CURVE, preKey.keyPair.publicKey);
|
|
2438
|
+
const payload = xPreKeySignaturePayload(preKey.keyPair.publicKey, "signed", this.cryptoProfile);
|
|
2451
2439
|
const opened = await xSignOpenAsync(preKey.signature, this.signKeys.publicKey);
|
|
2452
2440
|
return Boolean(opened && XUtils.bytesEqual(opened, payload));
|
|
2453
2441
|
});
|
|
@@ -2619,7 +2607,7 @@ export class Client {
|
|
|
2619
2607
|
let preKeys = await this.database.getPreKeys();
|
|
2620
2608
|
if (!preKeys ||
|
|
2621
2609
|
!(await this.isPreKeySignedByCurrentDevice(preKeys))) {
|
|
2622
|
-
const unsaved = await this.createPreKey();
|
|
2610
|
+
const unsaved = await this.createPreKey("signed");
|
|
2623
2611
|
const [saved] = await this.database.savePreKeys([unsaved], false);
|
|
2624
2612
|
if (!saved || saved.index == null)
|
|
2625
2613
|
throw new Error("Failed to save prekey — no index returned.");
|
|
@@ -2889,9 +2877,10 @@ export class Client {
|
|
|
2889
2877
|
: xConcat(DH1, DH2, DH3);
|
|
2890
2878
|
// shared secret key
|
|
2891
2879
|
const SK = xKDF(IKM);
|
|
2880
|
+
const initialSubkeys = xMessageKeySubkeys(SK);
|
|
2892
2881
|
const PK = (await xBoxKeyPairFromSecretAsync(SK))
|
|
2893
2882
|
.publicKey;
|
|
2894
|
-
const hmac = xHMAC(mail,
|
|
2883
|
+
const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
|
|
2895
2884
|
// associated data
|
|
2896
2885
|
const AD = fipsRead
|
|
2897
2886
|
? fipsP256AdFromIdentityPubs(IK_A, IK_BP)
|
|
@@ -2922,7 +2911,7 @@ export class Client {
|
|
|
2922
2911
|
this.acknowledgeRepeatedDecryptFailure(mail, failureCount, timestamp);
|
|
2923
2912
|
return;
|
|
2924
2913
|
}
|
|
2925
|
-
const unsealed = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce),
|
|
2914
|
+
const unsealed = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), initialSubkeys.encryptionKey);
|
|
2926
2915
|
if (unsealed) {
|
|
2927
2916
|
let plaintext = "";
|
|
2928
2917
|
if (!mail.forward) {
|
|
@@ -3076,15 +3065,17 @@ export class Client {
|
|
|
3076
3065
|
else if (hasRemoteDhChanged(candidateSession.DHr, ratchetHeader.dhPub)) {
|
|
3077
3066
|
await ratchetStepReceive(candidateSession, ratchetHeader.dhPub, ratchetHeader.pn);
|
|
3078
3067
|
}
|
|
3079
|
-
|
|
3080
|
-
let
|
|
3068
|
+
const messageKey = takeReceiveMessageKey(candidateSession, ratchetHeader.dhPub, ratchetHeader.n);
|
|
3069
|
+
let messageSubkeys = xMessageKeySubkeys(messageKey);
|
|
3070
|
+
let HMAC = xHMAC(mail, messageSubkeys.authenticationKey);
|
|
3081
3071
|
if (!XUtils.bytesEqual(HMAC, header) &&
|
|
3082
3072
|
firstInboundFromSubsequent &&
|
|
3083
3073
|
!originalSession.CKr) {
|
|
3084
3074
|
const ratchetedCandidate = cloneSessionCrypto(originalSession);
|
|
3085
3075
|
await ratchetStepReceive(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.pn);
|
|
3086
3076
|
const ratchetedMessageKey = takeReceiveMessageKey(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.n);
|
|
3087
|
-
const
|
|
3077
|
+
const ratchetedSubkeys = xMessageKeySubkeys(ratchetedMessageKey);
|
|
3078
|
+
const ratchetedHMAC = xHMAC(mail, ratchetedSubkeys.authenticationKey);
|
|
3088
3079
|
if (XUtils.bytesEqual(ratchetedHMAC, header)) {
|
|
3089
3080
|
if (libvexDebugDmEnabled()) {
|
|
3090
3081
|
debugLibvexDm("readMail subsequent: first inbound used DH-ratchet fallback", {
|
|
@@ -3093,7 +3084,7 @@ export class Client {
|
|
|
3093
3084
|
});
|
|
3094
3085
|
}
|
|
3095
3086
|
candidateSession = ratchetedCandidate;
|
|
3096
|
-
|
|
3087
|
+
messageSubkeys = ratchetedSubkeys;
|
|
3097
3088
|
HMAC = ratchetedHMAC;
|
|
3098
3089
|
}
|
|
3099
3090
|
}
|
|
@@ -3120,7 +3111,7 @@ export class Client {
|
|
|
3120
3111
|
return;
|
|
3121
3112
|
}
|
|
3122
3113
|
session = candidateSession;
|
|
3123
|
-
const decrypted = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce),
|
|
3114
|
+
const decrypted = await xSecretboxOpenAsync(new Uint8Array(mail.cipher), new Uint8Array(mail.nonce), messageSubkeys.encryptionKey);
|
|
3124
3115
|
if (decrypted) {
|
|
3125
3116
|
const fwdMsg2 = mail.forward
|
|
3126
3117
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
@@ -3243,6 +3234,75 @@ export class Client {
|
|
|
3243
3234
|
const res = await this.http.patch(this.getHost() + "/invite/" + inviteID);
|
|
3244
3235
|
return decodeHttpResponse(PermissionCodec, res.data);
|
|
3245
3236
|
}
|
|
3237
|
+
async registerAccountOrEnrollment(username, intent, password) {
|
|
3238
|
+
const resolvedUsername = username.trim().toLowerCase();
|
|
3239
|
+
if (!/^\w{3,19}$/.test(resolvedUsername)) {
|
|
3240
|
+
return [
|
|
3241
|
+
null,
|
|
3242
|
+
new Error("Username must be between three and nineteen letters, digits, or underscores."),
|
|
3243
|
+
];
|
|
3244
|
+
}
|
|
3245
|
+
while (!this.xKeyRing) {
|
|
3246
|
+
await sleep(100);
|
|
3247
|
+
}
|
|
3248
|
+
const regKey = await this.getToken("register");
|
|
3249
|
+
if (regKey) {
|
|
3250
|
+
const signKey = XUtils.encodeHex(this.signKeys.publicKey);
|
|
3251
|
+
const signed = XUtils.encodeHex(await xSignAsync(Uint8Array.from(uuid.parse(regKey.key)), this.signKeys.secretKey));
|
|
3252
|
+
const preKeyIndex = this.xKeyRing.preKeys.index;
|
|
3253
|
+
const regMsg = {
|
|
3254
|
+
deviceName: this.options?.deviceName ?? "unknown",
|
|
3255
|
+
intent,
|
|
3256
|
+
preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
|
|
3257
|
+
preKeyIndex,
|
|
3258
|
+
preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
|
|
3259
|
+
signed,
|
|
3260
|
+
signKey,
|
|
3261
|
+
username: resolvedUsername,
|
|
3262
|
+
};
|
|
3263
|
+
if (password !== undefined) {
|
|
3264
|
+
regMsg.password = password;
|
|
3265
|
+
}
|
|
3266
|
+
try {
|
|
3267
|
+
const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
3268
|
+
try {
|
|
3269
|
+
const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
|
|
3270
|
+
this.device = device;
|
|
3271
|
+
this.setUser(user);
|
|
3272
|
+
this.token = token;
|
|
3273
|
+
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
3274
|
+
}
|
|
3275
|
+
catch {
|
|
3276
|
+
const pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
|
|
3277
|
+
return [
|
|
3278
|
+
null,
|
|
3279
|
+
new DeviceApprovalRequiredError({
|
|
3280
|
+
challenge: pendingApproval.challenge,
|
|
3281
|
+
expiresAt: pendingApproval.expiresAt,
|
|
3282
|
+
requestID: pendingApproval.requestID,
|
|
3283
|
+
userID: pendingApproval.userID,
|
|
3284
|
+
}),
|
|
3285
|
+
];
|
|
3286
|
+
}
|
|
3287
|
+
return [this.getUser(), null];
|
|
3288
|
+
}
|
|
3289
|
+
catch (err) {
|
|
3290
|
+
if (isHttpError(err) && err.response) {
|
|
3291
|
+
return [
|
|
3292
|
+
null,
|
|
3293
|
+
new Error(spireErrorBodyMessage(err.response.data)),
|
|
3294
|
+
];
|
|
3295
|
+
}
|
|
3296
|
+
return [
|
|
3297
|
+
null,
|
|
3298
|
+
err instanceof Error ? err : new Error(String(err)),
|
|
3299
|
+
];
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
else {
|
|
3303
|
+
return [null, new Error("Couldn't get regkey from server.")];
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3246
3306
|
registerDecryptFailure(mail) {
|
|
3247
3307
|
const count = (this.decryptFailureCounts.get(mail.mailID) ?? 0) + 1;
|
|
3248
3308
|
this.decryptFailureCounts.set(mail.mailID, count);
|
|
@@ -3291,6 +3351,15 @@ export class Client {
|
|
|
3291
3351
|
requestID +
|
|
3292
3352
|
"/reject");
|
|
3293
3353
|
}
|
|
3354
|
+
async replaceAccountPassword(payload) {
|
|
3355
|
+
await this.http.patch(this.getHost() + "/user/" + this.getUser().userID + "/password", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
|
|
3356
|
+
}
|
|
3357
|
+
async resetPasswordWithPasskey(newPassword) {
|
|
3358
|
+
const passwordError = registrationPasswordError(newPassword);
|
|
3359
|
+
if (passwordError)
|
|
3360
|
+
throw passwordError;
|
|
3361
|
+
await this.replaceAccountPassword({ newPassword });
|
|
3362
|
+
}
|
|
3294
3363
|
async respond(msg) {
|
|
3295
3364
|
const response = {
|
|
3296
3365
|
signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
|
|
@@ -3370,7 +3439,7 @@ export class Client {
|
|
|
3370
3439
|
challenge: newDevice.challenge,
|
|
3371
3440
|
expiresAt: newDevice.expiresAt,
|
|
3372
3441
|
requestID: newDevice.requestID,
|
|
3373
|
-
userID: newDevice.userID
|
|
3442
|
+
userID: newDevice.userID,
|
|
3374
3443
|
});
|
|
3375
3444
|
}
|
|
3376
3445
|
else {
|
|
@@ -3672,6 +3741,7 @@ export class Client {
|
|
|
3672
3741
|
await ratchetStepSend(session);
|
|
3673
3742
|
}
|
|
3674
3743
|
const { messageKey, n } = takeSendMessageKey(session);
|
|
3744
|
+
const messageSubkeys = xMessageKeySubkeys(messageKey);
|
|
3675
3745
|
const ratchetHeader = {
|
|
3676
3746
|
dhPub: session.DHsPublic,
|
|
3677
3747
|
n,
|
|
@@ -3679,7 +3749,7 @@ export class Client {
|
|
|
3679
3749
|
version: 1,
|
|
3680
3750
|
};
|
|
3681
3751
|
const nonce = xMakeNonce();
|
|
3682
|
-
const cipher = await xSecretboxAsync(msg, nonce,
|
|
3752
|
+
const cipher = await xSecretboxAsync(msg, nonce, messageSubkeys.encryptionKey);
|
|
3683
3753
|
const extra = encodeRatchetHeader(ratchetHeader);
|
|
3684
3754
|
const mail = {
|
|
3685
3755
|
authorID: this.getUser().userID,
|
|
@@ -3701,7 +3771,7 @@ export class Client {
|
|
|
3701
3771
|
transmissionID: uuid.v4(),
|
|
3702
3772
|
type: "resource",
|
|
3703
3773
|
};
|
|
3704
|
-
const hmac = xHMAC(mail,
|
|
3774
|
+
const hmac = xHMAC(mail, messageSubkeys.authenticationKey);
|
|
3705
3775
|
const fwdOut = forward
|
|
3706
3776
|
? messageSchema.parse(msgpack.decode(msg))
|
|
3707
3777
|
: null;
|
|
@@ -3949,7 +4019,7 @@ export class Client {
|
|
|
3949
4019
|
async submitOTK(amount) {
|
|
3950
4020
|
const otks = [];
|
|
3951
4021
|
for (let i = 0; i < amount; i++) {
|
|
3952
|
-
otks.push(await this.createPreKey());
|
|
4022
|
+
otks.push(await this.createPreKey("one-time"));
|
|
3953
4023
|
}
|
|
3954
4024
|
const savedKeys = await this.database.savePreKeys(otks, true);
|
|
3955
4025
|
await this.http.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {
|