@vex-chat/libvex 7.4.0 → 9.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.
@@ -79,11 +79,46 @@ export function platformSuite(
79
79
  expect(true).toBe(true);
80
80
  });
81
81
 
82
- test("send and receive DM (self)", async () => {
82
+ test("existing-account device request requires password", async () => {
83
+ const SK2 = await e2eGenerateSecretKey();
84
+ const opts2: ClientOptions = e2eClientOptionsBase();
85
+ const storage2 = await makeStorage(SK2, opts2);
86
+ const client2 = await Client.create(SK2, opts2, storage2);
87
+
88
+ try {
89
+ const [duplicateUser, duplicateErr] = await client2.register(
90
+ username,
91
+ password,
92
+ );
93
+ expect(duplicateUser).toBeNull();
94
+ expect(duplicateErr?.message).toContain(
95
+ "Username is already registered. Sign in instead.",
96
+ );
97
+
98
+ const [missingPasswordUser, missingPasswordErr] =
99
+ await client2.requestDeviceEnrollmentWithPasskey(username);
100
+ expect(missingPasswordUser).toBeNull();
101
+ expect(missingPasswordErr?.message).toContain(
102
+ "Password is required to add this device.",
103
+ );
104
+
105
+ const [pendingUser, pendingErr] =
106
+ await client2.requestDeviceEnrollment(username, password);
107
+ expect(pendingUser).toBeNull();
108
+ expect(pendingErr?.name).toBe("DeviceApprovalRequiredError");
109
+ expect(
110
+ (pendingErr as null | { requestID?: unknown })?.requestID,
111
+ ).toEqual(expect.any(String));
112
+ } finally {
113
+ await client2.close().catch(() => {});
114
+ }
115
+ });
116
+
117
+ test("send self-DM without duplicating it as incoming", async () => {
83
118
  const me = client.me.user();
84
119
  const msgPromise = waitForMessage(
85
120
  client,
86
- (m) => m.direction === "incoming" && m.decrypted,
121
+ (m) => m.direction === "outgoing" && m.decrypted,
87
122
  `[${platformName}] self-DM`,
88
123
  );
89
124
  await client.messages.send(me.userID, "platform-test");
@@ -91,7 +126,7 @@ export function platformSuite(
91
126
  expect(msg.message).toBe("platform-test");
92
127
  });
93
128
 
94
- test("send and receive DM with encrypted extra", async () => {
129
+ test("send self-DM with encrypted extra", async () => {
95
130
  const me = client.me.user();
96
131
  const extra = JSON.stringify({
97
132
  reactionEvent: {
@@ -104,7 +139,7 @@ export function platformSuite(
104
139
  const msgPromise = waitForMessage(
105
140
  client,
106
141
  (m) =>
107
- m.direction === "incoming" &&
142
+ m.direction === "outgoing" &&
108
143
  m.decrypted &&
109
144
  m.extra === extra,
110
145
  `[${platformName}] self-DM extra`,
@@ -127,7 +162,7 @@ export function platformSuite(
127
162
  const msgPromise = waitForMessage(
128
163
  client,
129
164
  (m) =>
130
- m.direction === "incoming" &&
165
+ m.direction === "outgoing" &&
131
166
  m.decrypted &&
132
167
  m.message === body,
133
168
  "history DM",
@@ -156,11 +191,14 @@ export function platformSuite(
156
191
  try {
157
192
  const [user2, regErr] = await client2.register(
158
193
  username2,
159
- "test-pw-2",
194
+ "test password two",
160
195
  );
161
196
  expect(regErr).toBeNull();
162
197
 
163
- const loginErr = await client2.login(username2, "test-pw-2");
198
+ const loginErr = await client2.login(
199
+ username2,
200
+ "test password two",
201
+ );
164
202
  expect(loginErr.ok).toBe(true);
165
203
 
166
204
  await connectAndWait(client2, "client2");
@@ -186,14 +224,14 @@ export function platformSuite(
186
224
  const storage1 = await makeStorage(SK1, opts1);
187
225
  const client1 = await Client.create(SK1, opts1, storage1);
188
226
  const username1 = Client.randomUsername();
189
- const password1 = "test-pw-1";
227
+ const password1 = "test password one";
190
228
 
191
229
  const SK2 = await e2eGenerateSecretKey();
192
230
  const opts2: ClientOptions = e2eClientOptionsBase();
193
231
  const storage2 = await makeStorage(SK2, opts2);
194
232
  const client2 = await Client.create(SK2, opts2, storage2);
195
233
  const username2 = Client.randomUsername();
196
- const password2 = "test-pw-2";
234
+ const password2 = "test password two";
197
235
 
198
236
  try {
199
237
  const [_user1, regErr1] = await client1.register(
@@ -264,8 +302,16 @@ export function platformSuite(
264
302
 
265
303
  try {
266
304
  // Register + login + connect user2
267
- await client2.register(username2, "test-pw-2");
268
- await client2.login(username2, "test-pw-2");
305
+ const [_user2, registrationError] = await client2.register(
306
+ username2,
307
+ "test password two",
308
+ );
309
+ expect(registrationError).toBeNull();
310
+ const loginResult = await client2.login(
311
+ username2,
312
+ "test password two",
313
+ );
314
+ expect(loginResult.ok).toBe(true);
269
315
  await connectAndWait(client2, "client2");
270
316
 
271
317
  // user1 creates server + channel
@@ -504,6 +550,31 @@ export function platformSuite(
504
550
  });
505
551
  }
506
552
  });
553
+
554
+ test("changes the account password with current-password proof", async () => {
555
+ const replacementPassword = "updated platform test password";
556
+
557
+ await expect(
558
+ client.me.changePassword(
559
+ "incorrect platform test password",
560
+ replacementPassword,
561
+ ),
562
+ ).rejects.toThrow();
563
+ await client.me.changePassword(password, replacementPassword);
564
+ await client.logout();
565
+
566
+ const oldLogin = await client.login(username, password);
567
+ expect(oldLogin.ok).toBe(false);
568
+ const newLogin = await client.login(username, replacementPassword);
569
+ expect(newLogin.ok).toBe(true);
570
+ });
571
+
572
+ test("logout clears local authentication", async () => {
573
+ await client.logout();
574
+ await expect(client.connect()).rejects.toThrow(
575
+ "No token — call login() or loginWithDeviceKey() first.",
576
+ );
577
+ });
507
578
  });
508
579
  }
509
580
 
@@ -68,6 +68,28 @@ describe("FetchHttpClient", () => {
68
68
  expect(err.response?.data).toBe("plain failure");
69
69
  });
70
70
 
71
+ it("redacts credentials from request metadata on errors", async () => {
72
+ globalThis.fetch = () =>
73
+ Promise.resolve(new Response(null, { status: 401 }));
74
+ const client = createFetchHttpClient();
75
+ client.defaults.headers.common.Authorization =
76
+ "Bearer secret-user-token";
77
+ client.defaults.headers.common["X-Device-Token"] =
78
+ "secret-device-token";
79
+
80
+ const err = await captureError(() =>
81
+ client.get("https://example.test/private"),
82
+ );
83
+ if (!isHttpError(err)) {
84
+ throw err;
85
+ }
86
+
87
+ expect(err.config.headers["authorization"]).toBe("[REDACTED]");
88
+ expect(err.config.headers["x-device-token"]).toBe("[REDACTED]");
89
+ expect(JSON.stringify(err)).not.toContain("secret-user-token");
90
+ expect(JSON.stringify(err)).not.toContain("secret-device-token");
91
+ });
92
+
71
93
  it("emits a final upload progress event for FormData payloads", async () => {
72
94
  globalThis.fetch = () =>
73
95
  Promise.resolve(new Response(new ArrayBuffer(0), { status: 200 }));
@@ -0,0 +1,69 @@
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 { StoredCredentials } from "../types/index.js";
8
+
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+
13
+ import { afterEach, describe, expect, it } from "vitest";
14
+
15
+ import { NodeKeyStore } from "../keystore/node.js";
16
+
17
+ const dirs: string[] = [];
18
+
19
+ afterEach(() => {
20
+ for (const dir of dirs.splice(0)) {
21
+ fs.rmSync(dir, { force: true, recursive: true });
22
+ }
23
+ });
24
+
25
+ describe("NodeKeyStore", () => {
26
+ it("round-trips credentials through the encrypted file", async () => {
27
+ const dir = makeTempDir();
28
+ const store = new NodeKeyStore("correct horse battery staple", dir);
29
+ const credentials: StoredCredentials = {
30
+ deviceID: "device-id",
31
+ deviceKey: "ab".repeat(64),
32
+ preKey: "cd".repeat(32),
33
+ token: "saved-token",
34
+ username: "alice",
35
+ };
36
+
37
+ await store.save(credentials);
38
+
39
+ await expect(store.load("alice")).resolves.toEqual(credentials);
40
+ await expect(store.load()).resolves.toEqual(credentials);
41
+ const raw = fs.readFileSync(path.join(dir, "alice.vex"));
42
+ expect(raw.toString("utf8")).not.toContain("saved-token");
43
+ expect(fs.statSync(path.join(dir, "alice.vex")).mode & 0o777).toBe(
44
+ 0o600,
45
+ );
46
+ });
47
+
48
+ it("keeps usernames inside the configured directory", async () => {
49
+ const dir = makeTempDir();
50
+ const store = new NodeKeyStore("password", dir);
51
+
52
+ await store.save({
53
+ deviceID: "device-id",
54
+ deviceKey: "ab".repeat(64),
55
+ username: "../outside",
56
+ });
57
+
58
+ expect(fs.readdirSync(dir)).toEqual(["..%2Foutside.vex"]);
59
+ await expect(store.load("../outside")).resolves.toMatchObject({
60
+ username: "../outside",
61
+ });
62
+ });
63
+ });
64
+
65
+ function makeTempDir(): string {
66
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "vex-keystore-"));
67
+ dirs.push(dir);
68
+ return dir;
69
+ }
@@ -10,10 +10,9 @@ import type { CryptoProfile, KeyPair } from "@vex-chat/crypto";
10
10
  import {
11
11
  setCryptoProfile,
12
12
  xBoxKeyPairAsync,
13
- xConstants,
14
13
  xEcdhKeyPairFromEcdsaKeyPairAsync,
15
- xEncode,
16
14
  XKeyConvert,
15
+ xPreKeySignaturePayload,
17
16
  xSignAsync,
18
17
  xSignKeyPair,
19
18
  xSignKeyPairAsync,
@@ -24,12 +23,11 @@ import {
24
23
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
25
24
 
26
25
  import { Client } from "../Client.js";
27
- import { fipsP256PreKeySignPayload } from "../utils/fipsMailExtra.js";
28
26
 
29
27
  import { MemoryStorage } from "./harness/memory-storage.js";
30
28
 
31
29
  interface KeyRingHarness {
32
- createPreKey: () => Promise<UnsavedPreKey>;
30
+ createPreKey: (kind: "one-time" | "signed") => Promise<UnsavedPreKey>;
33
31
  cryptoProfile: CryptoProfile;
34
32
  database: MemoryStorage;
35
33
  idKeys: KeyPair;
@@ -41,7 +39,7 @@ interface KeyRingHarness {
41
39
  }
42
40
 
43
41
  const clientMethods = Client.prototype as unknown as {
44
- createPreKey: () => Promise<UnsavedPreKey>;
42
+ createPreKey: (kind: "one-time" | "signed") => Promise<UnsavedPreKey>;
45
43
  isPreKeySignedByCurrentDevice: (preKey: PreKeysCrypto) => Promise<boolean>;
46
44
  populateKeyRing: () => Promise<void>;
47
45
  runWithThisCryptoProfile: <T>(fn: () => Promise<T>) => Promise<T>;
@@ -54,10 +52,11 @@ async function isValidFor(
54
52
  ): Promise<boolean> {
55
53
  setCryptoProfile(profile);
56
54
  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);
55
+ const payload = xPreKeySignaturePayload(
56
+ preKey.keyPair.publicKey,
57
+ "signed",
58
+ profile,
59
+ );
61
60
  return Boolean(opened && XUtils.bytesEqual(opened, payload));
62
61
  }
63
62
 
@@ -67,10 +66,11 @@ async function makeSignedPreKey(
67
66
  ): Promise<UnsavedPreKey> {
68
67
  setCryptoProfile(profile);
69
68
  const keyPair = await xBoxKeyPairAsync();
70
- const payload =
71
- profile === "fips"
72
- ? fipsP256PreKeySignPayload(keyPair.publicKey)
73
- : xEncode(xConstants.CURVE, keyPair.publicKey);
69
+ const payload = xPreKeySignaturePayload(
70
+ keyPair.publicKey,
71
+ "signed",
72
+ profile,
73
+ );
74
74
  return {
75
75
  keyPair,
76
76
  signature: await xSignAsync(payload, signKeys.secretKey),
@@ -1,5 +1,6 @@
1
1
  import type { Message } from "../index.js";
2
2
  import type { ClientDatabase } from "../storage/schema.js";
3
+ import type { SessionSQL } from "@vex-chat/types";
3
4
 
4
5
  import BetterSqlite3 from "better-sqlite3";
5
6
  import { Kysely, SqliteDialect } from "kysely";
@@ -74,6 +75,69 @@ describe("SqliteStorage message at-rest encryption", () => {
74
75
  await storage.close();
75
76
  }
76
77
  });
78
+
79
+ it("uses a fresh local nonce when a stored message is edited", async () => {
80
+ const { db, storage } = makeStorage();
81
+ try {
82
+ await storage.init();
83
+ const message = makeMessage({
84
+ mailID: "editable-mail",
85
+ message: "before",
86
+ nonce: nonceHex(3),
87
+ });
88
+ await storage.saveMessage(message);
89
+
90
+ const before = await messageRow(db, message.mailID);
91
+ expect(before?.message).toMatch(/^vex-storage-cipher:2:/);
92
+ const localNonce = before?.message
93
+ .slice("vex-storage-cipher:2:".length)
94
+ .slice(0, 48);
95
+ expect(localNonce).not.toBe(message.nonce);
96
+
97
+ await expect(
98
+ storage.updateMessage(message.mailID, { message: "after" }),
99
+ ).resolves.toBe(true);
100
+ const after = await messageRow(db, message.mailID);
101
+ expect(after?.message).toMatch(/^vex-storage-cipher:2:/);
102
+ expect(after?.message).not.toBe(before?.message);
103
+ expect(after?.nonce).toBe(message.nonce);
104
+
105
+ const history = await storage.getMessageHistory(message.authorID);
106
+ expect(history).toMatchObject([
107
+ { mailID: message.mailID, message: "after" },
108
+ ]);
109
+ } finally {
110
+ await storage.close();
111
+ }
112
+ });
113
+
114
+ it("encrypts skipped Double Ratchet keys at rest", async () => {
115
+ const { db, storage } = makeStorage();
116
+ try {
117
+ await storage.init();
118
+ const skippedKeys = JSON.stringify({
119
+ "ratchet-key:7": "ab".repeat(32),
120
+ });
121
+ const session = makeSession({ skippedKeys });
122
+ await storage.saveSession(session);
123
+
124
+ const row = await db
125
+ .selectFrom("sessions")
126
+ .select("skippedKeys")
127
+ .where("sessionID", "=", session.sessionID)
128
+ .executeTakeFirstOrThrow();
129
+ expect(row.skippedKeys).toMatch(/^vex-storage-secret:1:/);
130
+ expect(row.skippedKeys).not.toContain("ratchet-key");
131
+ expect(row.skippedKeys).not.toContain("ab".repeat(32));
132
+
133
+ const roundTripped = await storage.getAllSessions();
134
+ expect(roundTripped).toMatchObject([
135
+ { sessionID: session.sessionID, skippedKeys },
136
+ ]);
137
+ } finally {
138
+ await storage.close();
139
+ }
140
+ });
77
141
  });
78
142
 
79
143
  function makeMessage(overrides: Partial<Message>): Message {
@@ -94,6 +158,31 @@ function makeMessage(overrides: Partial<Message>): Message {
94
158
  };
95
159
  }
96
160
 
161
+ function makeSession(overrides: Partial<SessionSQL>): SessionSQL {
162
+ return {
163
+ CKr: null,
164
+ CKs: null,
165
+ deviceID: "peer-device",
166
+ DHr: null,
167
+ DHsPrivate: "11".repeat(32),
168
+ DHsPublic: "22".repeat(32),
169
+ fingerprint: "33".repeat(32),
170
+ lastUsed: "2026-06-01T00:00:00.000Z",
171
+ mode: "initiator",
172
+ Nr: 0,
173
+ Ns: 0,
174
+ PN: 0,
175
+ publicKey: "44".repeat(32),
176
+ RK: "55".repeat(32),
177
+ sessionID: "session-id",
178
+ SK: "66".repeat(32),
179
+ skippedKeys: "{}",
180
+ userID: "peer-user",
181
+ verified: false,
182
+ ...overrides,
183
+ };
184
+ }
185
+
97
186
  function makeStorage(): {
98
187
  db: Kysely<ClientDatabase>;
99
188
  storage: SqliteStorage;
@@ -112,7 +201,7 @@ function makeStorage(): {
112
201
  async function messageRow(db: Kysely<ClientDatabase>, mailID: string) {
113
202
  return await db
114
203
  .selectFrom("messages")
115
- .select(["decrypted", "extra", "message"])
204
+ .select(["decrypted", "extra", "message", "nonce"])
116
205
  .where("mailID", "=", mailID)
117
206
  .executeTakeFirst();
118
207
  }
@@ -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
  }