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();
}
}
B.
6 comments:
Really useful!! This is what I was looking for, you just saved my day and code works very smooth, thanks!!
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.
You are welcome.
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!
I fixed my issue, I forgot to close the outputStream to finish the signature process.
had to retrofit to the latest library but it's really good!!
Post a Comment