Newer
Older
CrypticOreWallet / src / pages / backup / backupImportV1.ts
@Drew Lemmy Drew Lemmy on 19 Mar 2021 9 KB feat: early work on address book
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under GPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import {
  BackupKristWebV1, KristWebV1Wallet, KristWebV1Contact
} from "./backupFormats";
import {
  BackupError, BackupWalletError, BackupContactError, BackupResults
} from "./backupResults";
import { backupDecryptValue } from "./backupImport";
import {
  getShorthands, Shorthands, str, checkFormat, checkAddress, checkLabelValid,
  finalWalletImport,
  validateContactAddress, checkExistingContact, finalContactImport
} from "./backupImportUtils";

import { WalletMap } from "@wallets";
import { ContactMap } from "@contacts";

import { isPlainObject, memoize } from "lodash-es";
import to from "await-to-js";

import Debug from "debug";
const debug = Debug("kristweb:backup-import-v1");

/** Strips http/https from a sync node to properly compare it. */
const _cleanSyncNode = memoize((node: string) => node.replace(/^https?:/, ""));

/** Imports a KristWeb v1 backup. */
export async function importV1Backup(
  // Things regarding the app's existing state
  existingWallets: WalletMap,
  existingContacts: ContactMap,
  appMasterPassword: string,
  appSyncNode: string,
  addressPrefix: string,
  nameSuffix: string,

  // Things related to the backup
  backup: BackupKristWebV1,
  masterPassword: string,
  noOverwrite: boolean,

  results: BackupResults
): Promise<void> {
  // Import wallets
  for (const uuid in backup.wallets) {
    if (!uuid || !uuid.startsWith("Wallet-")) {
      // Not a wallet
      debug("skipping v1 wallet key %s", uuid);
      continue;
    }

    const rawWallet = backup.wallets[uuid];
    debug("importing v1 wallet uuid %s", uuid);

    try {
      await importV1Wallet(
        existingWallets, appMasterPassword, appSyncNode, addressPrefix,
        backup, masterPassword, noOverwrite,
        uuid, rawWallet,
        results
      );
    } catch (err) {
      debug("error importing v1 wallet", err);
      results.addErrorMessage("wallets", uuid, undefined, err);
    }
  }

  // Import contacts
  for (const uuid in backup.friends) {
    if (!uuid || !uuid.startsWith("Friend-")) {
      // Not a contact
      debug("skipping v1 contact key %s", uuid);
      continue;
    }

    const rawContact = backup.friends[uuid];
    debug("importing v1 contact uuid %s", uuid);

    try {
      await importV1Contact(
        existingContacts, appSyncNode, addressPrefix, nameSuffix,
        backup, masterPassword, noOverwrite,
        uuid, rawContact,
        results
      );
    } catch (err) {
      debug("error importing v1 contact", err);
      results.addErrorMessage("contacts", uuid, undefined, err);
    }
  }
}

/** Decrypts and validates a V1 wallet or contact. */
async function importV1Object<T extends KristWebV1Wallet | KristWebV1Contact>(
  backup: BackupKristWebV1,
  masterPassword: string,

  uuid: string,
  rawObject: string, // The encrypted wallet/contact

  mkErr: (key: string) => BackupError // Error constructor
): Promise<T> {
  const { type } = backup;

  // ---------------------------------------------------------------------------
  // DECRYPTION, BASIC VALIDATION
  // ---------------------------------------------------------------------------
  // Validate the type of the wallet/contact data
  if (!str(rawObject)) {
    debug("v1 object %s had type %s", uuid, typeof rawObject, rawObject);
    throw mkErr("errorInvalidTypeString");
  }

  // Attempt to decrypt the wallet/contact
  const dec = await backupDecryptValue(type, masterPassword, rawObject);
  if (dec === false) throw mkErr("errorDecrypt");

  // Parse JSON, promisify to catch syntax errors
  const [err, obj]: [Error | null, T] =
    await to((async () => JSON.parse(dec))());
  if (err) throw mkErr("errorDataJSON");

  // Validate the type of the decrypted wallet/contact data
  if (!isPlainObject(obj)) {
    debug("v1 object %s had decrypted type %s", uuid, typeof obj);
    throw mkErr("errorInvalidTypeObject");
  }

  return obj;
}

/** Validates and cleans the sync node */
function validateSyncNode(
  { importWarn }: Shorthands,
  appSyncNode: string,
  object: { syncNode?: string }
): void {
  // Check if the wallet or contact was using a custom sync node (ignoring
  // http/https)
  const cleanAppSyncNode = _cleanSyncNode(appSyncNode);
  const { syncNode } = object;
  if (syncNode && _cleanSyncNode(syncNode) !== cleanAppSyncNode)
    importWarn("warningSyncNode");
}

// =============================================================================
// WALLET IMPORT
// =============================================================================

/** Imports a single wallet in the KristWeb v1 format. */
export async function importV1Wallet(
  // Things regarding the app's existing state
  existingWallets: WalletMap,
  appMasterPassword: string,
  appSyncNode: string,
  addressPrefix: string,

  // Things related to the backup
  backup: BackupKristWebV1,
  masterPassword: string,
  noOverwrite: boolean,

  uuid: string,
  rawWallet: string, // The encrypted wallet

  results: BackupResults
): Promise<void> {
  const shorthands = getShorthands(results, uuid, "v1");
  const { success, importWarn } = shorthands;

  // Decrypt and validate the wallet
  const wallet = await importV1Object<KristWebV1Wallet>(
    backup, masterPassword, uuid, rawWallet,
    key => new BackupWalletError(key)
  );

  // ---------------------------------------------------------------------------
  // REQUIRED PROPERTY VALIDATION
  // ---------------------------------------------------------------------------
  const { format, username } = checkFormat(shorthands, wallet);

  // The password and masterkey must be present
  const { password, masterkey } = wallet;
  if (!str(password)) throw new BackupWalletError("errorPasswordMissing");
  if (!str(masterkey)) throw new BackupWalletError("errorMasterKeyMissing");

  // ---------------------------------------------------------------------------
  // OPTIONAL PROPERTY VALIDATION
  // ---------------------------------------------------------------------------
  // Check if the wallet was using a custom sync node (ignoring http/https)
  validateSyncNode(shorthands, appSyncNode, wallet);
  // Check if the wallet was using a custom icon
  if (str(wallet.icon)) importWarn("warningIcon");

  // Check that the label is valid
  const { label } = wallet;
  checkLabelValid(shorthands, label);

  // ---------------------------------------------------------------------------
  // WALLET IMPORT PREPARATION/VALIDATION
  // ---------------------------------------------------------------------------
  const { address, existingWallet, existingImportWallet } = await checkAddress(
    addressPrefix, existingWallets, results, uuid,
    masterkey, "errorMasterKeyMismatch",
    format, password, username
  );

  // Skip with no additional checks or updates if this wallet was already
  // handled by this backup import
  if (existingImportWallet) {
    results.skippedWallets++;
    return success({ key: "import.walletMessages.successImportSkipped", args: { address }});
  }

  // ---------------------------------------------------------------------------
  // WALLET IMPORT
  // ---------------------------------------------------------------------------
  await finalWalletImport(
    existingWallets, appMasterPassword, addressPrefix,
    shorthands, results, noOverwrite,
    existingWallet, address, password,
    { label, username, format }
  );
}

// =============================================================================
// CONTACT IMPORT
// =============================================================================

/** Imports a single contact in the KristWeb v1 format. */
export async function importV1Contact(
  // Things regarding the app's existing state
  existingContacts: ContactMap,
  appSyncNode: string,
  addressPrefix: string,
  nameSuffix: string,

  // Things related to the backup
  backup: BackupKristWebV1,
  masterPassword: string,
  noOverwrite: boolean,

  uuid: string,
  rawContact: string, // The encrypted contact

  results: BackupResults
): Promise<void> {
  const shorthands = getShorthands(results, uuid, "v1", "contact");
  const { success, importWarn } = shorthands;

  // Decrypt and validate the contact
  const contact = await importV1Object<KristWebV1Contact>(
    backup, masterPassword, uuid, rawContact,
    key => new BackupContactError(key)
  );

  // Validate the address, which is required
  const { address, isName } =
    validateContactAddress(addressPrefix, nameSuffix, contact);

  results.setResultLabel("contacts", uuid, address);

  // Check for unsupported properties
  validateSyncNode(shorthands, appSyncNode, contact);
  if (str(contact.icon)) importWarn("warningIcon");

  // Check that the label is valid
  const { label } = contact;
  checkLabelValid(shorthands, label);

  // Check for existing contacts
  const { existingContact, existingImportContact } = checkExistingContact(
    existingContacts, results, address);

  // Skip with no additional checks or updates if this contact was already
  // handled by this backup import
  if (existingImportContact) {
    results.skippedContacts++;
    return success({ key: "import.contactMessages.successImportSkipped", args: { address }});
  }

  // Import the contact
  finalContactImport(
    existingContacts,
    shorthands, results, noOverwrite,
    existingContact, address, label, isName
  );
}