@vex-chat/libvex 9.1.0 → 10.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.
Files changed (43) hide show
  1. package/README.md +1 -1
  2. package/dist/Client.d.ts +3 -21
  3. package/dist/Client.d.ts.map +1 -1
  4. package/dist/Client.js +42 -118
  5. package/dist/Client.js.map +1 -1
  6. package/dist/__tests__/harness/memory-storage.d.ts.map +1 -1
  7. package/dist/__tests__/harness/memory-storage.js +6 -19
  8. package/dist/__tests__/harness/memory-storage.js.map +1 -1
  9. package/dist/preset/node.d.ts.map +1 -1
  10. package/dist/preset/node.js +1 -2
  11. package/dist/preset/node.js.map +1 -1
  12. package/dist/preset/test.d.ts.map +1 -1
  13. package/dist/preset/test.js +1 -2
  14. package/dist/preset/test.js.map +1 -1
  15. package/dist/storage/sqlite.d.ts.map +1 -1
  16. package/dist/storage/sqlite.js +5 -15
  17. package/dist/storage/sqlite.js.map +1 -1
  18. package/dist/utils/resolveAtRestAesKey.d.ts +1 -3
  19. package/dist/utils/resolveAtRestAesKey.d.ts.map +1 -1
  20. package/dist/utils/resolveAtRestAesKey.js +7 -13
  21. package/dist/utils/resolveAtRestAesKey.js.map +1 -1
  22. package/dist/utils/verifyKeyBundle.d.ts +1 -2
  23. package/dist/utils/verifyKeyBundle.d.ts.map +1 -1
  24. package/dist/utils/verifyKeyBundle.js +7 -10
  25. package/dist/utils/verifyKeyBundle.js.map +1 -1
  26. package/package.json +2 -2
  27. package/src/Client.ts +57 -182
  28. package/src/__tests__/harness/memory-storage.ts +17 -48
  29. package/src/__tests__/harness/shared-suite.ts +7 -137
  30. package/src/__tests__/platform-browser.test.ts +1 -6
  31. package/src/__tests__/platform-node.test.ts +1 -7
  32. package/src/__tests__/prekey-reuse.test.ts +8 -77
  33. package/src/__tests__/verifyKeyBundle.test.ts +15 -43
  34. package/src/preset/node.ts +1 -5
  35. package/src/preset/test.ts +1 -5
  36. package/src/storage/sqlite.ts +4 -20
  37. package/src/utils/resolveAtRestAesKey.ts +7 -23
  38. package/src/utils/verifyKeyBundle.ts +2 -16
  39. package/dist/utils/fipsMailExtra.d.ts +0 -30
  40. package/dist/utils/fipsMailExtra.d.ts.map +0 -1
  41. package/dist/utils/fipsMailExtra.js +0 -114
  42. package/dist/utils/fipsMailExtra.js.map +0 -1
  43. package/src/utils/fipsMailExtra.ts +0 -164
@@ -11,27 +11,18 @@
11
11
  * Runs register → login → connect → send/receive DM against a real spire.
12
12
  *
13
13
  * **Vitest e2e only** — the published `Client` does not read the environment;
14
- * app code passes `ClientOptions` (`host`, `devApiKey`, `cryptoProfile`, …).
14
+ * app code passes `ClientOptions` (`host`, `devApiKey`, …).
15
15
  * These `process.env` values are for running **this** repo’s platform tests
16
16
  * (shell, CI, or your own env injection — not a “libvex .env” contract).
17
17
  * - `API_URL` — Spire base, e.g. `http://127.0.0.1:16777` or `host:port` (http assumed).
18
- * When set, `beforeAll` reads `GET …/status` `cryptoProfile` and **sets the
19
- * test process to match** (so a FIPS Spire does not require a separate client flag).
20
- * A post-check then fails if the client and server still disagree.
21
18
  * - `DEV_API_KEY` — must match the Spire `DEV_API_KEY` so the client can send
22
19
  * `x-dev-api-key` and avoid dev rate limits.
23
- * - `LIBVEX_E2E_SKIP_STATUS_CHECK=1` — opt out of the /status profile read + check
24
- * (e.g. older Spire). Then `LIBVEX_E2E_CRYPTO` or default tweetnacl is used.
25
- * - `LIBVEX_E2E_CRYPTO` — optional override: `fips` | `tweetnacl` (wins over auto
26
- * detect from /status; use to force a profile when you know what you need).
27
20
  * - `LIBVEX_DEBUG_DM=1` — logs DM/X3dh paths in `Client` to stderr (remove / gate off when done).
28
21
  */
29
22
 
30
23
  import type { ClientOptions, Message } from "../../index.js";
31
24
  import type { Storage } from "../../Storage.js";
32
25
 
33
- import { getCryptoProfile, setCryptoProfile } from "@vex-chat/crypto";
34
-
35
26
  import { isHttpError } from "../../http.js";
36
27
  import { Client } from "../../index.js";
37
28
 
@@ -47,14 +38,11 @@ export function platformSuite(
47
38
  const password = "platform-test-pw";
48
39
 
49
40
  beforeAll(async () => {
50
- const profile = await resolveE2eCryptoProfile();
51
- setCryptoProfile(profile);
52
41
  const SK = await e2eGenerateSecretKey();
53
42
 
54
43
  const opts: ClientOptions = e2eClientOptionsBase();
55
44
  const storage = await makeStorage(SK, opts);
56
45
  client = await Client.create(SK, opts, storage);
57
- await assertSpireCryptoProfileMatchesTest();
58
46
  });
59
47
 
60
48
  afterAll(async () => {
@@ -426,6 +414,12 @@ export function platformSuite(
426
414
  expect(client.servers.iconURL(withIcon.icon!)).toContain(
427
415
  `/server-icon/${withIcon.icon!}`,
428
416
  );
417
+ expect(client.servers.iconURL(` ${withIcon.icon!} `)).toBe(
418
+ client.servers.iconURL(withIcon.icon!),
419
+ );
420
+ expect(() => client.servers.iconURL(" ")).toThrow(
421
+ "Server icon ID cannot be empty.",
422
+ );
429
423
  const withoutIcon = await client.servers.removeIcon(
430
424
  server.serverID,
431
425
  );
@@ -634,53 +628,6 @@ function apiUrlOverrideFromEnv():
634
628
  };
635
629
  }
636
630
 
637
- async function assertSpireCryptoProfileMatchesTest(): Promise<void> {
638
- if (process.env["LIBVEX_E2E_SKIP_STATUS_CHECK"] === "1") {
639
- return;
640
- }
641
- const url = spireStatusUrlFromEnv();
642
- if (url === null) {
643
- return;
644
- }
645
- const want = getCryptoProfile();
646
- let res: Response;
647
- try {
648
- res = await fetch(url, { method: "GET" });
649
- } catch (e: unknown) {
650
- const msg = e instanceof Error ? e.message : String(e);
651
- throw new Error(
652
- `libvex e2e: could not GET ${url} (check API_URL; is Spire running?): ${msg}`,
653
- );
654
- }
655
- if (!res.ok) {
656
- throw new Error(
657
- `libvex e2e: ${url} returned HTTP ${String(res.status)} — Spire not reachable on this base URL?`,
658
- );
659
- }
660
- const data: unknown = await res.json();
661
- if (
662
- typeof data !== "object" ||
663
- data === null ||
664
- !("cryptoProfile" in data) ||
665
- typeof (data as { cryptoProfile: unknown }).cryptoProfile !== "string"
666
- ) {
667
- throw new Error(
668
- `libvex e2e: Spire /status is missing a string "cryptoProfile" (upgrade Spire) or set LIBVEX_E2E_SKIP_STATUS_CHECK=1 to skip this check`,
669
- );
670
- }
671
- const gotStr = (data as { cryptoProfile: string }).cryptoProfile;
672
- if (gotStr !== "fips" && gotStr !== "tweetnacl") {
673
- throw new Error(
674
- `libvex e2e: Spire /status cryptoProfile is not fips|tweetnacl: ${gotStr}`,
675
- );
676
- }
677
- if (gotStr !== want) {
678
- throw new Error(
679
- `libvex e2e: Spire is cryptoProfile=${gotStr} (see SPIRE_FIPS + SPK) but this test has getCryptoProfile()=${want}. Use matching keys/scripts (gen-spk.js vs gen-spk-fips.js) and the same mode on client and server.`,
680
- );
681
- }
682
- }
683
-
684
631
  function connectAndWait(
685
632
  c: Client,
686
633
  label: string,
@@ -707,90 +654,13 @@ function e2eClientOptionsBase(): ClientOptions {
707
654
  return {
708
655
  inMemoryDb: true,
709
656
  ...apiUrlOverrideFromEnv(),
710
- cryptoProfile: getCryptoProfile(),
711
657
  };
712
658
  }
713
659
 
714
- /** `LIBVEX_E2E_CRYPTO` only — used when status auto-detect is skipped. */
715
- function e2eCryptoProfileFromEnvOnly(): "fips" | "tweetnacl" {
716
- const v = process.env["LIBVEX_E2E_CRYPTO"]?.trim().toLowerCase();
717
- if (v === "fips" || v === "p-256" || v === "p256") {
718
- return "fips";
719
- }
720
- if (v === "tweetnacl" || v === "nacl" || v === "ed25519") {
721
- return "tweetnacl";
722
- }
723
- return "tweetnacl";
724
- }
725
-
726
660
  async function e2eGenerateSecretKey(): Promise<string> {
727
661
  return await Client.generateSecretKeyAsync();
728
662
  }
729
663
 
730
- /**
731
- * Picks the signing profile for the suite: optional env override, else `GET` Spire
732
- * `/status` when `API_URL` is set, else tweetnacl.
733
- */
734
- async function resolveE2eCryptoProfile(): Promise<"fips" | "tweetnacl"> {
735
- if (process.env["LIBVEX_E2E_SKIP_STATUS_CHECK"] === "1") {
736
- return e2eCryptoProfileFromEnvOnly();
737
- }
738
- const v = process.env["LIBVEX_E2E_CRYPTO"]?.trim().toLowerCase();
739
- if (v === "fips" || v === "p-256" || v === "p256") {
740
- return "fips";
741
- }
742
- if (v === "tweetnacl" || v === "nacl" || v === "ed25519") {
743
- return "tweetnacl";
744
- }
745
- if (v !== undefined && v.length > 0) {
746
- throw new Error(
747
- `libvex e2e: invalid LIBVEX_E2E_CRYPTO=${JSON.stringify(v)}. Use fips, tweetnacl, or leave unset to auto-detect from Spire /status`,
748
- );
749
- }
750
- const url = spireStatusUrlFromEnv();
751
- if (url === null) {
752
- return "tweetnacl";
753
- }
754
- let res: Response;
755
- try {
756
- res = await fetch(url, { method: "GET" });
757
- } catch {
758
- return "tweetnacl";
759
- }
760
- if (!res.ok) {
761
- return "tweetnacl";
762
- }
763
- const data: unknown = await res.json();
764
- if (
765
- typeof data !== "object" ||
766
- data === null ||
767
- !("cryptoProfile" in data)
768
- ) {
769
- return "tweetnacl";
770
- }
771
- const cp = (data as { cryptoProfile: unknown }).cryptoProfile;
772
- if (cp === "fips" || cp === "tweetnacl") {
773
- return cp;
774
- }
775
- return "tweetnacl";
776
- }
777
-
778
- /**
779
- * `GET` `{API_URL or http://}{host}/status` — used for crypto profile preflight
780
- * (must match `getCryptoProfile()` when running e2e against a custom Spire).
781
- */
782
- function spireStatusUrlFromEnv(): null | string {
783
- const raw = process.env["API_URL"]?.trim();
784
- if (raw === undefined || raw.length === 0) {
785
- return null;
786
- }
787
- if (/^https?:\/\//i.test(raw)) {
788
- const u = new URL(raw);
789
- return `${u.protocol}//${u.host}/status`;
790
- }
791
- return `http://${raw}/status`;
792
- }
793
-
794
664
  /*
795
665
  type ClientE2EInternals = { getMail(): Promise<void> };
796
666
  type ClientE2EDeviceList = {
@@ -6,8 +6,6 @@
6
6
 
7
7
  import type { ClientOptions } from "../index.js";
8
8
 
9
- import { getCryptoProfile } from "@vex-chat/crypto";
10
-
11
9
  import { resolveAtRestAesKeyFromSignKeyHex } from "../utils/resolveAtRestAesKey.js";
12
10
 
13
11
  import { MemoryStorage } from "./harness/memory-storage.js";
@@ -18,10 +16,7 @@ import { MemoryStorage } from "./harness/memory-storage.js";
18
16
  import { platformSuite } from "./harness/shared-suite.js";
19
17
 
20
18
  platformSuite("browser", async (SK: string, _opts: ClientOptions) => {
21
- const atRest = await resolveAtRestAesKeyFromSignKeyHex(
22
- SK,
23
- getCryptoProfile(),
24
- );
19
+ const atRest = await resolveAtRestAesKeyFromSignKeyHex(SK);
25
20
  const storage = new MemoryStorage(atRest);
26
21
  await storage.init();
27
22
  return storage;
@@ -6,18 +6,12 @@
6
6
 
7
7
  import type { ClientOptions } from "../index.js";
8
8
 
9
- import { getCryptoProfile } from "@vex-chat/crypto";
10
-
11
9
  import { createNodeStorage } from "../storage/node.js";
12
10
  import { resolveAtRestAesKeyFromSignKeyHex } from "../utils/resolveAtRestAesKey.js";
13
11
 
14
12
  import { platformSuite } from "./harness/shared-suite.js";
15
13
 
16
- // Profile is set in shared-suite `beforeAll` (see `LIBVEX_E2E_CRYPTO`).
17
14
  platformSuite("node", async (SK: string, _opts: ClientOptions) => {
18
- const atRest = await resolveAtRestAesKeyFromSignKeyHex(
19
- SK,
20
- getCryptoProfile(),
21
- );
15
+ const atRest = await resolveAtRestAesKeyFromSignKeyHex(SK);
22
16
  return createNodeStorage(":memory:", atRest);
23
17
  });
@@ -5,22 +5,19 @@
5
5
  */
6
6
 
7
7
  import type { PreKeysCrypto, UnsavedPreKey, XKeyRing } from "../types/index.js";
8
- import type { CryptoProfile, KeyPair } from "@vex-chat/crypto";
8
+ import type { KeyPair } from "@vex-chat/crypto";
9
9
 
10
10
  import {
11
- setCryptoProfile,
12
11
  xBoxKeyPairAsync,
13
- xEcdhKeyPairFromEcdsaKeyPairAsync,
14
12
  XKeyConvert,
15
13
  xPreKeySignaturePayload,
16
14
  xSignAsync,
17
15
  xSignKeyPair,
18
- xSignKeyPairAsync,
19
16
  xSignOpenAsync,
20
17
  XUtils,
21
18
  } from "@vex-chat/crypto";
22
19
 
23
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
20
+ import { describe, expect, it } from "vitest";
24
21
 
25
22
  import { Client } from "../Client.js";
26
23
 
@@ -28,11 +25,10 @@ import { MemoryStorage } from "./harness/memory-storage.js";
28
25
 
29
26
  interface KeyRingHarness {
30
27
  createPreKey: (kind: "one-time" | "signed") => Promise<UnsavedPreKey>;
31
- cryptoProfile: CryptoProfile;
32
28
  database: MemoryStorage;
33
29
  idKeys: KeyPair;
34
30
  isPreKeySignedByCurrentDevice: (preKey: PreKeysCrypto) => Promise<boolean>;
35
- runWithThisCryptoProfile: <T>(fn: () => Promise<T>) => Promise<T>;
31
+ runCrypto: <T>(fn: () => Promise<T>) => Promise<T>;
36
32
  sessionRecords: Record<string, unknown>;
37
33
  signKeys: KeyPair;
38
34
  xKeyRing?: XKeyRing;
@@ -42,35 +38,21 @@ const clientMethods = Client.prototype as unknown as {
42
38
  createPreKey: (kind: "one-time" | "signed") => Promise<UnsavedPreKey>;
43
39
  isPreKeySignedByCurrentDevice: (preKey: PreKeysCrypto) => Promise<boolean>;
44
40
  populateKeyRing: () => Promise<void>;
45
- runWithThisCryptoProfile: <T>(fn: () => Promise<T>) => Promise<T>;
41
+ runCrypto: <T>(fn: () => Promise<T>) => Promise<T>;
46
42
  };
47
43
 
48
44
  async function isValidFor(
49
45
  preKey: PreKeysCrypto,
50
46
  signKeys: KeyPair,
51
- profile: CryptoProfile = "tweetnacl",
52
47
  ): Promise<boolean> {
53
- setCryptoProfile(profile);
54
48
  const opened = await xSignOpenAsync(preKey.signature, signKeys.publicKey);
55
- const payload = xPreKeySignaturePayload(
56
- preKey.keyPair.publicKey,
57
- "signed",
58
- profile,
59
- );
49
+ const payload = xPreKeySignaturePayload(preKey.keyPair.publicKey, "signed");
60
50
  return Boolean(opened && XUtils.bytesEqual(opened, payload));
61
51
  }
62
52
 
63
- async function makeSignedPreKey(
64
- signKeys: KeyPair,
65
- profile: CryptoProfile = "tweetnacl",
66
- ): Promise<UnsavedPreKey> {
67
- setCryptoProfile(profile);
53
+ async function makeSignedPreKey(signKeys: KeyPair): Promise<UnsavedPreKey> {
68
54
  const keyPair = await xBoxKeyPairAsync();
69
- const payload = xPreKeySignaturePayload(
70
- keyPair.publicKey,
71
- "signed",
72
- profile,
73
- );
55
+ const payload = xPreKeySignaturePayload(keyPair.publicKey, "signed");
74
56
  return {
75
57
  keyPair,
76
58
  signature: await xSignAsync(payload, signKeys.secretKey),
@@ -78,14 +60,6 @@ async function makeSignedPreKey(
78
60
  }
79
61
 
80
62
  describe("local signed prekey reuse", () => {
81
- beforeEach(() => {
82
- setCryptoProfile("tweetnacl");
83
- });
84
-
85
- afterEach(() => {
86
- setCryptoProfile("tweetnacl");
87
- });
88
-
89
63
  it("regenerates a stored prekey signed by a different device key", async () => {
90
64
  const staleSignKeys = xSignKeyPair();
91
65
  const currentSignKeys = xSignKeyPair();
@@ -103,12 +77,11 @@ describe("local signed prekey reuse", () => {
103
77
 
104
78
  const harness: KeyRingHarness = {
105
79
  createPreKey: clientMethods.createPreKey,
106
- cryptoProfile: "tweetnacl",
107
80
  database: storage,
108
81
  idKeys,
109
82
  isPreKeySignedByCurrentDevice:
110
83
  clientMethods.isPreKeySignedByCurrentDevice,
111
- runWithThisCryptoProfile: clientMethods.runWithThisCryptoProfile,
84
+ runCrypto: clientMethods.runCrypto,
112
85
  sessionRecords: {},
113
86
  signKeys: currentSignKeys,
114
87
  };
@@ -127,46 +100,4 @@ describe("local signed prekey reuse", () => {
127
100
  expect(persisted).not.toBeNull();
128
101
  expect(await isValidFor(persisted!, currentSignKeys)).toBe(true);
129
102
  });
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
103
  });
@@ -4,48 +4,42 @@
4
4
  * Commercial licenses available at vex.wtf
5
5
  */
6
6
 
7
- import type { CryptoProfile, KeyPair } from "@vex-chat/crypto";
7
+ import type { KeyPair } from "@vex-chat/crypto";
8
8
  import type { Device, KeyBundle, KeyBundleEntry } from "@vex-chat/types";
9
9
 
10
10
  import {
11
- setCryptoProfile,
12
11
  xBoxKeyPairAsync,
13
- xEcdhKeyPairFromEcdsaKeyPairAsync,
14
12
  xPreKeySignaturePayload,
15
13
  xSignAsync,
16
14
  xSignKeyPairAsync,
17
15
  XUtils,
18
16
  } from "@vex-chat/crypto";
19
17
 
20
- import { afterEach, describe, expect, it } from "vitest";
18
+ import { describe, expect, it } from "vitest";
21
19
 
22
20
  import { verifyKeyBundleSignatures } from "../utils/verifyKeyBundle.js";
23
21
 
24
- describe.sequential("verifyKeyBundleSignatures", () => {
25
- afterEach(() => {
26
- setCryptoProfile("tweetnacl");
27
- });
28
-
29
- it("accepts a valid tweetnacl key bundle", async () => {
30
- const { device, keyBundle } = await makeBundle("tweetnacl");
22
+ describe("verifyKeyBundleSignatures", () => {
23
+ it("accepts a valid key bundle", async () => {
24
+ const { device, keyBundle } = await makeBundle();
31
25
 
32
26
  await expect(
33
- verifyKeyBundleSignatures(keyBundle, device, "tweetnacl"),
27
+ verifyKeyBundleSignatures(keyBundle, device),
34
28
  ).resolves.toBeUndefined();
35
29
  });
36
30
 
37
31
  it("rejects a tampered signed prekey public key", async () => {
38
- const { device, keyBundle } = await makeBundle("tweetnacl");
32
+ const { device, keyBundle } = await makeBundle();
39
33
  const tampered = cloneBundle(keyBundle);
40
34
  tampered.preKey.publicKey = flipFirstByte(tampered.preKey.publicKey);
41
35
 
42
36
  await expect(
43
- verifyKeyBundleSignatures(tampered, device, "tweetnacl"),
37
+ verifyKeyBundleSignatures(tampered, device),
44
38
  ).rejects.toThrow("signed prekey signature is invalid");
45
39
  });
46
40
 
47
41
  it("rejects a tampered one-time prekey public key", async () => {
48
- const { device, keyBundle } = await makeBundle("tweetnacl", true);
42
+ const { device, keyBundle } = await makeBundle(true);
49
43
  const tampered = cloneBundle(keyBundle);
50
44
  if (!tampered.otk) {
51
45
  throw new Error("Expected OTK fixture.");
@@ -53,28 +47,20 @@ describe.sequential("verifyKeyBundleSignatures", () => {
53
47
  tampered.otk.publicKey = flipFirstByte(tampered.otk.publicKey);
54
48
 
55
49
  await expect(
56
- verifyKeyBundleSignatures(tampered, device, "tweetnacl"),
50
+ verifyKeyBundleSignatures(tampered, device),
57
51
  ).rejects.toThrow("one-time prekey signature is invalid");
58
52
  });
59
53
 
60
54
  it("rejects a bundle identity key that does not match the device", async () => {
61
- const { device, keyBundle } = await makeBundle("tweetnacl");
55
+ const { device, keyBundle } = await makeBundle();
62
56
  const other = await xSignKeyPairAsync();
63
57
  const tampered = cloneBundle(keyBundle);
64
58
  tampered.signKey = other.publicKey;
65
59
 
66
60
  await expect(
67
- verifyKeyBundleSignatures(tampered, device, "tweetnacl"),
61
+ verifyKeyBundleSignatures(tampered, device),
68
62
  ).rejects.toThrow("identity key does not match");
69
63
  });
70
-
71
- it("accepts a valid FIPS key bundle", async () => {
72
- const { device, keyBundle } = await makeBundle("fips", true);
73
-
74
- await expect(
75
- verifyKeyBundleSignatures(keyBundle, device, "fips"),
76
- ).resolves.toBeUndefined();
77
- });
78
64
  });
79
65
 
80
66
  function cloneBundle(bundle: KeyBundle): KeyBundle {
@@ -105,15 +91,9 @@ function flipFirstByte(value: Uint8Array): Uint8Array {
105
91
  }
106
92
 
107
93
  async function makeBundle(
108
- profile: CryptoProfile,
109
94
  includeOtk = false,
110
95
  ): Promise<{ device: Device; keyBundle: KeyBundle }> {
111
- setCryptoProfile(profile);
112
96
  const signKeys = await xSignKeyPairAsync();
113
- const identityPublic =
114
- profile === "fips"
115
- ? (await xEcdhKeyPairFromEcdsaKeyPairAsync(signKeys)).publicKey
116
- : signKeys.publicKey;
117
97
  const device: Device = {
118
98
  deleted: false,
119
99
  deviceID: "device-a",
@@ -124,19 +104,12 @@ async function makeBundle(
124
104
  };
125
105
 
126
106
  const keyBundle: KeyBundle = {
127
- preKey: await makeBundleEntry(
128
- signKeys,
129
- profile,
130
- device.deviceID,
131
- 1,
132
- "signed",
133
- ),
134
- signKey: identityPublic,
107
+ preKey: await makeBundleEntry(signKeys, device.deviceID, 1, "signed"),
108
+ signKey: signKeys.publicKey,
135
109
  };
136
110
  if (includeOtk) {
137
111
  keyBundle.otk = await makeBundleEntry(
138
112
  signKeys,
139
- profile,
140
113
  device.deviceID,
141
114
  2,
142
115
  "one-time",
@@ -148,13 +121,12 @@ async function makeBundle(
148
121
 
149
122
  async function makeBundleEntry(
150
123
  signKeys: KeyPair,
151
- profile: CryptoProfile,
152
124
  deviceID: string,
153
125
  index: number,
154
126
  kind: "one-time" | "signed",
155
127
  ): Promise<KeyBundleEntry> {
156
128
  const preKey = await xBoxKeyPairAsync();
157
- const payload = xPreKeySignaturePayload(preKey.publicKey, kind, profile);
129
+ const payload = xPreKeySignaturePayload(preKey.publicKey, kind);
158
130
  return {
159
131
  deviceID,
160
132
  index,
@@ -16,14 +16,10 @@ import type { PlatformPreset } from "./common.js";
16
16
  export function nodePreset(): PlatformPreset {
17
17
  return {
18
18
  async createStorage(dbName, privateKey): Promise<Storage> {
19
- const { getCryptoProfile } = await import("@vex-chat/crypto");
20
19
  const { createNodeStorage } = await import("../storage/node.js");
21
20
  const { resolveAtRestAesKeyFromSignKeyHex } =
22
21
  await import("../utils/resolveAtRestAesKey.js");
23
- const atRest = await resolveAtRestAesKeyFromSignKeyHex(
24
- privateKey,
25
- getCryptoProfile(),
26
- );
22
+ const atRest = await resolveAtRestAesKeyFromSignKeyHex(privateKey);
27
23
  return createNodeStorage(dbName, atRest);
28
24
  },
29
25
  deviceName: process.platform,
@@ -15,15 +15,11 @@ import type { PlatformPreset } from "./common.js";
15
15
  export function testPreset(): PlatformPreset {
16
16
  return {
17
17
  async createStorage(_dbName, privateKey) {
18
- const { getCryptoProfile } = await import("@vex-chat/crypto");
19
18
  const { resolveAtRestAesKeyFromSignKeyHex } =
20
19
  await import("../utils/resolveAtRestAesKey.js");
21
20
  const { MemoryStorage } =
22
21
  await import("../__tests__/harness/memory-storage.js");
23
- const atRest = await resolveAtRestAesKeyFromSignKeyHex(
24
- privateKey,
25
- getCryptoProfile(),
26
- );
22
+ const atRest = await resolveAtRestAesKeyFromSignKeyHex(privateKey);
27
23
  const storage = new MemoryStorage(atRest);
28
24
  await storage.init();
29
25
  return storage;
@@ -37,13 +37,9 @@ import type { Device, PreKeysSQL, SessionSQL } from "@vex-chat/types";
37
37
  * smaller “crypto state” store separate from bulk message history.
38
38
  */
39
39
  import {
40
- getCryptoProfile,
41
40
  xBoxKeyPairFromSecret,
42
- xBoxKeyPairFromSecretAsync,
43
41
  xMakeNonce,
44
- xSecretbox,
45
42
  xSecretboxAsync,
46
- xSecretboxOpen,
47
43
  xSecretboxOpenAsync,
48
44
  XUtils,
49
45
  } from "@vex-chat/crypto";
@@ -223,10 +219,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
223
219
  const rawSk = await this.unsealHex(otkInfo.privateKey);
224
220
  return {
225
221
  index: otkInfo.index,
226
- keyPair:
227
- getCryptoProfile() === "fips"
228
- ? await xBoxKeyPairFromSecretAsync(XUtils.decodeHex(rawSk))
229
- : xBoxKeyPairFromSecret(XUtils.decodeHex(rawSk)),
222
+ keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(rawSk)),
230
223
  signature: XUtils.decodeHex(otkInfo.signature),
231
224
  };
232
225
  }
@@ -246,10 +239,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
246
239
  const rawPk = await this.unsealHex(preKeyInfo.privateKey);
247
240
  return {
248
241
  index: preKeyInfo.index,
249
- keyPair:
250
- getCryptoProfile() === "fips"
251
- ? await xBoxKeyPairFromSecretAsync(XUtils.decodeHex(rawPk))
252
- : xBoxKeyPairFromSecret(XUtils.decodeHex(rawPk)),
242
+ keyPair: xBoxKeyPairFromSecret(XUtils.decodeHex(rawPk)),
253
243
  signature: XUtils.decodeHex(preKeyInfo.signature),
254
244
  };
255
245
  }
@@ -947,10 +937,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
947
937
 
948
938
  private async sealBytes(plaintext: Uint8Array): Promise<string> {
949
939
  const nonce = xMakeNonce();
950
- const fips = getCryptoProfile() === "fips";
951
- const ct = fips
952
- ? await xSecretboxAsync(plaintext, nonce, this.atRestAesKey)
953
- : xSecretbox(plaintext, nonce, this.atRestAesKey);
940
+ const ct = await xSecretboxAsync(plaintext, nonce, this.atRestAesKey);
954
941
  const sealed = new Uint8Array(nonce.length + ct.length);
955
942
  sealed.set(nonce);
956
943
  sealed.set(ct, nonce.length);
@@ -1038,10 +1025,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
1038
1025
  }
1039
1026
  const nonce = bytes.slice(0, STORAGE_NONCE_BYTES);
1040
1027
  const ct = bytes.slice(STORAGE_NONCE_BYTES);
1041
- const fips = getCryptoProfile() === "fips";
1042
- const plain = fips
1043
- ? await xSecretboxOpenAsync(ct, nonce, this.atRestAesKey)
1044
- : xSecretboxOpen(ct, nonce, this.atRestAesKey);
1028
+ const plain = await xSecretboxOpenAsync(ct, nonce, this.atRestAesKey);
1045
1029
  if (!plain) {
1046
1030
  throw new Error("Failed to decrypt sealed column value.");
1047
1031
  }
@@ -4,36 +4,20 @@
4
4
  * Commercial licenses available at vex.wtf
5
5
  */
6
6
 
7
- import {
8
- type CryptoProfile,
9
- xEcdhKeyPairFromEcdsaKeyPairAsync,
10
- XKeyConvert,
11
- xSignKeyPairFromSecret,
12
- xSignKeyPairFromSecretAsync,
13
- XUtils,
14
- } from "@vex-chat/crypto";
7
+ import { XKeyConvert, xSignKeyPairFromSecret, XUtils } from "@vex-chat/crypto";
15
8
 
16
9
  /**
17
10
  * Produces the 32-byte at-rest key used by {@link Storage} (sqlite, memory) from
18
11
  * a hex signing secret, matching the derivation used by {@link Client.create}.
19
- * `setCryptoProfile(profile)` should already be in effect (or `tweetnacl` implied).
20
12
  */
21
- export async function resolveAtRestAesKeyFromSignKeyHex(
13
+ export function resolveAtRestAesKeyFromSignKeyHex(
22
14
  privateKeyHex: string,
23
- profile: CryptoProfile,
24
15
  ): Promise<Uint8Array> {
25
16
  const dec = XUtils.decodeHex(privateKeyHex);
26
- if (profile === "tweetnacl") {
27
- const sign = xSignKeyPairFromSecret(dec);
28
- const id = XKeyConvert.convertKeyPair(sign);
29
- if (!id) {
30
- throw new Error(
31
- "Could not convert signing key to X25519 identity.",
32
- );
33
- }
34
- return XUtils.deriveLocalAtRestAesKey(id.secretKey, "tweetnacl");
17
+ const sign = xSignKeyPairFromSecret(dec);
18
+ const id = XKeyConvert.convertKeyPair(sign);
19
+ if (!id) {
20
+ throw new Error("Could not convert signing key to X25519 identity.");
35
21
  }
36
- const sign = await xSignKeyPairFromSecretAsync(dec);
37
- const id = await xEcdhKeyPairFromEcdsaKeyPairAsync(sign);
38
- return XUtils.deriveLocalAtRestAesKey(id.secretKey, "fips");
22
+ return Promise.resolve(XUtils.deriveLocalAtRestAesKey(id.secretKey));
39
23
  }