
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.cert.CertStore;
import java.security.cert.Certificate;
import java.security.cert.CollectionCertStoreParameters;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import javax.crypto.Cipher;

import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.DERObject;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.CMSSignedDataGenerator;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.jce.provider.BouncyCastleProvider;

import com.lowagie.text.pdf.PdfDate;
import com.lowagie.text.pdf.PdfDictionary;
import com.lowagie.text.pdf.PdfName;
import com.lowagie.text.pdf.PdfPKCS7;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfSignature;
import com.lowagie.text.pdf.PdfSignatureAppearance;
import com.lowagie.text.pdf.PdfStamper;
import com.lowagie.text.pdf.PdfString;

public class SmartSign
{
	private final static String certAlias = "MyAlias";
	private final static char[] storePwd = "12345678".toCharArray();

	public class SignItems
	{
		private byte[] signature = null;
		private byte[] hashSigned = null;

		public SignItems(byte[] signature, byte[] hashSigned)
		{
			this.signature = signature;
			this.hashSigned = hashSigned;
		}

		public byte[] getSignature()
		{
			return signature;
		}

		public byte[] getHashSigned()
		{
			return hashSigned;
		}
	}

	public static void main(String[] args)
	{
		try
		{
			Security.addProvider(new BouncyCastleProvider());
			new SmartSign();
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}

	public SmartSign() throws Exception
	{
		byte[] plainData = readFile("/temp/test.pdf");
		byte[] sap = operationalStep0(plainData);
		SignItems signItems = operationalStep1(sap);
		byte[] docSigned = operationalStep2(plainData, signItems);
		write2File("/temp/test_signed.pdf", false, docSigned);
	}

	private byte[] readFile(String filename) throws Exception
	{
		FileInputStream inStream = new FileInputStream(filename);
		ByteArrayOutputStream outStream = new ByteArrayOutputStream();
		byte[] buffer = new byte[10240];
		while (true)
		{
			int r = inStream.read(buffer);
			if (r <= 0)
			{
				break;
			}
			outStream.write(buffer, 0, r);
		}
		inStream.close();
		outStream.flush();
		return outStream.toByteArray();
	}

	private void write2File(String filename, boolean append, byte[] bytes) throws Exception
	{
		FileOutputStream fos = new FileOutputStream(filename, append);
		fos.write(bytes);
		fos.flush();
		fos.close();
	}

	private byte[] operationalStep0(byte[] plainData) throws Exception
	{
		MessageDigest digester = MessageDigest.getInstance("sha1");
		digester.update(plainData);
		byte[] digested = digester.digest();
		return digested;
	}

	private byte[] convertBER2DER(byte[] signedBytes) throws Exception
	{
		ByteArrayInputStream bIn = new ByteArrayInputStream(signedBytes);
		ASN1InputStream aIn = new ASN1InputStream(bIn);
		DERObject aDERObject = aIn.readObject();
		bIn.close();
		ByteArrayOutputStream aOutStream = new ByteArrayOutputStream();
		DEROutputStream aDEROutStream = new DEROutputStream(aOutStream);
		aDEROutStream.writeObject(aDERObject);
		aOutStream.close();
		byte[] signedFixedLengthData = aOutStream.toByteArray();
		// byte[] hexFixedLengthData = Hex.encode(signedFixedLengthData);
		return signedFixedLengthData;
	}

	private byte[] p7sign(String signatureCryptographicProvider, PrivateKey privKey, X509Certificate x509Cert, Certificate[] certs, byte[] originalContent, boolean attached) throws Exception
	{
		CMSSignedDataGenerator generator = new CMSSignedDataGenerator();
		generator.addSigner(privKey, x509Cert, CMSSignedDataGenerator.DIGEST_SHA1);
		List certList = new ArrayList();
		for (int i = 0; i < certs.length; i++)
		{
			certList.add(certs[i]);
		}
		CertStore certsCollection = CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList), "BC");
		generator.addCertificatesAndCRLs(certsCollection);
		CMSSignedData signedData = generator.generate(new CMSProcessableByteArray(originalContent), attached, signatureCryptographicProvider);
		byte[] signedBytes = signedData.getEncoded();
		signedBytes = convertBER2DER(signedBytes);
		return signedBytes;
	}

	private SignItems operationalStep1(byte[] fromStep1) throws Exception
	{
		Provider provider = new sun.security.pkcs11.SunPKCS11(new FileInputStream("/temp/pkcs11.cfg"));
		Security.addProvider(provider);
		KeyStore keyStore = KeyStore.getInstance("pkcs11");
		keyStore.load(null, storePwd);
		Certificate[] certs = keyStore.getCertificateChain(certAlias);
		PrivateKey privateKey = (PrivateKey) keyStore.getKey(certAlias, null);
		byte[] signature = p7sign(provider.getName(), privateKey, (X509Certificate) certs[0], certs, fromStep1, true);
		//
		Cipher rsaCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
		rsaCipher.init(Cipher.ENCRYPT_MODE, privateKey);
		byte[] hashSigned = rsaCipher.doFinal(fromStep1);
		//
		SignItems signItems = new SignItems(signature, hashSigned);
		//
		Security.removeProvider(provider.getName());
		return signItems;
	}

	private byte[] operationalStep2(byte[] plainData, SignItems signItems) throws Exception
	{
		CMSSignedData signedData = new CMSSignedData(signItems.getSignature());
		CertStore certStore = signedData.getCertificatesAndCRLs("Collection", "BC");
		Iterator signersIterator = signedData.getSignerInfos().getSigners().iterator();
		X509Certificate signerCert = null;
		while (signersIterator.hasNext())
		{
			SignerInformation signer = (SignerInformation) signersIterator.next();
			Iterator senderCertificatesIt = certStore.getCertificates(signer.getSID()).iterator();
			signerCert = (X509Certificate) senderCertificatesIt.next();
			break;
		}
		//
		Object[] objs = certStore.getCertificates(null).toArray();
		Certificate[] certs = new Certificate[objs.length];
		for (int i = 0; i < objs.length; i++)
		{
			certs[i] = (Certificate) objs[i];
		}
		//
		PdfReader reader = new PdfReader(plainData);
		ByteArrayOutputStream fout = new ByteArrayOutputStream();
		PdfStamper stp = PdfStamper.createSignature(reader, fout, '\0');
		PdfSignatureAppearance sap = stp.getSignatureAppearance();
		sap.setSignDate(new GregorianCalendar());
		sap.setCrypto(null, certs, null, null);
		sap.setAcro6Layers(true);
		sap.setRender(PdfSignatureAppearance.SignatureRenderNameAndDescription);
		PdfSignature dic = new PdfSignature(PdfName.ADOBE_PPKMS, PdfName.ADBE_PKCS7_SHA1);
		dic.setDate(new PdfDate(sap.getSignDate()));
		dic.setName(PdfPKCS7.getSubjectFields(signerCert).getField("CN"));
		if (sap.getReason() != null)
			dic.setReason(sap.getReason());
		if (sap.getLocation() != null)
			dic.setLocation(sap.getLocation());
		sap.setCryptoDictionary(dic);
		int csize = 4000;
		HashMap exc = new HashMap();
		exc.put(PdfName.CONTENTS, new Integer(csize * 2 + 2));
		sap.preClose(exc);
		/*
		HashAlgorithm sha = new SHA1CryptoServiceProvider();
		Stream s = sap.RangeStream;
		int read = 0;
		byte[] buff = new byte[8192];
		while ((read = s.Read(buff, 0, 8192)) > 0)
		{
			sha.TransformBlock(buff, 0, read, buff, 0);
		}
		sha.TransformFinalBlock(buff, 0, 0);
		byte[] pk = SignMsg(sha.Hash, card, false);
		*/
		byte[] pk = signItems.getHashSigned();
		byte[] out = new byte[csize];
		PdfDictionary dic2 = new PdfDictionary();
		System.arraycopy(pk, 0, out, 0, pk.length);
		dic2.put(PdfName.CONTENTS, new PdfString(out).setHexWriting(true));
		sap.close(dic2);
		return fout.toByteArray();
	}
}
