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