@vex-chat/libvex 8.0.0 → 9.1.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.
@@ -10,9 +10,8 @@ import type { Device, KeyBundle, KeyBundleEntry } from "@vex-chat/types";
10
10
  import {
11
11
  setCryptoProfile,
12
12
  xBoxKeyPairAsync,
13
- xConstants,
14
13
  xEcdhKeyPairFromEcdsaKeyPairAsync,
15
- xEncode,
14
+ xPreKeySignaturePayload,
16
15
  xSignAsync,
17
16
  xSignKeyPairAsync,
18
17
  XUtils,
@@ -20,7 +19,6 @@ import {
20
19
 
21
20
  import { afterEach, describe, expect, it } from "vitest";
22
21
 
23
- import { fipsP256PreKeySignPayload } from "../utils/fipsMailExtra.js";
24
22
  import { verifyKeyBundleSignatures } from "../utils/verifyKeyBundle.js";
25
23
 
26
24
  describe.sequential("verifyKeyBundleSignatures", () => {
@@ -126,7 +124,13 @@ async function makeBundle(
126
124
  };
127
125
 
128
126
  const keyBundle: KeyBundle = {
129
- preKey: await makeBundleEntry(signKeys, profile, device.deviceID, 1),
127
+ preKey: await makeBundleEntry(
128
+ signKeys,
129
+ profile,
130
+ device.deviceID,
131
+ 1,
132
+ "signed",
133
+ ),
130
134
  signKey: identityPublic,
131
135
  };
132
136
  if (includeOtk) {
@@ -135,6 +139,7 @@ async function makeBundle(
135
139
  profile,
136
140
  device.deviceID,
137
141
  2,
142
+ "one-time",
138
143
  );
139
144
  }
140
145
 
@@ -146,12 +151,10 @@ async function makeBundleEntry(
146
151
  profile: CryptoProfile,
147
152
  deviceID: string,
148
153
  index: number,
154
+ kind: "one-time" | "signed",
149
155
  ): Promise<KeyBundleEntry> {
150
156
  const preKey = await xBoxKeyPairAsync();
151
- const payload =
152
- profile === "fips"
153
- ? fipsP256PreKeySignPayload(preKey.publicKey)
154
- : xEncode(xConstants.CURVE, preKey.publicKey);
157
+ const payload = xPreKeySignaturePayload(preKey.publicKey, kind, profile);
155
158
  return {
156
159
  deviceID,
157
160
  index,
package/src/codecs.ts CHANGED
@@ -93,11 +93,7 @@ export const RegisterPendingApprovalCodec = createCodec(
93
93
  expiresAt: z.string(),
94
94
  requestID: z.string(),
95
95
  status: z.literal("pending_approval"),
96
- // Optional for backward compat with older servers that don't
97
- // surface the existing user's ID in this response. When present,
98
- // clients can use it to fetch the unauthenticated avatar and
99
- // show an "is this you?" confirmation before proceeding.
100
- userID: z.string().optional(),
96
+ userID: z.string(),
101
97
  }),
102
98
  );
103
99
 
@@ -116,6 +112,7 @@ export const DeviceRegistrationResultCodec = createCodec(
116
112
  expiresAt: z.string(),
117
113
  requestID: z.string(),
118
114
  status: z.literal("pending_approval"),
115
+ userID: z.string(),
119
116
  }),
120
117
  ]),
121
118
  );
package/src/http.ts CHANGED
@@ -148,7 +148,7 @@ export class FetchHttpClient {
148
148
  maybeReportUploadProgress(data, config.onUploadProgress);
149
149
 
150
150
  const requestRecord: HttpRequestRecord = {
151
- headers: headersToRecord(headers),
151
+ headers: headersToSafeRecord(headers),
152
152
  method,
153
153
  url,
154
154
  };
@@ -393,6 +393,21 @@ function headersToRecord(headers: Headers): Record<string, string> {
393
393
  return out;
394
394
  }
395
395
 
396
+ function headersToSafeRecord(headers: Headers): Record<string, string> {
397
+ const sensitive = new Set([
398
+ "authorization",
399
+ "cookie",
400
+ "proxy-authorization",
401
+ "x-api-key",
402
+ "x-device-token",
403
+ ]);
404
+ const out: Record<string, string> = {};
405
+ headers.forEach((value, key) => {
406
+ out[key] = sensitive.has(key.toLowerCase()) ? "[REDACTED]" : value;
407
+ });
408
+ return out;
409
+ }
410
+
396
411
  function isFormDataValue(value: unknown): value is FormData {
397
412
  return typeof FormData !== "undefined" && value instanceof FormData;
398
413
  }
@@ -15,7 +15,7 @@ import * as path from "node:path";
15
15
  * Stores credentials as encrypted files on disk using XUtils.encryptKeyData.
16
16
  * Node-only — imports node:fs.
17
17
  */
18
- import { getCryptoProfile, XUtils } from "@vex-chat/crypto";
18
+ import { XUtils } from "@vex-chat/crypto";
19
19
 
20
20
  export class NodeKeyStore implements KeyStore {
21
21
  private readonly dir: string;
@@ -65,16 +65,18 @@ export class NodeKeyStore implements KeyStore {
65
65
  }
66
66
 
67
67
  async save(creds: StoredCredentials): Promise<void> {
68
- const data = JSON.stringify(creds);
69
- const encrypted =
70
- getCryptoProfile() === "fips"
71
- ? await XUtils.encryptKeyDataAsync(this.passphrase, data)
72
- : XUtils.encryptKeyData(this.passphrase, data);
73
- fs.writeFileSync(this.filePath(creds.username), encrypted);
68
+ const data = XUtils.encodeHex(XUtils.decodeUTF8(JSON.stringify(creds)));
69
+ const encrypted = await XUtils.encryptKeyDataAsync(
70
+ this.passphrase,
71
+ data,
72
+ );
73
+ const filePath = this.filePath(creds.username);
74
+ fs.writeFileSync(filePath, encrypted, { mode: 0o600 });
75
+ fs.chmodSync(filePath, 0o600);
74
76
  }
75
77
 
76
78
  private filePath(username: string): string {
77
- return path.join(this.dir, `${username}.vex`);
79
+ return path.join(this.dir, `${encodeURIComponent(username)}.vex`);
78
80
  }
79
81
 
80
82
  private async readFile(
@@ -82,17 +84,13 @@ export class NodeKeyStore implements KeyStore {
82
84
  ): Promise<null | StoredCredentials> {
83
85
  try {
84
86
  const data = fs.readFileSync(filePath);
85
- const decrypted =
86
- getCryptoProfile() === "fips"
87
- ? await XUtils.decryptKeyDataAsync(
88
- new Uint8Array(data),
89
- this.passphrase,
90
- )
91
- : XUtils.decryptKeyData(
92
- new Uint8Array(data),
93
- this.passphrase,
94
- );
95
- const parsed: unknown = JSON.parse(decrypted);
87
+ const decrypted = await XUtils.decryptKeyDataAsync(
88
+ new Uint8Array(data),
89
+ this.passphrase,
90
+ );
91
+ const parsed: unknown = JSON.parse(
92
+ XUtils.encodeUTF8(XUtils.decodeHex(decrypted)),
93
+ );
96
94
  if (isStoredCredentials(parsed)) {
97
95
  return parsed;
98
96
  }
@@ -55,6 +55,10 @@ import { effectiveMessageRetentionHintDays } from "../retention.js";
55
55
  import { parseSkippedKeysStrict } from "../utils/ratchet.js";
56
56
 
57
57
  const STORAGE_MESSAGE_BLOB_PREFIX = "vex-storage-message:1\n";
58
+ const STORAGE_CIPHERTEXT_PREFIX = "vex-storage-cipher:2:";
59
+ const STORAGE_SECRET_TEXT_PREFIX = "vex-storage-secret:1:";
60
+ const STORAGE_NONCE_BYTES = 24;
61
+ const STORAGE_MAC_BYTES = 16;
58
62
 
59
63
  export class SqliteStorage extends EventEmitter implements Storage {
60
64
  public ready = false;
@@ -408,10 +412,10 @@ export class SqliteStorage extends EventEmitter implements Storage {
408
412
  this.ready = true;
409
413
  this.emit("ready");
410
414
  } catch (err: unknown) {
411
- this.emit(
412
- "error",
413
- err instanceof Error ? err : new Error(String(err)),
414
- );
415
+ const error =
416
+ err instanceof Error ? err : new Error(String(err));
417
+ this.emit("error", error);
418
+ throw error;
415
419
  } finally {
416
420
  this.initInFlight = null;
417
421
  }
@@ -526,14 +530,25 @@ export class SqliteStorage extends EventEmitter implements Storage {
526
530
  return;
527
531
  }
528
532
  await this.untilReady();
533
+ if (this.isClosingNow()) {
534
+ return;
535
+ }
529
536
 
530
537
  // Fan-out to multiple devices reuses one `mailID` but each encrypt path
531
538
  // uses a fresh nonce (table PK). Keep a single local row per logical mail.
532
- const dupe = await this.db
533
- .selectFrom("messages")
534
- .select("nonce")
535
- .where("mailID", "=", message.mailID)
536
- .executeTakeFirst();
539
+ let dupe: undefined | { nonce: string };
540
+ try {
541
+ dupe = await this.db
542
+ .selectFrom("messages")
543
+ .select("nonce")
544
+ .where("mailID", "=", message.mailID)
545
+ .executeTakeFirst();
546
+ } catch (err: unknown) {
547
+ if (this.isClosingNow() || this.isTornDownError(err)) {
548
+ return;
549
+ }
550
+ throw err;
551
+ }
537
552
  if (dupe !== undefined) {
538
553
  return;
539
554
  }
@@ -543,22 +558,9 @@ export class SqliteStorage extends EventEmitter implements Storage {
543
558
  !message.decrypted &&
544
559
  message.message === "" &&
545
560
  message.extra === undefined;
546
- const fips = getCryptoProfile() === "fips";
547
561
  const encryptedMessage = isPlaintextFailurePlaceholder
548
562
  ? storedPlaintext
549
- : XUtils.encodeHex(
550
- fips
551
- ? await xSecretboxAsync(
552
- XUtils.decodeUTF8(storedPlaintext),
553
- XUtils.decodeHex(message.nonce),
554
- this.atRestAesKey,
555
- )
556
- : xSecretbox(
557
- XUtils.decodeUTF8(storedPlaintext),
558
- XUtils.decodeHex(message.nonce),
559
- this.atRestAesKey,
560
- ),
561
- );
563
+ : await this.sealMessagePlaintext(storedPlaintext);
562
564
  if (this.isClosingNow()) {
563
565
  return;
564
566
  }
@@ -656,61 +658,56 @@ export class SqliteStorage extends EventEmitter implements Storage {
656
658
  const sealedDHsPrivate = await this.sealHex(session.DHsPrivate);
657
659
  const sealedRK = await this.sealHex(session.RK);
658
660
  const sealedSK = await this.sealHex(session.SK);
659
- try {
660
- await this.db
661
- .insertInto("sessions")
662
- .values({
663
- CKr: sealedCKr,
664
- CKs: sealedCKs,
665
- deviceID: session.deviceID,
666
- DHr: session.DHr,
667
- DHsPrivate: sealedDHsPrivate,
668
- DHsPublic: session.DHsPublic,
669
- fingerprint: session.fingerprint,
670
- lastUsed: session.lastUsed,
671
- mode: session.mode,
672
- Nr: session.Nr,
673
- Ns: session.Ns,
674
- PN: session.PN,
675
- publicKey: session.publicKey,
676
- RK: sealedRK,
677
- sessionID: session.sessionID,
678
- SK: sealedSK,
679
- skippedKeys: session.skippedKeys,
680
- userID: session.userID,
681
- verified: session.verified ? 1 : 0,
682
- })
683
- .execute();
684
- } catch (err: unknown) {
685
- if (this.isDuplicateError(err)) {
686
- await this.db
687
- .updateTable("sessions")
688
- .set({
689
- CKr: sealedCKr,
690
- CKs: sealedCKs,
691
- deviceID: session.deviceID,
692
- DHr: session.DHr,
693
- DHsPrivate: sealedDHsPrivate,
694
- DHsPublic: session.DHsPublic,
695
- fingerprint: session.fingerprint,
696
- lastUsed: session.lastUsed,
697
- mode: session.mode,
698
- Nr: session.Nr,
699
- Ns: session.Ns,
700
- PN: session.PN,
701
- publicKey: session.publicKey,
702
- RK: sealedRK,
703
- SK: sealedSK,
704
- skippedKeys: session.skippedKeys,
705
- userID: session.userID,
706
- verified: session.verified ? 1 : 0,
707
- })
708
- .where("sessionID", "=", session.sessionID)
709
- .execute();
710
- } else {
711
- throw err;
712
- }
713
- }
661
+ const sealedSkippedKeys = await this.sealSecretText(
662
+ session.skippedKeys,
663
+ );
664
+ const values = {
665
+ CKr: sealedCKr,
666
+ CKs: sealedCKs,
667
+ deviceID: session.deviceID,
668
+ DHr: session.DHr,
669
+ DHsPrivate: sealedDHsPrivate,
670
+ DHsPublic: session.DHsPublic,
671
+ fingerprint: session.fingerprint,
672
+ lastUsed: session.lastUsed,
673
+ mode: session.mode,
674
+ Nr: session.Nr,
675
+ Ns: session.Ns,
676
+ PN: session.PN,
677
+ publicKey: session.publicKey,
678
+ RK: sealedRK,
679
+ sessionID: session.sessionID,
680
+ SK: sealedSK,
681
+ skippedKeys: sealedSkippedKeys,
682
+ userID: session.userID,
683
+ verified: session.verified ? 1 : 0,
684
+ };
685
+ await this.db
686
+ .insertInto("sessions")
687
+ .values(values)
688
+ .onConflict((conflict) =>
689
+ conflict.column("sessionID").doUpdateSet({
690
+ CKr: values.CKr,
691
+ CKs: values.CKs,
692
+ deviceID: values.deviceID,
693
+ DHr: values.DHr,
694
+ DHsPrivate: values.DHsPrivate,
695
+ DHsPublic: values.DHsPublic,
696
+ fingerprint: values.fingerprint,
697
+ lastUsed: values.lastUsed,
698
+ mode: values.mode,
699
+ Nr: values.Nr,
700
+ Ns: values.Ns,
701
+ PN: values.PN,
702
+ publicKey: values.publicKey,
703
+ RK: values.RK,
704
+ SK: values.SK,
705
+ skippedKeys: values.skippedKeys,
706
+ userID: values.userID,
707
+ verified: values.verified,
708
+ }),
709
+ )
710
+ .execute();
714
711
  }
715
712
 
716
713
  async updateMessage(
@@ -749,18 +746,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
749
746
  : {}),
750
747
  };
751
748
  const storedPlaintext = encodeStoredMessagePlaintext(next);
752
- const fips = getCryptoProfile() === "fips";
753
- const ct = fips
754
- ? await xSecretboxAsync(
755
- XUtils.decodeUTF8(storedPlaintext),
756
- XUtils.decodeHex(row.nonce),
757
- this.atRestAesKey,
758
- )
759
- : xSecretbox(
760
- XUtils.decodeUTF8(storedPlaintext),
761
- XUtils.decodeHex(row.nonce),
762
- this.atRestAesKey,
763
- );
749
+ const sealedMessage = await this.sealMessagePlaintext(storedPlaintext);
764
750
  if (this.isClosingNow()) {
765
751
  return false;
766
752
  }
@@ -768,7 +754,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
768
754
  .updateTable("messages")
769
755
  .set({
770
756
  extra: null,
771
- message: XUtils.encodeHex(ct),
757
+ message: sealedMessage,
772
758
  })
773
759
  .where("mailID", "=", mailID)
774
760
  .executeTakeFirst();
@@ -780,7 +766,6 @@ export class SqliteStorage extends EventEmitter implements Storage {
780
766
  private async decryptMessagesAsync(
781
767
  messages: MessageRow[],
782
768
  ): Promise<Message[]> {
783
- const fips = getCryptoProfile() === "fips";
784
769
  const out: Message[] = [];
785
770
  let processed = 0;
786
771
  /** Yield so RN / web UIs can paint between at-rest decrypt blocks. */
@@ -796,20 +781,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
796
781
  const isPlaintextFailurePlaceholder =
797
782
  !decryptedFlag && msg.message === "" && msg.extra === null;
798
783
  if (!isPlaintextFailurePlaceholder) {
799
- const cipher = XUtils.decodeHex(msg.message);
800
- const nonce = XUtils.decodeHex(msg.nonce);
801
- const decrypted = fips
802
- ? await xSecretboxOpenAsync(
803
- cipher,
804
- nonce,
805
- this.atRestAesKey,
806
- )
807
- : xSecretboxOpen(cipher, nonce, this.atRestAesKey);
808
- if (decrypted) {
809
- plaintext = XUtils.encodeUTF8(decrypted);
810
- } else {
811
- throw new Error("Couldn't decrypt messages on disk!");
812
- }
784
+ plaintext = await this.openMessagePlaintext(msg.message);
813
785
  }
814
786
  const direction =
815
787
  msg.direction === "incoming" ? "incoming" : "outgoing";
@@ -930,8 +902,14 @@ export class SqliteStorage extends EventEmitter implements Storage {
930
902
  }
931
903
 
932
904
  private isDuplicateError(err: unknown): boolean {
933
- if (err instanceof Error) {
934
- return err.message.includes("UNIQUE");
905
+ const message =
906
+ err instanceof Error
907
+ ? err.message
908
+ : typeof err === "string"
909
+ ? err
910
+ : "";
911
+ if (message.toUpperCase().includes("UNIQUE")) {
912
+ return true;
935
913
  }
936
914
  if (typeof err === "object" && err !== null && "errno" in err) {
937
915
  return err.errno === 19;
@@ -955,26 +933,52 @@ export class SqliteStorage extends EventEmitter implements Storage {
955
933
  return false;
956
934
  }
957
935
 
958
- /**
959
- * Encrypt a hex-encoded secret for at-rest storage.
960
- * Returns hex(nonce || ciphertext) where nonce is 24 random bytes.
961
- */
962
- private async sealHex(plainHex: string): Promise<string> {
936
+ private async openMessagePlaintext(stored: string): Promise<string> {
937
+ if (!stored.startsWith(STORAGE_CIPHERTEXT_PREFIX)) {
938
+ throw new Error(
939
+ "Stored message is missing its encryption envelope.",
940
+ );
941
+ }
942
+ const plain = await this.unsealBytes(
943
+ stored.slice(STORAGE_CIPHERTEXT_PREFIX.length),
944
+ );
945
+ return XUtils.encodeUTF8(plain);
946
+ }
947
+
948
+ private async sealBytes(plaintext: Uint8Array): Promise<string> {
963
949
  const nonce = xMakeNonce();
964
950
  const fips = getCryptoProfile() === "fips";
965
951
  const ct = fips
966
- ? await xSecretboxAsync(
967
- XUtils.decodeHex(plainHex),
968
- nonce,
969
- this.atRestAesKey,
970
- )
971
- : xSecretbox(XUtils.decodeHex(plainHex), nonce, this.atRestAesKey);
952
+ ? await xSecretboxAsync(plaintext, nonce, this.atRestAesKey)
953
+ : xSecretbox(plaintext, nonce, this.atRestAesKey);
972
954
  const sealed = new Uint8Array(nonce.length + ct.length);
973
955
  sealed.set(nonce);
974
956
  sealed.set(ct, nonce.length);
975
957
  return XUtils.encodeHex(sealed);
976
958
  }
977
959
 
960
+ /**
961
+ * Encrypt a hex-encoded secret for at-rest storage.
962
+ * Returns hex(nonce || ciphertext) where nonce is 24 random bytes.
963
+ */
964
+ private async sealHex(plainHex: string): Promise<string> {
965
+ return this.sealBytes(XUtils.decodeHex(plainHex));
966
+ }
967
+
968
+ private async sealMessagePlaintext(plaintext: string): Promise<string> {
969
+ return (
970
+ STORAGE_CIPHERTEXT_PREFIX +
971
+ (await this.sealBytes(XUtils.decodeUTF8(plaintext)))
972
+ );
973
+ }
974
+
975
+ private async sealSecretText(plaintext: string): Promise<string> {
976
+ return (
977
+ STORAGE_SECRET_TEXT_PREFIX +
978
+ (await this.sealBytes(XUtils.decodeUTF8(plaintext)))
979
+ );
980
+ }
981
+
978
982
  private async sessionRowToSQLAsync(row: SessionRow): Promise<SessionSQL> {
979
983
  const rawSK = await this.unsealHex(row.SK);
980
984
  const rawRK = row.RK ? await this.unsealHex(row.RK) : rawSK;
@@ -997,7 +1001,7 @@ export class SqliteStorage extends EventEmitter implements Storage {
997
1001
  RK: rawRK,
998
1002
  sessionID: row.sessionID,
999
1003
  SK: rawSK,
1000
- skippedKeys: row.skippedKeys,
1004
+ skippedKeys: await this.unsealSecretText(row.skippedKeys),
1001
1005
  userID: row.userID,
1002
1006
  verified: row.verified !== 0,
1003
1007
  };
@@ -1027,14 +1031,13 @@ export class SqliteStorage extends EventEmitter implements Storage {
1027
1031
  };
1028
1032
  }
1029
1033
 
1030
- /**
1031
- * Decrypt a value produced by sealHex().
1032
- * Expects hex(nonce || ciphertext), returns the original hex string.
1033
- */
1034
- private async unsealHex(sealed: string): Promise<string> {
1034
+ private async unsealBytes(sealed: string): Promise<Uint8Array> {
1035
1035
  const bytes = XUtils.decodeHex(sealed);
1036
- const nonce = bytes.slice(0, 24);
1037
- const ct = bytes.slice(24);
1036
+ if (bytes.length < STORAGE_NONCE_BYTES + STORAGE_MAC_BYTES) {
1037
+ throw new Error("Sealed column value is truncated.");
1038
+ }
1039
+ const nonce = bytes.slice(0, STORAGE_NONCE_BYTES);
1040
+ const ct = bytes.slice(STORAGE_NONCE_BYTES);
1038
1041
  const fips = getCryptoProfile() === "fips";
1039
1042
  const plain = fips
1040
1043
  ? await xSecretboxOpenAsync(ct, nonce, this.atRestAesKey)
@@ -1042,21 +1045,41 @@ export class SqliteStorage extends EventEmitter implements Storage {
1042
1045
  if (!plain) {
1043
1046
  throw new Error("Failed to decrypt sealed column value.");
1044
1047
  }
1045
- return XUtils.encodeHex(plain);
1048
+ return plain;
1049
+ }
1050
+
1051
+ /**
1052
+ * Decrypt a value produced by sealHex().
1053
+ * Expects hex(nonce || ciphertext), returns the original hex string.
1054
+ */
1055
+ private async unsealHex(sealed: string): Promise<string> {
1056
+ return XUtils.encodeHex(await this.unsealBytes(sealed));
1057
+ }
1058
+
1059
+ private async unsealSecretText(stored: string): Promise<string> {
1060
+ if (!stored.startsWith(STORAGE_SECRET_TEXT_PREFIX)) {
1061
+ throw new Error(
1062
+ "Stored secret is missing its encryption envelope.",
1063
+ );
1064
+ }
1065
+ const plain = await this.unsealBytes(
1066
+ stored.slice(STORAGE_SECRET_TEXT_PREFIX.length),
1067
+ );
1068
+ return XUtils.encodeUTF8(plain);
1046
1069
  }
1047
1070
 
1048
1071
  private async untilReady(): Promise<void> {
1049
- if (this.ready) return;
1050
- return new Promise((resolve) => {
1051
- const check = () => {
1052
- if (this.ready) {
1053
- resolve();
1054
- return;
1055
- }
1056
- setTimeout(check, 10);
1057
- };
1058
- check();
1059
- });
1072
+ if (this.closing) {
1073
+ throw new Error("SqliteStorage is closed.");
1074
+ }
1075
+ if (this.ready) {
1076
+ return;
1077
+ }
1078
+ if (this.initInFlight) {
1079
+ await this.initInFlight;
1080
+ } else {
1081
+ await this.init();
1082
+ }
1060
1083
  }
1061
1084
  }
1062
1085
 
@@ -9,14 +9,11 @@ import type { Device, KeyBundle, KeyBundleEntry } from "@vex-chat/types";
9
9
 
10
10
  import {
11
11
  fipsEcdhRawPublicKeyFromEcdsaSpkiAsync,
12
- xConstants,
13
- xEncode,
12
+ xPreKeySignaturePayload,
14
13
  xSignOpenAsync,
15
14
  XUtils,
16
15
  } from "@vex-chat/crypto";
17
16
 
18
- import { fipsP256PreKeySignPayload } from "./fipsMailExtra.js";
19
-
20
17
  export async function verifyKeyBundleSignatures(
21
18
  keyBundle: KeyBundle,
22
19
  device: Device,
@@ -37,7 +34,7 @@ export async function verifyKeyBundleSignatures(
37
34
  device,
38
35
  deviceSignKey,
39
36
  cryptoProfile,
40
- "signed prekey",
37
+ "signed",
41
38
  );
42
39
 
43
40
  if (keyBundle.otk) {
@@ -46,7 +43,7 @@ export async function verifyKeyBundleSignatures(
46
43
  device,
47
44
  deviceSignKey,
48
45
  cryptoProfile,
49
- "one-time prekey",
46
+ "one-time",
50
47
  );
51
48
  }
52
49
  }
@@ -56,19 +53,22 @@ async function verifyKeyBundleEntrySignature(
56
53
  device: Device,
57
54
  deviceSignKey: Uint8Array,
58
55
  cryptoProfile: CryptoProfile,
59
- label: string,
56
+ kind: "one-time" | "signed",
60
57
  ): Promise<void> {
61
58
  if (entry.deviceID !== device.deviceID) {
62
- throw new Error(`Key bundle ${label} belongs to a different device.`);
59
+ throw new Error(
60
+ `Key bundle ${kind} prekey belongs to a different device.`,
61
+ );
63
62
  }
64
63
 
65
- const payload =
66
- cryptoProfile === "fips"
67
- ? fipsP256PreKeySignPayload(entry.publicKey)
68
- : xEncode(xConstants.CURVE, entry.publicKey);
64
+ const payload = xPreKeySignaturePayload(
65
+ entry.publicKey,
66
+ kind,
67
+ cryptoProfile,
68
+ );
69
69
  const opened = await xSignOpenAsync(entry.signature, deviceSignKey);
70
70
 
71
71
  if (!opened || !XUtils.bytesEqual(opened, payload)) {
72
- throw new Error(`Key bundle ${label} signature is invalid.`);
72
+ throw new Error(`Key bundle ${kind} prekey signature is invalid.`);
73
73
  }
74
74
  }