/*
 * 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 DigitalEnvelope = require('./envelope');
var cryptoPolicy = require('scytl-cryptopolicy');
var asymmetric = require('scytl-asymmetric');
var symmetric = require('scytl-symmetric');
var validator = require('./input-validator');
var bitwise = require('scytl-bitwise');
var forge = require('node-forge');

module.exports = DigitalEnvelopeOpener;

/**
 * @class DigitalEnvelopeOpener
 * @classdesc The digital envelope opener API. To instantiate this object, use
 *            the method {@link DigitalEnvelopeService.newOpener}.
 * @hideconstructor
 * @param {Object}
 *            [options] An object containing optional arguments.
 * @param {Policy}
 *            [options.policy=Default policy] The cryptographic policy to use.
 * @param {SymmetricCryptographyService}
 *            [options.symmetricCryptographyService=Created internally] The
 *            symmetric cryptography service to use.
 * @param {AsymmetricCryptographyService}
 *            [options.asymmetricCryptographyService=Created internally] The
 *            asymmetric cryptography service to use.
 */
function DigitalEnvelopeOpener(options) {
  options = options || {};

  var policy_ = cryptoPolicy.newInstance();
  if (options.policy) {
    policy_.symmetric = options.policy.digitalEnvelope.symmetric;
    policy_.asymmetric.cipher =
        options.policy.digitalEnvelope.asymmetric.cipher;
  }

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

  var asymmetricService;
  if (options.asymmetricCryptographyService) {
    asymmetricService = options.asymmetricCryptographyService;
  } else {
    asymmetricService = asymmetric.newService({policy: policy_});
  }

  var symmetricCipher_ = symmetricService.newCipher();
  var macHandler_ = symmetricService.newMacHandler();

  var asymmetricDecrypter_ = asymmetricService.newDecrypter();

  var privateKey_;

  /**
   * Initializes the Digital envelope opener with the provided private key.
   *
   * @function init
   * @memberof DigitalEnvelopeOpener
   * @param {string}
   *            privateKey The private key, in PEM format.
   * @returns {DigitalEnvelopeOpener} A reference to this object, to
   *          facilitate method chaining.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.init = function(privateKey) {
    validator.checkIsNonEmptyString(
        privateKey,
        'private key (PEM encoded) to initialize digital envelope opener');

    privateKey_ = privateKey;

    return this;
  };

  /**
   * Opens a digital envelope and retrieves its data. The envelope can be
   * opened with any private key corresponding to a public key that was used
   * to generate the envelope. Before using this method, the opener must have
   * been initialized with a private key, via the method
   * {@link DigitalEnvelopeOpener.init}.
   *
   * @function open
   * @memberof DigitalEnvelopeOpener
   * @param {DigitalEnvelope}
   *            envelope The digital envelope to open.
   * @returns {Uint8Array} The data stored in the digital envelope.
   * @throws {Error}
   *             If the input data validation fails or the digital envelope
   *             could not be opened.
   */
  this.open = function(envelope) {
    if (typeof privateKey_ === 'undefined') {
      throw new Error(
          'Could not open digital envelope; Envelope opener has not been initialized with any private key');
    }

    validator.checkIsInstanceOf(
        envelope, DigitalEnvelope, 'DigitalEnvelope',
        'Digital envelope to open');

    try {
      // Retrieve data from digital envelope.
      var encryptedData = envelope.encryptedData;
      var mac = envelope.mac;
      var encryptedSecretKeyPairs = envelope.encryptedSecretKeyPairs;

      // Retrieve asymmetric encryption of bit-wise concatenation of encryption
      // and MAC secret keys that can be decrypted with private key provided as
      // input.
      var encryptedSecretKeyPair =
          getEncryptedSecretKeyPair(encryptedSecretKeyPairs, privateKey_);

      // Asymmetrically decrypt bit-wise concatenation of encryption and MAC
      // secret keys.
      var secretKeyPair = asymmetricDecrypter_.init(privateKey_)
                              .decrypt(encryptedSecretKeyPair);

      // Extract secret keys from bit-wise concatenation.
      var encryptionSecretKeyLength =
          policy_.symmetric.secretKey.encryption.lengthBytes;
      var macSecretKeyLength = policy_.symmetric.secretKey.mac.lengthBytes;
      var encryptionSecretKey =
          bitwise.slice(secretKeyPair, 0, encryptionSecretKeyLength);
      var macSecretKey = bitwise.slice(
          secretKeyPair, encryptionSecretKeyLength,
          encryptionSecretKeyLength + macSecretKeyLength);

      // Check integrity of digital envelope.
      if (!macHandler_.init(macSecretKey).verify(mac, encryptedData)) {
        throw new Error('Integrity of digital envelope could not be verified.');
      }

      return symmetricCipher_.init(encryptionSecretKey).decrypt(encryptedData);
    } catch (error) {
      throw new Error('Digital envelope could not be opened; ' + error);
    }
  };

  function getEncryptedSecretKeyPair(encryptedSecretKeyPairs, privateKeyPem) {
    // Extract modulus and public exponent from private key.
    var privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
    var modulus = privateKey.n;
    var publicExponent = privateKey.e;

    for (var i = 0; i < encryptedSecretKeyPairs.length; i++) {
      var publicKeyPem = encryptedSecretKeyPairs[i].publicKey;
      var publicKey = forge.pki.publicKeyFromPem(publicKeyPem);
      if (publicKey.n.equals(modulus) && publicKey.e.equals(publicExponent)) {
        return encryptedSecretKeyPairs[i].encryptedKeyPair;
      }
    }

    throw new Error(
        'Could not find an asymmetric encryption of the secret key concatenation that could be decrypted using the provided private key.');
  }
}
