@vex-chat/libvex 8.0.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 +34 -21
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +193 -131
- 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 +305 -180
- 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,8 +1250,8 @@ export class Client {
|
|
|
1202
1250
|
/**
|
|
1203
1251
|
* Registers a new account on the server.
|
|
1204
1252
|
*
|
|
1205
|
-
* @param username -
|
|
1206
|
-
* @param password - Password for
|
|
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
|
|
@@ -1212,113 +1260,35 @@ export class Client {
|
|
|
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
|
-
: undefined;
|
|
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
|
-
preKey: XUtils.encodeHex(this.xKeyRing.preKeys.keyPair.publicKey),
|
|
1235
|
-
preKeyIndex,
|
|
1236
|
-
preKeySignature: XUtils.encodeHex(this.xKeyRing.preKeys.signature),
|
|
1237
|
-
signed,
|
|
1238
|
-
signKey,
|
|
1239
|
-
username: resolvedUsername,
|
|
1240
|
-
};
|
|
1241
|
-
if (resolvedPassword !== undefined) {
|
|
1242
|
-
regMsg.password = resolvedPassword;
|
|
1243
|
-
}
|
|
1244
|
-
try {
|
|
1245
|
-
const res = await this.http.post(this.getHost() + "/register", msgpack.encode(regMsg), { headers: { "Content-Type": "application/msgpack" } });
|
|
1246
|
-
// New key-cluster server response: { device, token, user }.
|
|
1247
|
-
// Legacy response (still deployed in some environments): user only.
|
|
1248
|
-
let didDecodeRegisterResponse = false;
|
|
1249
|
-
let pendingApproval = null;
|
|
1250
|
-
try {
|
|
1251
|
-
const { device, token, user } = decodeHttpResponse(RegisterResponseCodec, res.data);
|
|
1252
|
-
this.device = device;
|
|
1253
|
-
this.setUser(user);
|
|
1254
|
-
this.token = token;
|
|
1255
|
-
this.http.defaults.headers.common.Authorization = `Bearer ${token}`;
|
|
1256
|
-
didDecodeRegisterResponse = true;
|
|
1257
|
-
}
|
|
1258
|
-
catch {
|
|
1259
|
-
// fall through to legacy decode path
|
|
1260
|
-
}
|
|
1261
|
-
if (!didDecodeRegisterResponse) {
|
|
1262
|
-
try {
|
|
1263
|
-
pendingApproval = decodeHttpResponse(RegisterPendingApprovalCodec, res.data);
|
|
1264
|
-
}
|
|
1265
|
-
catch {
|
|
1266
|
-
// fall through to legacy decode path
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
if (!didDecodeRegisterResponse) {
|
|
1270
|
-
if (pendingApproval !== null) {
|
|
1271
|
-
return [
|
|
1272
|
-
null,
|
|
1273
|
-
new DeviceApprovalRequiredError({
|
|
1274
|
-
challenge: pendingApproval.challenge,
|
|
1275
|
-
expiresAt: pendingApproval.expiresAt,
|
|
1276
|
-
requestID: pendingApproval.requestID,
|
|
1277
|
-
userID: pendingApproval.userID ?? null,
|
|
1278
|
-
}),
|
|
1279
|
-
];
|
|
1280
|
-
}
|
|
1281
|
-
const legacyUser = decodeHttpResponse(UserCodec, res.data);
|
|
1282
|
-
this.setUser(legacyUser);
|
|
1283
|
-
// Legacy servers require /auth after /register to get a JWT.
|
|
1284
|
-
if (resolvedPassword === undefined) {
|
|
1285
|
-
return [
|
|
1286
|
-
null,
|
|
1287
|
-
new Error("Legacy register succeeded without a password, so the SDK could not complete login."),
|
|
1288
|
-
];
|
|
1289
|
-
}
|
|
1290
|
-
const loginResult = await this.login(resolvedUsername, resolvedPassword);
|
|
1291
|
-
if (!loginResult.ok) {
|
|
1292
|
-
return [
|
|
1293
|
-
null,
|
|
1294
|
-
new Error(loginResult.error ??
|
|
1295
|
-
"Legacy register succeeded but login failed."),
|
|
1296
|
-
];
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
return [this.getUser(), null];
|
|
1300
|
-
}
|
|
1301
|
-
catch (err) {
|
|
1302
|
-
if (isHttpError(err) && err.response) {
|
|
1303
|
-
return [
|
|
1304
|
-
null,
|
|
1305
|
-
new Error(spireErrorBodyMessage(err.response.data)),
|
|
1306
|
-
];
|
|
1307
|
-
}
|
|
1308
|
-
return [
|
|
1309
|
-
null,
|
|
1310
|
-
err instanceof Error ? err : new Error(String(err)),
|
|
1311
|
-
];
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
else {
|
|
1315
|
-
return [null, new Error("Couldn't get regkey from server.")];
|
|
1263
|
+
const passwordError = registrationPasswordError(password);
|
|
1264
|
+
if (passwordError) {
|
|
1265
|
+
return [null, passwordError];
|
|
1316
1266
|
}
|
|
1267
|
+
return this.registerAccountOrEnrollment(username, "create-account", password);
|
|
1317
1268
|
}
|
|
1318
1269
|
removeAllListeners(event) {
|
|
1319
1270
|
this.emitter.removeAllListeners(event);
|
|
1320
1271
|
return this;
|
|
1321
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
|
+
}
|
|
1322
1292
|
/**
|
|
1323
1293
|
* Updates the local retention cap (1–30 days) and prunes immediately.
|
|
1324
1294
|
* Does not affect server-side storage.
|
|
@@ -1456,6 +1426,16 @@ export class Client {
|
|
|
1456
1426
|
signature: XUtils.decodeHex(preKey.signature),
|
|
1457
1427
|
};
|
|
1458
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
|
+
}
|
|
1459
1439
|
async createChannel(name, serverID) {
|
|
1460
1440
|
const body = { name };
|
|
1461
1441
|
const res = await this.http.post(this.getHost() + "/server/" + serverID + "/channels", msgpack.encode(body), { headers: { "Content-Type": "application/msgpack" } });
|
|
@@ -1523,12 +1503,10 @@ export class Client {
|
|
|
1523
1503
|
const res = await this.http.post(this.getHost() + "/server/" + serverID + "/invites", msgpack.encode(payload), { headers: { "Content-Type": "application/msgpack" } });
|
|
1524
1504
|
return decodeHttpResponse(InviteCodec, res.data);
|
|
1525
1505
|
}
|
|
1526
|
-
async createPreKey() {
|
|
1506
|
+
async createPreKey(kind) {
|
|
1527
1507
|
return this.runWithThisCryptoProfile(async () => {
|
|
1528
1508
|
const preKeyPair = await xBoxKeyPairAsync();
|
|
1529
|
-
const toSign = this.cryptoProfile
|
|
1530
|
-
? fipsP256PreKeySignPayload(preKeyPair.publicKey)
|
|
1531
|
-
: xEncode(xConstants.CURVE, preKeyPair.publicKey);
|
|
1509
|
+
const toSign = xPreKeySignaturePayload(preKeyPair.publicKey, kind, this.cryptoProfile);
|
|
1532
1510
|
return {
|
|
1533
1511
|
keyPair: preKeyPair,
|
|
1534
1512
|
signature: await xSignAsync(toSign, this.signKeys.secretKey),
|
|
@@ -1604,12 +1582,13 @@ export class Client {
|
|
|
1604
1582
|
: XUtils.numberToUint8Arr(0);
|
|
1605
1583
|
// shared secret key
|
|
1606
1584
|
const SK = xKDF(IKM);
|
|
1585
|
+
const initialSubkeys = xMessageKeySubkeys(SK);
|
|
1607
1586
|
const PK = (await xBoxKeyPairFromSecretAsync(SK)).publicKey;
|
|
1608
1587
|
const AD = fips
|
|
1609
1588
|
? fipsP256AdFromIdentityPubs(IK_AP, new Uint8Array(keyBundle.signKey))
|
|
1610
1589
|
: xConcat(xEncode(xConstants.CURVE, IK_AP), xEncode(xConstants.CURVE, IK_B));
|
|
1611
1590
|
const nonce = xMakeNonce();
|
|
1612
|
-
const cipher = await xSecretboxAsync(message, nonce,
|
|
1591
|
+
const cipher = await xSecretboxAsync(message, nonce, initialSubkeys.encryptionKey);
|
|
1613
1592
|
const signKeyWire = fips ? IK_AP : this.signKeys.publicKey;
|
|
1614
1593
|
const ephKeyWire = ephemeralKeys.publicKey;
|
|
1615
1594
|
const extra = fips
|
|
@@ -1628,7 +1607,7 @@ export class Client {
|
|
|
1628
1607
|
recipient: device.deviceID,
|
|
1629
1608
|
sender: this.getDevice().deviceID,
|
|
1630
1609
|
};
|
|
1631
|
-
const hmac = xHMAC(mail,
|
|
1610
|
+
const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
|
|
1632
1611
|
const msg = {
|
|
1633
1612
|
action: "CREATE",
|
|
1634
1613
|
data: mail,
|
|
@@ -2122,6 +2101,9 @@ export class Client {
|
|
|
2122
2101
|
"/device/" +
|
|
2123
2102
|
this.getDevice().deviceID +
|
|
2124
2103
|
"/mail");
|
|
2104
|
+
if (!this.token || this.isManualCloseInFlight()) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2125
2107
|
const mailBuffer = new Uint8Array(res.data);
|
|
2126
2108
|
const rawInbox = z
|
|
2127
2109
|
.array(mailInboxEntry)
|
|
@@ -2453,9 +2435,7 @@ export class Client {
|
|
|
2453
2435
|
}
|
|
2454
2436
|
async isPreKeySignedByCurrentDevice(preKey) {
|
|
2455
2437
|
return this.runWithThisCryptoProfile(async () => {
|
|
2456
|
-
const payload =
|
|
2457
|
-
? fipsP256PreKeySignPayload(preKey.keyPair.publicKey)
|
|
2458
|
-
: xEncode(xConstants.CURVE, preKey.keyPair.publicKey);
|
|
2438
|
+
const payload = xPreKeySignaturePayload(preKey.keyPair.publicKey, "signed", this.cryptoProfile);
|
|
2459
2439
|
const opened = await xSignOpenAsync(preKey.signature, this.signKeys.publicKey);
|
|
2460
2440
|
return Boolean(opened && XUtils.bytesEqual(opened, payload));
|
|
2461
2441
|
});
|
|
@@ -2627,7 +2607,7 @@ export class Client {
|
|
|
2627
2607
|
let preKeys = await this.database.getPreKeys();
|
|
2628
2608
|
if (!preKeys ||
|
|
2629
2609
|
!(await this.isPreKeySignedByCurrentDevice(preKeys))) {
|
|
2630
|
-
const unsaved = await this.createPreKey();
|
|
2610
|
+
const unsaved = await this.createPreKey("signed");
|
|
2631
2611
|
const [saved] = await this.database.savePreKeys([unsaved], false);
|
|
2632
2612
|
if (!saved || saved.index == null)
|
|
2633
2613
|
throw new Error("Failed to save prekey — no index returned.");
|
|
@@ -2897,9 +2877,10 @@ export class Client {
|
|
|
2897
2877
|
: xConcat(DH1, DH2, DH3);
|
|
2898
2878
|
// shared secret key
|
|
2899
2879
|
const SK = xKDF(IKM);
|
|
2880
|
+
const initialSubkeys = xMessageKeySubkeys(SK);
|
|
2900
2881
|
const PK = (await xBoxKeyPairFromSecretAsync(SK))
|
|
2901
2882
|
.publicKey;
|
|
2902
|
-
const hmac = xHMAC(mail,
|
|
2883
|
+
const hmac = xHMAC(mail, initialSubkeys.authenticationKey);
|
|
2903
2884
|
// associated data
|
|
2904
2885
|
const AD = fipsRead
|
|
2905
2886
|
? fipsP256AdFromIdentityPubs(IK_A, IK_BP)
|
|
@@ -2930,7 +2911,7 @@ export class Client {
|
|
|
2930
2911
|
this.acknowledgeRepeatedDecryptFailure(mail, failureCount, timestamp);
|
|
2931
2912
|
return;
|
|
2932
2913
|
}
|
|
2933
|
-
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);
|
|
2934
2915
|
if (unsealed) {
|
|
2935
2916
|
let plaintext = "";
|
|
2936
2917
|
if (!mail.forward) {
|
|
@@ -3084,15 +3065,17 @@ export class Client {
|
|
|
3084
3065
|
else if (hasRemoteDhChanged(candidateSession.DHr, ratchetHeader.dhPub)) {
|
|
3085
3066
|
await ratchetStepReceive(candidateSession, ratchetHeader.dhPub, ratchetHeader.pn);
|
|
3086
3067
|
}
|
|
3087
|
-
|
|
3088
|
-
let
|
|
3068
|
+
const messageKey = takeReceiveMessageKey(candidateSession, ratchetHeader.dhPub, ratchetHeader.n);
|
|
3069
|
+
let messageSubkeys = xMessageKeySubkeys(messageKey);
|
|
3070
|
+
let HMAC = xHMAC(mail, messageSubkeys.authenticationKey);
|
|
3089
3071
|
if (!XUtils.bytesEqual(HMAC, header) &&
|
|
3090
3072
|
firstInboundFromSubsequent &&
|
|
3091
3073
|
!originalSession.CKr) {
|
|
3092
3074
|
const ratchetedCandidate = cloneSessionCrypto(originalSession);
|
|
3093
3075
|
await ratchetStepReceive(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.pn);
|
|
3094
3076
|
const ratchetedMessageKey = takeReceiveMessageKey(ratchetedCandidate, ratchetHeader.dhPub, ratchetHeader.n);
|
|
3095
|
-
const
|
|
3077
|
+
const ratchetedSubkeys = xMessageKeySubkeys(ratchetedMessageKey);
|
|
3078
|
+
const ratchetedHMAC = xHMAC(mail, ratchetedSubkeys.authenticationKey);
|
|
3096
3079
|
if (XUtils.bytesEqual(ratchetedHMAC, header)) {
|
|
3097
3080
|
if (libvexDebugDmEnabled()) {
|
|
3098
3081
|
debugLibvexDm("readMail subsequent: first inbound used DH-ratchet fallback", {
|
|
@@ -3101,7 +3084,7 @@ export class Client {
|
|
|
3101
3084
|
});
|
|
3102
3085
|
}
|
|
3103
3086
|
candidateSession = ratchetedCandidate;
|
|
3104
|
-
|
|
3087
|
+
messageSubkeys = ratchetedSubkeys;
|
|
3105
3088
|
HMAC = ratchetedHMAC;
|
|
3106
3089
|
}
|
|
3107
3090
|
}
|
|
@@ -3128,7 +3111,7 @@ export class Client {
|
|
|
3128
3111
|
return;
|
|
3129
3112
|
}
|
|
3130
3113
|
session = candidateSession;
|
|
3131
|
-
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);
|
|
3132
3115
|
if (decrypted) {
|
|
3133
3116
|
const fwdMsg2 = mail.forward
|
|
3134
3117
|
? messageSchema.parse(msgpack.decode(decrypted))
|
|
@@ -3251,6 +3234,75 @@ export class Client {
|
|
|
3251
3234
|
const res = await this.http.patch(this.getHost() + "/invite/" + inviteID);
|
|
3252
3235
|
return decodeHttpResponse(PermissionCodec, res.data);
|
|
3253
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
|
+
}
|
|
3254
3306
|
registerDecryptFailure(mail) {
|
|
3255
3307
|
const count = (this.decryptFailureCounts.get(mail.mailID) ?? 0) + 1;
|
|
3256
3308
|
this.decryptFailureCounts.set(mail.mailID, count);
|
|
@@ -3299,6 +3351,15 @@ export class Client {
|
|
|
3299
3351
|
requestID +
|
|
3300
3352
|
"/reject");
|
|
3301
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
|
+
}
|
|
3302
3363
|
async respond(msg) {
|
|
3303
3364
|
const response = {
|
|
3304
3365
|
signed: await xSignAsync(new Uint8Array(msg.challenge), this.signKeys.secretKey),
|
|
@@ -3378,7 +3439,7 @@ export class Client {
|
|
|
3378
3439
|
challenge: newDevice.challenge,
|
|
3379
3440
|
expiresAt: newDevice.expiresAt,
|
|
3380
3441
|
requestID: newDevice.requestID,
|
|
3381
|
-
userID: newDevice.userID
|
|
3442
|
+
userID: newDevice.userID,
|
|
3382
3443
|
});
|
|
3383
3444
|
}
|
|
3384
3445
|
else {
|
|
@@ -3680,6 +3741,7 @@ export class Client {
|
|
|
3680
3741
|
await ratchetStepSend(session);
|
|
3681
3742
|
}
|
|
3682
3743
|
const { messageKey, n } = takeSendMessageKey(session);
|
|
3744
|
+
const messageSubkeys = xMessageKeySubkeys(messageKey);
|
|
3683
3745
|
const ratchetHeader = {
|
|
3684
3746
|
dhPub: session.DHsPublic,
|
|
3685
3747
|
n,
|
|
@@ -3687,7 +3749,7 @@ export class Client {
|
|
|
3687
3749
|
version: 1,
|
|
3688
3750
|
};
|
|
3689
3751
|
const nonce = xMakeNonce();
|
|
3690
|
-
const cipher = await xSecretboxAsync(msg, nonce,
|
|
3752
|
+
const cipher = await xSecretboxAsync(msg, nonce, messageSubkeys.encryptionKey);
|
|
3691
3753
|
const extra = encodeRatchetHeader(ratchetHeader);
|
|
3692
3754
|
const mail = {
|
|
3693
3755
|
authorID: this.getUser().userID,
|
|
@@ -3709,7 +3771,7 @@ export class Client {
|
|
|
3709
3771
|
transmissionID: uuid.v4(),
|
|
3710
3772
|
type: "resource",
|
|
3711
3773
|
};
|
|
3712
|
-
const hmac = xHMAC(mail,
|
|
3774
|
+
const hmac = xHMAC(mail, messageSubkeys.authenticationKey);
|
|
3713
3775
|
const fwdOut = forward
|
|
3714
3776
|
? messageSchema.parse(msgpack.decode(msg))
|
|
3715
3777
|
: null;
|
|
@@ -3957,7 +4019,7 @@ export class Client {
|
|
|
3957
4019
|
async submitOTK(amount) {
|
|
3958
4020
|
const otks = [];
|
|
3959
4021
|
for (let i = 0; i < amount; i++) {
|
|
3960
|
-
otks.push(await this.createPreKey());
|
|
4022
|
+
otks.push(await this.createPreKey("one-time"));
|
|
3961
4023
|
}
|
|
3962
4024
|
const savedKeys = await this.database.savePreKeys(otks, true);
|
|
3963
4025
|
await this.http.post(this.getHost() + "/device/" + this.getDevice().deviceID + "/otk", msgpack.encode(savedKeys.map((key) => this.censorPreKey(key))), {
|