/*
 * 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
 */
/**
 * Scytl Key Store. It is a custom keystore.
 */
cryptolib.modules.stores.sks = function(box) {
  'use strict';

  box.stores = box.stores || {};
  box.stores.sks = {};

  var converters;
  var secretKeyGeneratorFactory;
  var cipher;
  var pkcs12Module;
  var exceptions;
  var homomorphicKeyFactory;

  var f = function(box) {
    converters = new box.commons.utils.Converters();
    cipher = (new box.symmetric.cipher.factory.SymmetricCipherFactory())
                 .getCryptoSymmetricCipher();
    secretKeyGeneratorFactory =
        new box.primitives.derivation.factory.SecretKeyGeneratorFactory();
    pkcs12Module = box.stores.pkcs12;
    exceptions = box.commons.exceptions;
    homomorphicKeyFactory = new box.homomorphic.keypair.factory.KeyFactory();
  };

  f.policies = {
    symmetric: {
      secretkey: {
        encryption:
            {length: box.policies.symmetric.secretkey.encryption.length},
        mac: {length: box.policies.symmetric.secretkey.mac.length},
        secureRandom:
            {provider: box.policies.symmetric.secretkey.secureRandom.provider}
      },
      cipher: {
        provider: box.policies.symmetric.cipher.provider,
        algorithm: box.policies.symmetric.cipher.algorithm,
        initializationVectorLengthBytes:
            box.policies.symmetric.cipher.initializationVectorLengthBytes,
        secureRandom:
            {provider: box.policies.symmetric.cipher.secureRandom.provider}
      }
    },
    primitives: {
      derivation: {
        pbkdf: {
          provider: box.policies.primitives.derivation.pbkdf.provider,
          hash: box.policies.primitives.derivation.pbkdf.hash,
          saltLengthBytes:
              box.policies.primitives.derivation.pbkdf.saltLengthBytes,
          keyLengthBytes:
              box.policies.primitives.derivation.pbkdf.keyLengthBytes,
          iterations: box.policies.primitives.derivation.pbkdf.iterations
        }
      }
    }
  };

  cryptolib(
      'commons', 'primitives.derivation', 'symmetric.cipher', 'stores.pkcs12',
      'homomorphic.keypair', f);

  /**
   * Defines a SksReader.
   *
   * @param data.
   */
  box.stores.sks.SksReader = function(data) {
    // A cache of keys derived from store passwords, to prevent consecutive
    // operations on a keystore to derive keys each time.
    var bigintKeys = {};
    var derivedKeys = {};

    // Create the PBKDF generator.
    var cryptoPbkdfSecretKeyGenerator = secretKeyGeneratorFactory.createPBKDF();

    try {
      var loadKeyStore = function(data) {
        var keyStore;
        if (data) {
          if (typeof data === 'string') {
            keyStore = JSON.parse(data);
          } else if (typeof data === 'object') {
            keyStore = data;
          }
        }

        return keyStore;
      };

      var validateKeyStoreStructure = function(keyStore) {
        if (typeof keyStore.salt === 'undefined') {
          throw new Error('Missing salt');
        }

        if (typeof keyStore.store === 'undefined') {
          throw new Error('Missing store');
        }
      };

      this.keyStore = loadKeyStore(data);
      validateKeyStoreStructure(this.keyStore);

      this.store = new pkcs12Module.Pkcs12(this.keyStore.store);

    } catch (error) {
      throw new Error('Invalid keyStore: ' + error.message);
    }

    function validateEncryptedKey(encryptedKey, keyType) {
      if (!encryptedKey) {
        throw new exceptions.CryptoLibException(
            'The keyStore does not contain a key of type ' + keyType);
      }
    }

    /**
     * Derive a key from the keystore password.
     *
     * @param {string} passwordB64 the Base64-encoded keystore password
     * @returns the Base64-encoded key derived from the password
     */
    this._deriveKey = function(passwordB64) {
      return cryptoPbkdfSecretKeyGenerator.generateSecret(
          passwordB64, this.keyStore.salt);
    };

    /**
     * Get the Base64-encoded key derived from the store password.
     *
     * @param {string} passwordB64 the Base64-encoded store password
     * @return the Base64-encoded key derived from the store password
     */
    this._getDerivedKey = function(passwordB64) {
      if (!derivedKeys.hasOwnProperty(passwordB64)) {
        derivedKeys[passwordB64] = this._deriveKey(passwordB64);
      }

      return derivedKeys[passwordB64];
    };

    /**
     * Get the Base64-encoded BigInteger representation of the key derived from
     * the store password.
     *
     * @param {string} passwordB64 the Base64-encoded store password
     * @returns the Base64-encoded BigInteger representation of the key derived
     * from the store password
     */
    this._getBigIntKey = function(passwordB64) {
      if (!bigintKeys.hasOwnProperty(passwordB64)) {
        var derivedPasswordBase64 = this._getDerivedKey(passwordB64);

        var derivedPassword = converters.bytesFromString(
            converters.base64Decode(derivedPasswordBase64));
        var derivedPasswordString =
            new box.forge.jsbn.BigInteger(derivedPassword);
        var longPasswordB64 =
            converters.base64Encode(derivedPasswordString.toString(36));

        bigintKeys[passwordB64] = longPasswordB64;
      }

      return bigintKeys[passwordB64];
    };

    this._getKey = function(alias, encryptedKey, passwordB64, keyType) {

      validateEncryptedKey(encryptedKey, keyType);

      var aliasAndKeyBase64 = this.decryptKey(encryptedKey, passwordB64);

      var aliasAndKey = converters.base64Decode(aliasAndKeyBase64);

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

      if (decryptedAlias !== alias) {
        throw new exceptions.CryptoLibException(
            'The decrypted ' + keyType +
            ' key does not correspond to the given alias.');
      }

      var key = aliasAndKey.slice(alias.length, aliasAndKey.length);
      var keyBase64 = converters.base64Encode(key);
      return keyBase64;
    };
  };

  box.stores.sks.SksReader.prototype = {
    getPrivateKeys: function(passwordB64) {
      var updateArguments = function(bigIntKey, args) {
        var argumentsCopy = Array.prototype.slice.call(args, 1);
        argumentsCopy.unshift(bigIntKey);

        return argumentsCopy;
      };

      return this.store.getPrivateKey.apply(
          this.store,
          updateArguments(this._getBigIntKey(passwordB64), arguments));
    },

    getSecretKey: function(alias, passwordB64) {
      var encryptedKey = this.keyStore.secrets[alias];
      return this._getKey(alias, encryptedKey, passwordB64, 'secret');
    },

    getElGamalPrivateKey: function(alias, passwordB64) {
      var encryptedKey = this.keyStore.egPrivKeys[alias];
      var keyBase64 =
          this._getKey(alias, encryptedKey, passwordB64, 'ElGamal private');
      return homomorphicKeyFactory.createPrivateKey(keyBase64);
    },

    decryptKey: function(encryptedKey, passwordB64) {
      return cipher.decrypt(this._getDerivedKey(passwordB64), encryptedKey);
    },

    /**
     * Retrieves the private key chain
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @return Private key chain of as PrivateKeyChain object, which
     *         contains aliases and their related private keys in PEM
     *         format.
     */
    getPrivateKeyChain: function(passwordB64) {
      return this.store.getPrivateKeyChain(this._getBigIntKey(passwordB64));
    },

    /**
     * Retrieves the full certificate chain.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @return Certificate chain as array of strings in PEM format.
     */
    getCertificateChain: function(passwordB64) {
      return this.store.getCertificateChain(this._getBigIntKey(passwordB64));
    },

    /**
     * Retrieves a certificate chain by private key alias
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @param alias
     *            Alias to filter
     * @return Certificate chain as array of strings in PEM format.
     */
    getCertificateChainByAlias: function(passwordB64, alias) {
      return this.store.getCertificateChainByAlias(
          this._getBigIntKey(passwordB64), alias);
    },

    /**
     * Retrieves a certificate by subject common name.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @param subjectCn
     *            Subject common name of certificate to retrieve, as string.
     * @return Certificate with given subject common name, as string in PEM
     *         format.
     */
    getCertificateBySubject: function(passwordB64, subjectCn) {
      return this.store.getCertificateBySubject(
          this._getBigIntKey(passwordB64), subjectCn);
    }
  };
};
