identity.ts raw

   1  import {
   2    CryptoHelper,
   3    Identity_DECRYPTED,
   4    Identity_ENCRYPTED,
   5    NostrHelper,
   6    StorageService,
   7  } from '@common';
   8  
   9  export const addIdentity = async function (
  10    this: StorageService,
  11    data: {
  12      nick: string;
  13      privkeyString: string;
  14    }
  15  ): Promise<void> {
  16    this.assureIsInitialized();
  17  
  18    const privkey = NostrHelper.getNostrPrivkeyObject(
  19      data.privkeyString.toLowerCase()
  20    ).hex;
  21  
  22    // Check if an identity with the same privkey already exists.
  23    const existingIdentity = (
  24      this.getBrowserSessionHandler().browserSessionData?.identities ?? []
  25    ).find((x) => x.privkey === privkey);
  26    if (existingIdentity) {
  27      throw new Error(
  28        `An identity with the same private key already exists: ${existingIdentity.nick}`
  29      );
  30    }
  31  
  32    const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
  33    if (!browserSessionData) {
  34      throw new Error('Browser session data is undefined.');
  35    }
  36  
  37    const decryptedIdentity: Identity_DECRYPTED = {
  38      id: CryptoHelper.v4(),
  39      nick: data.nick,
  40      privkey,
  41      createdAt: new Date().toISOString(),
  42    };
  43  
  44    // Add the new identity to the session data.
  45    browserSessionData.identities.push(decryptedIdentity);
  46    let isFirstIdentity = false;
  47    if (browserSessionData.identities.length === 1) {
  48      isFirstIdentity = true;
  49      browserSessionData.selectedIdentityId = decryptedIdentity.id;
  50    }
  51    this.getBrowserSessionHandler().saveFullData(browserSessionData);
  52  
  53    // Encrypt the new identity and add it to the sync data.
  54    const encryptedIdentity = await encryptIdentity.call(this, decryptedIdentity);
  55    const encryptedIdentities = [
  56      ...(this.getBrowserSyncHandler().browserSyncData?.identities ?? []),
  57      encryptedIdentity,
  58    ];
  59  
  60    await this.getBrowserSyncHandler().saveAndSetPartialData_Identities({
  61      identities: encryptedIdentities,
  62    });
  63  
  64    if (isFirstIdentity) {
  65      await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId(
  66        {
  67          selectedIdentityId: encryptedIdentity.id,
  68        }
  69      );
  70    }
  71  };
  72  
  73  export const deleteIdentity = async function (
  74    this: StorageService,
  75    identityId: string | undefined
  76  ): Promise<void> {
  77    this.assureIsInitialized();
  78  
  79    if (!identityId) {
  80      return;
  81    }
  82  
  83    const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
  84    const browserSyncData = this.getBrowserSyncHandler().browserSyncData;
  85    if (!browserSessionData || !browserSyncData) {
  86      throw new Error('Browser session or sync data is undefined.');
  87    }
  88  
  89    browserSessionData.identities = browserSessionData.identities.filter(
  90      (x) => x.id !== identityId
  91    );
  92    browserSessionData.permissions = browserSessionData.permissions.filter(
  93      (x) => x.identityId !== identityId
  94    );
  95    browserSessionData.relays = browserSessionData.relays.filter(
  96      (x) => x.identityId !== identityId
  97    );
  98    if (browserSessionData.selectedIdentityId === identityId) {
  99      // Choose another identity to be selected or null if there is none.
 100      browserSessionData.selectedIdentityId =
 101        browserSessionData.identities.length > 0
 102          ? browserSessionData.identities[0].id
 103          : null;
 104    }
 105    await this.getBrowserSessionHandler().saveFullData(browserSessionData);
 106  
 107    // Handle Sync data.
 108    const encryptedIdentityId = await this.encrypt(identityId);
 109    await this.getBrowserSyncHandler().saveAndSetPartialData_Identities({
 110      identities: browserSyncData.identities.filter(
 111        (x) => x.id !== encryptedIdentityId
 112      ),
 113    });
 114    await this.getBrowserSyncHandler().saveAndSetPartialData_Permissions({
 115      permissions: browserSyncData.permissions.filter(
 116        (x) => x.identityId !== encryptedIdentityId
 117      ),
 118    });
 119    await this.getBrowserSyncHandler().saveAndSetPartialData_Relays({
 120      relays: browserSyncData.relays.filter(
 121        (x) => x.identityId !== encryptedIdentityId
 122      ),
 123    });
 124    await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId({
 125      selectedIdentityId:
 126        browserSessionData.selectedIdentityId === null
 127          ? null
 128          : await this.encrypt(browserSessionData.selectedIdentityId),
 129    });
 130  };
 131  
 132  export const switchIdentity = async function (
 133    this: StorageService,
 134    identityId: string | null
 135  ): Promise<void> {
 136    this.assureIsInitialized();
 137  
 138    // Check, if the identity really exists.
 139    const browserSessionData = this.getBrowserSessionHandler().browserSessionData;
 140  
 141    if (!browserSessionData?.identities.find((x) => x.id === identityId)) {
 142      return;
 143    }
 144  
 145    browserSessionData.selectedIdentityId = identityId;
 146    await this.getBrowserSessionHandler().saveFullData(browserSessionData);
 147  
 148    const encryptedIdentityId =
 149      identityId === null ? null : await this.encrypt(identityId);
 150    await this.getBrowserSyncHandler().saveAndSetPartialData_SelectedIdentityId({
 151      selectedIdentityId: encryptedIdentityId,
 152    });
 153  };
 154  
 155  export const encryptIdentity = async function (
 156    this: StorageService,
 157    identity: Identity_DECRYPTED
 158  ): Promise<Identity_ENCRYPTED> {
 159    const encryptedIdentity: Identity_ENCRYPTED = {
 160      id: await this.encrypt(identity.id),
 161      nick: await this.encrypt(identity.nick),
 162      createdAt: await this.encrypt(identity.createdAt),
 163      privkey: await this.encrypt(identity.privkey),
 164    };
 165  
 166    return encryptedIdentity;
 167  };
 168  
 169  /**
 170   * Locked vault context for decryption during unlock
 171   * - v1 vaults use password (PBKDF2)
 172   * - v2 vaults use keyBase64 (pre-derived Argon2id key)
 173   */
 174  export type LockedVaultContext =
 175    | { iv: string; password: string; keyBase64?: undefined }
 176    | { iv: string; keyBase64: string; password?: undefined };
 177  
 178  export const decryptIdentities = async function (
 179    this: StorageService,
 180    identities: Identity_ENCRYPTED[],
 181    withLockedVault: LockedVaultContext | undefined = undefined
 182  ): Promise<Identity_DECRYPTED[]> {
 183    const decryptedIdentities: Identity_DECRYPTED[] = [];
 184  
 185    for (const identity of identities) {
 186      const decryptedIdentity = await decryptIdentity.call(
 187        this,
 188        identity,
 189        withLockedVault
 190      );
 191      decryptedIdentities.push(decryptedIdentity);
 192    }
 193  
 194    return decryptedIdentities;
 195  };
 196  
 197  export const decryptIdentity = async function (
 198    this: StorageService,
 199    identity: Identity_ENCRYPTED,
 200    withLockedVault: LockedVaultContext | undefined = undefined
 201  ): Promise<Identity_DECRYPTED> {
 202    if (typeof withLockedVault === 'undefined') {
 203      const decryptedIdentity: Identity_DECRYPTED = {
 204        id: await this.decrypt(identity.id, 'string'),
 205        nick: await this.decrypt(identity.nick, 'string'),
 206        createdAt: await this.decrypt(identity.createdAt, 'string'),
 207        privkey: await this.decrypt(identity.privkey, 'string'),
 208      };
 209  
 210      return decryptedIdentity;
 211    }
 212  
 213    // v2: Use pre-derived key
 214    if (withLockedVault.keyBase64) {
 215      const decryptedIdentity: Identity_DECRYPTED = {
 216        id: await this.decryptWithLockedVaultV2(
 217          identity.id,
 218          'string',
 219          withLockedVault.iv,
 220          withLockedVault.keyBase64
 221        ),
 222        nick: await this.decryptWithLockedVaultV2(
 223          identity.nick,
 224          'string',
 225          withLockedVault.iv,
 226          withLockedVault.keyBase64
 227        ),
 228        createdAt: await this.decryptWithLockedVaultV2(
 229          identity.createdAt,
 230          'string',
 231          withLockedVault.iv,
 232          withLockedVault.keyBase64
 233        ),
 234        privkey: await this.decryptWithLockedVaultV2(
 235          identity.privkey,
 236          'string',
 237          withLockedVault.iv,
 238          withLockedVault.keyBase64
 239        ),
 240      };
 241      return decryptedIdentity;
 242    }
 243  
 244    // v1: Use password (PBKDF2)
 245    const decryptedIdentity: Identity_DECRYPTED = {
 246      id: await this.decryptWithLockedVault(
 247        identity.id,
 248        'string',
 249        withLockedVault.iv,
 250        withLockedVault.password!
 251      ),
 252      nick: await this.decryptWithLockedVault(
 253        identity.nick,
 254        'string',
 255        withLockedVault.iv,
 256        withLockedVault.password!
 257      ),
 258      createdAt: await this.decryptWithLockedVault(
 259        identity.createdAt,
 260        'string',
 261        withLockedVault.iv,
 262        withLockedVault.password!
 263      ),
 264      privkey: await this.decryptWithLockedVault(
 265        identity.privkey,
 266        'string',
 267        withLockedVault.iv,
 268        withLockedVault.password!
 269      ),
 270    };
 271  
 272    return decryptedIdentity;
 273  };
 274