@vex-chat/libvex 5.5.2 → 6.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.map +1 -1
- package/dist/Client.js +76 -21
- package/dist/Client.js.map +1 -1
- package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
- package/dist/__tests__/harness/memory-storage.js +23 -1
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/storage/schema.d.ts +10 -0
- package/dist/storage/schema.d.ts.map +1 -1
- package/dist/storage/sqlite.d.ts +3 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +121 -4
- package/dist/storage/sqlite.js.map +1 -1
- package/dist/types/crypto.d.ts +11 -0
- package/dist/types/crypto.d.ts.map +1 -1
- package/dist/utils/ratchet.d.ts +72 -0
- package/dist/utils/ratchet.d.ts.map +1 -0
- package/dist/utils/ratchet.js +172 -0
- package/dist/utils/ratchet.js.map +1 -0
- package/dist/utils/sqlSessionToCrypto.d.ts.map +1 -1
- package/dist/utils/sqlSessionToCrypto.js +30 -0
- package/dist/utils/sqlSessionToCrypto.js.map +1 -1
- package/package.json +3 -3
- package/src/Client.ts +111 -31
- package/src/__tests__/harness/memory-storage.ts +23 -1
- package/src/__tests__/ratchet.test.ts +141 -0
- package/src/storage/schema.ts +10 -0
- package/src/storage/sqlite.ts +131 -5
- package/src/types/crypto.ts +11 -0
- package/src/utils/ratchet.ts +289 -0
- package/src/utils/sqlSessionToCrypto.ts +30 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlSessionToCrypto.js","sourceRoot":"","sources":["../../src/utils/sqlSessionToCrypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,MAAM,UAAU,kBAAkB,CAAC,OAAmB;IAClD,OAAO;QACH,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC;QAClD,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC;QAC9C,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,MAAM,EAAE,OAAO,CAAC,MAAM;
|
|
1
|
+
{"version":3,"file":"sqlSessionToCrypto.js","sourceRoot":"","sources":["../../src/utils/sqlSessionToCrypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,MAAM,UAAU,kBAAkB,CAAC,OAAmB;IAClD,MAAM,WAAW,GAAG,gBAAgB,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IAC1D,OAAO;QACH,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI;QACvD,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI;QACvD,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI;QACvD,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC;QAChD,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC;QAC9C,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC;QAClD,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC;QAC9C,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,EAAE,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;QAChC,WAAW;QACX,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,QAAQ,EAAE,OAAO,CAAC,QAAQ;KAC7B,CAAC;AACN,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAW;IACjC,IAAI,CAAC;QACD,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YAChD,OAAO,EAAE,CAAC;QACd,CAAC;QACD,MAAM,GAAG,GAA2B,EAAE,CAAC;QACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1C,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;gBACxB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;YACf,CAAC;QACL,CAAC;QACD,OAAO,GAAG,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,EAAE,CAAC;IACd,CAAC;AACL,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vex-chat/libvex",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Library for communicating with xchat server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"msgpackr": "^1.11.9",
|
|
94
94
|
"uuid": "^14.0.0",
|
|
95
95
|
"zod": "^4.3.6",
|
|
96
|
-
"@vex-chat/crypto": "^
|
|
97
|
-
"@vex-chat/types": "^
|
|
96
|
+
"@vex-chat/crypto": "^3.0.0",
|
|
97
|
+
"@vex-chat/types": "^3.0.0"
|
|
98
98
|
},
|
|
99
99
|
"peerDependencies": {
|
|
100
100
|
"better-sqlite3": ">=11.0.0"
|
package/src/Client.ts
CHANGED
|
@@ -80,14 +80,22 @@ import { z } from "zod/v4";
|
|
|
80
80
|
import { WebSocketAdapter } from "./transport/websocket.js";
|
|
81
81
|
import {
|
|
82
82
|
decodeFipsInitialExtraV1,
|
|
83
|
-
decodeFipsSubsequentExtraV1,
|
|
84
83
|
encodeFipsInitialExtraV1,
|
|
85
|
-
encodeFipsSubsequentExtraV1,
|
|
86
84
|
fipsP256AdFromIdentityPubs,
|
|
87
85
|
fipsP256PreKeySignPayload,
|
|
88
86
|
isFipsInitialExtraV1,
|
|
89
|
-
isFipsSubsequentExtraV1,
|
|
90
87
|
} from "./utils/fipsMailExtra.js";
|
|
88
|
+
import {
|
|
89
|
+
decodeRatchetHeader,
|
|
90
|
+
encodeRatchetHeader,
|
|
91
|
+
hasRemoteDhChanged,
|
|
92
|
+
initRatchetSession,
|
|
93
|
+
ratchetStepReceive,
|
|
94
|
+
ratchetStepSend,
|
|
95
|
+
sessionToSqlPatch,
|
|
96
|
+
takeReceiveMessageKey,
|
|
97
|
+
takeSendMessageKey,
|
|
98
|
+
} from "./utils/ratchet.js";
|
|
91
99
|
|
|
92
100
|
function debugLibvexDm(
|
|
93
101
|
msg: string,
|
|
@@ -1309,9 +1317,6 @@ export class Client {
|
|
|
1309
1317
|
return [signKey, ephKey, ad, index];
|
|
1310
1318
|
}
|
|
1311
1319
|
case MailType.subsequent:
|
|
1312
|
-
if (isFipsSubsequentExtraV1(extra)) {
|
|
1313
|
-
return [decodeFipsSubsequentExtraV1(extra)];
|
|
1314
|
-
}
|
|
1315
1320
|
return [extra];
|
|
1316
1321
|
default:
|
|
1317
1322
|
return [];
|
|
@@ -2094,7 +2099,9 @@ export class Client {
|
|
|
2094
2099
|
// discard the ephemeral keys
|
|
2095
2100
|
await this.newEphemeralKeys();
|
|
2096
2101
|
|
|
2102
|
+
const ratchet = await initRatchetSession(SK, "initiator");
|
|
2097
2103
|
const sessionEntry: SessionSQL = {
|
|
2104
|
+
...ratchet,
|
|
2098
2105
|
deviceID: device.deviceID,
|
|
2099
2106
|
fingerprint: XUtils.encodeHex(AD),
|
|
2100
2107
|
lastUsed: new Date().toISOString(),
|
|
@@ -3271,7 +3278,12 @@ export class Client {
|
|
|
3271
3278
|
deviceEntry;
|
|
3272
3279
|
|
|
3273
3280
|
// save session
|
|
3281
|
+
const ratchet = await initRatchetSession(
|
|
3282
|
+
SK,
|
|
3283
|
+
"receiver",
|
|
3284
|
+
);
|
|
3274
3285
|
const newSession: SessionSQL = {
|
|
3286
|
+
...ratchet,
|
|
3275
3287
|
deviceID: mail.sender,
|
|
3276
3288
|
fingerprint: XUtils.encodeHex(AD),
|
|
3277
3289
|
lastUsed: new Date().toISOString(),
|
|
@@ -3305,19 +3317,12 @@ export class Client {
|
|
|
3305
3317
|
}
|
|
3306
3318
|
break;
|
|
3307
3319
|
case MailType.subsequent: {
|
|
3308
|
-
const
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
)[0];
|
|
3315
|
-
if (!publicKey) {
|
|
3316
|
-
throw new Error(
|
|
3317
|
-
"Malformed subsequent mail extra: missing publicKey",
|
|
3318
|
-
);
|
|
3319
|
-
}
|
|
3320
|
-
let session = await this.getSessionByPubkey(publicKey);
|
|
3320
|
+
const ratchetHeader = decodeRatchetHeader(
|
|
3321
|
+
new Uint8Array(mail.extra),
|
|
3322
|
+
);
|
|
3323
|
+
let session = await this.database.getSessionByDeviceID(
|
|
3324
|
+
mail.sender,
|
|
3325
|
+
);
|
|
3321
3326
|
let retries = 0;
|
|
3322
3327
|
while (!session) {
|
|
3323
3328
|
if (retries >= 3) {
|
|
@@ -3325,14 +3330,32 @@ export class Client {
|
|
|
3325
3330
|
}
|
|
3326
3331
|
await sleep(100 * 2 ** retries);
|
|
3327
3332
|
retries++;
|
|
3328
|
-
session = await this.
|
|
3333
|
+
session = await this.database.getSessionByDeviceID(
|
|
3334
|
+
mail.sender,
|
|
3335
|
+
);
|
|
3329
3336
|
}
|
|
3330
3337
|
|
|
3331
3338
|
if (!session) {
|
|
3332
3339
|
void healSession();
|
|
3333
3340
|
return;
|
|
3334
3341
|
}
|
|
3335
|
-
|
|
3342
|
+
|
|
3343
|
+
if (
|
|
3344
|
+
hasRemoteDhChanged(session.DHr, ratchetHeader.dhPub)
|
|
3345
|
+
) {
|
|
3346
|
+
await ratchetStepReceive(
|
|
3347
|
+
session,
|
|
3348
|
+
ratchetHeader.dhPub,
|
|
3349
|
+
ratchetHeader.pn,
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
const messageKey = takeReceiveMessageKey(
|
|
3354
|
+
session,
|
|
3355
|
+
ratchetHeader.dhPub,
|
|
3356
|
+
ratchetHeader.n,
|
|
3357
|
+
);
|
|
3358
|
+
const HMAC = xHMAC(mail, messageKey);
|
|
3336
3359
|
|
|
3337
3360
|
if (!XUtils.bytesEqual(HMAC, header)) {
|
|
3338
3361
|
void healSession();
|
|
@@ -3342,7 +3365,7 @@ export class Client {
|
|
|
3342
3365
|
const decrypted = await xSecretboxOpenAsync(
|
|
3343
3366
|
new Uint8Array(mail.cipher),
|
|
3344
3367
|
new Uint8Array(mail.nonce),
|
|
3345
|
-
|
|
3368
|
+
messageKey,
|
|
3346
3369
|
);
|
|
3347
3370
|
|
|
3348
3371
|
if (decrypted) {
|
|
@@ -3374,9 +3397,34 @@ export class Client {
|
|
|
3374
3397
|
};
|
|
3375
3398
|
this.emitter.emit("message", message);
|
|
3376
3399
|
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3400
|
+
const sqlPatch = sessionToSqlPatch(session);
|
|
3401
|
+
const persisted: SessionSQL = {
|
|
3402
|
+
CKr: sqlPatch.CKr,
|
|
3403
|
+
CKs: sqlPatch.CKs,
|
|
3404
|
+
deviceID: mail.sender,
|
|
3405
|
+
DHr: sqlPatch.DHr,
|
|
3406
|
+
DHsPrivate: sqlPatch.DHsPrivate,
|
|
3407
|
+
DHsPublic: sqlPatch.DHsPublic,
|
|
3408
|
+
fingerprint: XUtils.encodeHex(
|
|
3409
|
+
session.fingerprint,
|
|
3410
|
+
),
|
|
3411
|
+
lastUsed: new Date().toISOString(),
|
|
3412
|
+
mode: session.mode,
|
|
3413
|
+
Nr: sqlPatch.Nr,
|
|
3414
|
+
Ns: sqlPatch.Ns,
|
|
3415
|
+
PN: sqlPatch.PN,
|
|
3416
|
+
publicKey: XUtils.encodeHex(session.publicKey),
|
|
3417
|
+
RK: sqlPatch.RK,
|
|
3418
|
+
sessionID: session.sessionID,
|
|
3419
|
+
SK: XUtils.encodeHex(session.SK),
|
|
3420
|
+
skippedKeys: sqlPatch.skippedKeys,
|
|
3421
|
+
userID: session.userID,
|
|
3422
|
+
verified: session.verified,
|
|
3423
|
+
};
|
|
3424
|
+
await this.database.saveSession(persisted);
|
|
3425
|
+
this.sessionRecords[
|
|
3426
|
+
XUtils.encodeHex(session.publicKey)
|
|
3427
|
+
] = session;
|
|
3380
3428
|
} else {
|
|
3381
3429
|
void healSession();
|
|
3382
3430
|
this.emitter.emit("retryRequest", {
|
|
@@ -3718,12 +3766,19 @@ export class Client {
|
|
|
3718
3766
|
});
|
|
3719
3767
|
}
|
|
3720
3768
|
|
|
3769
|
+
if (!session.CKs) {
|
|
3770
|
+
await ratchetStepSend(session);
|
|
3771
|
+
}
|
|
3772
|
+
const { messageKey, n } = takeSendMessageKey(session);
|
|
3773
|
+
const ratchetHeader = {
|
|
3774
|
+
dhPub: session.DHsPublic,
|
|
3775
|
+
n,
|
|
3776
|
+
pn: session.PN,
|
|
3777
|
+
version: 1 as const,
|
|
3778
|
+
};
|
|
3721
3779
|
const nonce = xMakeNonce();
|
|
3722
|
-
const cipher = await xSecretboxAsync(msg, nonce,
|
|
3723
|
-
const extra =
|
|
3724
|
-
this.cryptoProfile === "fips"
|
|
3725
|
-
? encodeFipsSubsequentExtraV1(session.publicKey)
|
|
3726
|
-
: session.publicKey;
|
|
3780
|
+
const cipher = await xSecretboxAsync(msg, nonce, messageKey);
|
|
3781
|
+
const extra = encodeRatchetHeader(ratchetHeader);
|
|
3727
3782
|
|
|
3728
3783
|
const mail: MailWS = {
|
|
3729
3784
|
authorID: this.getUser().userID,
|
|
@@ -3747,7 +3802,7 @@ export class Client {
|
|
|
3747
3802
|
type: "resource",
|
|
3748
3803
|
};
|
|
3749
3804
|
|
|
3750
|
-
const hmac = xHMAC(mail,
|
|
3805
|
+
const hmac = xHMAC(mail, messageKey);
|
|
3751
3806
|
|
|
3752
3807
|
const fwdOut = forward
|
|
3753
3808
|
? messageSchema.parse(msgpack.decode(msg))
|
|
@@ -3770,6 +3825,31 @@ export class Client {
|
|
|
3770
3825
|
};
|
|
3771
3826
|
this.emitter.emit("message", outMsg);
|
|
3772
3827
|
|
|
3828
|
+
const sqlPatch = sessionToSqlPatch(session);
|
|
3829
|
+
const persisted: SessionSQL = {
|
|
3830
|
+
CKr: sqlPatch.CKr,
|
|
3831
|
+
CKs: sqlPatch.CKs,
|
|
3832
|
+
deviceID: device.deviceID,
|
|
3833
|
+
DHr: sqlPatch.DHr,
|
|
3834
|
+
DHsPrivate: sqlPatch.DHsPrivate,
|
|
3835
|
+
DHsPublic: sqlPatch.DHsPublic,
|
|
3836
|
+
fingerprint: XUtils.encodeHex(session.fingerprint),
|
|
3837
|
+
lastUsed: new Date().toISOString(),
|
|
3838
|
+
mode: session.mode,
|
|
3839
|
+
Nr: sqlPatch.Nr,
|
|
3840
|
+
Ns: sqlPatch.Ns,
|
|
3841
|
+
PN: sqlPatch.PN,
|
|
3842
|
+
publicKey: XUtils.encodeHex(session.publicKey),
|
|
3843
|
+
RK: sqlPatch.RK,
|
|
3844
|
+
sessionID: session.sessionID,
|
|
3845
|
+
SK: XUtils.encodeHex(session.SK),
|
|
3846
|
+
skippedKeys: sqlPatch.skippedKeys,
|
|
3847
|
+
userID: session.userID,
|
|
3848
|
+
verified: session.verified,
|
|
3849
|
+
};
|
|
3850
|
+
await this.database.saveSession(persisted);
|
|
3851
|
+
this.sessionRecords[XUtils.encodeHex(session.publicKey)] = session;
|
|
3852
|
+
|
|
3773
3853
|
await new Promise((res, rej) => {
|
|
3774
3854
|
const callback = (packedMsg: Uint8Array) => {
|
|
3775
3855
|
const [_header, receivedMsg] =
|
|
@@ -234,7 +234,12 @@ export class MemoryStorage extends EventEmitter implements Storage {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
saveSession(session: SessionSQL): Promise<void> {
|
|
237
|
-
|
|
237
|
+
const idx = this.sessions.findIndex(
|
|
238
|
+
(s) => s.sessionID === session.sessionID,
|
|
239
|
+
);
|
|
240
|
+
if (idx >= 0) {
|
|
241
|
+
this.sessions[idx] = session;
|
|
242
|
+
} else {
|
|
238
243
|
this.sessions.push(session);
|
|
239
244
|
}
|
|
240
245
|
return Promise.resolve();
|
|
@@ -261,14 +266,31 @@ export class MemoryStorage extends EventEmitter implements Storage {
|
|
|
261
266
|
}
|
|
262
267
|
|
|
263
268
|
private sqlToCrypto(s: SessionSQL): SessionCrypto {
|
|
269
|
+
let skippedKeys: Record<string, string> = {};
|
|
270
|
+
try {
|
|
271
|
+
skippedKeys = JSON.parse(s.skippedKeys) as Record<string, string>;
|
|
272
|
+
} catch {
|
|
273
|
+
skippedKeys = {};
|
|
274
|
+
}
|
|
264
275
|
return {
|
|
276
|
+
CKr: s.CKr ? XUtils.decodeHex(s.CKr) : null,
|
|
277
|
+
CKs: s.CKs ? XUtils.decodeHex(s.CKs) : null,
|
|
278
|
+
DHr: s.DHr ? XUtils.decodeHex(s.DHr) : null,
|
|
279
|
+
DHsPrivate: XUtils.decodeHex(s.DHsPrivate),
|
|
280
|
+
DHsPublic: XUtils.decodeHex(s.DHsPublic),
|
|
265
281
|
fingerprint: XUtils.decodeHex(s.fingerprint),
|
|
266
282
|
lastUsed: s.lastUsed,
|
|
267
283
|
mode: s.mode,
|
|
284
|
+
Nr: s.Nr,
|
|
285
|
+
Ns: s.Ns,
|
|
286
|
+
PN: s.PN,
|
|
268
287
|
publicKey: XUtils.decodeHex(s.publicKey),
|
|
288
|
+
RK: XUtils.decodeHex(s.RK),
|
|
269
289
|
sessionID: s.sessionID,
|
|
270
290
|
SK: XUtils.decodeHex(s.SK),
|
|
291
|
+
skippedKeys,
|
|
271
292
|
userID: s.userID,
|
|
293
|
+
verified: s.verified,
|
|
272
294
|
};
|
|
273
295
|
}
|
|
274
296
|
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2020-2026 Vex Heavy Industries LLC
|
|
3
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
4
|
+
* Commercial licenses available at vex.wtf
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { XUtils } from "@vex-chat/crypto";
|
|
8
|
+
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
decodeRatchetHeader,
|
|
13
|
+
encodeRatchetHeader,
|
|
14
|
+
hasRemoteDhChanged,
|
|
15
|
+
initRatchetSession,
|
|
16
|
+
ratchetStepReceive,
|
|
17
|
+
ratchetStepSend,
|
|
18
|
+
takeReceiveMessageKey,
|
|
19
|
+
takeSendMessageKey,
|
|
20
|
+
} from "../utils/ratchet.js";
|
|
21
|
+
|
|
22
|
+
describe("double ratchet helpers", () => {
|
|
23
|
+
it("derives matching message keys for first exchange and reply", async () => {
|
|
24
|
+
const sk = XUtils.decodeHex(
|
|
25
|
+
"1111111111111111111111111111111111111111111111111111111111111111",
|
|
26
|
+
);
|
|
27
|
+
const alice = await initRatchetSession(sk, "initiator");
|
|
28
|
+
const bob = await initRatchetSession(sk, "receiver");
|
|
29
|
+
|
|
30
|
+
const aliceState = {
|
|
31
|
+
CKr: alice.CKr ? XUtils.decodeHex(alice.CKr) : null,
|
|
32
|
+
CKs: alice.CKs ? XUtils.decodeHex(alice.CKs) : null,
|
|
33
|
+
DHr: alice.DHr ? XUtils.decodeHex(alice.DHr) : null,
|
|
34
|
+
DHsPrivate: XUtils.decodeHex(alice.DHsPrivate),
|
|
35
|
+
DHsPublic: XUtils.decodeHex(alice.DHsPublic),
|
|
36
|
+
Nr: alice.Nr,
|
|
37
|
+
Ns: alice.Ns,
|
|
38
|
+
PN: alice.PN,
|
|
39
|
+
RK: XUtils.decodeHex(alice.RK),
|
|
40
|
+
skippedKeys: {} as Record<string, string>,
|
|
41
|
+
};
|
|
42
|
+
const bobState = {
|
|
43
|
+
CKr: bob.CKr ? XUtils.decodeHex(bob.CKr) : null,
|
|
44
|
+
CKs: bob.CKs ? XUtils.decodeHex(bob.CKs) : null,
|
|
45
|
+
DHr: bob.DHr ? XUtils.decodeHex(bob.DHr) : null,
|
|
46
|
+
DHsPrivate: XUtils.decodeHex(bob.DHsPrivate),
|
|
47
|
+
DHsPublic: XUtils.decodeHex(bob.DHsPublic),
|
|
48
|
+
Nr: bob.Nr,
|
|
49
|
+
Ns: bob.Ns,
|
|
50
|
+
PN: bob.PN,
|
|
51
|
+
RK: XUtils.decodeHex(bob.RK),
|
|
52
|
+
skippedKeys: {} as Record<string, string>,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await ratchetStepSend(aliceState);
|
|
56
|
+
const a1 = takeSendMessageKey(aliceState);
|
|
57
|
+
const h1 = decodeRatchetHeader(
|
|
58
|
+
encodeRatchetHeader({
|
|
59
|
+
dhPub: aliceState.DHsPublic,
|
|
60
|
+
n: a1.n,
|
|
61
|
+
pn: aliceState.PN,
|
|
62
|
+
version: 1,
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(hasRemoteDhChanged(bobState.DHr, h1.dhPub)).toBe(true);
|
|
67
|
+
await ratchetStepReceive(bobState, h1.dhPub, h1.pn);
|
|
68
|
+
const b1 = takeReceiveMessageKey(bobState, h1.dhPub, h1.n);
|
|
69
|
+
expect(XUtils.bytesEqual(a1.messageKey, b1)).toBe(true);
|
|
70
|
+
|
|
71
|
+
await ratchetStepSend(bobState);
|
|
72
|
+
const bReply = takeSendMessageKey(bobState);
|
|
73
|
+
const h2 = decodeRatchetHeader(
|
|
74
|
+
encodeRatchetHeader({
|
|
75
|
+
dhPub: bobState.DHsPublic,
|
|
76
|
+
n: bReply.n,
|
|
77
|
+
pn: bobState.PN,
|
|
78
|
+
version: 1,
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
await ratchetStepReceive(aliceState, h2.dhPub, h2.pn);
|
|
82
|
+
const aReply = takeReceiveMessageKey(aliceState, h2.dhPub, h2.n);
|
|
83
|
+
expect(XUtils.bytesEqual(aReply, bReply.messageKey)).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("supports skipped keys for out-of-order messages", async () => {
|
|
87
|
+
const sk = XUtils.decodeHex(
|
|
88
|
+
"2222222222222222222222222222222222222222222222222222222222222222",
|
|
89
|
+
);
|
|
90
|
+
const initiator = await initRatchetSession(sk, "initiator");
|
|
91
|
+
const receiver = await initRatchetSession(sk, "receiver");
|
|
92
|
+
|
|
93
|
+
const s = {
|
|
94
|
+
CKr: initiator.CKr ? XUtils.decodeHex(initiator.CKr) : null,
|
|
95
|
+
CKs: initiator.CKs ? XUtils.decodeHex(initiator.CKs) : null,
|
|
96
|
+
DHr: initiator.DHr ? XUtils.decodeHex(initiator.DHr) : null,
|
|
97
|
+
DHsPrivate: XUtils.decodeHex(initiator.DHsPrivate),
|
|
98
|
+
DHsPublic: XUtils.decodeHex(initiator.DHsPublic),
|
|
99
|
+
Nr: initiator.Nr,
|
|
100
|
+
Ns: initiator.Ns,
|
|
101
|
+
PN: initiator.PN,
|
|
102
|
+
RK: XUtils.decodeHex(initiator.RK),
|
|
103
|
+
skippedKeys: {} as Record<string, string>,
|
|
104
|
+
};
|
|
105
|
+
const r = {
|
|
106
|
+
CKr: receiver.CKr ? XUtils.decodeHex(receiver.CKr) : null,
|
|
107
|
+
CKs: receiver.CKs ? XUtils.decodeHex(receiver.CKs) : null,
|
|
108
|
+
DHr: receiver.DHr ? XUtils.decodeHex(receiver.DHr) : null,
|
|
109
|
+
DHsPrivate: XUtils.decodeHex(receiver.DHsPrivate),
|
|
110
|
+
DHsPublic: XUtils.decodeHex(receiver.DHsPublic),
|
|
111
|
+
Nr: receiver.Nr,
|
|
112
|
+
Ns: receiver.Ns,
|
|
113
|
+
PN: receiver.PN,
|
|
114
|
+
RK: XUtils.decodeHex(receiver.RK),
|
|
115
|
+
skippedKeys: {} as Record<string, string>,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
await ratchetStepSend(s);
|
|
119
|
+
const m0 = takeSendMessageKey(s);
|
|
120
|
+
const m1 = takeSendMessageKey(s);
|
|
121
|
+
const h0 = {
|
|
122
|
+
dhPub: s.DHsPublic,
|
|
123
|
+
n: m0.n,
|
|
124
|
+
pn: s.PN,
|
|
125
|
+
version: 1 as const,
|
|
126
|
+
};
|
|
127
|
+
const h1 = {
|
|
128
|
+
dhPub: s.DHsPublic,
|
|
129
|
+
n: m1.n,
|
|
130
|
+
pn: s.PN,
|
|
131
|
+
version: 1 as const,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
await ratchetStepReceive(r, h1.dhPub, h1.pn);
|
|
135
|
+
const r1 = takeReceiveMessageKey(r, h1.dhPub, h1.n);
|
|
136
|
+
expect(XUtils.bytesEqual(r1, m1.messageKey)).toBe(true);
|
|
137
|
+
|
|
138
|
+
const r0 = takeReceiveMessageKey(r, h0.dhPub, h0.n);
|
|
139
|
+
expect(XUtils.bytesEqual(r0, m0.messageKey)).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
package/src/storage/schema.ts
CHANGED
|
@@ -88,13 +88,23 @@ interface PreKeysTable {
|
|
|
88
88
|
userID: ColumnType<string, string | undefined, string>;
|
|
89
89
|
}
|
|
90
90
|
interface SessionsTable {
|
|
91
|
+
CKr: null | string;
|
|
92
|
+
CKs: null | string;
|
|
91
93
|
deviceID: string;
|
|
94
|
+
DHr: null | string;
|
|
95
|
+
DHsPrivate: string;
|
|
96
|
+
DHsPublic: string;
|
|
92
97
|
fingerprint: string;
|
|
93
98
|
lastUsed: string;
|
|
94
99
|
mode: string;
|
|
100
|
+
Nr: number;
|
|
101
|
+
Ns: number;
|
|
102
|
+
PN: number;
|
|
95
103
|
publicKey: string;
|
|
104
|
+
RK: string;
|
|
96
105
|
sessionID: string;
|
|
97
106
|
SK: string;
|
|
107
|
+
skippedKeys: string;
|
|
98
108
|
userID: string;
|
|
99
109
|
verified: number;
|
|
100
110
|
}
|