Source

keystore.js

// Copyright © 2020 Findora. All rights reserved.
//
// This module exposes a serializable and deterministic keystore that makes it easy for users to manage their asset key pairs.
// The keystore is encrypted by a user-provided password.

/** @module KeyStore */

const crypto = require('crypto');
const Ledger = require('./pkg/wasm.js');
const Encrypt = require('./encrypt.js');

// WARNING: DO NOT MODIFY. Making this constant smaller may undermine the security of the KeyStore.
// **************************************************************************************************************
const N_SEED_BYTES = 64;
// **************************************************************************************************************

const createRandomSeed = function () {
  const seedString = String.fromCharCode.apply(null, crypto.randomBytes(N_SEED_BYTES));
  return seedString;
};

/** Class representing a Findora keystore. At the core of a Findora keystore is an encrypted seed with which users can
 * deterministically generate ledger keypairs. The keystore seed is encrypted by a master key derived from a user-provided password.
 */
class KeyStore {
  /**
   * Creates a new keystore with an underlying seed encrypted by the provided password.
   * @param {string} password - Password used to derive the master key that keystore seed will be encrypted under.
   * @constructor
   */
  constructor(password) {
    if (!password) { // So that we can use Object.assign to instantiate via static method.
      this.publicKeys = undefined;
      this.encryptedSeed = undefined;
      return;
    }
    // Create an empty set of public keys
    this.publicKeys = new Map();

    // Create new seed string
    const seed = createRandomSeed(N_SEED_BYTES);

    // Encrypt the seed string
    this.encryptedSeed = Encrypt.encryptWithPassword(seed, password);
  }

  /**
   * Generates the keystore's master key. The master key decrypts the encrypted seed that is used to
   * deterministically generate ledger keypairs.
   * @param {string} password - Password that the keystore seed is decrypted under.
   */
  genMasterKey(password) {
    return Encrypt.deriveEncryptionKey(password, this.encryptedSeed.salt);
  }

  /**
   * Deterministically generates a ledger keypair from the keystore seed and the provided name.
   * @param {string} masterKey - Derived key that the keystore seed is decrypted under.
   * @param {string} name - Name of the key.
   * @returns {XfrKeyPair}
   * @see {@link module:KeyStore~KeyStore#genMasterKey|genMasterKey} for information about how generate the master key.
   */
  genKeyPair(masterKey, name) {
    const seed = this.getSeed(masterKey);
    return Ledger.new_keypair_from_seed(seed, name);
  }

  /**
   * Adds a ledger public key to the keystore.
   * @param {string} masterKey - Derived key that the keystore seed is decrypted under.
   * @param {string} name - Name of the key.
   * @see {@link module:KeyStore~KeyStore#genMasterKey|genMasterKey} for information about how generate the master key.
   */
  addPublicKey(masterKey, name) {
    const kp = this.genKeyPair(masterKey, name);
    const pkBase64 = Ledger.public_key_to_base64(kp.get_pk());
    if (this.publicKeys.has(name)) {
      throw new Error('Public key with name ${name} already exists');
    }
    this.publicKeys.set(name, pkBase64);
  }

  /**
   * Returns a base64-encoded public key, or undefined if a keypair with the given named isn't currently cached in the keystore.
   * @see {@link module:KeyStore~KeyStore#addPublicKey|addPublicKey} for information about how to add a public key to the keystore.
   * @returns {String | undefined}
   */
  getPublicKey(name) {
    return this.publicKeys.get(name);
  }

  /**
   * Returns a list of base64-encoded public key and name pairs
   * (e.g. [["Alice", "g0Kge-wOwr_2CF0yVNkxKPVFVfR5kvo3HpGu0lG3utk="], ["Bob", "yeg4zFQLLs-XRktnYRIkINzeJaJeLsMrf_GpuRFEFE8="]]).
   * @returns {Array}
   */
  getPublicKeys() {
    return Array.from(this.publicKeys);
  }

  /**
   * Decrypt and return the underlying keystore seed. Use with caution, as access to the keystore seed renders the keystore compleetly insecure.
   * @returns {String}
   * @see {@link module:KeyStore~KeyStore#genMasterKey|genMasterKey} for information about how generate the master key.
   */
  getSeed(masterKey) {
    return Encrypt.decryptWithPbkKey(this.encryptedSeed, masterKey);
  }

  /**
   * Serialize the keystore as a string.
   * @returns {String}
   */
  serialize() {
    const toSerialize = { encryptedSeed: this.encryptedSeed, publicKeys: Array.from(this.publicKeys) };
    return JSON.stringify(toSerialize);
  }

  /**
   * Deserializes a serialized keystore.
   * @returns {KeyStore}
   */
  static deserialize(serializedKeyStore) {
    const json = JSON.parse(serializedKeyStore);
    const keyMap = new Map();
    for (const key of json.publicKeys) {
      keyMap.set(key[0], key[1]);
    }
    json.publicKeys = keyMap;
    return Object.assign(new KeyStore(), json);
  }

  /**
   * Generates a KeyStore from an existing seed.
   * @returns {KeyStore}
   */
  static fromSeed(seed, password) {
    const ks = {};
    ks.encryptedSeed = Encrypt.encryptWithPassword(seed, password);
    ks.publicKeys = new Map();
    return Object.assign(new KeyStore(), ks);
  }
}

module.exports = {
  KeyStore,
};