/*
 * 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 EncryptedSecretKeyPair = require('./encrypted-secret-keypair');
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 codec = require('scytl-codec');

module.exports = DigitalEnvelopeGenerator;

/**
 * @class DigitalEnvelopeGenerator
 * @classdesc The digital envelope generator API. To instantiate this object,
 *            use the method {@link DigitalEnvelopeService.newGenerator}.
 * @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 DigitalEnvelopeGenerator(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 secretKeyGenerator_ = symmetricService.newKeyGenerator();
  var symmetricCipher_ = symmetricService.newCipher();
  var macHandler_ = symmetricService.newMacHandler();

  var asymmetricEncrypter_ = asymmetricService.newEncrypter();

  var publicKeys_;

  /**
   * Initializes the Digital envelope generator with one or more public keys.
   *
   * @function init
   * @memberof DigitalEnvelopeGenerator
   * @param {...string}
   *            arguments A comma separated list of the public keys, each in
   *            PEM format.
   * @returns {DigitalEnvelopeGenerator} A reference to this object, to
   *          facilitate method chaining.
   * @throws {Error}
   *             If the input data validation fails.
   */
  this.init = function() {
    publicKeys_ = Array.prototype.slice.call(arguments);
    validator.checkIsNonEmptyStringArray(
        publicKeys_,
        'public keys (PEM encoded) to initialize digital envelope generator');

    return this;
  };

  /**
   * Generates a digital envelope, given the data to store in the envelope.
   * The envelope can be generated with one or more public keys, each of whose
   * corresponding private key can be used to open the envelope. Before using
   * this method, the generator must have been initialized with at least one
   * public key, via the method {@link DigitalEnvelopeGenerator.init}.
   *
   * @function generate
   * @memberof DigitalEnvelopeGenerator
   * @param {Uint8Array|
   *            string} data The data to store in the digital envelope. Data
   *            of type <code>string</code> will be UTF-8 encoded.
   * @returns {DigitalEnvelope} The generated digital envelope.
   * @throws {Error}
   *             If the input data validation fails or the digital envelope
   *             could not be generated.
   */
  this.generate = function(data) {
    if (typeof publicKeys_ === 'undefined') {
      throw new Error(
          'Could not generate digital envelope; Envelope generator has not been initialized with any public key');
    }

    if (typeof data === 'string') {
      data = codec.utf8Encode(data);
    }
    validator.checkIsInstanceOf(
        data, Uint8Array, 'Uint8Array', 'Data to store in digital envelope');

    try {
      // Generate encryption secret key and use it to symmetrically encrypt
      // data.
      var encryptionSecretKey = secretKeyGenerator_.nextEncryptionKey();
      var encryptedData =
          symmetricCipher_.init(encryptionSecretKey).encrypt(data);

      // Generate MAC secret key and use it to generate MAC of symmetrically
      // encrypted data.
      var macSecretKey = secretKeyGenerator_.nextMacKey();
      var mac = macHandler_.init(macSecretKey).generate(encryptedData);

      // Construct bit-wise concatenation of encryption and MAC secret keys.
      var secretKeyPair =
          bitwise.concatenate(encryptionSecretKey, macSecretKey);

      // Generate list of asymmetric encryptions of the secret key
      // concatenation, each encryption created using a different one of the
      // public keys provided as input for the digital envelope generation.
      var encryptedSecretKeyPairs = [];
      for (var i = 0; i < publicKeys_.length; i++) {
        var publicKeyPem = publicKeys_[i];
        var encryptedSecretKeyPair =
            asymmetricEncrypter_.init(publicKeyPem).encrypt(secretKeyPair);
        encryptedSecretKeyPairs.push(
            new EncryptedSecretKeyPair(encryptedSecretKeyPair, publicKeyPem));
      }

      return new DigitalEnvelope(encryptedData, mac, encryptedSecretKeyPairs);
    } catch (error) {
      throw new Error('Digital envelope could not be generated. ' + error);
    }
  };
}
