@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.
- package/README.md +5 -5
- package/dist/Client.d.ts +11 -0
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +97 -39
- 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 +6 -6
- package/src/Client.ts +145 -52
- 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"
|
|
@@ -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
|
-
|
|
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
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3348
|
-
|
|
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
|
-
|
|
3364
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
}
|