@vex-chat/libvex 7.1.2 → 7.1.4
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 +2 -0
- package/dist/Client.d.ts.map +1 -1
- package/dist/Client.js +72 -40
- 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 +3 -0
- package/dist/__tests__/harness/memory-storage.js.map +1 -1
- package/dist/storage/sqlite.d.ts.map +1 -1
- package/dist/storage/sqlite.js +3 -0
- package/dist/storage/sqlite.js.map +1 -1
- package/package.json +1 -1
- package/src/Client.ts +87 -41
- package/src/__tests__/dm-own-device-forward.test.ts +113 -0
- package/src/__tests__/harness/memory-storage.ts +3 -0
- package/src/__tests__/prekey-reuse.test.ts +172 -0
- package/src/storage/sqlite.ts +4 -0
|
@@ -0,0 +1,172 @@
|
|
|
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 type { PreKeysCrypto, UnsavedPreKey, XKeyRing } from "../types/index.js";
|
|
8
|
+
import type { CryptoProfile, KeyPair } from "@vex-chat/crypto";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
setCryptoProfile,
|
|
12
|
+
xBoxKeyPairAsync,
|
|
13
|
+
xConstants,
|
|
14
|
+
xEcdhKeyPairFromEcdsaKeyPairAsync,
|
|
15
|
+
xEncode,
|
|
16
|
+
XKeyConvert,
|
|
17
|
+
xSignAsync,
|
|
18
|
+
xSignKeyPair,
|
|
19
|
+
xSignKeyPairAsync,
|
|
20
|
+
xSignOpenAsync,
|
|
21
|
+
XUtils,
|
|
22
|
+
} from "@vex-chat/crypto";
|
|
23
|
+
|
|
24
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
25
|
+
|
|
26
|
+
import { Client } from "../Client.js";
|
|
27
|
+
import { fipsP256PreKeySignPayload } from "../utils/fipsMailExtra.js";
|
|
28
|
+
|
|
29
|
+
import { MemoryStorage } from "./harness/memory-storage.js";
|
|
30
|
+
|
|
31
|
+
interface KeyRingHarness {
|
|
32
|
+
createPreKey: () => Promise<UnsavedPreKey>;
|
|
33
|
+
cryptoProfile: CryptoProfile;
|
|
34
|
+
database: MemoryStorage;
|
|
35
|
+
idKeys: KeyPair;
|
|
36
|
+
isPreKeySignedByCurrentDevice: (preKey: PreKeysCrypto) => Promise<boolean>;
|
|
37
|
+
runWithThisCryptoProfile: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
38
|
+
sessionRecords: Record<string, unknown>;
|
|
39
|
+
signKeys: KeyPair;
|
|
40
|
+
xKeyRing?: XKeyRing;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const clientMethods = Client.prototype as unknown as {
|
|
44
|
+
createPreKey: () => Promise<UnsavedPreKey>;
|
|
45
|
+
isPreKeySignedByCurrentDevice: (preKey: PreKeysCrypto) => Promise<boolean>;
|
|
46
|
+
populateKeyRing: () => Promise<void>;
|
|
47
|
+
runWithThisCryptoProfile: <T>(fn: () => Promise<T>) => Promise<T>;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
async function isValidFor(
|
|
51
|
+
preKey: PreKeysCrypto,
|
|
52
|
+
signKeys: KeyPair,
|
|
53
|
+
profile: CryptoProfile = "tweetnacl",
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
setCryptoProfile(profile);
|
|
56
|
+
const opened = await xSignOpenAsync(preKey.signature, signKeys.publicKey);
|
|
57
|
+
const payload =
|
|
58
|
+
profile === "fips"
|
|
59
|
+
? fipsP256PreKeySignPayload(preKey.keyPair.publicKey)
|
|
60
|
+
: xEncode(xConstants.CURVE, preKey.keyPair.publicKey);
|
|
61
|
+
return Boolean(opened && XUtils.bytesEqual(opened, payload));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function makeSignedPreKey(
|
|
65
|
+
signKeys: KeyPair,
|
|
66
|
+
profile: CryptoProfile = "tweetnacl",
|
|
67
|
+
): Promise<UnsavedPreKey> {
|
|
68
|
+
setCryptoProfile(profile);
|
|
69
|
+
const keyPair = await xBoxKeyPairAsync();
|
|
70
|
+
const payload =
|
|
71
|
+
profile === "fips"
|
|
72
|
+
? fipsP256PreKeySignPayload(keyPair.publicKey)
|
|
73
|
+
: xEncode(xConstants.CURVE, keyPair.publicKey);
|
|
74
|
+
return {
|
|
75
|
+
keyPair,
|
|
76
|
+
signature: await xSignAsync(payload, signKeys.secretKey),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe("local signed prekey reuse", () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
setCryptoProfile("tweetnacl");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
setCryptoProfile("tweetnacl");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("regenerates a stored prekey signed by a different device key", async () => {
|
|
90
|
+
const staleSignKeys = xSignKeyPair();
|
|
91
|
+
const currentSignKeys = xSignKeyPair();
|
|
92
|
+
const idKeys = XKeyConvert.convertKeyPair(currentSignKeys);
|
|
93
|
+
if (!idKeys) {
|
|
94
|
+
throw new Error("Could not convert current signing key.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const storage = new MemoryStorage(new Uint8Array(32).fill(7));
|
|
98
|
+
await storage.init();
|
|
99
|
+
await storage.savePreKeys(
|
|
100
|
+
[await makeSignedPreKey(staleSignKeys)],
|
|
101
|
+
false,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const harness: KeyRingHarness = {
|
|
105
|
+
createPreKey: clientMethods.createPreKey,
|
|
106
|
+
cryptoProfile: "tweetnacl",
|
|
107
|
+
database: storage,
|
|
108
|
+
idKeys,
|
|
109
|
+
isPreKeySignedByCurrentDevice:
|
|
110
|
+
clientMethods.isPreKeySignedByCurrentDevice,
|
|
111
|
+
runWithThisCryptoProfile: clientMethods.runWithThisCryptoProfile,
|
|
112
|
+
sessionRecords: {},
|
|
113
|
+
signKeys: currentSignKeys,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await clientMethods.populateKeyRing.call(harness);
|
|
117
|
+
|
|
118
|
+
expect(harness.xKeyRing).toBeDefined();
|
|
119
|
+
expect(
|
|
120
|
+
await isValidFor(harness.xKeyRing!.preKeys, currentSignKeys),
|
|
121
|
+
).toBe(true);
|
|
122
|
+
expect(await isValidFor(harness.xKeyRing!.preKeys, staleSignKeys)).toBe(
|
|
123
|
+
false,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const persisted = await storage.getPreKeys();
|
|
127
|
+
expect(persisted).not.toBeNull();
|
|
128
|
+
expect(await isValidFor(persisted!, currentSignKeys)).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("reuses a valid FIPS prekey when the global profile is tweetnacl", async () => {
|
|
132
|
+
setCryptoProfile("fips");
|
|
133
|
+
const currentSignKeys = await xSignKeyPairAsync();
|
|
134
|
+
const idKeys = await xEcdhKeyPairFromEcdsaKeyPairAsync(currentSignKeys);
|
|
135
|
+
const storedPreKey = await makeSignedPreKey(currentSignKeys, "fips");
|
|
136
|
+
|
|
137
|
+
const storage = new MemoryStorage(new Uint8Array(32).fill(8));
|
|
138
|
+
await storage.init();
|
|
139
|
+
await storage.savePreKeys([storedPreKey], false);
|
|
140
|
+
|
|
141
|
+
setCryptoProfile("tweetnacl");
|
|
142
|
+
const harness: KeyRingHarness = {
|
|
143
|
+
createPreKey: clientMethods.createPreKey,
|
|
144
|
+
cryptoProfile: "fips",
|
|
145
|
+
database: storage,
|
|
146
|
+
idKeys,
|
|
147
|
+
isPreKeySignedByCurrentDevice:
|
|
148
|
+
clientMethods.isPreKeySignedByCurrentDevice,
|
|
149
|
+
runWithThisCryptoProfile: clientMethods.runWithThisCryptoProfile,
|
|
150
|
+
sessionRecords: {},
|
|
151
|
+
signKeys: currentSignKeys,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
await clientMethods.populateKeyRing.call(harness);
|
|
155
|
+
|
|
156
|
+
expect(harness.xKeyRing).toBeDefined();
|
|
157
|
+
expect(harness.xKeyRing!.preKeys.index).toBe(1);
|
|
158
|
+
expect(
|
|
159
|
+
XUtils.bytesEqual(
|
|
160
|
+
harness.xKeyRing!.preKeys.keyPair.publicKey,
|
|
161
|
+
storedPreKey.keyPair.publicKey,
|
|
162
|
+
),
|
|
163
|
+
).toBe(true);
|
|
164
|
+
expect(
|
|
165
|
+
await isValidFor(
|
|
166
|
+
harness.xKeyRing!.preKeys,
|
|
167
|
+
currentSignKeys,
|
|
168
|
+
"fips",
|
|
169
|
+
),
|
|
170
|
+
).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
});
|
package/src/storage/sqlite.ts
CHANGED
|
@@ -609,6 +609,10 @@ export class SqliteStorage extends EventEmitter implements Storage {
|
|
|
609
609
|
const table = oneTime ? ("oneTimeKeys" as const) : ("preKeys" as const);
|
|
610
610
|
const saved: PreKeysSQL[] = [];
|
|
611
611
|
|
|
612
|
+
if (!oneTime) {
|
|
613
|
+
await this.db.deleteFrom("preKeys").execute();
|
|
614
|
+
}
|
|
615
|
+
|
|
612
616
|
for (const preKey of preKeys) {
|
|
613
617
|
const row = await this.db
|
|
614
618
|
.insertInto(table)
|