/*
 * Copyright (c) 2013 ICM Uniwersytet Warszawski All rights reserved.
 * See LICENCE.txt file for licensing information.
 */
package eu.emi.security.authn.x509.helpers.trust;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.security.auth.x500.X500Principal;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.IOFileFilter;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1OutputStream;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.DERBMPString;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERPrintableString;
import org.bouncycastle.asn1.DERT61String;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.DERUniversalString;
import org.bouncycastle.asn1.DERVisibleString;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.crypto.digests.MD5Digest;
import org.bouncycastle.crypto.digests.SHA1Digest;

import eu.emi.security.authn.x509.helpers.CertificateHelpers;

/**
 * Several static methods helping to mangle truststore file paths in openssl style.
 * 
 * @author K. Benedyczak
 */
public class OpensslTruststoreHelper
{
	public static final String CERT_REGEXP = "^([0-9a-fA-F]{8})\\.[\\d]+$";
	
	/**
	 * @param certLocation certificate location
	 * @param suffix either '.namespaces' or '.signing_policy' (other will work but rather doesn't make sense)
	 * @return A proper name of a namespaces or signing policy file for the given base
	 * path of CA certificate. 
	 */
	public static String getNsFile(String certLocation, String suffix)
	{
		String fileHash = getFileHash(certLocation, CERT_REGEXP);
		if (fileHash == null)
			return null;
		File f = new File(certLocation);
		String parent = f.getParent();
		if (parent == null)
			parent = ".";
		return parent + File.separator + fileHash + suffix;
	}
	
	public static String getFileHash(String path, String regexp)
	{
		File f = new File(path);
		String name = f.getName();
		Pattern pattern = Pattern.compile(regexp);
		Matcher m = pattern.matcher(name);
		if (!m.matches())
			return null;
		return m.group(1);
	}

	public static Collection<File> getFilesWithRegexp(String regexp, File directory)
	{
		final Pattern pattern = Pattern.compile(regexp);
		
		return FileUtils.listFiles(directory, new IOFileFilter()
		{
			@Override
			public boolean accept(File dir, String name)
			{
				return pattern.matcher(name).matches();
			}
			
			@Override
			public boolean accept(File file)
			{
				return accept(null, file.getName());
			}
		}, null);
	}
	
	public static String getOpenSSLCAHash(X500Principal name, boolean openssl1Mode)
	{
		return openssl1Mode ? getOpenSSLCAHashNew(name) : getOpenSSLCAHashOld(name);
	}
	
	/**
	 * Generates the hex hash of the DN used by openssl to name the CA
	 * certificate files. The hash is actually the hex of 8 least
	 * significant bytes of a MD5 digest of the the ASN.1 encoded DN.
	 * 
	 * @param name the DN to hash.
	 * @return the 8 character string of the hexadecimal MD5 hash.
	 */
	private static String getOpenSSLCAHashOld(X500Principal name)
	{
		byte[] bytes = name.getEncoded();
		MD5Digest digest = new MD5Digest();
		digest.update(bytes, 0, bytes.length);
		byte output[] = new byte[digest.getDigestSize()];
		digest.doFinal(output, 0);
		
		String ret = String.format("%02x%02x%02x%02x", output[3] & 0xFF,
				output[2] & 0xFF, output[1] & 0xFF, output[0] & 0xFF);
		return ret;
	}
	
	/**
	 * Generates the hex hash of the DN used by openssl 1.0.0 and above to name the CA
	 * certificate files. The hash is actually the hex of 8 least
	 * significant bytes of a SHA1 digest of the the ASN.1 encoded DN after normalization.
	 * <p>
	 * The normalization is performed as follows:
	 * all strings are converted to UTF8, leading, trailing and multiple spaces collapsed, 
	 * converted to lower case and the leading SEQUENCE header is removed.
	 * 
	 * @param name the DN to hash.
	 * @return the 8 character string of the hexadecimal MD5 hash.
	 */
	private static String getOpenSSLCAHashNew(X500Principal name)
	{
		byte[] bytes;
		try
		{
			RDN[] c19nrdns = getNormalizedRDNs(name);
			bytes = encodeWithoutSeqHeader(c19nrdns);
		} catch (IOException e)
		{
			throw new IllegalArgumentException("Can't parse the input DN", e);
		}
		Digest digest = new SHA1Digest();
		digest.update(bytes, 0, bytes.length);
		byte output[] = new byte[digest.getDigestSize()];
		digest.doFinal(output, 0);
		
		return String.format("%02x%02x%02x%02x", output[3] & 0xFF,
				output[2] & 0xFF, output[1] & 0xFF, output[0] & 0xFF);	
	}
	
	public static RDN[] getNormalizedRDNs(X500Principal name) throws IOException
	{
		X500Name dn = CertificateHelpers.toX500Name(name);
		RDN[] rdns = dn.getRDNs();
		RDN[] c19nrdns = new RDN[rdns.length];
		int i=0;
		for (RDN rdn: rdns)
		{
			AttributeTypeAndValue[] atvs = rdn.getTypesAndValues();
			sortAVAs(atvs);
			AttributeTypeAndValue[] c19natvs = new AttributeTypeAndValue[atvs.length];
			for (int j=0; j<atvs.length; j++)
			{
				c19natvs[j] = normalizeStringAVA(atvs[j]);
			}
			c19nrdns[i++] = new RDN(c19natvs);
		}
		return c19nrdns;
	}
	
	private static void sortAVAs(AttributeTypeAndValue[] atvs) throws IOException
	{
		for (int i=0; i<atvs.length; i++)
			for (int j=i+1; j<atvs.length; j++)
			{
				if (memcmp(atvs[i].getEncoded(), atvs[j].getEncoded()) < 0)
				{
					AttributeTypeAndValue tmp = atvs[i];
					atvs[i] = atvs[j];
					atvs[j] = tmp;
				}
			}
	}
	
	private static int memcmp(byte[] a, byte[] b)
	{
		int min = a.length > b.length ? b.length : a.length;
		for (int i=0; i<min; i++)
			if (a[i] < b[i])
				return -1;
			else if (a[i] > b[i])
				return 1;
		return a.length - b.length;
	}
	
	private static AttributeTypeAndValue normalizeStringAVA(AttributeTypeAndValue src)
	{
		ASN1Encodable srcVal = src.getValue();
		if (	!((srcVal instanceof DERPrintableString) ||
			(srcVal instanceof DERUTF8String) ||
			(srcVal instanceof DERIA5String) ||
			(srcVal instanceof DERBMPString) ||
			(srcVal instanceof DERUniversalString) ||
			(srcVal instanceof DERT61String) ||
			(srcVal instanceof DERVisibleString)))
			return src;
		ASN1String srcString = (ASN1String) srcVal;
		String value = srcString.getString();
		value = value.trim();
		value = value.replaceAll("[ \t\n\f][ \t\n\f]+", " ");
		value = value.toLowerCase();
		DERUTF8String newValue = new DERUTF8String(value);
		return new AttributeTypeAndValue(src.getType(), newValue);
	}
	
	private static byte[] encodeWithoutSeqHeader(RDN[] rdns) throws IOException
	{
	        ByteArrayOutputStream bOut = new ByteArrayOutputStream();
	        ASN1OutputStream      aOut = new ASN1OutputStream(bOut);

		for (RDN rdn: rdns)
		{
			aOut.writeObject(rdn);
		}
		aOut.close();
		return bOut.toByteArray();
	}
}
