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

/**
 * Defines the asymmetric signers.
 *
 * @namespace asymmetric/xmlsigner
 */
cryptolib.modules.asymmetric.xmlsigner = function(box) {
  'use strict';

  box.asymmetric = box.asymmetric || {};
  box.asymmetric.xmlsigner = {};

  /**
   * A module that holds certificates functionalities.
   *
   * @exports asymmetric/xmlsigner/factory
   */
  box.asymmetric.xmlsigner.factory = {};

  var policies = {
    algorithm: box.policies.asymmetric.signer.algorithm,
    provider: box.policies.asymmetric.signer.provider,
    hash: box.policies.asymmetric.signer.hash
  };

  var utils, exceptions, converters, parsers, stringUtils;

  var f = function(box) {
    exceptions = box.commons.exceptions;
    utils = box.commons.utils;
    converters = new box.commons.utils.Converters();
    parsers = new utils.Parsers();
    stringUtils = new utils.StringUtils();
  };

  cryptolib('commons', f);

  /**
   * A factory class for creating an XML digital signature verifier.
   *
   * @class
   */
  box.asymmetric.xmlsigner.factory.XmlSignerFactory = function() {};

  box.asymmetric.xmlsigner.factory.XmlSignerFactory.prototype = {
    /**
     * Gets a {@link asymmetric/xmlsigner.CryptoForgeXmlSigner}.
     *
     * @function
     * @return {asymmetric/xmlsigner.CryptoForgeXmlSigner}
     */
    getCryptoXmlSigner: function() {
      try {
        if (policies.provider === Config.asymmetric.signer.provider.FORGE) {
          return new CryptoForgeXmlSigner();
        } else {
          throw new exceptions.CryptoLibException(
              'No suitable provider for the signer was provided.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'A CryptoXmlSigner could not be obtained.', error);
      }
    }
  };

  /**
   * Holds xml signature properties.
   *
   * @class
   * @memberof asymmetric/xmlsigner
   */
  function XmlSignature(signatureNode) {
    // go through all the nodes to load all relevant information
    var signedInfoNode;
    var signatureValueNode;
    var canonicalizationMethodNode;
    var signatureMethodNode;
    var referenceNode;
    var digestMethodNode;
    var digestValueNode;
    var transformsNode;
    try {
      signedInfoNode = new utils.XPath(signatureNode.getXml(), 'SignedInfo');
      signatureValueNode =
          new utils.XPath(signatureNode.getXml(), 'SignatureValue');

      canonicalizationMethodNode =
          new utils.XPath(signedInfoNode.getXml(), 'CanonicalizationMethod');
      signatureMethodNode =
          new utils.XPath(signedInfoNode.getXml(), 'SignatureMethod');
      referenceNode = new utils.XPath(signedInfoNode.getXml(), 'Reference');

      digestMethodNode =
          new utils.XPath(referenceNode.getXml(), 'DigestMethod');
      digestValueNode = new utils.XPath(referenceNode.getXml(), 'DigestValue');

      transformsNode = new utils.XPath(referenceNode.getXml(), 'Transforms');
    } catch (error) {
      throw new exceptions.CryptoLibException(
          'Could not parse signed XML file', error);
    }

    // there are signatures without key, so we ignore them
    var rsaKeyValueNode;
    var modulusNode;
    var exponentNode;
    try {
      rsaKeyValueNode = new utils.XPath(
          signatureNode.getXml(), 'KeyInfo/KeyValue/RSAKeyValue');
      modulusNode = new utils.XPath(rsaKeyValueNode.getXml(), 'Modulus');
      exponentNode = new utils.XPath(rsaKeyValueNode.getXml(), 'Exponent');
    } catch (error) {
      // ignore if this part fails
    }

    // build the object structure
    /** @property {object} */
    this.info = {
      method: signatureMethodNode.getAttribute('Algorithm'),
      value: signatureValueNode.getValue(),
      meta: signatureNode.getAttribute('xmlns')
    };
    /** @property {object} */
    this.canonicalization = {
      method: canonicalizationMethodNode.getAttribute('Algorithm')
    };
    /** @property {object} */
    this.reference = {
      uri: referenceNode.getAttribute('URI'),
      transforms: [],
      digest: {
        method: digestMethodNode.getAttribute('Algorithm'),
        value: digestValueNode.getValue()
      }
    };
    /** @property {object} */
    this.rsakey = {
      modulus: modulusNode ? modulusNode.getValue() : '',
      exponent: exponentNode ? exponentNode.getValue() : ''
    };

    // update the transforms references
    var transforms = transformsNode.getChildren();
    for (var i = 0; i < transforms.length; i++) {
      if (transforms[i].nodeType === Node.ELEMENT_NODE) {
        var transformNode = new utils.XPath(transforms[i]);
        this.reference.transforms.push(transformNode.getAttribute('Algorithm'));
      }
    }
  }

  /**
   * @class
   * @memberof asymmetric/xmlsigner
   */
  function ExclusiveCanonicalization() {}

  /**
   * Sorts the attributes
   *
   * @function
   * @param xmlNode
   *            xml node, as a DOM.
   */
  ExclusiveCanonicalization.prototype.sortAttributes = function(xmlNode) {
    // gather all attributes and remove them
    var list = [];
    var attr;
    if (xmlNode.attributes) {
      for (var i = 0; i < xmlNode.attributes.length; i++) {
        attr = xmlNode.attributes[i];
        list.push({id: attr.name, val: xmlNode.getAttribute(attr.name)});
        xmlNode.removeAttribute(attr.name);
        i--;
      }
    }

    // sort the attributes
    list.sort(function(a, b) {
      if (a.id < b.id) {
        return -1;
      }
      if (a.id > b.id) {
        return 1;
      }
      return 0;
    });

    // reinsert the attributes
    for (var j = 0; j < list.length; j++) {
      attr = list[j];
      xmlNode.setAttribute(attr.id, attr.val);
    }
  };

  /**
   * Inits the process of canonicalization.
   *
   * @function
   * @param xmlNode
   *            xml node, as a DOM.
   *
   * @return xml node processed, as DOM.
   */
  ExclusiveCanonicalization.prototype.process = function(xml) {
    // traverse the tree through all the children
    for (var i = 0; i < xml.childNodes.length; i++) {
      var child = xml.childNodes[i];

      if (child.nodeType === Node.ELEMENT_NODE) {
        this.process(child);
      }
      // if are comments or other stuff, remove them
      else if (child.nodeType !== Node.TEXT_NODE) {
        child.parentNode.removeChild(child);
        --i;
      }
    }

    // sort the attributes
    this.sortAttributes(xml);

    // return the final object
    return xml;
  };

  /**
   * An xml signer.
   *
   * @class
   * @memberof asymmetric/xmlsigner
   */
  function CryptoForgeXmlSigner() {}

  CryptoForgeXmlSigner.prototype = {

    /**
     * Verifies the digital signature of a selfsigned xml.
     *
     * @function
     * @param publicKey
     *            Public key, as an object.
     * @param signedXml
     *            XML data, as a string
     * @param signatureParentNode
     *            The node where the signature is, as a string.
     *
     * @returns Boolean indicating whether signature verification was
     *          successful.
     */
    verify: function(publicKey, signedXml, signatureParentNode) {

      // clean the comments in the xml
      signedXml = this._removeXmlStringComments(signedXml);

      // loads the text to an xml
      var xml = parsers.xmlFromString(signedXml);

      // loads the xml signature structure to json
      var signature = this._loadSignature(xml, signatureParentNode);

      // check all the algorithms are the expected ones
      this._verifyAlgorithmsInSignature(signature);

      // check the public key is the expected one
      this._verifyPublicKeyInSignature(signature, publicKey);

      // check the references are well encoded
      this._validateReferences(signature, signedXml);

      // validates the signatures are right
      this._validateSignature(signature, publicKey);

      // if not exception raised, all went good
      return true;
    },

    /**
     * Gets the XML signature node from the main XML node
     *
     * @function
     * @private
     * @param xml
     *            XML where the signature is, as a Document Object
     * @param signatureParentNode
     *            The node where the signature is, as a string.
     *
     * @returns Object with the DOM node of the signature and and the
     *          signature in json.
     */
    _loadSignature: function(xml, signatureParentNode) {
      // access to the node where the signature is, and take the methods
      try {
        // get the signature node
        var xmlNode = new utils.XPath(xml, signatureParentNode);

        // create signature object to load the data from the signature
        var data = new XmlSignature(xmlNode);

        // return the main object
        return {xmlNode: xmlNode, data: data};
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not load the signature from the XML', error);
      }
    },

    /**
     * Verifies that the method algorithms in the EML and the properties
     * match.
     *
     * @function
     * @private
     * @param signature
     *            Signature, as a Javascript Object
     */
    _verifyAlgorithmsInSignature: function(signature) {
      try {
        if (signature.data.canonicalization.method !==
            'http://www.w3.org/2001/10/xml-exc-c14n#') {
          throw new exceptions.CryptoLibException(
              'Invalid canonicalization algorithm');
        }

        if (!stringUtils.containsSubString(
                signature.data.info.method, policies.algorithm) &&
            !stringUtils.containsSubString(
                signature.data.info.method, policies.hash)) {
          throw new exceptions.CryptoLibException(
              'Invalid signature algorithm');
        }

        if (!stringUtils.containsSubString(
                signature.data.reference.digest.method, policies.hash)) {
          throw new exceptions.CryptoLibException('Invalid digest algorithm');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not verify the data in the XML.', error);
      }
    },

    /**
     * Verifies the public key is the same than the one in the EML.
     *
     * @function
     * @private
     * @param signature
     *            Signature, as a Javascript Object
     * @param publicKey
     *            Public key, as an object.
     */
    _verifyPublicKeyInSignature: function(signature, publicKey) {
      // it has no key
      if (!signature.data.rsakey.exponent || !signature.data.rsakey.modulus) {
        return;
      }

      // if it has key, we validate it
      try {
        var signatureExponent =
            converters.base64ToBigInteger(signature.data.rsakey.exponent);
        var signatureModulus =
            converters.base64ToBigInteger(signature.data.rsakey.modulus);

        if (publicKey.e.compareTo(signatureExponent) !== 0 ||
            publicKey.n.compareTo(signatureModulus) !== 0) {
          throw new exceptions.CryptoLibException('Invalid public key');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not verify the data in the XML.', error);
      }
    },

    /**
     * Verifies the references of a selfsigned xml.
     *
     * @function
     * @private
     * @param signature
     *            Signature, as a Javascript Object
     */
    _validateReferences: function(signature, xmlString) {
      try {
        var xmlCanonString;

        // The canonicalization transformations are performed here.
        // Due to differences in the behaviour of Internet Explorer,
        // these transformations must be applied manually in the case
        // of that browser. For all other browsers, the transformations
        // are performed by applying a canonicalization algorithm.

        if (parsers.isIE()) {
          xmlCanonString = this._removeXmlStringHeader(xmlString);

          xmlCanonString = this._removeXmlStringSelfClosingTags(xmlCanonString);

          xmlCanonString = this._removeXmlStringSignature(xmlCanonString);

          xmlCanonString = this._removeXmlStringFirstCharacter(xmlCanonString);
        } else {
          var xmlCanon = null;

          // apply all transforms to the xml
          for (var i = 0; i < signature.data.reference.transforms.length; i++) {
            // get the algorithm name
            var algorithm = signature.data.reference.transforms[i];

            // consume it if it exists
            if (algorithm in this._transformAlgorithmMap) {
              xmlCanon = this._transformAlgorithmMap[algorithm](
                  signature.xmlNode.getXml());
            }
          }

          xmlCanon = this._removeXmlHeader(xmlCanon);
          xmlCanonString = parsers.xmlToString(xmlCanon, true);
        }

        var digester =
            this._getHashMethod(signature.data.reference.digest.method);
        digester.start();

        digester.update(
            parsers.removeCarriageReturnChars(xmlCanonString), 'utf8');
        var messageDigest = digester.digest();

        // check if the result is valid
        var encodedMessage = forge.util.encode64(messageDigest.getBytes());

        if (encodedMessage !== signature.data.reference.digest.value) {
          throw new exceptions.CryptoLibException(
              'Invalid reference: \'' + encodedMessage +
              '\' when was expected \'' +
              signature.data.reference.digest.value + '\'');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not validate references.', error);
      }
    },

    /**
     * Verifies the digital signature of a selfsigned xml.
     *
     * @function
     * @private
     * @param signature
     *            Signature, as a Javascript Object
     * @param publicKey
     *            Public key, as an object.
     */
    _validateSignature: function(signature, publicKey) {
      try {
        var signedInfoNode =
            new utils.XPath(signature.xmlNode.getXml(), '/SignedInfo');
        var algorithm = signature.data.canonicalization.method;

        var xmlCanon = null;
        if (algorithm in this._transformAlgorithmMap) {
          xmlCanon =
              this._transformAlgorithmMap[algorithm](signedInfoNode.getXml());
        }

        var digester = this._getHashMethod(signature.data.info.method);
        digester.start();
        digester.update(parsers.xmlToString(xmlCanon, true), 'utf8');

        var signatureValue = forge.util.decode64(signature.data.info.value);

        var result =
            publicKey.verify(digester.digest().getBytes(), signatureValue);

        if (!result) {
          throw new exceptions.CryptoLibException(
              'Could not validate the signature.');
        }
      } catch (error) {
        throw new exceptions.CryptoLibException(
            'Could not validate signatures.', error);
      }
    },

    /**
     * Creates the hash method
     *
     * @function
     * @private
     * @param method
     *            method type, as string.
     *
     * @return Object, the disgester of that method.
     */
    _getHashMethod: function(method) {
      if (stringUtils.containsSubString(method, 'sha256')) {
        return forge.md.sha256.create();
      }
    },

    /**
     * Map with all tarnsform algorithms
     *
     * @function
     * @private
     */
    _transformAlgorithmMap: {
      /**
       * Removes the signature from a signed XML
       *
       * @param xmlSignature
       *            the xml signature, as DOM.
       *
       * @return the XML top document, as DOM
       */
      'http://www.w3.org/2000/09/xmldsig#enveloped-signature': function(
          xmlSignature) {

        var topElement = xmlSignature;

        // get the top document
        while (topElement.parentNode) {
          topElement = topElement.parentNode;
        }

        // remove the signature from the node
        xmlSignature.parentNode.removeChild(xmlSignature);

        return topElement;
      },

      /**
       * c14n the XML
       *
       * @param xmlNode
       *            the xml node, as DOM.
       *
       * @return the XML top document, as DOM
       */
      'http://www.w3.org/2001/10/xml-exc-c14n#': function(xmlNode) {

        var transform = new ExclusiveCanonicalization();
        return transform.process(xmlNode);
      }
    },

    /**
     * Removes the comments in the xml
     *
     * @function
     * @private
     * @param xml
     *            the xml, as string.
     *
     * @return {string} the xml without comments
     */
    _removeXmlStringComments: function(xml) {
      return xml.replace(/<!--[\s\S]*?-->/g, '');
    },

    /**
     * Removes the header in the xml structure. <?xml version="1.0"
     * encoding="UTF-8"?>
     *
     * @function
     * @private
     * @param {object}
     *            xmlDocument the xml, as DOM.
     *
     * @returns {object} the xml, as DOM without the header
     */
    _removeXmlHeader: function(xmlDocument) {
      return xmlDocument.documentElement ? xmlDocument.documentElement :
                                           xmlDocument;
    },

    /**
     * Removes the header in the XML string.
     *
     * @param xmlString
     *            the xml, as a string.
     *
     * @return the xml string without the header
     */
    _removeXmlStringHeader: function(xmlString) {
      return parsers.removeXmlHeaderFromString(xmlString);
    },

    /**
     * Removes the signature node in the XML string.
     *
     * @param xmlString
     *            the xml, as a string.
     *
     * @return the xml string without the signature tag
     */
    _removeXmlStringSignature: function(xmlString) {
      return parsers.removeXmlSignature(xmlString);
    },

    /**
     * Converts any self-closing tags to opening and closing tags, within an
     * XML string.
     *
     * @param xmlString
     *            the xml, as a string.
     *
     * @return the xml string with self-closing tags converted to opening
     *         and closing tags
     */
    _removeXmlStringSelfClosingTags: function(xmlString) {
      return parsers.xmlRemoveSelfClosingTags(xmlString);
    },

    /**
     * Removes the first character from an XML string if that character is
     * not the expected initial character.
     *
     * @param xmlString
     *            the xml, as a string.
     *
     * @return the xml string with the initial character removed if it is
     *         not wanted
     */
    _removeXmlStringFirstCharacter: function(xmlString) {

      if (xmlString[0] !== '<') {
        return xmlString.slice(1, xmlString.length);
      } else {
        return xmlString;
      }
    }
  };
};
