@vex-chat/libvex 5.5.1 → 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.
@@ -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;KACzB,CAAC;AACN,CAAC"}
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": "5.5.1",
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": "^2.1.2",
97
- "@vex-chat/types": "^2.0.0"
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"
@@ -109,13 +109,13 @@
109
109
  },
110
110
  "repository": {
111
111
  "type": "git",
112
- "url": "git+https://github.com/vex-protocol/protocol.git",
112
+ "url": "git+https://github.com/vex-protocol/vex-protocol.git",
113
113
  "directory": "packages/libvex"
114
114
  },
115
115
  "bugs": {
116
- "url": "https://github.com/vex-protocol/protocol/issues"
116
+ "url": "https://github.com/vex-protocol/vex-protocol/issues"
117
117
  },
118
- "homepage": "https://github.com/vex-protocol/protocol/tree/master/packages/libvex#readme",
118
+ "homepage": "https://github.com/vex-protocol/vex-protocol/tree/master/packages/libvex#readme",
119
119
  "publishConfig": {
120
120
  "access": "public",
121
121
  "registry": "https://registry.npmjs.org/"
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,
@@ -492,6 +500,16 @@ export interface PendingDeviceRequest {
492
500
  username: string;
493
501
  }
494
502
 
503
+ /**
504
+ * Retry request emitted when message decryption fails and session healing starts.
505
+ */
506
+ export interface RetryRequest {
507
+ /** Mail ID that should be retried after session healing. */
508
+ mailID: string;
509
+ /** Origin of the retry signal. */
510
+ source: "decrypt_failure" | "server_notify";
511
+ }
512
+
495
513
  /** Zod schema matching the {@link Message} interface for forwarded-message decode. */
496
514
  const messageSchema: z.ZodType<Message> = z.object({
497
515
  authorID: z.string(),
@@ -522,6 +540,12 @@ const deviceRequestNotifyData = z.object({
522
540
  z.literal("rejected"),
523
541
  ]),
524
542
  });
543
+ const retryRequestNotifyData = z.union([
544
+ z.string(),
545
+ z.object({
546
+ mailID: z.string(),
547
+ }),
548
+ ]);
525
549
 
526
550
  /**
527
551
  * Event signatures emitted by {@link Client}.
@@ -554,6 +578,8 @@ export interface ClientEvents {
554
578
  permission: (permission: Permission) => void;
555
579
  /** Post-auth setup complete — safe to call messaging/user APIs. */
556
580
  ready: () => void;
581
+ /** Session healing requested a retry for a specific mail ID. */
582
+ retryRequest: (retry: RetryRequest) => void;
557
583
  /** A new encryption session was established with a peer device. */
558
584
  session: (session: Session, user: User) => void;
559
585
  }
@@ -1291,9 +1317,6 @@ export class Client {
1291
1317
  return [signKey, ephKey, ad, index];
1292
1318
  }
1293
1319
  case MailType.subsequent:
1294
- if (isFipsSubsequentExtraV1(extra)) {
1295
- return [decodeFipsSubsequentExtraV1(extra)];
1296
- }
1297
1320
  return [extra];
1298
1321
  default:
1299
1322
  return [];
@@ -2076,7 +2099,9 @@ export class Client {
2076
2099
  // discard the ephemeral keys
2077
2100
  await this.newEphemeralKeys();
2078
2101
 
2102
+ const ratchet = await initRatchetSession(SK, "initiator");
2079
2103
  const sessionEntry: SessionSQL = {
2104
+ ...ratchet,
2080
2105
  deviceID: device.deviceID,
2081
2106
  fingerprint: XUtils.encodeHex(AD),
2082
2107
  lastUsed: new Date().toISOString(),
@@ -2649,7 +2674,19 @@ export class Client {
2649
2674
  );
2650
2675
  break;
2651
2676
  case "retryRequest":
2652
- // msg.data is the messageID for retry
2677
+ {
2678
+ const parsed = retryRequestNotifyData.safeParse(msg.data);
2679
+ if (parsed.success) {
2680
+ const mailID =
2681
+ typeof parsed.data === "string"
2682
+ ? parsed.data
2683
+ : parsed.data.mailID;
2684
+ this.emitter.emit("retryRequest", {
2685
+ mailID,
2686
+ source: "server_notify",
2687
+ });
2688
+ }
2689
+ }
2653
2690
  break;
2654
2691
  default:
2655
2692
  break;
@@ -3241,7 +3278,12 @@ export class Client {
3241
3278
  deviceEntry;
3242
3279
 
3243
3280
  // save session
3281
+ const ratchet = await initRatchetSession(
3282
+ SK,
3283
+ "receiver",
3284
+ );
3244
3285
  const newSession: SessionSQL = {
3286
+ ...ratchet,
3245
3287
  deviceID: mail.sender,
3246
3288
  fingerprint: XUtils.encodeHex(AD),
3247
3289
  lastUsed: new Date().toISOString(),
@@ -3275,19 +3317,12 @@ export class Client {
3275
3317
  }
3276
3318
  break;
3277
3319
  case MailType.subsequent: {
3278
- const extraBuf = new Uint8Array(mail.extra);
3279
- const publicKey = isFipsSubsequentExtraV1(extraBuf)
3280
- ? decodeFipsSubsequentExtraV1(extraBuf)
3281
- : Client.deserializeExtra(
3282
- mail.mailType,
3283
- extraBuf,
3284
- )[0];
3285
- if (!publicKey) {
3286
- throw new Error(
3287
- "Malformed subsequent mail extra: missing publicKey",
3288
- );
3289
- }
3290
- 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
+ );
3291
3326
  let retries = 0;
3292
3327
  while (!session) {
3293
3328
  if (retries >= 3) {
@@ -3295,14 +3330,32 @@ export class Client {
3295
3330
  }
3296
3331
  await sleep(100 * 2 ** retries);
3297
3332
  retries++;
3298
- session = await this.getSessionByPubkey(publicKey);
3333
+ session = await this.database.getSessionByDeviceID(
3334
+ mail.sender,
3335
+ );
3299
3336
  }
3300
3337
 
3301
3338
  if (!session) {
3302
3339
  void healSession();
3303
3340
  return;
3304
3341
  }
3305
- const HMAC = xHMAC(mail, session.SK);
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);
3306
3359
 
3307
3360
  if (!XUtils.bytesEqual(HMAC, header)) {
3308
3361
  void healSession();
@@ -3312,7 +3365,7 @@ export class Client {
3312
3365
  const decrypted = await xSecretboxOpenAsync(
3313
3366
  new Uint8Array(mail.cipher),
3314
3367
  new Uint8Array(mail.nonce),
3315
- session.SK,
3368
+ messageKey,
3316
3369
  );
3317
3370
 
3318
3371
  if (decrypted) {
@@ -3344,32 +3397,40 @@ export class Client {
3344
3397
  };
3345
3398
  this.emitter.emit("message", message);
3346
3399
 
3347
- void this.database.markSessionUsed(
3348
- session.sessionID,
3349
- );
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;
3350
3428
  } else {
3351
3429
  void healSession();
3352
-
3353
- // emit the message
3354
- const message: Message = {
3355
- authorID: mail.authorID,
3356
- decrypted: false,
3357
- direction: "incoming",
3358
- forward: mail.forward,
3359
- group: mail.group
3360
- ? uuid.stringify(mail.group)
3361
- : null,
3430
+ this.emitter.emit("retryRequest", {
3362
3431
  mailID: mail.mailID,
3363
- message: "",
3364
- nonce: XUtils.encodeHex(
3365
- new Uint8Array(mail.nonce),
3366
- ),
3367
- readerID: mail.readerID,
3368
- recipient: mail.recipient,
3369
- sender: mail.sender,
3370
- timestamp: timestamp,
3371
- };
3372
- this.emitter.emit("message", message);
3432
+ source: "decrypt_failure",
3433
+ });
3373
3434
  }
3374
3435
  break;
3375
3436
  }
@@ -3705,12 +3766,19 @@ export class Client {
3705
3766
  });
3706
3767
  }
3707
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
+ };
3708
3779
  const nonce = xMakeNonce();
3709
- const cipher = await xSecretboxAsync(msg, nonce, session.SK);
3710
- const extra =
3711
- this.cryptoProfile === "fips"
3712
- ? encodeFipsSubsequentExtraV1(session.publicKey)
3713
- : session.publicKey;
3780
+ const cipher = await xSecretboxAsync(msg, nonce, messageKey);
3781
+ const extra = encodeRatchetHeader(ratchetHeader);
3714
3782
 
3715
3783
  const mail: MailWS = {
3716
3784
  authorID: this.getUser().userID,
@@ -3734,7 +3802,7 @@ export class Client {
3734
3802
  type: "resource",
3735
3803
  };
3736
3804
 
3737
- const hmac = xHMAC(mail, session.SK);
3805
+ const hmac = xHMAC(mail, messageKey);
3738
3806
 
3739
3807
  const fwdOut = forward
3740
3808
  ? messageSchema.parse(msgpack.decode(msg))
@@ -3757,6 +3825,31 @@ export class Client {
3757
3825
  };
3758
3826
  this.emitter.emit("message", outMsg);
3759
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
+
3760
3853
  await new Promise((res, rej) => {
3761
3854
  const callback = (packedMsg: Uint8Array) => {
3762
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
- if (!this.sessions.find((s) => s.SK === session.SK)) {
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
+ });
@@ -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
  }