Monday, January 2, 2012

Java: Implementing PGP Single Pass Sign and Encrypt using League of Bouncy Castle library

The League of Bouncy Castle Cryptography library is chock-full of goodies but it is hard to convert what is in there to more practical examples.
The example files are a solid basis but I seam to need to fiddle quite a bit until it something is usable for me. The PGP Single Pass Sign and Encrypt process is one of these things that took me for a long time to figure out. I owe much of the actual solution impementation to John Opincar who solved this puzzle for C#.
Here is my implementation for Java:




/**
 * 
 */
package net.boncode.crypto;

//bouncy castle imports
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.CompressionAlgorithmTags;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignature;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureList;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.bouncycastle.openpgp.PGPUtil;

//java imports
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Date;
import java.util.Iterator;

/**
 * @author Bilal Soylu
 * 
 */

public class OnePassSignatureProcessor {

 /**
  * This is the primary function that will create encrypt a file and sign it
  * with a one pass signature. This leans on an C# example by John Opincar
  * @author Bilal Soylu
  * @param targetFileName
  *            -- file name on drive systems that will contain encrypted content
  * @param embeddedFileName
  *            -- the original file name before encryption
  * @param secretKeyRingInputStream
  *            -- Private Key Ring File
  * @param targetFileStream
  *            -- The stream for the encrypted target file
  * @param secretKeyPassphrase
  *            -- The private key password for the key retrieved from
  *            collection used for signing
  * @param signPublicKeyInputStream
  *            -- the public key of the target recipient to be used to
  *            encrypt the file
  * @throws Exception
  */
 public void fEncryptOnePassSignatureLocal(String targetFileName,
   String embeddedFileName, InputStream secretKeyRingInputStream,
    OutputStream targetFileStream, String secretKeyPassphrase,   
   InputStream signPublicKeyInputStream, InputStream contentStream) throws Exception {
  // ** INIT
  // read public Key from stream (file, if keyring we use the first working key)
  PGPPublicKey encKey = readPublicKey(signPublicKeyInputStream);
  // need to convert the password to a character array
  char[] password = secretKeyPassphrase.toCharArray();
  int BUFFER_SIZE = 1 << 16; // should always be power of 2(one shifted bitwise 16 places)
  //for now we will always do integrity checks and armor file
  boolean armor = true;
  boolean withIntegretyCheck = true;
  //set default provider, we will pass this along
  BouncyCastleProvider bcProvider = new BouncyCastleProvider();

  // armor stream if set
  if (armor)
   targetFileStream = new ArmoredOutputStream(targetFileStream);

  // Init encrypted data generator
  PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
    SymmetricKeyAlgorithmTags.CAST5, withIntegretyCheck,
    new SecureRandom(), bcProvider);
  encryptedDataGenerator.addMethod(encKey);
  OutputStream encryptedOut = encryptedDataGenerator.open(targetFileStream,new byte[BUFFER_SIZE]);

  // start compression
  PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(
    CompressionAlgorithmTags.ZIP);
  OutputStream compressedOut = compressedDataGenerator.open(encryptedOut);

  //start signature
  //PGPSecretKeyRingCollection pgpSecBundle = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(secretKeyRingInputStream));
  //PGPSecretKey pgpSecKey = pgpSecBundle.getSecretKey(keyId);
  PGPSecretKey pgpSecKey = readSecretKey(secretKeyRingInputStream);
  if (pgpSecKey == null)
   throw new Exception("No secret key could be found in specified key ring collection.");
  PGPPrivateKey pgpPrivKey = pgpSecKey.extractPrivateKey(password,bcProvider);

  PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
    pgpSecKey.getPublicKey().getAlgorithm(),
    HashAlgorithmTags.SHA1, bcProvider);
  
  signatureGenerator.initSign(PGPSignature.BINARY_DOCUMENT, pgpPrivKey);
  // iterate to find first signature to use
  for (@SuppressWarnings("rawtypes")
  Iterator i = pgpSecKey.getPublicKey().getUserIDs(); i.hasNext();) {
   String userId = (String) i.next();
   PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator();
   spGen.setSignerUserID(false, userId);
   signatureGenerator.setHashedSubpackets(spGen.generate());
   // Just the first one!
   break;
  }
  signatureGenerator.generateOnePassVersion(false).encode(compressedOut);

  // Create the Literal Data generator output stream
  PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator();
  // get file handle
  File actualFile = new File(targetFileName);
  // create output stream
  OutputStream literalOut = literalDataGenerator.open(compressedOut,
    PGPLiteralData.BINARY, embeddedFileName,
    new Date(actualFile.lastModified()), new byte[BUFFER_SIZE]);
  
  
  // read input file and write to target file using a buffer
  byte[] buf = new byte[BUFFER_SIZE];
  int len;
  while ((len = contentStream.read(buf, 0, buf.length)) > 0) {
   literalOut.write(buf, 0, len);
   signatureGenerator.update(buf, 0, len);
  }
  // close everything down we are done
  literalOut.close();
  literalDataGenerator.close();
  signatureGenerator.generate().encode(compressedOut);
  compressedOut.close();
  compressedDataGenerator.close();
  encryptedOut.close();
  encryptedDataGenerator.close();
  

  if (armor) targetFileStream.close();

 }
 /**
  * Try to find a public key in the Key File or Key Ring File
  * We will use the first one for now.
  * @author Bilal Soylu
  * @param in -- File Stream to KeyRing or Key
  * @return first public key
  * @throws IOException
  * @throws PGPException
  */
 private static PGPPublicKey readPublicKey(InputStream in)
   throws IOException, PGPException {
  in = PGPUtil.getDecoderStream(in);

  PGPPublicKeyRingCollection pgpPub = new PGPPublicKeyRingCollection(in);

  //
  // we are only looking for the first key that matches
  //

  //
  // iterate through the key rings.
  //
  Iterator rIt = pgpPub.getKeyRings();

  while (rIt.hasNext()) {
   PGPPublicKeyRing kRing = (PGPPublicKeyRing) rIt.next();
   Iterator kIt = kRing.getPublicKeys();

   while (kIt.hasNext()) {
    PGPPublicKey k = (PGPPublicKey) kIt.next();

    if (k.isEncryptionKey()) {
     return k;
    }
   }
  }

  throw new IllegalArgumentException(
    "Can't find encryption key in key ring.");
 }

 
 
 /**
  * Find first secret key in key ring or key file. 
  * A secret key contains a private key that can be accessed with a password.
  * @author Bilal Soylu
  * @param in -- input Key file or key ring file
  * @param passwd -- password for key
  * @return matching private key
  * @throws IOException
  * @throws PGPException
  * @throws NoSuchProviderException
  */
 private static PGPSecretKey readSecretKey(InputStream in)
   throws IOException, PGPException, NoSuchProviderException {
  
  PGPSecretKey               sKey = null;
  try {
   in = PGPUtil.getDecoderStream(in);
   PGPSecretKeyRingCollection pgpPriv = new PGPSecretKeyRingCollection(in);
 
   // we just loop through the collection till we find a key suitable for
   // decrypt
   Iterator  it = pgpPriv.getKeyRings();       
   PGPSecretKeyRing   pbr = null;
 
         while (sKey == null && it.hasNext())
         {
          Object readData = it.next();
          if (readData instanceof PGPSecretKeyRing) {           
           pbr = (PGPSecretKeyRing)readData;             
              sKey =  pbr.getSecretKey();
             }
         }
         
         if (sKey == null)
         {
             throw new IllegalArgumentException("secret key for message not found.");
         }
  }
        catch (PGPException e)
        {
            System.err.println(e);
            if (e.getUnderlyingException() != null)
            {
                e.getUnderlyingException().printStackTrace();
            }
        }
        return sKey; 
 } 
 
 

 /**
  * fDecryptOnePassSignature will decrypt a file that was encrypted using
  * public key, then signed with a private key as one pass signature based on
  * example of verifyAndDecrypt() by Raul
  * 
  * @param encryptedInputStream
  * @param signPublicKeyInputStream
  * @param secretKeyInputStream
  * @param secretKeyPassphrase
  * @return
  * @throws Exception
  */
 public void fDecryptOnePassSignatureLocal(InputStream encryptedInputStream,
   InputStream signPublicKeyInputStream,
   InputStream secretKeyInputStream, String secretKeyPassphrase,
   OutputStream targetStream) throws Exception {

  Security.addProvider(new BouncyCastleProvider());

  // The decrypted results.
  // StringBuffer result = new StringBuffer();
  // The private key we use to decrypt contents.
  PGPPrivateKey privateKey = null;
  // The PGP encrypted object representing the data to decrypt.
  PGPPublicKeyEncryptedData encryptedData = null;

  // Get the list of encrypted objects in the message. The first object in
  // the
  // message might be a PGP marker, however, so we skip it if necessary.
  PGPObjectFactory objectFactory = new PGPObjectFactory(PGPUtil.getDecoderStream(encryptedInputStream));
  Object firstObject = objectFactory.nextObject();
  System.out.println("firstObject is " + firstObject);
  PGPEncryptedDataList dataList = (PGPEncryptedDataList) (firstObject instanceof PGPEncryptedDataList ? firstObject
    : objectFactory.nextObject());

  // Find the encrypted object associated with a private key in our key
  // ring.
  @SuppressWarnings("rawtypes")
  Iterator dataObjectsIterator = dataList.getEncryptedDataObjects();
  PGPSecretKeyRingCollection secretKeyCollection = new PGPSecretKeyRingCollection(
    PGPUtil.getDecoderStream(secretKeyInputStream));
  while (dataObjectsIterator.hasNext()) {
   encryptedData = (PGPPublicKeyEncryptedData) dataObjectsIterator.next();
   System.out.println("next data object is " + encryptedData);
   PGPSecretKey secretKey = secretKeyCollection.getSecretKey(encryptedData.getKeyID());
   
   if (secretKey != null) {
    // This object was encrypted for this key. If the passphrase is
    // incorrect, this will generate an error.
    privateKey = secretKey.extractPrivateKey(secretKeyPassphrase.toCharArray(), "BC");
    break;
   }
  }

  if (privateKey == null) {
   System.out.println();
   throw new RuntimeException("secret key for message not found");
  }

  // Get a handle to the decrypted data as an input stream
  InputStream clearDataInputStream = encryptedData.getDataStream( privateKey, "BC");
  PGPObjectFactory clearObjectFactory = new PGPObjectFactory( clearDataInputStream);
  Object message = clearObjectFactory.nextObject();

  System.out.println("message for PGPCompressedData check is " + message);

  // Handle case where the data is compressed
  if (message instanceof PGPCompressedData) {
   PGPCompressedData compressedData = (PGPCompressedData) message;
   objectFactory = new PGPObjectFactory(compressedData.getDataStream());
   message = objectFactory.nextObject();
  }

  System.out.println("message for PGPOnePassSignature check is " + message);

  PGPOnePassSignature calculatedSignature = null;
  if (message instanceof PGPOnePassSignatureList) {
   calculatedSignature = ((PGPOnePassSignatureList) message).get(0);
   PGPPublicKeyRingCollection publicKeyRingCollection = new PGPPublicKeyRingCollection(
     PGPUtil.getDecoderStream(signPublicKeyInputStream));
   PGPPublicKey signPublicKey = publicKeyRingCollection
     .getPublicKey(calculatedSignature.getKeyID());
   calculatedSignature.initVerify(signPublicKey, "BC");
   message = objectFactory.nextObject();
  }

  System.out.println("message for PGPLiteralData check is " + message);

  // We should only have literal data, from which we can finally read the
  // decrypted message.
  if (message instanceof PGPLiteralData) {
   InputStream literalDataInputStream = ((PGPLiteralData) message).getInputStream();
   int nextByte;

   while ((nextByte = literalDataInputStream.read()) >= 0) {
    // InputStream.read guarantees to return a byte (range 0-255),
    // so we
    // can safely cast to char.
    calculatedSignature.update((byte) nextByte); // also update
                // calculated
                // one pass
                // signature
    // result.append((char) nextByte);
    // add to file instead of StringBuffer
    targetStream.write((char) nextByte);
   }
   targetStream.close();
  } else {
   throw new RuntimeException("unexpected message type " + message.getClass().getName());
  }

  if (calculatedSignature != null) {
   PGPSignatureList signatureList = (PGPSignatureList) objectFactory.nextObject();
   System.out.println("signature list (" + signatureList.size() + " sigs) is " + signatureList);
   PGPSignature messageSignature = (PGPSignature) signatureList.get(0);
   System.out.println("verification signature is " + messageSignature);
   if (!calculatedSignature.verify(messageSignature)) {
    throw new RuntimeException("signature verification failed");
   }
  }

  if (encryptedData.isIntegrityProtected()) {
   if (encryptedData.verify()) {
    System.out.println("message integrity protection verification succeeded");
   } else {
    throw new RuntimeException("message failed integrity check");
   }
  } else {
   System.out.println("message not integrity protected");
  }

  //close streams
  clearDataInputStream.close();
  
  
 }

}




Cheers,
B.

6 comments:

Mauricio Sanaphre said...

Really useful!! This is what I was looking for, you just saved my day and code works very smooth, thanks!!

liester7 said...

Encryption has to be one of the most irritating things I have ever come across. It took me hours/days to find the right kind of example that would allow me to sign AND encrypt a file. I did modify the code to my needs (temp files, keys as strings) but was able to get it to work . Thank you for the post. The days were getting darker.

bman said...

You are welcome.

Kris said...

When I go to decrypt the file, I am getting this message. Any suggestions?

You need a passphrase to unlock the secret key for
user: "NEW_KEY"
2048-bit RSA key, ID AA281F85, created 2018-02-26 (main key ID 07137B98)

gpg: encrypted with 2048-bit RSA key, ID AA281F85, created 2018-02-26
"NEW_KEY"
gpg: no valid OpenPGP data found.
gpg: Signature made 09/10/18 13:28:27 Eastern Daylight Time using RSA key ID 780DA3A4
gpg: BAD signature from "SOME_KEY" [ultimate]
gpg: WARNING: encrypted message has been manipulated!

Kris said...

I fixed my issue, I forgot to close the outputStream to finish the signature process.

nick go said...

had to retrofit to the latest library but it's really good!!