/*
 * Copyright  1999-2004 The Apache Software Foundation.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */
package org.apache.xml.security.test.encryption;

import java.io.File;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;

import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import junit.framework.Assert;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

import org.apache.xml.security.algorithms.JCEMapper;
import org.apache.xml.security.encryption.EncryptedData;
import org.apache.xml.security.encryption.EncryptedKey;
import org.apache.xml.security.encryption.XMLCipher;
import org.apache.xml.security.keys.KeyInfo;
import org.apache.xml.security.keys.content.KeyName;
import org.apache.xml.security.keys.content.X509Data;
import org.apache.xml.security.keys.content.x509.XMLX509Certificate;
import org.apache.xml.security.keys.keyresolver.KeyResolver;
import org.apache.xml.security.test.TestUtils;
import org.apache.xml.security.utils.JavaUtils;
import org.apache.xpath.XPathAPI;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Interop test for XML Encryption
 *
 * @author Berin Lautenbach
 */

/*

Tests for merlin-xmlenc-five not currently done

bad-encrypt-content-aes128-cbc-kw-aes192.xml
decryption-transform-except.xml 
decryption-transform.xml        
encrypt-content-aes192-cbc-dh-sha512.xml
encrypt-data-tripledes-cbc-rsa-oaep-mgf1p-sha256.xml
encrypt-element-aes256-cbc-carried-kw-aes256.xml
encrypt-element-aes256-cbc-kw-aes256-dh-ripemd160.xml
encrypt-element-aes256-cbc-retrieved-kw-aes256.xml
encsig-hmac-sha256-dh.xml
encsig-hmac-sha256-kw-tripledes-dh.xml
encsig-hmac-sha256-rsa-1_5.xml
encsig-hmac-sha256-rsa-oaep-mgf1p.xml
encsig-ripemd160-hmac-ripemd160-kw-tripledes.xml
encsig-sha256-hmac-sha256-kw-aes128.xml
encsig-sha384-hmac-sha384-kw-aes192.xml
encsig-sha512-hmac-sha512-kw-aes256.xml

*/
public class BaltimoreEncTest extends TestCase {

	private static String cardNumber;
	private static String rsaCertSerialNumber;
	private static String testDecryptString;
	private static int nodeCount = 0;
	private static byte[] jebBytes;
	private static byte[] jobBytes;
	private static byte[] jedBytes;
	private static PrivateKey rsaKey;
	private boolean haveISOPadding;
	private boolean haveKeyWraps;

	/** {@link org.apache.commons.logging} logging facility */
    static org.apache.commons.logging.Log log = 
        org.apache.commons.logging.LogFactory.getLog(BaltimoreEncTest.class.getName());
	
	/**
	 * Method suite
	 *
	 *
	 */
	public static Test suite() throws Exception {
		return new TestSuite(BaltimoreEncTest.class);
	}

	/**
	 *  Constructor BaltimoreEncTest
	 *
	 *  @param Name_
	 */
	public BaltimoreEncTest(String Name_) {
		super(Name_);
	}
	
	/**
	 * Method setUp
	 */
	protected void setUp() throws Exception {
		// Create the comparison strings
	
		DocumentBuilderFactory dbf =
			DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		dbf.setAttribute("http://xml.org/sax/features/namespaces", Boolean.TRUE);
		
		String filename = 
			"data/ie/baltimore/merlin-examples/merlin-xmlenc-five/plaintext.xml";
		String basedir = System.getProperty("basedir");
		if(basedir != null && !"".equals(basedir)) {
			filename = basedir + "/" + filename;
		}
		File f = new File(filename);
	
		DocumentBuilder db = dbf.newDocumentBuilder();
		Document doc = db.parse(new java.io.FileInputStream(f));
	
		cardNumber = retrieveCCNumber(doc);
	
		// Test decrypt
		testDecryptString = new String("top secret message\n");
	
		// Count the nodes in the document as a secondary test
		nodeCount = countNodes(doc);
	
		// Create the keys
		jebBytes =
			"abcdefghijklmnopqrstuvwx".getBytes("ASCII");
		jobBytes = 
			"abcdefghijklmnop".getBytes("ASCII");
		jedBytes = 
			"abcdefghijklmnopqrstuvwxyz012345".getBytes("ASCII");
	
		// Certificate information
		rsaCertSerialNumber = new String("1014918766910");
	
		// rsaKey
		filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/rsa.p8";
		if(basedir != null && !"".equals(basedir)) {
			filename = basedir + "/" + filename;
		}
		
		byte[] pkcs8Bytes = JavaUtils.getBytesFromFile(filename);
	
		PKCS8EncodedKeySpec pkcs8Spec = 
			new PKCS8EncodedKeySpec(pkcs8Bytes);
	
		// Create a key factory 
		KeyFactory keyFactory = 
			KeyFactory.getInstance("RSA");
		rsaKey = keyFactory.generatePrivate(pkcs8Spec);
	
		// Initialise the library
	
		org.apache.xml.security.Init.init();
	
		// Register our key resolver
		KeyResolver.register("org.apache.xml.security.test.encryption.BobKeyResolver");

		// Check what algorithms are available

		haveISOPadding = false;
		String algorithmId = 
			JCEMapper.translateURItoJCEID(org.apache.xml.security.utils.EncryptionConstants.ALGO_ID_BLOCKCIPHER_AES128);

		if (algorithmId != null) {
			try {
				if (Cipher.getInstance(algorithmId) != null)
					haveISOPadding = true;
			} catch (NoSuchAlgorithmException nsae) {
			} catch (NoSuchPaddingException nspe) {
			}
		}

		haveKeyWraps = (JCEMapper.translateURItoJCEID(org.apache.xml.security.utils.EncryptionConstants.ALGO_ID_KEYWRAP_AES128) != null);
	}

	/**
	 * Method retrieveCCNumber
	 *
	 * Retrieve the credit card number from the payment info document
	 *
	 * @param doc The document to retreive the card number from
	 * @return The retrieved credit card number
	 */

	public static String retrieveCCNumber (Document doc) 
		throws javax.xml.transform.TransformerException {

		Element nscontext =
			TestUtils.createDSctx(doc, "x",
                              "urn:example:po");
		Node ccnumElt = 
			XPathAPI.selectSingleNode(doc, "//x:Number/text()", nscontext);
		
		if (ccnumElt != null)
			return ccnumElt.getNodeValue();

		return null;

	}

	/*
	 * Check we have retrieved a Credit Card number and that it is OK
	 * Check that the document has the correct number of nodes
	 */

	private void checkDecryptedDoc(Document d, boolean doNodeCheck) throws Exception {

		String cc = retrieveCCNumber(d);
		log.debug("Retrieved Credit Card : " + cc);
		assertTrue(cc, ((cc!= null) && (cc.equals(cardNumber))));

		// Test cc numbers

		if (doNodeCheck) {
			int myNodeCount = countNodes(d);

			assertTrue("Node count mismatches", 
					   ((myNodeCount > 0) && myNodeCount == nodeCount));
		}
	}

	/**
	 * Check a decrypt of data was OK
	 */

	private void checkDecryptedData(byte [] data) throws Exception {

		String input = new String(data, "ASCII");
        Assert.assertEquals(testDecryptString, input);
	}

	/**
	 * Method test_five_content_3des_cbc
	 *
	 * Check the merlin-enc-five element content test for 3DES
	 *
	 */


	public void test_five_content_3des_cbc() throws Exception {

		if (haveISOPadding) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-content-tripledes-cbc.xml";

			Document dd = decryptElement(filename);
			checkDecryptedDoc(dd, true);
		}
		else {
			log.warn("Skipping test test_five_content_3des_cbs as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_content_aes256_cbc
	 *
	 * Check the merlin-enc-five element content test for AES256
	 *
	 */

	public void test_five_content_aes256_cbc() throws Exception {

		if (haveISOPadding) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-content-aes256-cbc-prop.xml";

			Document dd = decryptElement(filename);
			checkDecryptedDoc(dd, true);
		}
		else {
			log.warn("Skipping test test_five_content_aes256_cbc as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_content_aes128_cbc_kw_aes192
	 *
	 * Check the merlin-enc-five element content test for AES128 with
	 * AES 192 key wrap
	 *
	 */

	public void test_five_content_aes128_cbc_kw_aes192() throws Exception {

		if (haveISOPadding && haveKeyWraps) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-content-aes128-cbc-kw-aes192.xml";

			Document dd = decryptElement(filename);
			checkDecryptedDoc(dd, true);
		}
		else {
			log.warn("Skipping test test_five_content_aes128_cbc_kw_aes192 as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_content_3des_cbc_kw_aes128
	 *
	 * Check the merlin-enc-five element content test for 3DES with
	 * AES 128 key wrap
	 *
	 */

	public void test_five_content_3des_cbc_kw_aes128() throws Exception {

		if (haveISOPadding && haveKeyWraps) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-element-tripledes-cbc-kw-aes128.xml";

			Document dd = decryptElement(filename);
			checkDecryptedDoc(dd, true);
		}
		else {
			log.warn("Skipping test test_five_content_3des_cbc_kw_aes128 as necessary crypto algorithms are not available");
		}

    }

	/**
	 * Method test_five_content_aes128_cbc_kw_rsa_15
	 *
	 * Check the merlin-enc-five element content test for AES128 with
	 * RSA key wrap (PKCS 1.5 padding)
	 *
	 */

	public void test_five_content_aes128_cbc_rsa_15() throws Exception {

		if (haveISOPadding) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-element-aes128-cbc-rsa-1_5.xml";

			Document dd = decryptElement(filename);
			checkDecryptedDoc(dd, true);
		}
		else {
			log.warn("Skipping test test_five_content_aes128_cbc_rsa_15 as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_element_aes192_cbc_ref
	 *
	 * Check the merlin-enc-five element data test for AES192 with
	 * a CipherReference element
	 *
	 */

	public void test_five_element_aes192_cbc_ref() throws Exception {

		if (haveISOPadding) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-element-aes192-cbc-ref.xml";

			Document dd = decryptElement(filename);
			// Note - we don't check the node count, as it will be different
			// due to the encrypted text remainin in the reference nodes
			checkDecryptedDoc(dd, false);
		}
		else {
			log.warn("Skipping test test_five_element_aes192_cbc_ref as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_data_aes128_cbc
	 *
	 * Check the merlin-enc-five element data test for AES128 with no
	 * key wrap
	 *
	 */

	public void test_five_data_aes128_cbc() throws Exception {

		if (haveISOPadding) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-data-aes128-cbc.xml";

			byte[] decrypt = decryptData(filename);
			checkDecryptedData(decrypt);
		}
		else {
			log.warn("Skipping test test_five_data_aes128_cbc as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_data_aes256_cbc_3des
	 *
	 * Check the merlin-enc-five element data test for AES256 with 3DES
	 * key wrap
	 *
	 */

	public void test_five_data_aes256_cbc_3des() throws Exception {

		if (haveISOPadding && haveKeyWraps) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-data-aes256-cbc-kw-tripledes.xml";

			byte[] decrypt = decryptData(filename);
			checkDecryptedData(decrypt);
		}
		else {
			log.warn("Skipping test test_five_data_aes256_cbc_3des as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_data_aes192_cbc_aes256
	 *
	 * Check the merlin-enc-five element data test for AES192 with AES256
	 * key wrap
	 *
	 */

	public void test_five_data_aes192_cbc_aes256() throws Exception {

		if (haveISOPadding && haveKeyWraps) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-data-aes192-cbc-kw-aes256.xml";

			byte[] decrypt = decryptData(filename);
			checkDecryptedData(decrypt);
		}
		else {
			log.warn("Skipping test test_five_data_aes192_cbc_aes256 as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method test_five_data_3des_cbc_rsa_oaep
	 *
	 * Check the merlin-enc-five element data test for 3DES with
	 * RSA key wrap (OAEP and no parameters)
	 *
	 */

	public void test_five_data_3des_cbc_rsa_oaep() throws Exception {

                // Work-around for the fact that BC currently doesn't support
                // the standard JCE name for oaep padding
                java.security.Provider bc = java.security.Security.getProvider("BC");
                if (bc != null)
                    bc.put("Alg.Alias.Cipher.RSA/ECB/OAEPWithSHA1AndMGF1Padding","RSA/OAEP");

		if (haveISOPadding) {
			String filename = "data/ie/baltimore/merlin-examples/merlin-xmlenc-five/encrypt-data-tripledes-cbc-rsa-oaep-mgf1p.xml";

			byte[] decrypt = decryptData(filename);
			checkDecryptedData(decrypt);
		}
		else {
			log.warn("Skipping test test_five_data_3des_cbc_rsa_oaep as necessary crypto algorithms are not available");
		}
    }

	/**
	 * Method decryptElement
	 *
	 * Take a key, encryption type and a file, find an encrypted element
	 * decrypt it and return the resulting document
	 *
	 * @param filename File to decrypt from
	 */

	public Document decryptElement (String filename) 
		throws Exception {

		XMLCipher cipher;
		
		// Parse the document in question

		javax.xml.parsers.DocumentBuilderFactory dbf =
			javax.xml.parsers.DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		dbf.setAttribute("http://xml.org/sax/features/namespaces", Boolean.TRUE);
		String basedir = System.getProperty("basedir");
		if(basedir != null && !"".equals(basedir)) {
			filename = basedir + "/" + filename;
		}
		File f = new File(filename);

		DocumentBuilder db = dbf.newDocumentBuilder();
		
		Document doc = db.parse(new java.io.FileInputStream(f));
		
		// Now we have the document, lets build the XMLCipher element

		Element ee = null;
		
		// Create the XMLCipher element
		
		cipher = XMLCipher.getInstance();

		// Need to pre-load the Encrypted Data so we can get the key info

		ee = (Element) doc.getElementsByTagName("EncryptedData").item(0);
		cipher.init(XMLCipher.DECRYPT_MODE, null);
		EncryptedData encryptedData = cipher.loadEncryptedData(doc, ee);
   
		Key key = findKey(encryptedData);
		cipher.init(XMLCipher.DECRYPT_MODE, key);
		Document dd = cipher.doFinal(doc, ee);

		return dd;
			
    }

	/**
	 * Method decryptData
	 *
	 * Take a file, find an encrypted element decrypt it and return the 
	 * resulting byte array
	 *
	 * @param filename File to decrypt from
	 */

	public byte[] decryptData (String filename) 
		throws Exception {

		XMLCipher cipher;
		
		// Parse the document in question

		javax.xml.parsers.DocumentBuilderFactory dbf =
			javax.xml.parsers.DocumentBuilderFactory.newInstance();
		dbf.setNamespaceAware(true);
		dbf.setAttribute("http://xml.org/sax/features/namespaces", Boolean.TRUE);
		String basedir = System.getProperty("basedir");
		if(basedir != null && !"".equals(basedir)) {
			filename = basedir + "/" + filename;
		}
		File f = new File(filename);

		DocumentBuilder db = dbf.newDocumentBuilder();
		
		Document doc = db.parse(new java.io.FileInputStream(f));
		
		// Now we have the document, lets build the XMLCipher element

		Element ee = null;
		
		// Create the XMLCipher element
		
		cipher = XMLCipher.getInstance();

		// Need to pre-load the Encrypted Data so we can get the key info

		ee = (Element) doc.getElementsByTagName("EncryptedData").item(0);
		cipher.init(XMLCipher.DECRYPT_MODE, null);
		EncryptedData encryptedData = cipher.loadEncryptedData(doc, ee);

		Key key = findKey(encryptedData);
		
		cipher.init(XMLCipher.DECRYPT_MODE, key);
               
		byte[] dd = cipher.decryptToByteArray(ee);

		return dd;
			
    }

	/** 
	 * Method mapKeyName
	 *
	 * Create a secret key from a key name for merlin-five
	 *
	 * @param name Name to map a key from
	 */

	public SecretKey mapKeyName(String name) throws Exception {

        if (name.equals("job")) {

			// Jeb is a AES-128 key
	        SecretKey key = new SecretKeySpec(jobBytes, "AES");
			return key;
		}
		if (name.equals("jeb")) {

			// Jeb is a AES-192 key
	        SecretKey key = new SecretKeySpec(jebBytes, "AES");
			return key;
		}
		if (name.equals("jed")) {

			// Jeb is a AES-256 key
	        SecretKey key = new SecretKeySpec(jedBytes, "AES");
			return key;
		}

		return null;

	}

	/**
	 * Method findKey
	 *
	 * Given an encryptedData structure, return the key that will decrypt
	 * it
	 *
	 * @param encryptedData EncryptedData to get key for
	 */

	public Key findKey(EncryptedData encryptedData) throws Exception {

		KeyInfo ki = encryptedData.getKeyInfo();
   
		Key key = null;
		Key kek = null;

		if (ki == null)
			return null;
		

		// First check for a known key name
		KeyName keyName = ki.itemKeyName(0);
		if (keyName != null) {
			return (mapKeyName(keyName.getKeyName()));
		}

		// Decrypt any encryptedKey structures
		EncryptedKey encryptedKey = ki.itemEncryptedKey(0);
		
		if (encryptedKey == null)
			return null;
	 
		KeyInfo kiek = encryptedKey.getKeyInfo();
		if (kiek == null) {
			return null;
		}

		KeyName kekKeyName = kiek.itemKeyName(0);
		if (kekKeyName != null) {
			kek = mapKeyName(kekKeyName.getKeyName());
		}
		else {

			X509Data certData = kiek.itemX509Data(0);
			XMLX509Certificate xcert = certData.itemCertificate(0);
			X509Certificate cert = xcert.getX509Certificate();

			if (cert != null) {

				if (cert.getSerialNumber().toString().equals(rsaCertSerialNumber)) {

					kek = rsaKey;
					
				}
			}
		}
		if (kek != null) {
			XMLCipher cipher = XMLCipher.getInstance();
			cipher.init(XMLCipher.UNWRAP_MODE, kek);
			key = cipher.decryptKey(encryptedKey,
									encryptedData.
									getEncryptionMethod().
									getAlgorithm());
		}
		
		return key;
	}
	/**
	 * Method countNodes
	 *
	 * Recursively count the number of nodes in the document
	 *
	 * @param n Node to count beneath
	 */

	private static int countNodes(Node n) {

		if (n == null)
			return 0;  // Paranoia

		int count = 1;  // Always count myself
		Node c = n.getFirstChild();

		while (c != null) {

			count += countNodes(c);
			c = c.getNextSibling();

		}

		return count;

	}
}
