@vex-chat/libvex 5.5.2 → 6.0.1
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 +95 -23
- 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 +73 -0
- package/dist/utils/ratchet.d.ts.map +1 -0
- package/dist/utils/ratchet.js +173 -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 +133 -33
- package/src/__tests__/harness/memory-storage.ts +23 -1
- package/src/__tests__/harness/shared-suite.ts +74 -0
- package/src/__tests__/ratchet.test.ts +481 -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 +287 -0
- package/src/utils/sqlSessionToCrypto.ts +30 -0
|
@@ -0,0 +1,481 @@
|
|
|
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
|
+
deriveBootstrapSendChain,
|
|
14
|
+
encodeRatchetHeader,
|
|
15
|
+
hasRemoteDhChanged,
|
|
16
|
+
initRatchetSession,
|
|
17
|
+
ratchetStepReceive,
|
|
18
|
+
ratchetStepSend,
|
|
19
|
+
sessionToSqlPatch,
|
|
20
|
+
takeReceiveMessageKey,
|
|
21
|
+
takeSendMessageKey,
|
|
22
|
+
} from "../utils/ratchet.js";
|
|
23
|
+
import { sqlSessionToCrypto } from "../utils/sqlSessionToCrypto.js";
|
|
24
|
+
|
|
25
|
+
describe("double ratchet helpers", () => {
|
|
26
|
+
it("derives matching message keys for first exchange and reply", async () => {
|
|
27
|
+
const sk = XUtils.decodeHex(
|
|
28
|
+
"1111111111111111111111111111111111111111111111111111111111111111",
|
|
29
|
+
);
|
|
30
|
+
const alice = await initRatchetSession(sk, "initiator");
|
|
31
|
+
const bob = await initRatchetSession(sk, "receiver");
|
|
32
|
+
|
|
33
|
+
const aliceState = {
|
|
34
|
+
CKr: alice.CKr ? XUtils.decodeHex(alice.CKr) : null,
|
|
35
|
+
CKs: alice.CKs ? XUtils.decodeHex(alice.CKs) : null,
|
|
36
|
+
DHr: alice.DHr ? XUtils.decodeHex(alice.DHr) : null,
|
|
37
|
+
DHsPrivate: XUtils.decodeHex(alice.DHsPrivate),
|
|
38
|
+
DHsPublic: XUtils.decodeHex(alice.DHsPublic),
|
|
39
|
+
Nr: alice.Nr,
|
|
40
|
+
Ns: alice.Ns,
|
|
41
|
+
PN: alice.PN,
|
|
42
|
+
RK: XUtils.decodeHex(alice.RK),
|
|
43
|
+
skippedKeys: {} as Record<string, string>,
|
|
44
|
+
};
|
|
45
|
+
const bobState = {
|
|
46
|
+
CKr: bob.CKr ? XUtils.decodeHex(bob.CKr) : null,
|
|
47
|
+
CKs: bob.CKs ? XUtils.decodeHex(bob.CKs) : null,
|
|
48
|
+
DHr: bob.DHr ? XUtils.decodeHex(bob.DHr) : null,
|
|
49
|
+
DHsPrivate: XUtils.decodeHex(bob.DHsPrivate),
|
|
50
|
+
DHsPublic: XUtils.decodeHex(bob.DHsPublic),
|
|
51
|
+
Nr: bob.Nr,
|
|
52
|
+
Ns: bob.Ns,
|
|
53
|
+
PN: bob.PN,
|
|
54
|
+
RK: XUtils.decodeHex(bob.RK),
|
|
55
|
+
skippedKeys: {} as Record<string, string>,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (!aliceState.CKs) {
|
|
59
|
+
await ratchetStepSend(aliceState);
|
|
60
|
+
}
|
|
61
|
+
const a1 = takeSendMessageKey(aliceState);
|
|
62
|
+
const h1 = decodeRatchetHeader(
|
|
63
|
+
encodeRatchetHeader({
|
|
64
|
+
dhPub: aliceState.DHsPublic,
|
|
65
|
+
n: a1.n,
|
|
66
|
+
pn: aliceState.PN,
|
|
67
|
+
version: 1,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(hasRemoteDhChanged(bobState.DHr, h1.dhPub)).toBe(true);
|
|
72
|
+
if (!bobState.DHr && bobState.CKr) {
|
|
73
|
+
bobState.DHr = h1.dhPub;
|
|
74
|
+
} else {
|
|
75
|
+
await ratchetStepReceive(bobState, h1.dhPub, h1.pn);
|
|
76
|
+
}
|
|
77
|
+
const b1 = takeReceiveMessageKey(bobState, h1.dhPub, h1.n);
|
|
78
|
+
expect(XUtils.bytesEqual(a1.messageKey, b1)).toBe(true);
|
|
79
|
+
|
|
80
|
+
if (!bobState.CKs) {
|
|
81
|
+
await ratchetStepSend(bobState);
|
|
82
|
+
}
|
|
83
|
+
const bReply = takeSendMessageKey(bobState);
|
|
84
|
+
const h2 = decodeRatchetHeader(
|
|
85
|
+
encodeRatchetHeader({
|
|
86
|
+
dhPub: bobState.DHsPublic,
|
|
87
|
+
n: bReply.n,
|
|
88
|
+
pn: bobState.PN,
|
|
89
|
+
version: 1,
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
if (!aliceState.DHr && aliceState.CKr) {
|
|
93
|
+
aliceState.DHr = h2.dhPub;
|
|
94
|
+
} else if (hasRemoteDhChanged(aliceState.DHr, h2.dhPub)) {
|
|
95
|
+
await ratchetStepReceive(aliceState, h2.dhPub, h2.pn);
|
|
96
|
+
}
|
|
97
|
+
const aReply = takeReceiveMessageKey(aliceState, h2.dhPub, h2.n);
|
|
98
|
+
expect(XUtils.bytesEqual(aReply, bReply.messageKey)).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("supports skipped keys for out-of-order messages", async () => {
|
|
102
|
+
const sk = XUtils.decodeHex(
|
|
103
|
+
"2222222222222222222222222222222222222222222222222222222222222222",
|
|
104
|
+
);
|
|
105
|
+
const initiator = await initRatchetSession(sk, "initiator");
|
|
106
|
+
const receiver = await initRatchetSession(sk, "receiver");
|
|
107
|
+
|
|
108
|
+
const s = {
|
|
109
|
+
CKr: initiator.CKr ? XUtils.decodeHex(initiator.CKr) : null,
|
|
110
|
+
CKs: initiator.CKs ? XUtils.decodeHex(initiator.CKs) : null,
|
|
111
|
+
DHr: initiator.DHr ? XUtils.decodeHex(initiator.DHr) : null,
|
|
112
|
+
DHsPrivate: XUtils.decodeHex(initiator.DHsPrivate),
|
|
113
|
+
DHsPublic: XUtils.decodeHex(initiator.DHsPublic),
|
|
114
|
+
Nr: initiator.Nr,
|
|
115
|
+
Ns: initiator.Ns,
|
|
116
|
+
PN: initiator.PN,
|
|
117
|
+
RK: XUtils.decodeHex(initiator.RK),
|
|
118
|
+
skippedKeys: {} as Record<string, string>,
|
|
119
|
+
};
|
|
120
|
+
const r = {
|
|
121
|
+
CKr: receiver.CKr ? XUtils.decodeHex(receiver.CKr) : null,
|
|
122
|
+
CKs: receiver.CKs ? XUtils.decodeHex(receiver.CKs) : null,
|
|
123
|
+
DHr: receiver.DHr ? XUtils.decodeHex(receiver.DHr) : null,
|
|
124
|
+
DHsPrivate: XUtils.decodeHex(receiver.DHsPrivate),
|
|
125
|
+
DHsPublic: XUtils.decodeHex(receiver.DHsPublic),
|
|
126
|
+
Nr: receiver.Nr,
|
|
127
|
+
Ns: receiver.Ns,
|
|
128
|
+
PN: receiver.PN,
|
|
129
|
+
RK: XUtils.decodeHex(receiver.RK),
|
|
130
|
+
skippedKeys: {} as Record<string, string>,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (!s.CKs) {
|
|
134
|
+
await ratchetStepSend(s);
|
|
135
|
+
}
|
|
136
|
+
const m0 = takeSendMessageKey(s);
|
|
137
|
+
const m1 = takeSendMessageKey(s);
|
|
138
|
+
const h0 = {
|
|
139
|
+
dhPub: s.DHsPublic,
|
|
140
|
+
n: m0.n,
|
|
141
|
+
pn: s.PN,
|
|
142
|
+
version: 1 as const,
|
|
143
|
+
};
|
|
144
|
+
const h1 = {
|
|
145
|
+
dhPub: s.DHsPublic,
|
|
146
|
+
n: m1.n,
|
|
147
|
+
pn: s.PN,
|
|
148
|
+
version: 1 as const,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (!r.DHr && r.CKr) {
|
|
152
|
+
r.DHr = h1.dhPub;
|
|
153
|
+
} else {
|
|
154
|
+
await ratchetStepReceive(r, h1.dhPub, h1.pn);
|
|
155
|
+
}
|
|
156
|
+
const r1 = takeReceiveMessageKey(r, h1.dhPub, h1.n);
|
|
157
|
+
expect(XUtils.bytesEqual(r1, m1.messageKey)).toBe(true);
|
|
158
|
+
|
|
159
|
+
const r0 = takeReceiveMessageKey(r, h0.dhPub, h0.n);
|
|
160
|
+
expect(XUtils.bytesEqual(r0, m0.messageKey)).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("advances sending chain for every message within same DH epoch", async () => {
|
|
164
|
+
const sk = XUtils.decodeHex(
|
|
165
|
+
"5555555555555555555555555555555555555555555555555555555555555555",
|
|
166
|
+
);
|
|
167
|
+
const initiator = await initRatchetSession(sk, "initiator");
|
|
168
|
+
const sender = {
|
|
169
|
+
CKr: initiator.CKr ? XUtils.decodeHex(initiator.CKr) : null,
|
|
170
|
+
CKs: initiator.CKs ? XUtils.decodeHex(initiator.CKs) : null,
|
|
171
|
+
DHr: initiator.DHr ? XUtils.decodeHex(initiator.DHr) : null,
|
|
172
|
+
DHsPrivate: XUtils.decodeHex(initiator.DHsPrivate),
|
|
173
|
+
DHsPublic: XUtils.decodeHex(initiator.DHsPublic),
|
|
174
|
+
Nr: initiator.Nr,
|
|
175
|
+
Ns: initiator.Ns,
|
|
176
|
+
PN: initiator.PN,
|
|
177
|
+
RK: XUtils.decodeHex(initiator.RK),
|
|
178
|
+
skippedKeys: {} as Record<string, string>,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (!sender.CKs) {
|
|
182
|
+
await ratchetStepSend(sender);
|
|
183
|
+
}
|
|
184
|
+
const dhPubBefore = XUtils.encodeHex(sender.DHsPublic);
|
|
185
|
+
|
|
186
|
+
const m0 = takeSendMessageKey(sender);
|
|
187
|
+
const m1 = takeSendMessageKey(sender);
|
|
188
|
+
const m2 = takeSendMessageKey(sender);
|
|
189
|
+
|
|
190
|
+
expect(m0.n).toBe(0);
|
|
191
|
+
expect(m1.n).toBe(1);
|
|
192
|
+
expect(m2.n).toBe(2);
|
|
193
|
+
expect(sender.Ns).toBe(3);
|
|
194
|
+
expect(XUtils.bytesEqual(m0.messageKey, m1.messageKey)).toBe(false);
|
|
195
|
+
expect(XUtils.bytesEqual(m1.messageKey, m2.messageKey)).toBe(false);
|
|
196
|
+
expect(XUtils.encodeHex(sender.DHsPublic)).toBe(dhPubBefore);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("decrypts first subsequent reply after initial mail via bootstrap chain", async () => {
|
|
200
|
+
const sk = XUtils.decodeHex(
|
|
201
|
+
"6666666666666666666666666666666666666666666666666666666666666666",
|
|
202
|
+
);
|
|
203
|
+
const initiator = await initRatchetSession(sk, "initiator");
|
|
204
|
+
const receiver = await initRatchetSession(sk, "receiver");
|
|
205
|
+
|
|
206
|
+
const alice = {
|
|
207
|
+
CKr: initiator.CKr ? XUtils.decodeHex(initiator.CKr) : null,
|
|
208
|
+
CKs: initiator.CKs ? XUtils.decodeHex(initiator.CKs) : null,
|
|
209
|
+
DHr: initiator.DHr ? XUtils.decodeHex(initiator.DHr) : null,
|
|
210
|
+
DHsPrivate: XUtils.decodeHex(initiator.DHsPrivate),
|
|
211
|
+
DHsPublic: XUtils.decodeHex(initiator.DHsPublic),
|
|
212
|
+
Nr: initiator.Nr,
|
|
213
|
+
Ns: initiator.Ns,
|
|
214
|
+
PN: initiator.PN,
|
|
215
|
+
RK: XUtils.decodeHex(initiator.RK),
|
|
216
|
+
skippedKeys: {} as Record<string, string>,
|
|
217
|
+
};
|
|
218
|
+
const bob = {
|
|
219
|
+
CKr: receiver.CKr ? XUtils.decodeHex(receiver.CKr) : null,
|
|
220
|
+
CKs: receiver.CKs ? XUtils.decodeHex(receiver.CKs) : null,
|
|
221
|
+
DHr: receiver.DHr ? XUtils.decodeHex(receiver.DHr) : null,
|
|
222
|
+
DHsPrivate: XUtils.decodeHex(receiver.DHsPrivate),
|
|
223
|
+
DHsPublic: XUtils.decodeHex(receiver.DHsPublic),
|
|
224
|
+
Nr: receiver.Nr,
|
|
225
|
+
Ns: receiver.Ns,
|
|
226
|
+
PN: receiver.PN,
|
|
227
|
+
RK: XUtils.decodeHex(receiver.RK),
|
|
228
|
+
skippedKeys: {} as Record<string, string>,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (!bob.CKs) {
|
|
232
|
+
await ratchetStepSend(bob);
|
|
233
|
+
}
|
|
234
|
+
const outbound = takeSendMessageKey(bob);
|
|
235
|
+
const header = decodeRatchetHeader(
|
|
236
|
+
encodeRatchetHeader({
|
|
237
|
+
dhPub: bob.DHsPublic,
|
|
238
|
+
n: outbound.n,
|
|
239
|
+
pn: bob.PN,
|
|
240
|
+
version: 1,
|
|
241
|
+
}),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
if (!alice.DHr) {
|
|
245
|
+
alice.DHr = header.dhPub;
|
|
246
|
+
if (!alice.CKr) {
|
|
247
|
+
alice.CKr = deriveBootstrapSendChain(alice.RK);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const inbound = takeReceiveMessageKey(alice, header.dhPub, header.n);
|
|
251
|
+
expect(XUtils.bytesEqual(inbound, outbound.messageKey)).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("keeps sessions robust over long back-and-forth with persistence", async () => {
|
|
255
|
+
const sk = XUtils.decodeHex(
|
|
256
|
+
"3333333333333333333333333333333333333333333333333333333333333333",
|
|
257
|
+
);
|
|
258
|
+
const initiator = await initRatchetSession(sk, "initiator");
|
|
259
|
+
const receiver = await initRatchetSession(sk, "receiver");
|
|
260
|
+
|
|
261
|
+
let alice = {
|
|
262
|
+
CKr: initiator.CKr ? XUtils.decodeHex(initiator.CKr) : null,
|
|
263
|
+
CKs: initiator.CKs ? XUtils.decodeHex(initiator.CKs) : null,
|
|
264
|
+
DHr: initiator.DHr ? XUtils.decodeHex(initiator.DHr) : null,
|
|
265
|
+
DHsPrivate: XUtils.decodeHex(initiator.DHsPrivate),
|
|
266
|
+
DHsPublic: XUtils.decodeHex(initiator.DHsPublic),
|
|
267
|
+
Nr: initiator.Nr,
|
|
268
|
+
Ns: initiator.Ns,
|
|
269
|
+
PN: initiator.PN,
|
|
270
|
+
RK: XUtils.decodeHex(initiator.RK),
|
|
271
|
+
skippedKeys: {} as Record<string, string>,
|
|
272
|
+
};
|
|
273
|
+
let bob = {
|
|
274
|
+
CKr: receiver.CKr ? XUtils.decodeHex(receiver.CKr) : null,
|
|
275
|
+
CKs: receiver.CKs ? XUtils.decodeHex(receiver.CKs) : null,
|
|
276
|
+
DHr: receiver.DHr ? XUtils.decodeHex(receiver.DHr) : null,
|
|
277
|
+
DHsPrivate: XUtils.decodeHex(receiver.DHsPrivate),
|
|
278
|
+
DHsPublic: XUtils.decodeHex(receiver.DHsPublic),
|
|
279
|
+
Nr: receiver.Nr,
|
|
280
|
+
Ns: receiver.Ns,
|
|
281
|
+
PN: receiver.PN,
|
|
282
|
+
RK: XUtils.decodeHex(receiver.RK),
|
|
283
|
+
skippedKeys: {} as Record<string, string>,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const rounds = 120;
|
|
287
|
+
for (let i = 0; i < rounds; i += 1) {
|
|
288
|
+
if (!alice.CKs) {
|
|
289
|
+
await ratchetStepSend(alice);
|
|
290
|
+
}
|
|
291
|
+
const aOut = takeSendMessageKey(alice);
|
|
292
|
+
const aHdr = decodeRatchetHeader(
|
|
293
|
+
encodeRatchetHeader({
|
|
294
|
+
dhPub: alice.DHsPublic,
|
|
295
|
+
n: aOut.n,
|
|
296
|
+
pn: alice.PN,
|
|
297
|
+
version: 1,
|
|
298
|
+
}),
|
|
299
|
+
);
|
|
300
|
+
if (!bob.DHr && bob.CKr) {
|
|
301
|
+
bob.DHr = aHdr.dhPub;
|
|
302
|
+
} else if (hasRemoteDhChanged(bob.DHr, aHdr.dhPub)) {
|
|
303
|
+
await ratchetStepReceive(bob, aHdr.dhPub, aHdr.pn);
|
|
304
|
+
}
|
|
305
|
+
const bIn = takeReceiveMessageKey(bob, aHdr.dhPub, aHdr.n);
|
|
306
|
+
expect(XUtils.bytesEqual(aOut.messageKey, bIn)).toBe(true);
|
|
307
|
+
|
|
308
|
+
if (!bob.CKs) {
|
|
309
|
+
await ratchetStepSend(bob);
|
|
310
|
+
}
|
|
311
|
+
const bOut = takeSendMessageKey(bob);
|
|
312
|
+
const bHdr = decodeRatchetHeader(
|
|
313
|
+
encodeRatchetHeader({
|
|
314
|
+
dhPub: bob.DHsPublic,
|
|
315
|
+
n: bOut.n,
|
|
316
|
+
pn: bob.PN,
|
|
317
|
+
version: 1,
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
if (!alice.DHr && alice.CKr) {
|
|
321
|
+
alice.DHr = bHdr.dhPub;
|
|
322
|
+
} else if (hasRemoteDhChanged(alice.DHr, bHdr.dhPub)) {
|
|
323
|
+
await ratchetStepReceive(alice, bHdr.dhPub, bHdr.pn);
|
|
324
|
+
}
|
|
325
|
+
const aIn = takeReceiveMessageKey(alice, bHdr.dhPub, bHdr.n);
|
|
326
|
+
expect(XUtils.bytesEqual(aIn, bOut.messageKey)).toBe(true);
|
|
327
|
+
|
|
328
|
+
// Simulate periodic app restarts by serializing and reloading session state.
|
|
329
|
+
if (i > 0 && i % 10 === 0) {
|
|
330
|
+
alice = hydrateState(alice, "alice-session");
|
|
331
|
+
bob = hydrateState(bob, "bob-session");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
expect(alice.Nr + alice.Ns).toBeGreaterThan(0);
|
|
336
|
+
expect(bob.Nr + bob.Ns).toBeGreaterThan(0);
|
|
337
|
+
expect(alice.CKr ?? alice.CKs).not.toBeNull();
|
|
338
|
+
expect(bob.CKr ?? bob.CKs).not.toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("nightly: survives 1000-message randomized streaks with persistence", async () => {
|
|
342
|
+
if (process.env["LIBVEX_NIGHTLY_STRESS"] !== "1") {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const sk = XUtils.decodeHex(
|
|
346
|
+
"4444444444444444444444444444444444444444444444444444444444444444",
|
|
347
|
+
);
|
|
348
|
+
const initiator = await initRatchetSession(sk, "initiator");
|
|
349
|
+
const receiver = await initRatchetSession(sk, "receiver");
|
|
350
|
+
|
|
351
|
+
let alice = {
|
|
352
|
+
CKr: initiator.CKr ? XUtils.decodeHex(initiator.CKr) : null,
|
|
353
|
+
CKs: initiator.CKs ? XUtils.decodeHex(initiator.CKs) : null,
|
|
354
|
+
DHr: initiator.DHr ? XUtils.decodeHex(initiator.DHr) : null,
|
|
355
|
+
DHsPrivate: XUtils.decodeHex(initiator.DHsPrivate),
|
|
356
|
+
DHsPublic: XUtils.decodeHex(initiator.DHsPublic),
|
|
357
|
+
Nr: initiator.Nr,
|
|
358
|
+
Ns: initiator.Ns,
|
|
359
|
+
PN: initiator.PN,
|
|
360
|
+
RK: XUtils.decodeHex(initiator.RK),
|
|
361
|
+
skippedKeys: {} as Record<string, string>,
|
|
362
|
+
};
|
|
363
|
+
let bob = {
|
|
364
|
+
CKr: receiver.CKr ? XUtils.decodeHex(receiver.CKr) : null,
|
|
365
|
+
CKs: receiver.CKs ? XUtils.decodeHex(receiver.CKs) : null,
|
|
366
|
+
DHr: receiver.DHr ? XUtils.decodeHex(receiver.DHr) : null,
|
|
367
|
+
DHsPrivate: XUtils.decodeHex(receiver.DHsPrivate),
|
|
368
|
+
DHsPublic: XUtils.decodeHex(receiver.DHsPublic),
|
|
369
|
+
Nr: receiver.Nr,
|
|
370
|
+
Ns: receiver.Ns,
|
|
371
|
+
PN: receiver.PN,
|
|
372
|
+
RK: XUtils.decodeHex(receiver.RK),
|
|
373
|
+
skippedKeys: {} as Record<string, string>,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const rng = mulberry32(0xdecafbad);
|
|
377
|
+
const totalMessages = 1000;
|
|
378
|
+
for (let i = 0; i < totalMessages; i += 1) {
|
|
379
|
+
const aliceSends = rng() < 0.5;
|
|
380
|
+
const sender = aliceSends ? alice : bob;
|
|
381
|
+
const receiverState = aliceSends ? bob : alice;
|
|
382
|
+
|
|
383
|
+
if (!sender.CKs) {
|
|
384
|
+
await ratchetStepSend(sender);
|
|
385
|
+
}
|
|
386
|
+
const outbound = takeSendMessageKey(sender);
|
|
387
|
+
const header = decodeRatchetHeader(
|
|
388
|
+
encodeRatchetHeader({
|
|
389
|
+
dhPub: sender.DHsPublic,
|
|
390
|
+
n: outbound.n,
|
|
391
|
+
pn: sender.PN,
|
|
392
|
+
version: 1,
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
if (!receiverState.DHr && receiverState.CKr) {
|
|
397
|
+
receiverState.DHr = header.dhPub;
|
|
398
|
+
} else if (hasRemoteDhChanged(receiverState.DHr, header.dhPub)) {
|
|
399
|
+
await ratchetStepReceive(
|
|
400
|
+
receiverState,
|
|
401
|
+
header.dhPub,
|
|
402
|
+
header.pn,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
const inbound = takeReceiveMessageKey(
|
|
406
|
+
receiverState,
|
|
407
|
+
header.dhPub,
|
|
408
|
+
header.n,
|
|
409
|
+
);
|
|
410
|
+
expect(XUtils.bytesEqual(outbound.messageKey, inbound)).toBe(true);
|
|
411
|
+
|
|
412
|
+
if (i > 0 && i % 25 === 0) {
|
|
413
|
+
alice = hydrateState(alice, "alice-nightly");
|
|
414
|
+
bob = hydrateState(bob, "bob-nightly");
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
expect(alice.Nr + alice.Ns + bob.Nr + bob.Ns).toBeGreaterThan(500);
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
function hydrateState(
|
|
423
|
+
state: {
|
|
424
|
+
CKr: null | Uint8Array;
|
|
425
|
+
CKs: null | Uint8Array;
|
|
426
|
+
DHr: null | Uint8Array;
|
|
427
|
+
DHsPrivate: Uint8Array;
|
|
428
|
+
DHsPublic: Uint8Array;
|
|
429
|
+
Nr: number;
|
|
430
|
+
Ns: number;
|
|
431
|
+
PN: number;
|
|
432
|
+
RK: Uint8Array;
|
|
433
|
+
skippedKeys: Record<string, string>;
|
|
434
|
+
},
|
|
435
|
+
sessionID: string,
|
|
436
|
+
) {
|
|
437
|
+
const sql = sessionToSqlPatch(state);
|
|
438
|
+
const roundTripped = sqlSessionToCrypto({
|
|
439
|
+
CKr: sql.CKr,
|
|
440
|
+
CKs: sql.CKs,
|
|
441
|
+
deviceID: "device",
|
|
442
|
+
DHr: sql.DHr,
|
|
443
|
+
DHsPrivate: sql.DHsPrivate,
|
|
444
|
+
DHsPublic: sql.DHsPublic,
|
|
445
|
+
fingerprint: "00",
|
|
446
|
+
lastUsed: new Date().toISOString(),
|
|
447
|
+
mode: "initiator",
|
|
448
|
+
Nr: sql.Nr,
|
|
449
|
+
Ns: sql.Ns,
|
|
450
|
+
PN: sql.PN,
|
|
451
|
+
publicKey: "00",
|
|
452
|
+
RK: sql.RK,
|
|
453
|
+
sessionID,
|
|
454
|
+
SK: "00",
|
|
455
|
+
skippedKeys: sql.skippedKeys,
|
|
456
|
+
userID: "user",
|
|
457
|
+
verified: false,
|
|
458
|
+
});
|
|
459
|
+
return {
|
|
460
|
+
CKr: roundTripped.CKr,
|
|
461
|
+
CKs: roundTripped.CKs,
|
|
462
|
+
DHr: roundTripped.DHr,
|
|
463
|
+
DHsPrivate: roundTripped.DHsPrivate,
|
|
464
|
+
DHsPublic: roundTripped.DHsPublic,
|
|
465
|
+
Nr: roundTripped.Nr,
|
|
466
|
+
Ns: roundTripped.Ns,
|
|
467
|
+
PN: roundTripped.PN,
|
|
468
|
+
RK: roundTripped.RK,
|
|
469
|
+
skippedKeys: roundTripped.skippedKeys,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function mulberry32(seed: number): () => number {
|
|
474
|
+
let t = seed >>> 0;
|
|
475
|
+
return () => {
|
|
476
|
+
t = (t + 0x6d2b79f5) | 0;
|
|
477
|
+
let x = Math.imul(t ^ (t >>> 15), 1 | t);
|
|
478
|
+
x ^= x + Math.imul(x ^ (x >>> 7), 61 | x);
|
|
479
|
+
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
|
|
480
|
+
};
|
|
481
|
+
}
|
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
|
}
|