package lu.tudor.santec.gecamed.esante.gui.luxtrust;

import java.io.StringReader;
import java.util.Iterator;

import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import lu.luxtrust.crypto.digest.DigestAlgorithm;
import lu.luxtrust.crypto.digest.Digester;
import lu.luxtrust.cryptoti.CryptoTI_Mechanism;
import lu.luxtrust.cryptoti.CryptoTI_PrivateKey;
import lu.luxtrust.cryptoti.CryptoTI_Session;
import lu.luxtrust.cryptoti.exceptions.CryptoTI_Exception;
import lu.tudor.santec.gecamed.esante.gui.utils.ESanteGuiUtils;
import lu.tudor.santec.gecamed.esante.gui.webservice.WebserviceConstants;
import lu.tudor.santec.gecamed.esante.utils.Utility;
import lu.tudor.santec.gecamed.esante.utils.exceptions.SmartCardException;

import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.apache.xml.security.c14n.Canonicalizer;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;

/**
 * @author donako
 * 
 * Signs an X509 authentication request with a X509V3 key from a smartcard
 * 
 * @version 
 * <br>$Log: AuthenticationSignature.java,v $
 * <br>Revision 1.10  2014-01-30 11:58:26  ferring
 * <br>replace all occurrences instead of only the first
 * <br>
 * <br>Revision 1.9  2014-01-28 13:31:09  donak
 * <br>Removed debug output and TODOs
 * <br>
 * <br>Revision 1.8  2014-01-27 13:13:47  donak
 * <br>* Properties are now saved in db in respect to the following contexts:
 * <br>  - specific to a GECAMed user
 * <br>  - specific to a GECAMed physician
 * <br>  - specific to an eSanté plattform user
 * <br>  - general (independend of GECAMed user, physician, eSanté platform user)
 * <br>* Improved authentication handling. A signed authentication request is now only done for user authentication. for dsp authentication requests the provided saml assertion is used. (authentication speed up). ATTENTION: This fix is currently disabled till a bug in the eSanté platform has been fixed.
 * <br>* Naming of message loggings had been adapted to the naming in the connection kit (e.g. DSP-10, DSP-22)
 * <br>* Changed behavior for handling of dsps for which physician has insufficient access permissions. If physician does not want to provide presence password, a DSP-11 is sent instead of a DSP-12 to allow the physician to at least access the documents he is author of.
 * <br>
 * <br>Revision 1.7  2013-12-27 18:09:26  donak
 * <br>Cleanup of imports
 * <br>
 * <br>Revision 1.6  2013-12-24 17:33:15  donak
 * <br>Changed exception handling to focus assertion related exceptions to gerValidSamlAssertionForPatient()
 * <br>Included exception handling for SmartCardExceptions
 * <br>Fixed bug in UserInactivityMonitor which caused inactive sessions
 * <br>Partially rewrote SamlAssertion to ease and centralize logic
 * <br>
 * <br>Revision 1.00  2013-12-23 18:07:36  donak
 */

public class AuthenticationSignature {

	private static Logger logger = Logger.getLogger(AuthenticationSignature.class.getName());

	private static UserInactivityMonitor watchdog = null;

	public AuthenticationSignature(UserInactivityMonitor watchdog) {
		AuthenticationSignature.watchdog = watchdog;
		// initialize the security framework - otherwise the canonicalization won't work
		org.apache.xml.security.Init.init();
	}

	/**
	 * Fills the authentication request template with the user data and signs the relevant elements, which are: <br/>
	 * <ul>
	 * <li>The security token (the certificate itself)</li>
	 * <li>The validity period of the requested token</li>
	 * <li>The request body</li>
	 * </ul>
	 * 
	 * @param template
	 *            The X509 authentication template
	 * @param password
	 *            The user password for the private key
	 * @return The filled and signed authentication request
	 * @throws SmartCardException
	 *             If a problem with the smartcard access occurred
	 * @throws Exception
	 *             Multiple reasons - please check exception message/stacktrace for futher details
	 */
	public String createAuthRequest(String template, String password) throws SmartCardException, Exception {

		// define elements that will be signed
		Node securityToken = null;
		Node validityPeriod = null;
		Node body = null;
		Node signedInfo = null;
		Node signatureValue = null;
		NodeList digestTags = null;
		Document document = null;

		// add the binary certificate to the template
		String certificate;
		certificate = new String(Base64.encodeBase64(watchdog.getAuthenticationCertificate().getEncoded()));
		template = template.replace(WebserviceConstants.PLACEHOLDER_CERT_AS_BASE64, certificate);

		try {
			// load the template into a dom tree
			DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
			domFactory.setNamespaceAware(true);
			DocumentBuilder builder = domFactory.newDocumentBuilder();
			document = builder.parse(new InputSource(new StringReader(template)));

			// Evaluate XPath against Document itself
			XPath xPath = XPathFactory.newInstance().newXPath();
			xPath.setNamespaceContext(new SOAPNamespaceContext());

			// create the x-path to the nodes that should be signed
			XPathExpression expSecurityToken = xPath.compile("/soapenv:Envelope/soapenv:Header/wsse:Security/wsse:BinarySecurityToken");
			XPathExpression expValidityPeriod = xPath.compile("/soapenv:Envelope/soapenv:Header/wsse:Security/wsu:Timestamp");
			XPathExpression expBody = xPath.compile("/soapenv:Envelope/soapenv:Body");
			// x-path to the nodes where the base64 encoded sha1 digests will be stored
			XPathExpression expDigestTags = xPath
					.compile("/soapenv:Envelope/soapenv:Header/wsse:Security/ds:Signature/ds:SignedInfo/ds:Reference/ds:DigestValue");
			// the x-path of the signed info tag that will be used for creating the signature (first digest with sha1 the signing the digest with rsa)
			XPathExpression expSignedInfo = xPath.compile("/soapenv:Envelope/soapenv:Header/wsse:Security/ds:Signature/ds:SignedInfo");
			// finally the x-path to the node where the signature value will be stored
			XPathExpression expSignatureValue = xPath.compile("/soapenv:Envelope/soapenv:Header/wsse:Security/ds:Signature/ds:SignatureValue");

			// fetch the corresponding nodes from the DOM-tree
			securityToken = (Node) expSecurityToken.evaluate(document, XPathConstants.NODE);
			validityPeriod = (Node) expValidityPeriod.evaluate(document, XPathConstants.NODE);
			body = (Node) expBody.evaluate(document, XPathConstants.NODE);
			digestTags = (NodeList) expDigestTags.evaluate(document, XPathConstants.NODESET);

			// add the digests
			String nodeName = null;
			for (int counter = 0; counter < digestTags.getLength(); counter++) {
				nodeName = digestTags.item(counter).getFirstChild().getTextContent();
				if (WebserviceConstants.PLACEHOLDER_AUTHNREQUEST_DIGEST.equals(nodeName)) {
					generateDigest(body, digestTags.item(counter));
				} else if (WebserviceConstants.PLACEHOLDER_CERTIFICATE_DIGEST.equals(nodeName)) {
					generateDigest(securityToken, digestTags.item(counter));
				} else if (WebserviceConstants.PLACEHOLDER_TIMESTAMP_TAG_DIGEST.equals(nodeName)) {
					generateDigest(validityPeriod, digestTags.item(counter));
				}
			}
		
			// and also fetch the nodes for calculating and storing the signature value
			signedInfo = (Node) expSignedInfo.evaluate(document, XPathConstants.NODE);
			signatureValue = (Node) expSignatureValue.evaluate(document, XPathConstants.NODE);

			// add the signature
			addSignature(signedInfo, signatureValue, password);

		} catch (XPathExpressionException xee) {
			throw new Exception("A x-path referenced node could not be found.", xee);
		} catch (CryptoTI_Exception cEx) {
			throw new SmartCardException(cEx);
		}

		return Utility.getObjectContent(document);
	}

	/**
	 * Generates the signature of the authentication request and adds it to the template
	 * 
	 * @param signedInfo
	 *            Reference to the signed info node
	 * @param signatureValue
	 *            Reference to the signature value node
	 * @param password
	 *            The private key password
	 * @throws Exception
	 *             If signature creation went wrong - please see stack trace for further details
	 */
	private void addSignature(Node signedInfo, Node signatureValue, String password) throws CryptoTI_Exception, Exception {
		byte[] contentToSign = null;
		byte[] signature = null;

		// any comments will be ignored when calculating digest
		Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS);
		// here it is crucial to provide the prefix namespaces - which seems quite strange as they are not needed for the reference sections
		contentToSign = canon.canonicalizeSubtree(signedInfo, "soapenv xd xe");

		// at first obtain all tools that are needed for signing
		CryptoTI_Session session = watchdog.getSession(password);
		CryptoTI_Mechanism mechanism = watchdog.getBestMechanism();
		CryptoTI_PrivateKey privateKey = watchdog.getPrivateKey(password);

		// initialize the session for signing
		watchdog.getLuxtrustCard().signInit(privateKey, session, mechanism);

		// add the content that should be digested and signed
		watchdog.getLuxtrustCard().signUpdate(contentToSign, session);

		// and create the actual signature
		signature = watchdog.getLuxtrustCard().signFinal(session);
		// finally write the generated signature to the corresponding tag
		signatureValue.getFirstChild().setTextContent(new String(Base64.encodeBase64(signature)));
	}

	/**
	 * Generates an SHA1 digest of the content of a Node
	 * 
	 * @param contentToDigest
	 *            The Node of which the content should be digested (it does not matter if the node represents a branch or a leaf element in the document)
	 * @return the binary digest (SHA1 hash) of the provided Node content
	 * @throws Exception
	 *             If digest could not be generated - please check exception message for further details
	 */
	public void generateDigest(Node contentToDigest, Node digestValueLocation) throws CryptoTI_Exception, Exception {
		final Digester hash = new Digester();
		byte[] canonicalizedNode = null;
		byte[] digestValue = null;

		// any comments will be ignored when calculating digest
		Canonicalizer canon = Canonicalizer.getInstance(Canonicalizer.ALGO_ID_C14N_OMIT_COMMENTS);
		// create a canonical representation of the xml segment that should be signed - strangely the indication of the prefix namespaces is here not needed (in
		// contrary to the signing)
		canonicalizedNode = canon.canonicalizeSubtree(contentToDigest);
		// generate the digest and put it to the list of digests upon which the signature will be generated
		digestValue = hash.calculateHash(canonicalizedNode, DigestAlgorithm.SHA1);
		// and add the digest in base64 representation to the message template
		digestValueLocation.getFirstChild().setTextContent(new String(Base64.encodeBase64(digestValue)));
	}

	/**
	 * Simple namespace context implementation to inform x-path about the used namespaces
	 * 
	 * @author donak
	 * 
	 */
	private static class SOAPNamespaceContext implements NamespaceContext {

		/**
		 * Provides the namespaces that define the namespace prefixes to the x-path expressions
		 */
		public String getNamespaceURI(String prefix) {
			if ("soapenv".equals(prefix)) {
				return "http://schemas.xmlsoap.org/soap/envelope/";
			} else if ("wsse".equals(prefix)) {
				return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
			} else if ("wsu".equals(prefix)) {
				return "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
			} else if ("ds".equals(prefix)) {
				return "http://www.w3.org/2000/09/xmldsig#";
			}

			return null;
		}

		public String getPrefix(String namespaceURI) {
			return null;
		}

		public Iterator getPrefixes(String namespaceURI) {
			return null;
		}

	}

}
