wallets/wallet.js

import { Keypair } from '../base'
import sjcl from 'sjcl-tokend'
import * as crypto from './crypto'
import isNil from 'lodash/isNil'
import isString from 'lodash/isString'

/**
 * Manages user's key pair.
 *
 * @class
 */
export class Wallet {
  /**
   * Create a new instance from user's key pair.
   *
   * @constructor
   *
   * @param {Keypair|string} keypair User's key pair or a secret seed.
   * @param {string} accountId User's account ID.
   * @param {string} [walletId] Wallet ID.
   */
  constructor (keypair, accountId, walletId) {
    if (isNil(keypair)) {
      throw new Error('No keypair provided.')
    } else if (isString(keypair)) {
      if (!Keypair.isValidSecretKey(keypair)) {
        throw new Error('Invalid secret seed.')
      }
      keypair = Keypair.fromSecret(keypair)
    } else if (!(keypair instanceof Keypair)) {
      throw new Error('Invalid keypair. Expected a Keypair instance or a string seed.')
    }

    if (!Keypair.isValidPublicKey(accountId)) {
      throw new Error('Invalid account ID.')
    }

    if (walletId && !isString(walletId)) {
      throw new Error('Hex encoded wallet ID expected.')
    }

    this._keypair = keypair
    this._accountId = accountId
    this._id = walletId
  }

  /**
   * Clone a wallet.
   *
   * @return {Wallet} Cloned wallet.
   */
  clone () {
    return new Wallet(
      this._keypair,
      this._accountId,
      this._id
    )
  }

  /**
   * Generate a new wallet.
   *
   * @param {string} [accountId] User's account ID.
   *
   * @return {Wallet} The new wallet.
   */
  static generate (accountId = null) {
    let keypair = Keypair.random()
    accountId = accountId || keypair.accountId()

    return new Wallet(
      keypair,
      accountId
    )
  }

  /**
   * Decrypt a wallet obtained from a wallet server.
   *
   * @param {object} keychainData Encrypted wallet seed.
   * @param {object} kdfParams Scrypt params used for encryption.
   * @param {string} salt Salt used for encryption.
   * @param {string} password User's password.
   */
  static fromEncrypted (keychainData, kdfParams, salt, password) {
    let rawMasterKey = crypto.calculateMasterKey(
      salt,
      password,
      kdfParams
    )
    let rawWalletId = crypto.deriveWalletId(rawMasterKey)
    let rawWalletKey = crypto.deriveWalletKey(rawMasterKey)
    let rawDecrypted = crypto.decryptData(keychainData, rawWalletKey)
    let decryptedKeychain = JSON.parse(rawDecrypted)

    return new Wallet(
      Keypair.fromSecret(decryptedKeychain.masterSeed),
      decryptedKeychain.accountId,
      sjcl.codec.base64.fromBits(rawWalletId)
    )
  }

  /**
   * Derive the wallet ID.
   *
   * @param {string} password
   * @param {object} kdfParams
   * @param {string} salt
   *
   * @return {string} Wallet ID.
   */
  static deriveId (password, kdfParams, salt) {
    let masterKey = crypto.calculateMasterKey(salt, password, kdfParams)
    let walletId = crypto.deriveWalletId(masterKey)

    return sjcl.codec.base64.fromBits(walletId)
  }

  /**
   * Wallet ID.
   */
  get id () {
    if (!this._id) {
      throw new Error('This wallet has no wallet ID yet.')
    }

    return this._id
  }

  /**
   * Account ID.
   */
  get accountId () {
    return this._accountId
  }

  /**
   * Email used for login.
   */
  get email () {
    return this._email
  }

  /**
   * Secret seed.
   */
  get secretSeed () {
    return this._keypair.secret()
  }

  /**
   * Get signing keypair.
   */
  get keypair () {
    return this._keypair
  }

  /**
   * Encrypt wallet to securely store it.
   *
   * @param {object} kdfParams Scrypt params.
   * @param {string} password User's password.
   * @return {object} Encrypted keychain and metadata.
   */
  encrypt (kdfParams, password) {
    if (isNil(kdfParams)) {
      throw new Error('KDF params required')
    }
    if (!isString(password) || password.length === 0) {
      throw new TypeError('Password must be a non-empty string')
    }

    let salt = crypto.randomBytes(16).toString('base64')
    let masterKey = crypto.calculateMasterKey(
      salt,
      password,
      kdfParams
    )

    // Encrypt data
    let walletKey = crypto.deriveWalletKey(masterKey)

    let rawKeychainData = {
      accountId: this.accountId,
      masterSeed: this._keypair.secret()
    }
    let keychainData = crypto.encryptData(
      JSON.stringify(rawKeychainData),
      walletKey
    )

    // Derive wallet ID
    let rawWalletId = crypto.deriveWalletId(masterKey)
    this._id = sjcl.codec.base64.fromBits(rawWalletId)

    return {
      kdfParams: JSON.stringify(kdfParams),
      id: this._id,
      accountId: this.accountId,
      salt,
      keychainData
    }
  }
}