/*
 * 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 validator = require('./input-validator');
var codec = require('scytl-codec');
var forge = require('node-forge');

module.exports = Pkcs12KeyStore;

/**
 * @class Pkcs12KeyStore
 * @classdesc Encapsulates a PKCS #12 key store. To instantiate this object, use
 *            the method {@link KeyStoreService.newPkcs12KeyStore}.
 * @hideconstructor
 * @param {Uint8Array|string}
 *            keyStore The provided PKCS #12 key store as a DER encoded ASN.1
 *            structure <b>OR</b> such a structure Base64 encoded.
 * @param {string}
 *            password The password to load the PKCS #12 key store.
 * @throws {Error}
 *             If the input data validation fails or the underlying key store
 *             could not be loaded.
 */
function Pkcs12KeyStore(keyStore, password) {
  var pkcs12Der;
  if (typeof keyStore !== 'string') {
    pkcs12Der = keyStore;
  } else {
    validator.checkIsNonEmptyString(
        keyStore, 'PKCS12 key store, Base64 encoded, to load');
    pkcs12Der = codec.base64Decode(keyStore);
  }
  validator.checkIsInstanceOf(
      pkcs12Der, Uint8Array, 'Uint8Array',
      'PKCS12 key store, DER encoded, to load');

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

  var forgePkcs12Asn1_;
  var forgePkcs12_;
  try {
    forgePkcs12Asn1_ = forge.asn1.fromDer(codec.binaryEncode(pkcs12Der), false);
    forgePkcs12_ =
        forge.pkcs12.pkcs12FromAsn1(forgePkcs12Asn1_, false, password);
  } catch (error) {
    throw new Error('Could not load PKCS12 key store; ' + error);
  }

  /**
   * Retrieves a private key stored inside the PKCS #12 key store, given the
   * key's storage alias name and password
   *
   * @function getPrivateKey
   * @memberof Pkcs12KeyStore
   * @param {string}
   *            alias The storage alias name of the private key to retrieve.
   * @param {string}
   *            password The password used to store 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 PKCS12 key store');
    checkPassword(
        password, forgePkcs12Asn1_,
        'Password to retrieve private key from PKCS12 key store');

    try {
      var privateKeyPem;
      var privateKeyFound = false;
      for (var i = 0; i < forgePkcs12_.safeContents.length; i++) {
        var safeContents = forgePkcs12_.safeContents[i];
        for (var j = 0; j < safeContents.safeBags.length; j++) {
          var safeBag = safeContents.safeBags[j];
          if (safeBag.type === forge.pki.oids.pkcs8ShroudedKeyBag &&
              safeBag.attributes.friendlyName[0] === alias) {
              privateKeyPem = forge.pki.privateKeyToPem(safeBag.key);
              privateKeyFound = true;
              break;
          }
        }
        if (privateKeyFound) {
          break;
        }
      }

      if (!privateKeyPem) {
        throw new Error(
            'Could not find any private key with alias \'' + alias +
            '\' in PKCS12 key store.');
      }

      return privateKeyPem;
    } catch (error) {
      throw new Error(
          'Could not retrieve private key with alias \'' + alias +
          '\' from PKCS12 key store; ' + error);
    }
  };

  /**
   * Retrieves a certificate stored inside the PKCS #12 key store, given the
   * storage alias name of the certificate or that of its associated private
   * key entry.
   *
   * @function getCertificate
   * @memberof Pkcs12KeyStore
   * @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 or that of its associated private key entry, to retrieve from PKCS12 key store');

    try {
      var certificatePem;
      var certificateFound = false;
      for (var i = 0; i < forgePkcs12_.safeContents.length; i++) {
        var safeContents = forgePkcs12_.safeContents[i];
        for (var j = 0; j < safeContents.safeBags.length; j++) {
          var safeBag = safeContents.safeBags[j];
          if (safeBag.type === forge.pki.oids.certBag &&
              safeBag.attributes && safeBag.attributes.friendlyName &&
              safeBag.attributes.friendlyName[0] === alias) {
              certificatePem = forge.pki.certificateToPem(safeBag.cert);
              certificateFound = true;
              break;            
          }
        }
        if (certificateFound) {
          break;
        }
      }

      if (!certificatePem) {
        throw new Error(
            'Could not find any certificate with alias \'' + alias +
            '\' in PKCS12 key store.');
      }

      return certificatePem;
    } catch (error) {
      throw new Error(
          'Could not retrieve certificate with alias \'' + alias +
          '\' from PKCS12 key store; ' + error);
    }
  };

  /**
   * Retrieves a certificate stored inside the PKCS #12 key store, given the
   * certificate's subject common name.
   *
   * @function getCertificateBySubject
   * @memberof Pkcs12KeyStore
   * @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 PKCS12 key store');

    try {
      var certificatePem;
      var certificateFound = false;
      for (var i = 0; i < forgePkcs12_.safeContents.length; i++) {
        var safeContents = forgePkcs12_.safeContents[i];
        for (var j = 0; j < safeContents.safeBags.length; j++) {
          var safeBag = safeContents.safeBags[j];

          if (safeBag.type === forge.pki.oids.certBag &&
              safeBag.cert.subject.getField('CN').value === subjectCN) {
              certificatePem = forge.pki.certificateToPem(safeBag.cert);
              certificateFound = true;
              break;
          }
        }
        if (certificateFound) {
          break;
        }
      }

      if (!certificatePem) {
        throw new Error(
            'Could not find any certificate with subject common name \'' +
            subjectCN + '\' in PKCS12 key store.');
      }

      return certificatePem;
    } catch (error) {
      throw Error(
          'Could not retrieve certificate with subject common name \'' +
          subjectCN + '\' from PKCS12 key store; ' + error);
    }
  };

  /**
   * Retrieves a certificate chain stored inside the PKCS #12 key store, given
   * the storage alias name of the chain's associated private key entry.
   *
   * @function getCertificateChain
   * @memberof Pkcs12KeyStore
   * @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 private key associated with certificate chain to retrieve from PKCS12 key store');

    try {
      // Loop through certificate safe bags of PKCS12.
      var certificatePemChain = [];
      var savedBags = {};
      var nextBagId;
      for (var i = 0; i < forgePkcs12_.safeContents.length; i++) {
        var safeContents = forgePkcs12_.safeContents[i];
        for (var j = 0; j < safeContents.safeBags.length; j++) {
          var safeBag = safeContents.safeBags[j];
          if (safeBag.type === forge.pki.oids.certBag) {
            var nextBag;

            // If no bags have been found yet and this is the safe bag
            // associated with the alias (i.e. the leaf safe bag).
            // Or if this happens to be the next bag in the chain.
            if ((!nextBagId && safeBag.attributes &&
                 safeBag.attributes.friendlyName &&
                 safeBag.attributes.friendlyName[0] === alias) ||
                (nextBagId && nextBagId === safeBag.cert.subject.hash)) {
              nextBag = safeBag;
            }            
            // If this is a bag higher up in the chain, and so will need to be
            // added later.
            else {
              savedBags[safeBag.cert.subject.hash] = safeBag;
            }

            // Add this bag and any saved bags to the chain, and in the proper
            // order.
            while (nextBag) {
              var certificatePem = forge.pki.certificateToPem(nextBag.cert);
              certificatePemChain.push(certificatePem);

              // If this bag is the root certificate, then the process is
              // finished.
              if (nextBag.cert.subject.hash === nextBag.cert.issuer.hash) {
                return certificatePemChain;
              }

              // Save the issuer of this bag as the ID of the next bag to add to
              // the chain.
              nextBagId = nextBag.cert.issuer.hash;

              // Add the saved bag corresponding to the ID of the next bag to
              // the chain.
              nextBag = savedBags[nextBagId];
            }
          }
        }
      }

      // Check whether any certificate safe bags were found.
      if (certificatePemChain.length === 0) {
        throw new Error(
            'Could not find any certificate chains with storage alias name \'' +
            alias + '\' in PKCS12 key store');
      }

      return certificatePemChain;
    } catch (error) {
      throw new Error(
          'Could not retrieve certificate chain with storage alias name \'' +
          alias + '\' from PKCS12 key store; ' + error);
    }
  };

  function checkPassword(password, pkcs12Asn1, label) {
    validator.checkIsType(password, 'string', label);

    try {
      forge.pkcs12.pkcs12FromAsn1(pkcs12Asn1, false, password);
    } catch (error) {
      throw new Error(label + ' is not valid');
    }
  }
}
