vault.ts raw
1 import {
2 BrowserSessionData,
3 BrowserSyncData,
4 CryptoHelper,
5 StorageService,
6 generateSalt,
7 generateIV,
8 deriveKeyArgon2,
9 } from '@common';
10 import { Buffer } from 'buffer';
11 import { decryptCashuMints, encryptCashuMint } from './cashu';
12 import { decryptIdentities, encryptIdentity, LockedVaultContext } from './identity';
13 import { decryptNwcConnections, encryptNwcConnection } from './nwc';
14 import { decryptPermissions } from './permission';
15 import { decryptRelays, encryptRelay } from './relay';
16
17 export const createNewVault = async function (
18 this: StorageService,
19 password: string
20 ): Promise<void> {
21 this.assureIsInitialized();
22
23 const vaultHash = await CryptoHelper.hash(password);
24
25 // v2: Generate random salt and derive key with Argon2id
26 const salt = generateSalt();
27 const iv = generateIV();
28 const saltBytes = Buffer.from(salt, 'base64');
29 const keyBytes = await deriveKeyArgon2(password, saltBytes);
30 const vaultKey = Buffer.from(keyBytes).toString('base64');
31
32 const sessionData: BrowserSessionData = {
33 iv,
34 salt,
35 vaultKey, // v2: Store pre-derived key instead of password
36 identities: [],
37 permissions: [],
38 relays: [],
39 nwcConnections: [],
40 cashuMints: [],
41 selectedIdentityId: null,
42 };
43 await this.getBrowserSessionHandler().saveFullData(sessionData);
44 this.getBrowserSessionHandler().setFullData(sessionData);
45
46 const syncData: BrowserSyncData = {
47 version: this.latestVersion,
48 salt, // v2: Random salt for Argon2id
49 iv,
50 vaultHash,
51 identities: [],
52 permissions: [],
53 relays: [],
54 nwcConnections: [],
55 cashuMints: [],
56 selectedIdentityId: null,
57 };
58 await this.getBrowserSyncHandler().saveAndSetFullData(syncData);
59 };
60
61 export const unlockVault = async function (
62 this: StorageService,
63 password: string
64 ): Promise<void> {
65 this.assureIsInitialized();
66 // console.log('[vault] Starting unlock...');
67
68 let browserSessionData = this.getBrowserSessionHandler().browserSessionData;
69 if (browserSessionData) {
70 throw new Error(
71 'Browser session data is available. Should only happen when the vault is unlocked'
72 );
73 }
74
75 const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
76 if (!browserSyncData) {
77 throw new Error(
78 'Browser sync data is not available. Should have been loaded before.'
79 );
80 }
81
82 // console.log('[vault] Checking password hash...');
83 const passwordHash = await CryptoHelper.hash(password);
84 if (passwordHash !== browserSyncData.vaultHash) {
85 throw new Error('Invalid password.');
86 }
87 // console.log('[vault] Password hash verified');
88
89 // Detect vault version
90 const isV2 = !!browserSyncData.salt;
91 // console.log('[vault] Vault version:', isV2 ? 'v2' : 'v1');
92
93 let withLockedVault: LockedVaultContext;
94 let vaultKey: string | undefined;
95 let vaultPassword: string | undefined;
96
97 if (isV2) {
98 // v2: Derive key with Argon2id (~3 seconds)
99 // console.log('[vault] Deriving key with Argon2id...');
100 const saltBytes = Buffer.from(browserSyncData.salt!, 'base64');
101 const keyBytes = await deriveKeyArgon2(password, saltBytes);
102 // console.log('[vault] Key derived, length:', keyBytes.length);
103 vaultKey = Buffer.from(keyBytes).toString('base64');
104 withLockedVault = {
105 iv: browserSyncData.iv,
106 keyBase64: vaultKey,
107 };
108 } else {
109 // v1: Use password with PBKDF2
110 vaultPassword = password;
111 withLockedVault = {
112 iv: browserSyncData.iv,
113 password,
114 };
115 }
116
117 // Decrypt the data
118 // console.log('[vault] Decrypting identities...');
119 const decryptedIdentities = await decryptIdentities.call(
120 this,
121 browserSyncData.identities,
122 withLockedVault
123 );
124 // console.log('[vault] Decrypted', decryptedIdentities.length, 'identities');
125
126 // console.log('[vault] Decrypting permissions...');
127 const decryptedPermissions = await decryptPermissions.call(
128 this,
129 browserSyncData.permissions,
130 withLockedVault
131 );
132 // console.log('[vault] Decrypted', decryptedPermissions.length, 'permissions');
133
134 // console.log('[vault] Decrypting relays...');
135 const decryptedRelays = await decryptRelays.call(
136 this,
137 browserSyncData.relays,
138 withLockedVault
139 );
140 // console.log('[vault] Decrypted', decryptedRelays.length, 'relays');
141
142 // console.log('[vault] Decrypting NWC connections...');
143 const decryptedNwcConnections = await decryptNwcConnections.call(
144 this,
145 browserSyncData.nwcConnections ?? [],
146 withLockedVault
147 );
148 // console.log('[vault] Decrypted', decryptedNwcConnections.length, 'NWC connections');
149
150 // console.log('[vault] Decrypting Cashu mints...');
151 const decryptedCashuMints = await decryptCashuMints.call(
152 this,
153 browserSyncData.cashuMints ?? [],
154 withLockedVault
155 );
156 // console.log('[vault] Decrypted', decryptedCashuMints.length, 'Cashu mints');
157
158 // console.log('[vault] Decrypting selectedIdentityId...');
159 let decryptedSelectedIdentityId: string | null = null;
160 if (browserSyncData.selectedIdentityId !== null) {
161 if (isV2) {
162 decryptedSelectedIdentityId = await this.decryptWithLockedVaultV2(
163 browserSyncData.selectedIdentityId,
164 'string',
165 browserSyncData.iv,
166 vaultKey!
167 );
168 } else {
169 decryptedSelectedIdentityId = await this.decryptWithLockedVault(
170 browserSyncData.selectedIdentityId,
171 'string',
172 browserSyncData.iv,
173 password
174 );
175 }
176 }
177 // console.log('[vault] selectedIdentityId:', decryptedSelectedIdentityId);
178
179 browserSessionData = {
180 vaultPassword: isV2 ? undefined : vaultPassword,
181 vaultKey: isV2 ? vaultKey : undefined,
182 iv: browserSyncData.iv,
183 salt: browserSyncData.salt,
184 permissions: decryptedPermissions,
185 identities: decryptedIdentities,
186 selectedIdentityId: decryptedSelectedIdentityId,
187 relays: decryptedRelays,
188 nwcConnections: decryptedNwcConnections,
189 cashuMints: decryptedCashuMints,
190 };
191
192 // console.log('[vault] Saving session data...');
193 await this.getBrowserSessionHandler().saveFullData(browserSessionData);
194 this.getBrowserSessionHandler().setFullData(browserSessionData);
195 // console.log('[vault] Session data saved');
196
197 // Auto-migrate v1 to v2 after successful unlock
198 if (!isV2) {
199 // console.log('[vault] Migrating v1 to v2...');
200 await migrateVaultV1ToV2.call(this, password);
201 // console.log('[vault] Migration complete');
202 }
203
204 // console.log('[vault] Unlock complete!');
205 };
206
207 /**
208 * Migrate a v1 vault (PBKDF2) to v2 (Argon2id)
209 * Called automatically after successful v1 unlock
210 */
211 async function migrateVaultV1ToV2(
212 this: StorageService,
213 password: string
214 ): Promise<void> {
215 const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
216 const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
217 if (!browserSyncData || !browserSessionData) {
218 throw new Error('Cannot migrate: data not available');
219 }
220
221 // Generate new salt and derive Argon2id key
222 const newSalt = generateSalt();
223 const newIv = generateIV();
224 const saltBytes = Buffer.from(newSalt, 'base64');
225 const keyBytes = await deriveKeyArgon2(password, saltBytes);
226 const vaultKey = Buffer.from(keyBytes).toString('base64');
227
228 // Update session data with new v2 credentials
229 browserSessionData.salt = newSalt;
230 browserSessionData.iv = newIv;
231 browserSessionData.vaultKey = vaultKey;
232 browserSessionData.vaultPassword = undefined; // Remove v1 password
233
234 // Re-encrypt all data with new v2 key
235 const encryptedIdentities = [];
236 for (const identity of browserSessionData.identities) {
237 const encrypted = await encryptIdentity.call(this, identity);
238 encryptedIdentities.push(encrypted);
239 }
240
241 const encryptedRelays = [];
242 for (const relay of browserSessionData.relays) {
243 const encrypted = await encryptRelay.call(this, relay);
244 encryptedRelays.push(encrypted);
245 }
246
247 // For permissions, we need to re-encrypt them too
248 const encryptedPermissions = [];
249 for (const permission of browserSessionData.permissions) {
250 const encryptedPermission = {
251 id: await this.encrypt(permission.id),
252 identityId: await this.encrypt(permission.identityId),
253 host: await this.encrypt(permission.host),
254 method: await this.encrypt(permission.method),
255 methodPolicy: await this.encrypt(permission.methodPolicy),
256 kind: permission.kind !== undefined ? await this.encrypt(permission.kind.toString()) : undefined,
257 };
258 encryptedPermissions.push(encryptedPermission);
259 }
260
261 // Re-encrypt NWC connections
262 const encryptedNwcConnections = [];
263 for (const nwcConnection of browserSessionData.nwcConnections ?? []) {
264 const encrypted = await encryptNwcConnection.call(this, nwcConnection);
265 encryptedNwcConnections.push(encrypted);
266 }
267
268 // Re-encrypt Cashu mints
269 const encryptedCashuMints = [];
270 for (const cashuMint of browserSessionData.cashuMints ?? []) {
271 const encrypted = await encryptCashuMint.call(this, cashuMint);
272 encryptedCashuMints.push(encrypted);
273 }
274
275 const encryptedSelectedIdentityId = browserSessionData.selectedIdentityId
276 ? await this.encrypt(browserSessionData.selectedIdentityId)
277 : null;
278
279 // Update sync data with v2 format
280 const migratedSyncData: BrowserSyncData = {
281 version: this.latestVersion,
282 salt: newSalt,
283 iv: newIv,
284 vaultHash: browserSyncData.vaultHash, // Keep same password hash
285 identities: encryptedIdentities,
286 permissions: encryptedPermissions,
287 relays: encryptedRelays,
288 nwcConnections: encryptedNwcConnections,
289 cashuMints: encryptedCashuMints,
290 selectedIdentityId: encryptedSelectedIdentityId,
291 };
292
293 // Save migrated data
294 await this.getBrowserSyncHandler().saveAndSetFullData(migratedSyncData);
295 await this.getBrowserSessionHandler().saveFullData(browserSessionData);
296
297 console.log('Vault migrated from v1 (PBKDF2) to v2 (Argon2id)');
298 }
299
300 export const changePassword = async function (
301 this: StorageService,
302 newPassword: string
303 ): Promise<void> {
304 this.assureIsInitialized();
305
306 const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
307 const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
308 if (!browserSyncData || !browserSessionData) {
309 throw new Error('Vault must be unlocked to change password');
310 }
311
312 const newVaultHash = await CryptoHelper.hash(newPassword);
313 const newSalt = generateSalt();
314 const newIv = generateIV();
315 const saltBytes = Buffer.from(newSalt, 'base64');
316 const keyBytes = await deriveKeyArgon2(newPassword, saltBytes);
317 const vaultKey = Buffer.from(keyBytes).toString('base64');
318
319 // Update session with new credentials so encrypt() uses them
320 browserSessionData.salt = newSalt;
321 browserSessionData.iv = newIv;
322 browserSessionData.vaultKey = vaultKey;
323 browserSessionData.vaultPassword = undefined;
324
325 // Re-encrypt everything with the new key
326 const encryptedIdentities = [];
327 for (const identity of browserSessionData.identities) {
328 encryptedIdentities.push(await encryptIdentity.call(this, identity));
329 }
330
331 const encryptedRelays = [];
332 for (const relay of browserSessionData.relays) {
333 encryptedRelays.push(await encryptRelay.call(this, relay));
334 }
335
336 const encryptedPermissions = [];
337 for (const permission of browserSessionData.permissions) {
338 encryptedPermissions.push({
339 id: await this.encrypt(permission.id),
340 identityId: await this.encrypt(permission.identityId),
341 host: await this.encrypt(permission.host),
342 method: await this.encrypt(permission.method),
343 methodPolicy: await this.encrypt(permission.methodPolicy),
344 kind: permission.kind !== undefined ? await this.encrypt(permission.kind.toString()) : undefined,
345 });
346 }
347
348 const encryptedNwcConnections = [];
349 for (const nwc of browserSessionData.nwcConnections ?? []) {
350 encryptedNwcConnections.push(await encryptNwcConnection.call(this, nwc));
351 }
352
353 const encryptedCashuMints = [];
354 for (const cashuMint of browserSessionData.cashuMints ?? []) {
355 encryptedCashuMints.push(await encryptCashuMint.call(this, cashuMint));
356 }
357
358 const encryptedSelectedIdentityId = browserSessionData.selectedIdentityId
359 ? await this.encrypt(browserSessionData.selectedIdentityId)
360 : null;
361
362 const newSyncData: BrowserSyncData = {
363 version: this.latestVersion,
364 salt: newSalt,
365 iv: newIv,
366 vaultHash: newVaultHash,
367 identities: encryptedIdentities,
368 permissions: encryptedPermissions,
369 relays: encryptedRelays,
370 nwcConnections: encryptedNwcConnections,
371 cashuMints: encryptedCashuMints,
372 selectedIdentityId: encryptedSelectedIdentityId,
373 };
374
375 await this.getBrowserSyncHandler().saveAndSetFullData(newSyncData);
376 await this.getBrowserSessionHandler().saveFullData(browserSessionData);
377 };
378
379 export const deleteVault = async function (
380 this: StorageService,
381 doNotSetIsInitializedToFalse: boolean
382 ): Promise<void> {
383 this.assureIsInitialized();
384 const syncFlow = this.getSignerMetaHandler().signerMetaData?.syncFlow;
385 if (typeof syncFlow === 'undefined') {
386 throw new Error('Sync flow is not set.');
387 }
388
389 await this.getBrowserSyncHandler().clearData();
390 await this.getBrowserSessionHandler().clearData();
391
392 if (!doNotSetIsInitializedToFalse) {
393 this.isInitialized = false;
394 }
395 };
396