package mkl.testarea.pdfbox2.sign;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;

import javax.crypto.Cipher;
import javax.xml.bind.DatatypeConverter;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.io.IOUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.interactive.digitalsignature.PDSignature;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1InputStream;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.cms.Attribute;
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cms.CMSException;
import org.bouncycastle.cms.CMSProcessable;
import org.bouncycastle.cms.CMSProcessableByteArray;
import org.bouncycastle.cms.CMSSignedData;
import org.bouncycastle.cms.SignerInformation;
import org.bouncycastle.cms.SignerInformationVerifier;
import org.bouncycastle.cms.jcajce.JcaSimpleSignerInfoVerifierBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.Store;
import org.junit.BeforeClass;
import org.junit.Test;

/**
 * @author mkl
 */
public class CalculateDigest {
    @BeforeClass
    public static void setUpBeforeClass() throws Exception {
        Security.addProvider(new BouncyCastleProvider());
    }

    /**
     * <a href="https://stackoverflow.com/questions/57926872/signed-pdf-content-digest-that-was-calculated-during-verification-is-diffrent-th">
     * Signed PDF content digest that was calculated during verification is diffrent than decripted digest from signature
     * </a>
     * <br/>
     * <a href="https://drive.google.com/open?id=1UlOZOp-UYllK7Ra35dggccoWdhcb_Ntp">
     * TEST-signed-pades-baseline-b.pdf
     * </a>
     * <p>
     * The code in {@link #verifyPDF(String)} compares the wrong CMS
     * hash with the has of the signed byte ranges, it compares the
     * hash in the primitive PKCS1 signature which actually signs the
     * signed attributes of its SignerInfo, not the signed PDF byte
     * ranges directly.
     * </p>
     */
    @Test
    public void testVerifyPdfLikeUser2893427() throws Exception {
        verifyPDF("src\\test\\resources\\mkl\\testarea\\pdfbox2\\sign\\TEST-signed-pades-baseline-b.pdf");
    }

    /** @see #testVerifyPdfLikeUser2893427() */
    public static void verifyPDF(String fileName) throws Exception {
        File fileDoc = new File(fileName);
        PDDocument document = Loader.loadPDF(fileDoc);
        List<PDSignature> signatures = document.getSignatureDictionaries();
        PDSignature sig = signatures.get(0);
        if (sig != null) {
            String subFilter = sig.getSubFilter();
            if (subFilter != null) {
                Collection<X509Certificate> certs = new ArrayList<X509Certificate>();
                switch (subFilter) {
                case "ETSI.CAdES.detached":
                case "adbe.pkcs7.detached":
                    FileInputStream fis = new FileInputStream(fileDoc);
                    byte[] signatureContent = sig.getContents(fis);
                    System.out.println("---------signatureContent length------------");
                    System.out.println(signatureContent.length);
                    String signatureContentB64 = Base64.getEncoder().encodeToString(signatureContent);
                    System.out.println("---------signatureContent b64------------");
                    System.out.println(signatureContentB64);
                    fis = new FileInputStream(fileDoc);
                    byte[] signedContent = sig.getSignedContent(fis);
                    String signedContentB64 = Base64.getEncoder().encodeToString(signedContent);
                    System.out.println("---------signedContent length------------");
                    System.out.println(signedContent.length);
                    System.out.println("---------signedContent b64------------");
                    System.out.println(signedContentB64);

                    // Now we construct a PKCS #7 or CMS.
                    CMSProcessable cmsProcessableInputStream = new CMSProcessableByteArray(signedContent);
                    CMSSignedData cmsSignedData = new CMSSignedData(cmsProcessableInputStream, signatureContent);
                    Store<X509CertificateHolder> certificatesStore = cmsSignedData.getCertificates();
                    Collection<SignerInformation> signers = cmsSignedData.getSignerInfos().getSigners();
                    SignerInformation signerInformation = signers.iterator().next();
                    @SuppressWarnings("unchecked")
                    Collection<X509CertificateHolder> matches = certificatesStore.getMatches(signerInformation.getSID());
                    X509CertificateHolder certificateHolder = (X509CertificateHolder) matches.iterator().next();
                    certificateHolder.getSerialNumber();
                    X509Certificate certFromSignedData = new JcaX509CertificateConverter()
                            .getCertificate(certificateHolder);
                    certs.add(certFromSignedData);

                    SignerInformationVerifier signerInformationVerifier = new JcaSimpleSignerInfoVerifierBuilder()
                            .build(certificateHolder);
                    boolean isValid = signerInformation.verify(signerInformationVerifier);

                    System.out.println("---------isValid------------");
                    System.out.println(isValid);
                    System.out.println("---------certSerialNumber dec------------");
                    System.out.println(certificateHolder.getSerialNumber());
                    System.out.println("---------certSerialNumber hex------------");
                    System.out.println(String.format("0x%08X", certificateHolder.getSerialNumber()));
                    System.out.println("---------certSubject------------");
                    System.out.println(certificateHolder.getSubject().toString());
                    System.out.println("---------getContentType------------");
                    System.out.println(signerInformation.getContentType().toString());
                    System.out.println("---------contentDigest base64------------");
                    byte[] contentDigest = signerInformation.getContentDigest();
                    String contentDigestB64 = Base64.getEncoder().encodeToString(contentDigest);
                    System.out.println(contentDigestB64);
                    System.out.println("---------contentDigest hex------------");
                    String contentDigestHex = DatatypeConverter.printHexBinary(contentDigest);
                    System.out.println(contentDigestHex);
                    System.out.println("---------digestAlgOID------------");
                    System.out.println(signerInformation.getDigestAlgOID());
                    System.out.println(signerInformation.getDigestAlgorithmID());
                    System.out.println("---------encryptionAlgOID------------");
                    System.out.println(signerInformation.getEncryptionAlgOID());
                    ;

                    // https://gist.github.com/nielsutrecht/855f3bef0cf559d8d23e94e2aecd4ede

                    byte[] signatureBytes = signerInformation.getSignature();
                    String signatureBytesB64 = Base64.getEncoder().encodeToString(signatureBytes);
                    System.out.println("---------getSignature (encripted) base64------------");
                    System.out.println(signatureBytesB64);
                    System.out.println("---------getSignature (encripted) hex------------");
                    String signatureBytesHex = DatatypeConverter.printHexBinary(signatureBytes);
                    System.out.println(signatureBytesHex);

                    System.out.println("---------getSignature (decripted) base64------------");
                    Cipher encryptCipher = Cipher.getInstance("RSA");
                    PublicKey publicKey = certFromSignedData.getPublicKey();
                    encryptCipher.init(Cipher.DECRYPT_MODE, publicKey);
                    byte[] cipherText = encryptCipher.doFinal(signatureBytes);
                    String cipherTextB64 = Base64.getEncoder().encodeToString(cipherText);
                    System.out.println(cipherTextB64);
                    System.out.println("---------getSignature (decripted) hex------------");
                    String cipherTextHex = DatatypeConverter.printHexBinary(cipherText);
                    System.out.println(cipherTextHex);

                    byte[] digest = null;

                    ASN1InputStream ais = new ASN1InputStream(cipherText);
                    ASN1Primitive obj = ais.readObject();
                    // System.out.println("---------getSignature ASN1 parse------------");
                    // System.out.println(ASN1Dump.dumpAsString(obj, true));
                    DigestInfo digestInfo = new DigestInfo((ASN1Sequence) obj);
                    System.out.println("---------getAlgorithmId------------");
                    System.out.println(digestInfo.getAlgorithmId().getAlgorithm().getId());

                    System.out.println("---------getDigest hex AND contentDigest hex DIFFER !!!!------------");
                    System.out.println("---------getDigest hex------------");
                    digest = digestInfo.getDigest();
                    System.out.println(DatatypeConverter.printHexBinary(digest));

                    ais.close();

                    System.out.println("---------contentDigest hex------------");
                    System.out.println(contentDigestHex);

                    final Signature signature = Signature.getInstance("SHA256withRSA");
                    signature.initVerify(publicKey);
                    signature.update(digest);
                    System.out.println("---------signature.verify------------");
                    System.out.println(signature.verify(signatureBytes));

                    Security.addProvider(new BouncyCastleProvider());
                    Signature bcSignature = Signature.getInstance("RSA", "BC");
                    bcSignature.initVerify(publicKey);
                    bcSignature.update(digest);
                    System.out.println("---------signature bc.verify------------");
                    System.out.println(bcSignature.verify(signatureBytes));

                    break;

                default:
                    throw new IOException("Unknown certificate type " + subFilter);

                }
                ;
            }
            ;
        }
        ;
    }

    /**
     * <a href="https://stackoverflow.com/questions/57926872/signed-pdf-content-digest-that-was-calculated-during-verification-is-diffrent-th">
     * Signed PDF content digest that was calculated during verification is diffrent than decripted digest from signature
     * </a>
     * <br/>
     * <a href="https://drive.google.com/open?id=1UlOZOp-UYllK7Ra35dggccoWdhcb_Ntp">
     * TEST-signed-pades-baseline-b.pdf
     * </a>
     * <p>
     * The code here demonstrates how to retrieve the messageDigest
     * signed attribute value from a signed PDF. For production use
     * obviously some null checks are required.
     * </p>
     */
    @Test
    public void testExtractMessageDigestAttributeForUser2893427() throws IOException, CMSException {
        try (   InputStream resource = getClass().getResourceAsStream("TEST-signed-pades-baseline-b.pdf")   ) {
            byte[] bytes = IOUtils.toByteArray(resource);
            PDDocument document = Loader.loadPDF(bytes);
            List<PDSignature> signatures = document.getSignatureDictionaries();
            PDSignature sig = signatures.get(0);
            byte[] cmsBytes = sig.getContents(bytes);
            CMSSignedData cms = new CMSSignedData(cmsBytes);
            SignerInformation signerInformation = cms.getSignerInfos().iterator().next();
            Attribute attribute = signerInformation.getSignedAttributes().get(PKCSObjectIdentifiers.pkcs_9_at_messageDigest);
            ASN1Encodable value = attribute.getAttributeValues()[0];
            System.out.printf("MessageDigest attribute value: %s\n", value);
        }
    }
}