PgpHelper.java

package com.hsbc.baas;

import lombok.NoArgsConstructor;
import lombok.extern.java.Log;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.bcpg.*;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.*;
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.jcajce.*;
import org.bouncycastle.util.io.Streams;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;


/**
 * Created by 44024985 on 04/09/2018.
 */
@Log
public class PgpHelper {
    private JcaKeyFingerprintCalculator keyFingerPrintCalculator;

    private Provider bcp;

    public PgpHelper() {
        this.keyFingerPrintCalculator = new JcaKeyFingerprintCalculator();
        this.bcp = new BouncyCastleProvider();
    }

    /**
     * @param publicKeyInputStream bank/public key input stream.
     * @return List of PGP Public Keys
     * @throws IOException, PGPException
     */
    public List<PGPPublicKey> readPublicKey(InputStream publicKeyInputStream) throws IOException, PGPException {

        // public key file is decoded and used to generate a key ring object that stores a list of key rings. These contain the public keys.
        // The fingerPrintCalculator is used to calculate the fingerprint of each key which is needed to verify key authenticity.
        PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(publicKeyInputStream),
                this.keyFingerPrintCalculator);

        List<PGPPublicKey> keys = new ArrayList<>();

        pgpPub.getKeyRings().forEachRemaining(keyRing -> keyRing.getPublicKeys().forEachRemaining(keys::add));

        return keys;
    }



    /**
     * @param pgpSecKey Secret Key.
     * @param pass      passphrase to decrypt secret key with.
     * @return PGPPrivate key.
     * @throws PGPException
     */
    public PGPPrivateKey findSecretKey(PGPSecretKey pgpSecKey, char[] pass)
            throws PGPException {

        // Extract a private key from a PGPSecretKey object.
        PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder(
                new JcaPGPDigestCalculatorProviderBuilder().setProvider(this.bcp).build()).setProvider(this.bcp).build(pass);

        return pgpSecKey.extractPrivateKey(decryptor);
    }

    public List<PGPSecretKey> readSecretKey(InputStream input) throws IOException, PGPException {

        // Secret key ring collection is generated from the private key file input stream.
        PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(
                PGPUtil.getDecoderStream(input), this.keyFingerPrintCalculator);

        // Loop through key rings, then through keys and add keys found to list.
        List<PGPSecretKey> keys = new ArrayList<>();
        pgpSec.getKeyRings().forEachRemaining(keyRing -> keyRing.getSecretKeys().forEachRemaining(keys::add));

        return keys;
    }


    /**
     * @param outf the output stream
     * @param inputStream the input stream
     * @param encKey the PGP Public key
     * @param privateKey the private key
     * @throws PGPException
     */
    public void encryptAndSign(OutputStream outf, InputStream inputStream, PGPPublicKey encKey,
                                      PGPPrivateKey privateKey) throws IOException {
        OutputStream out = new ArmoredOutputStream(outf);
        OutputStream lOut = null;
        try {
            // Encrypted Data Generator
            PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(
                    new JcePGPDataEncryptorBuilder(SymmetricKeyAlgorithmTags.AES_256).setWithIntegrityPacket(true)
                            .setSecureRandom(new SecureRandom()).setProvider(this.bcp));
            encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(encKey).setProvider(this.bcp));
            OutputStream encryptedOut = encGen.open(out, new byte[inputStream.available()]);

            // Compressed Data Generator
            PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP);
            OutputStream compressedData = comData.open(encryptedOut);

            // Signature Generator
            PGPSignatureGenerator sGen = new PGPSignatureGenerator(
                    new JcaPGPContentSignerBuilder(privateKey.getPublicKeyPacket().getAlgorithm(), HashAlgorithmTags.SHA512)
                            .setProvider(this.bcp));
            sGen.init(PGPSignature.BINARY_DOCUMENT, privateKey);

            // Get the user ID from the encryption key, and use this to generate a signed subpacket.
            Iterator it = encKey.getUserIDs();
            if (it.hasNext()) {
                PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
                spGen.setSignerUserID(false, (String) it.next());
                sGen.setHashedSubpackets(spGen.generate());
            }

            // Write the encoded packet to compressed data stream.
            sGen.generateOnePassVersion(false).encode(compressedData); // bOut

            // Create a literal data output stream
            PGPLiteralDataGenerator lGen = new PGPLiteralDataGenerator();
            lOut = lGen.open(compressedData, PGPLiteralData.BINARY, "Sample-Data", new Date(),
                    new byte[inputStream.available()]);

            // Write and sign data.
            byte[] data = IOUtils.toByteArray(inputStream);
            lOut.write(data);
            sGen.update(data);

            lGen.close();

            // Generate signature for compressed data.
            sGen.generate().encode(compressedData);

            comData.close();
            compressedData.close();

            encryptedOut.close();
            encGen.close();

            out.close();
        } catch (PGPException e) {
            log.warning(e.getLocalizedMessage());
            if (e.getUnderlyingException() != null) {
                e.getUnderlyingException().printStackTrace();
            }
        } catch (Exception e) {
            System.err.println(e);
        } finally {
            if (lOut != null) lOut.close();
        }
    }

//    private Object parseMessage(Object message, PGPObjectFactory plainFact, PGPCompressedData compressedData, PGPOnePassSignatureList onePassSignatureList, PGPSignatureList signatureList, ByteArrayOutputStream actualOutput) throws IOException, PGPException {
//        while (message != null) {
//            if (message instanceof PGPCompressedData) {
//                compressedData = (PGPCompressedData) message;
//                plainFact = new PGPObjectFactory(compressedData.getDataStream(), this.keyFingerPrintCalculator);
//                message = plainFact.nextObject();
//            }
//
//            if (message instanceof PGPLiteralData) {
//                // have to read it and keep it somewhere.
//                Streams.pipeAll(((PGPLiteralData) message).getInputStream(), actualOutput);
//            } else if (message instanceof PGPOnePassSignatureList) {
//                onePassSignatureList = (PGPOnePassSignatureList) message;
//            } else if (message instanceof PGPSignatureList) {
//                signatureList = (PGPSignatureList) message;
//            } else {
//                throw new PGPException("message unknown message type.");
//            }
//            message = plainFact.nextObject();
//        }
//    }

    private boolean checkSignatureVerified(PGPOnePassSignatureList onePassSignatureList, PGPSignatureList signatureList, List<PGPPublicKey> publicKeys, byte[] output) throws PGPException {
        for (int i = 0; i < onePassSignatureList.size(); i++) {
            PGPOnePassSignature ops = onePassSignatureList.get(0);
            if (publicKeys != null) {
                for (PGPPublicKey publicKey : publicKeys) {
                    ops.init(new JcaPGPContentVerifierBuilderProvider().setProvider(this.bcp), publicKey);
                    ops.update(output);
                    PGPSignature signature = signatureList.get(i);
                    if (ops.verify(signature)) {
                        return(true);
                    }
                }
            }
        }
        return(false);
    }

    public void decryptStream(InputStream in, OutputStream out, List<PGPPrivateKey> keysIn,
                                     List<PGPPublicKey> publicKeys) throws Exception {
        PGPObjectFactory pgpF = new PGPObjectFactory(PGPUtil.getDecoderStream(in), this.keyFingerPrintCalculator);
        PGPEncryptedDataList enc;
        Object o = pgpF.nextObject();


        // Parse first object, which might be a PGP marker packet.
        if (o instanceof PGPEncryptedDataList) {
            enc = (PGPEncryptedDataList) o;
        } else {
            enc = (PGPEncryptedDataList) pgpF.nextObject();
        }

        // Find the secret key
        Iterator<PGPEncryptedData> it = enc.getEncryptedDataObjects();
        PGPPublicKeyEncryptedData pbe = null;

        while (it.hasNext())
        {
            PGPEncryptedData encryptedData = it.next();
            if (encryptedData instanceof PGPPublicKeyEncryptedData)
            {
                pbe = (PGPPublicKeyEncryptedData)encryptedData;
            }
        }
        PublicKeyDataDecryptorFactory b;
        InputStream clear = null;
        for (PGPPrivateKey keyIn : keysIn) {
            if (keyIn.getKeyID() == pbe.getKeyID()) {
                b = new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(this.bcp).setContentProvider(this.bcp).build(keyIn);
                clear = pbe.getDataStream(b);
                break;
            }
        }

        PGPObjectFactory plainFact = new PGPObjectFactory(clear, this.keyFingerPrintCalculator);

        Object message = plainFact.nextObject();

        PGPOnePassSignatureList onePassSignatureList = null;
        PGPSignatureList signatureList = null;
        PGPCompressedData compressedData;
        ByteArrayOutputStream actualOutput = new ByteArrayOutputStream();

        while (message != null) {
            if (message instanceof PGPCompressedData) {
                compressedData = (PGPCompressedData) message;
                plainFact = new PGPObjectFactory(compressedData.getDataStream(), this.keyFingerPrintCalculator);
                message = plainFact.nextObject();
            }

            if (message instanceof PGPLiteralData) {
                // have to read it and keep it somewhere.
                Streams.pipeAll(((PGPLiteralData) message).getInputStream(), actualOutput);
            } else if (message instanceof PGPOnePassSignatureList) {
                onePassSignatureList = (PGPOnePassSignatureList) message;
            } else if (message instanceof PGPSignatureList) {
                signatureList = (PGPSignatureList) message;
            } else {
                throw new PGPException("message unknown message type.");
            }
            message = plainFact.nextObject();
        }

        actualOutput.close();
        byte[] output = actualOutput.toByteArray();
        if (onePassSignatureList == null || signatureList == null) {
            throw new PGPException("Poor PGP. Signatures not found.");
        } else {
            boolean signatureVerified = checkSignatureVerified(onePassSignatureList,signatureList,publicKeys,output);

            if (!signatureVerified) {
                throw new SignatureException("Signature verification failed");
            }

        }

        if (pbe.isIntegrityProtected() && !pbe.verify()) {
            throw new PGPDataValidationException("Data is integrity protected but integrity is lost.");
        } else if (publicKeys == null || publicKeys.isEmpty()) {
            throw new SignatureException("Signature not found");
        } else {
            out.write(output);
            out.flush();
            out.close();
        }
    }




}