/*
 * 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
 */
cryptolib.modules.stores = cryptolib.modules.stores || {};

/**
 * Defines a pkcs12 store.
 */
cryptolib.modules.stores.pkcs12 = function(box) {
  'use strict';

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

  var converters;
  var exceptions;

  cryptolib('commons', function(box) {
    converters = new box.commons.utils.Converters();
    exceptions = box.commons.exceptions;
  });

  /**
   * Defines a PKCS12.
   *
   * @param pkcs12DerB64
   *            PKCS12, as string in Base64 encoded DER format.
   */
  box.stores.pkcs12.Pkcs12 = function(pkcs12DerB64) {
    this.pkcs12Der = converters.base64Decode(pkcs12DerB64);
    this.pkcs12Asn1 = forge.asn1.fromDer(this.pkcs12Der, false);
  };

  box.stores.pkcs12.Pkcs12.prototype = {

    /**
     * Retrieves the private key of the PKCS12.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @return Private key of PKCS12, as string in PEM format.
     */
    getPrivateKey: function(passwordB64) {
      var aliases = Array.prototype.slice.call(arguments, 1);

      function isAliasInList(alias) {
        var returnValue = false;

        if (aliases.indexOf(alias) >= 0) {
          returnValue = true;
        }

        return returnValue;
      }

      var privateKeyAlias = '';
      try {
        var password = converters.base64Decode(passwordB64);

        var pkcs12 =
            forge.pkcs12.pkcs12FromAsn1(this.pkcs12Asn1, false, password);

        // Retrieve private key safe bag from PKCS12.
        var privateKeys = {};
        for (var i = 0; i < pkcs12.safeContents.length; i++) {
          var safeContents = pkcs12.safeContents[i];
          for (var j = 0; j < safeContents.safeBags.length; j++) {
            var safeBag = safeContents.safeBags[j];
            if (safeBag.type === forge.pki.oids.pkcs8ShroudedKeyBag) {
              privateKeyAlias = safeBag.attributes.friendlyName[0];
              if (aliases.length === 0 || isAliasInList(privateKeyAlias)) {
                privateKeys[privateKeyAlias] =
                    forge.pki.privateKeyToPem(safeBag.key);
              }
            }
          }
        }

        // Check whether any private key safe bags were found.
        if (Object.getOwnPropertyNames(privateKeys).length === 0) {
          throw new exceptions.CryptoLibException(
              'Could not find any private key safe bags in PKCS12.');
        }

        return privateKeys;
      } catch (error) {
        var errorMsg = 'Could not retrieve private key from PKCS12.';
        if (privateKeyAlias.length > 0) {
          errorMsg = 'Could not retrieve private key with alias ' +
              privateKeyAlias + ' from PKCS12.';
        }

        throw new exceptions.CryptoLibException(errorMsg);
      }
    },

    /**
     * Retrieves the private key chain from the PKCS12.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @return Private key chain of PKCS12, as PrivateKeyChain object.
     */
    getPrivateKeyChain: function(passwordB64) {
      try {
        var password = converters.base64Decode(passwordB64);

        var pkcs12 =
            forge.pkcs12.pkcs12FromAsn1(this.pkcs12Asn1, false, password);

        // Retrieve private key safe bags from PKCS12.
        var privateKeyChain = new box.stores.pkcs12.PrivateKeyChain();
        for (var i = 0; i < pkcs12.safeContents.length; i++) {
          var safeContents = pkcs12.safeContents[i];
          for (var j = 0; j < safeContents.safeBags.length; j++) {
            var safeBag = safeContents.safeBags[j];

            if (safeBag.type === forge.pki.oids.pkcs8ShroudedKeyBag) {
              var alias = safeBag.attributes.friendlyName[0];
              var privateKeyPem = forge.pki.privateKeyToPem(safeBag.key);
              privateKeyChain.add(alias, privateKeyPem);
            }
          }
        }

        // Check whether any private key safe bags were found.
        if (privateKeyChain.length === 0) {
          throw new exceptions.CryptoLibException(
              'Could not find any private key safe bags in PKCS12.');
        }

        return privateKeyChain;
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not retrieve private key chain from PKCS12.', error);
      }
    },

    /**
     * Retrieves the certificate chain of the PKCS12.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @return Certificate chain of PKCS12, as array of strings in PEM
     *         format.
     */
    getCertificateChain: function(passwordB64) {
      try {
        var password = converters.base64Decode(passwordB64);

        var pkcs12 =
            forge.pkcs12.pkcs12FromAsn1(this.pkcs12Asn1, false, password);

        // Retrieve certificate safe bags from PKCS12.
        var certificatePemChain = [];
        for (var i = 0; i < pkcs12.safeContents.length; i++) {
          var safeContents = pkcs12.safeContents[i];
          for (var j = 0; j < safeContents.safeBags.length; j++) {
            var safeBag = safeContents.safeBags[j];

            if (safeBag.type === forge.pki.oids.certBag) {
              var certificatePem = forge.pki.certificateToPem(safeBag.cert);
              certificatePemChain.push(certificatePem);
            }
          }
        }

        // Check whether any certificate safe bags were found.
        if (certificatePemChain.length === 0) {
          throw new exceptions.CryptoLibException(
              'Could not find any certificate safe bags in PKCS12.');
        }

        return certificatePemChain;
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not retrieve certificate chain from PKCS12.', error);
      }
    },

    /**
     * Retrieves the certificate chain of the PKCS12 filtered by alias.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @param alias
     *            Alias to filter
     * @return Certificate chain of PKCS12, as array of strings in PEM
     *         format.
     */
    getCertificateChainByAlias: function(passwordB64, alias) {
      try {
        var password = converters.base64Decode(passwordB64);

        var pkcs12 =
            forge.pkcs12.pkcs12FromAsn1(this.pkcs12Asn1, false, password);

        var discardedBags = {};
        var nextBagId = null;

        // Retrieve certificate safe bags from PKCS12.
        var certificatePemChain = [];
        for (var i = 0; i < pkcs12.safeContents.length; i++) {
          var safeContents = pkcs12.safeContents[i];
          for (var j = 0; j < safeContents.safeBags.length; j++) {
            var safeBag = safeContents.safeBags[j];
            if (safeBag.type === forge.pki.oids.certBag) {
              var bagFound = null;

              // if not initializated the chain, find the one with alias
              // or if is the one that goes next into the list take it
              if ((!nextBagId && safeBag.attributes &&
                  safeBag.attributes.friendlyName &&
                  safeBag.attributes.friendlyName[0] === alias) ||
                  (nextBagId && nextBagId === safeBag.cert.subject.hash)) {
                bagFound = safeBag;
              }
              // otherwise, if not initializated and it's not the alias, save it
              // for later use
              else {
                discardedBags[safeBag.cert.subject.hash] = safeBag;
              }

              // add the bag and check if we have the next one in the list we
              // had
              while (bagFound) {
                // transform the current to PEM and save it
                var certificatePem = forge.pki.certificateToPem(bagFound.cert);
                certificatePemChain.push(certificatePem);

                // we arrived to the root so we are done!
                if (bagFound.cert.subject.hash === bagFound.cert.issuer.hash) {
                  return certificatePemChain;
                }

                // save which is the next bag we need
                nextBagId = bagFound.cert.issuer.hash;
                // go for the next in the list of discarded ones
                bagFound = discardedBags[nextBagId];
              }
            }
          }
        }

        // Check whether any certificate safe bags were found.
        if (certificatePemChain.length === 0) {
          throw new exceptions.CryptoLibException(
              'Could not find any certificates for that alias.');
        }

        return certificatePemChain;

      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not retrieve certificate chain from PKCS12.', error);
      }
    },

    /**
     * Retrieves an issuer certificate from the certificate chain of the
     * PKCS12, given the issuer common name.
     *
     * @param passwordB64
     *            Password as string in Base64 encoded format.
     * @param issuerCn
     *            Issuer common name of certificate to retrieve, as string.
     * @return Certificate with given issuer common name, as string in PEM
     *         format.
     */
    getCertificateByIssuer: function(passwordB64, issuerCn) {
      try {
        var password = converters.base64Decode(passwordB64);

        var pkcs12 =
            forge.pkcs12.pkcs12FromAsn1(this.pkcs12Asn1, false, password);

        // Retrieve certificate from PKCS12.
        var certificate = null;
        var certificateFound = false;
        for (var i = 0; i < pkcs12.safeContents.length; i++) {
          var safeContents = pkcs12.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.issuer.getField('CN').value === issuerCn) {
                certificate = safeBag.cert;
                certificateFound = true;
                break;
            }
          }
          if (certificateFound) {
            break;
          }
        }

        // Check whether certificate was found.
        if (certificate === null) {
          throw new exceptions.CryptoLibException(
              'Could not find certificate in PKCS12 with issuer common name ' +
              issuerCn);
        }

        return forge.pki.certificateToPem(certificate);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not retrieve certificate with issuer common name ' +
                issuerCn + ' from PKCS12.',
            error);
      }
    },

    /**
     * Retrieves the subject certificate from the certificate chain of the
     * PKCS12, given the 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 issuer common name, as string in PEM
     *         format.
     */
    getCertificateBySubject: function(passwordB64, subjectCn) {
      try {
        var password = converters.base64Decode(passwordB64);

        var pkcs12 =
            forge.pkcs12.pkcs12FromAsn1(this.pkcs12Asn1, false, password);

        // Retrieve subject certificate from PKCS12.
        var certificate = null;
        var certificateFound = false;
        for (var i = 0; i < pkcs12.safeContents.length; i++) {
          var safeContents = pkcs12.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) {
                certificate = safeBag.cert;
                certificateFound = true;
                break;
            }
          }
          if (certificateFound) {
            break;
          }
        }

        // Check whether certificate was found.
        if (certificate === null) {
          throw new exceptions.CryptoLibException(
              'Could not find certificate in PKCS12 with subject common name ' +
              subjectCn);
        }

        return forge.pki.certificateToPem(certificate);
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not retrieve certificate with subject common name ' +
                subjectCn + ' from PKCS12.',
            error);
      }
    },

    /**
     * Retrieves the PKCS12 as a Base64 encoded DER string.
     *
     * @return PKCS12, as Base64 encoded DER string.
     */
    getBase64Encoded: function() {
      return converters.base64Encode(this.pkcs12Der);
    }
  };

  /**
   * Container class for a chain of private keys to be inserted in a key
   * store.
   */
  box.stores.pkcs12.PrivateKeyChain = function() {
    this.map = {};
  };

  box.stores.pkcs12.PrivateKeyChain.prototype = {

    /**
     * Adds a private key to the chain.
     *
     * @param alias
     *            Alias of private key, as string.
     * @param privateKeyPem
     *            Private key to add, as string in PEM format.
     */
    add: function(alias, privateKeyPem) {
      this.map[alias] = privateKeyPem;
    },

    /**
     * Retrieves a private key from the chain.
     *
     * @return Private key chain, as array of strings in PEM format.
     */
    get: function(alias) {
      return this.map[alias];
    }
  };
};
