/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
/* jshint node:true */
'use strict';

var Pkcs12KeyStore = require('./pkcs12-keystore');
var cryptoPolicy = require('scytl-cryptopolicy');
var elGamal = require('scytl-elgamal');
var symmetric = require('scytl-symmetric');
var pbkdf = require('scytl-pbkdf');
var constants = require('./constants');
var validator = require('./input-validator');
var codec = require('scytl-codec');
var forge = require('node-forge');

module.exports = ScytlKeyStore;

var BigInteger = forge.jsbn.BigInteger;

/**
 * @class ScytlKeyStore
 * @classdesc Encapsulates a Scytl key store. To instantiate this object, use
 *            the method {@link KeyStoreService.newScytlKeyStore}.
 * @hideconstructor
 * @param {Object|string}
 *            keyStore The provided Scytl key store, as an object with expected
 *            properties <b>OR</b> its JSON string representation.
 * @param {string}
 *            password The password to load the Scytl key store.
 * @param {Object}
 *            [options] An object containing optional arguments.
 * @param {Policy}
 *            [options.policy=Default policy] The cryptographic policy to use.
 * @param {PbkdfRandomService}
 *            [options.pbkdfRandomService=Created internally] The PBKDF service
 *            to use.
 * @param {SymmetricCryptographyService}
 *            [options.symmetricCryptographyService=Created internally] The
 *            symmetric cryptography service to use.
 * @param {ElGamalCryptographyService}
 *            [options.elGamalCryptographyService=Created internally] The
 *            ElGamal cryptography service to use.
 * @throws {Error}
 *             If the input data validation fails or the underlying key store
 *             could not be loaded.
 */
function ScytlKeyStore(keyStore, password, options) {
  var scytlKeyStore = keyStore;
  if (typeof keyStore === 'string') {
    validator.checkIsJsonString(
        keyStore, 'Scytl key store (in JSON format) to load');
    scytlKeyStore = JSON.parse(keyStore);
  }
  checkScytlKeyStore(scytlKeyStore);

  validator.checkIsType(password, 'string', 'Password to load Scytl key store');

  options = options || {};

  var policy_;
  if (options.policy) {
    policy_ = options.policy;
  } else {
    policy_ = cryptoPolicy.newIntance();
  }

  var pbkdfService;
  if (options.pbkdfService) {
    pbkdfService = options.pbkdfService;
  } else {
    pbkdfService = pbkdf.newService({policy: policy_});
  }

  var symmetricService_;
  if (options.symmetricCryptographyService) {
    symmetricService_ = options.symmetricCryptographyService;
  } else {
    symmetricService_ = symmetric.newService({policy: policy_});
  }

  var elGamalService_;
  if (options.elGamalCryptographyService) {
    elGamalService_ = options.elGamalCryptographyService;
  } else {
    elGamalService_ = elGamal.newService();
  }

  var pbkdfDeriver_ = pbkdfService.newDeriver();
  var symmetricCipher_ = symmetricService_.newCipher();

  var derivedKeys_ = {};
  var derivedPasswords_ = {};

  var pkcs12KeyStore_;
  try {
    pkcs12KeyStore_ =
        new Pkcs12KeyStore(scytlKeyStore.store, getDerivedPassword(password));
  } catch (error) {
    throw new Error('Could not load underlying PKCS12 key store; ' + error);
  }

  /**
   * Retrieves a private key stored inside the Scytl key store, given the
   * key's storage alias name and password.
   *
   * @function getPrivateKey
   * @memberof ScytlKeyStore
   * @param {string}
   *            alias The storage alias name of the private key to retrieve.
   * @param {string}
   *            password The storage password of the private key.
   * @returns {string} The private key, in PEM format.
   * @throws {Error}
   *             If the input data validation fails or the private key could
   *             not be retrieved.
   */
  this.getPrivateKey = function(alias, password) {
    validator.checkIsNonEmptyString(
        alias,
        'Storage alias name of private key to retrieve from Scytl key store');
    validator.checkIsType(
        password, 'string',
        'Password of private key to retrieve from Scytl key store');

    try {
      return pkcs12KeyStore_.getPrivateKey(alias, getDerivedPassword(password));
    } catch (error) {
      throw new Error(
          'Could not retrieve private key with alias \'' + alias +
          '\' from Scytl key store; ' + error);
    }
  };

  /**
   * Retrieves a certificate stored inside the Scytl key store, given the
   * storage alias name of the certificate or that of its associated private
   * key entry.
   *
   * @function getCertificate
   * @memberof ScytlKeyStore
   * @param {string}
   *            alias The storage alias name of the certificate or that of its
   *            associated private key entry.
   * @returns {string} The certificate, in PEM format.
   * @throws {Error}
   *             If the input data validation fails or the certificate could
   *             not be retrieved.
   */
  this.getCertificate = function(alias) {
    validator.checkIsNonEmptyString(
        alias,
        'Storage alias name of certificate to retrieve from Scytl key store,');

    try {
      return pkcs12KeyStore_.getCertificate(alias);
    } catch (error) {
      throw new Error(
          'Could not retrieve certificate with alias \'' + alias +
          '\' from Scytl key store; ' + error);
    }
  };

  /**
   * Retrieves a certificate stored inside the Scytl key store, given the
   * certificate's subject common name.
   *
   * @function getCertificateBySubject
   * @memberof ScytlKeyStore
   * @param {string}
   *            subjectCn The subject common name of the certificate.
   * @returns {string} The certificate, in PEM format.
   * @throws {Error}
   *             If the input data validation fails or the certificate could
   *             not be retrieved.
   */
  this.getCertificateBySubject = function(subjectCn) {
    validator.checkIsNonEmptyString(
        subjectCn,
        'Subject common name of certificate to retrieve from Scytl key store');

    try {
      return pkcs12KeyStore_.getCertificateBySubject(subjectCn);
    } catch (error) {
      throw new Error(
          'Could not retrieve certificate with subject common name \'' +
          subjectCn + '\' from Scytl key store; ' + error);
    }
  };

  /**
   * Retrieves a certificate chain stored inside the Scytl key store, given
   * the storage alias name of the chain's associated private key entry.
   *
   * @function getCertificateChain
   * @memberof ScytlKeyStore
   * @param {string}
   *            alias The storage alias name of the chain's associated private
   *            key entry.
   * @returns {string[]} The certificate chain, as an array of strings in PEM
   *          format.
   * @throws {Error}
   *             If the input data validation fails or the certificate chain
   *             could not be retrieved.
   */
  this.getCertificateChain = function(alias) {
    validator.checkIsNonEmptyString(
        alias,
        'Storage alias name of certificate chain to retrieve from Scytl key store');

    try {
      return pkcs12KeyStore_.getCertificateChain(alias);
    } catch (error) {
      throw new Error(
          'Could not retrieve certificate chain with alias \'' + alias +
          '\' from Scytl key store; ' + error);
    }
  };

  /**
   * Retrieves a secret key stored inside the Scytl key store, given the key's
   * storage alias name and password.
   *
   * @function getSecretKey
   * @memberof ScytlKeyStore
   * @param {string}
   *            alias The storage alias name of the secret key to retrieve.
   * @param {string}
   *            password The storage password of the secret key.
   * @returns {Uint8Array} The secret key.
   * @throws {Error}
   *             If the input data validation fails or the secret key could
   *             not be retrieved.
   */
  this.getSecretKey = function(alias, password) {
    validator.checkIsNonEmptyString(
        alias,
        'Storage alias name of secret key to retrieve from Scytl key store');
    validator.checkIsType(
        password, 'string',
        'Password of secret key to retrieve from key store');

    var encryptedKey = scytlKeyStore.secrets[alias];
    checkEncryptedKey(encryptedKey, 'secret', alias);

    var secretKey = getKey(password, encryptedKey, alias, 'secret');

    return codec.binaryDecode(secretKey);
  };

  /**
   * Retrieves an ElGamal private key stored inside the Scytl key store, given
   * the key's storage alias name and password.
   *
   * @function getElGamalPrivateKey
   * @memberof ScytlKeyStore
   * @param {string}
   *            alias The storage alias name of the ElGamal private key to
   *            retrieve.
   * @param {string}
   *            password The storage password of the ElGamal private key.
   * @returns {ElGamalPrivateKey} The ElGamal private key.
   * @throws {Error}
   *             If the input data validation fails or the ElGamal private key
   *             could not be retrieved.
   */
  this.getElGamalPrivateKey = function(alias, password) {
    validator.checkIsNonEmptyString(
        alias,
        'Storage alias name of ElGamal private key to retrieve from Scytl key store');
    validator.checkIsType(
        password, 'string',
        'Password of ElGamal private key to retrieve from Scytl key store');

    var encryptedKey = scytlKeyStore.egPrivKeys[alias];
    checkEncryptedKey(encryptedKey, 'ElGamal private', alias);

    var elGamalPrivateKeyJson =
        getKey(password, encryptedKey, alias, 'ElGamal private');

    return elGamalService_.newPrivateKey(elGamalPrivateKeyJson);
  };

  function getDerivedPassword(password) {
    if (!derivedPasswords_.hasOwnProperty(password)) {
      var derivedKeyBytes = getDerivedKey(password);
      var derivedKeyBigInteger = new BigInteger(derivedKeyBytes);
      var derivedPassword =
          derivedKeyBigInteger.toString(constants.CHARACTER_MAX_RADIX);

      derivedPasswords_[password] = derivedPassword;
    }

    return derivedPasswords_[password];
  }

  function getDerivedKey(password) {
    if (!derivedKeys_.hasOwnProperty(password)) {
      derivedKeys_[password] = pbkdfDeriver_.derive(
          password, codec.base64Decode(scytlKeyStore.salt));
    }

    return derivedKeys_[password];
  }

  function checkEncryptedKey(key, keyType, alias) {
    if (!key) {
      throw new Error(
          'Could not find ' + keyType + ' key with storge alias name \'' +
          alias + '\'');
    }
  }

  function getKey(password, encryptedKey, alias, keyType) {
    var aliasAndKey = symmetricCipher_.init(getDerivedKey(password))
                          .decrypt(codec.base64Decode(encryptedKey));

    var aliasAndKeyBinaryEncoded = codec.binaryEncode(aliasAndKey);

    var decryptedAlias = aliasAndKeyBinaryEncoded.slice(0, alias.length);

    if (decryptedAlias !== alias) {
      throw new Error(
          'Expected decrypted alias for ' + keyType + ' key to be \'' + alias +
          '\'; Found \'' + decryptedAlias +
          '\' Check password provided to retrieve key from store.');
    }

    return aliasAndKeyBinaryEncoded.slice(alias.length, aliasAndKey.length);
  }

  function checkScytlKeyStore(scytlKeyStore) {
    validator.checkIsObjectWithProperties(
        scytlKeyStore, 'Scytl key store to load');

    if (typeof scytlKeyStore.salt === 'undefined') {
      throw new Error('Field \'salt\' is undefined in Scytl key store to load');
    }

    if (typeof scytlKeyStore.store === 'undefined') {
      throw new Error(
          'Field \'store\' is undefined in Scytl key store to load');
    }
  }
}
