package com.wesabe.grendel.openpgp;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Collection;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.PGPSignatureGenerator;
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
/**
* A writer class capable of producing encrypted+signed OpenPGP messages.
*
* <p>
* This class only produces messages of the following form:
*
* <pre>
* +---------------------------------------------------------------------------+
* | Public-Key Encrypted Session Key Packet |
* +---------------------------------------------------------------------------+
* | ... (repeated for all recipients) |
* +---------------------------------------------------------------------------+
* | Symmetrically Encrypted Integrity Protected Data Packet |
* | |
* | +-----------------------------------------------------------------------+ |
* | | Compressed Data Packet | |
* | | | |
* | | +-------------------------------------------------------------------+ | |
* | | | One-Pass Signature Packet | | |
* | | +-------------------------------------------------------------------+ | |
* | | | ... (repeated for all signers) | | |
* | | +-------------------------------------------------------------------+ | |
* | | | Literal Data Packet | | |
* | | | | | |
* | | | +---------------------------------------------------------------+ | | |
* | | | | | | | |
* | | | | message body | | | |
* | | | | | | | |
* | | | +---------------------------------------------------------------+ | | |
* | | | | | |
* | | +-------------------------------------------------------------------+ | |
* | | | Signature Packet | | |
* | | +-------------------------------------------------------------------+ | |
* | | | ... (repeated for all signers) | | |
* | | +-------------------------------------------------------------------+ | |
* | | | |
* | +-----------------------------------------------------------------------+ |
* | | Modification Detection Code Packet | |
* | +-----------------------------------------------------------------------+ |
* | |
* +---------------------------------------------------------------------------+
* </pre>
*
* First, a signature of the message body is generated using the owner's private
* key. The body and signature are then compressed and encrypted using a random
* symmetric session key and stored in a integrity-protected data packet with a
* matching modification detection code packet. The session key is then
* encrypted with the owner and receipients' public keys.
* <p>
* To prevent adaptive chosen-plaintext attacks, this class enforces two
* constraints:
* <ul>
* <li>All signed data is compressed before being encrypted.
* <li>All encrypted data has an accompanying modification detection code
* packet.
* </ul>
*
* @author coda
* @see <a href="http://eprint.iacr.org/2005/033.pdf">An Attack on CFB Mode Encryption As Used By OpenPGP</a>
* @see <a href="http://www.cs.umd.edu/~jkatz/papers/pgp-attack.pdf">Implementation of Chosen-Ciphertext Attacks against PGP and GnuPG</a>
* @see AsymmetricAlgorithm#ENCRYPTION_DEFAULT
* @see AsymmetricAlgorithm#SIGNING_DEFAULT
* @see SymmetricAlgorithm#DEFAULT
* @see HashAlgorithm#DEFAULT
* @see CompressionAlgorithm#DEFAULT
*/
public class MessageWriter {
private static final int BUFFER_SIZE = 1 << 16;
private static final double ENVELOPE_OVERHEAD = 1.2;
private static final double RECIPIENT_OVERHEAD = 300;
private final UnlockedKeySet owner;
private final Collection<KeySet> recipients;
private final SecureRandom random;
/**
* Creates a new writer for an encrypted+signed message.
*
* @param owner
* the {@link UnlockedKeySet} belonging to the message owner
* @param recipients
* the {@link KeySet}s belonging to the recipients
* @param random
* a {@link SecureRandom} instance
*/
public MessageWriter(UnlockedKeySet owner, Collection<KeySet> recipients, SecureRandom random) {
this.owner = owner;
this.recipients = recipients;
this.random = random;
}
/**
* Signs, compresses, and encrypts a message.
*
* @param body
* the message body
* @return the message, in an encrypted+signed OpenPGP envelope
* @throws CryptographicException
* if any error occurs while processing the message
*/
public byte[] write(byte[] body) throws CryptographicException {
try {
final ByteArrayOutputStream output = new ByteArrayOutputStream(estimateEncryptedSize(body.length));
signAndCompressAndEncrypt(body, output);
return output.toByteArray();
} catch (Exception e) {
throw new CryptographicException(e);
}
}
/*
* This formula was empirically determined to return a buffer size which
* will fit most messages, including envelope overhead and per-recipient
* overhead. Some messages may require another buffer allocation, but this
* should be rare.
*/
private int estimateEncryptedSize(int unencryptedSize) {
return (int) Math.round(Math.ceil(
(unencryptedSize * ENVELOPE_OVERHEAD) +
(recipients.size() * RECIPIENT_OVERHEAD)
));
}
private void signAndCompressAndEncrypt(byte[] body, OutputStream output) throws Exception {
final OutputStream encryptedOutput = getEncryptionWrapper(output);
signAndCompress(body, encryptedOutput);
encryptedOutput.close();
}
private void signAndCompress(byte[] body, OutputStream encryptedOutput) throws Exception {
final OutputStream compressedOutput = getCompressionWrapper(encryptedOutput);
sign(body, compressedOutput);
compressedOutput.close();
}
private void sign(byte[] body, OutputStream compressedOutput) throws Exception {
final PGPSignatureGenerator signatureGenerator = getSignatureGenerator(owner.getUnlockedMasterKey());
signatureGenerator.generateOnePassVersion(false).encode(compressedOutput);
final OutputStream literalOutput = getLiteralWrapper(compressedOutput);
literalOutput.write(body);
signatureGenerator.update(body);
literalOutput.close();
signatureGenerator.generate().encode(compressedOutput);
}
private OutputStream getEncryptionWrapper(OutputStream out) throws Exception {
final PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
SymmetricAlgorithm.DEFAULT.toInteger(), true, random,
"BC");
for (KeySet recipient : recipients) {
if (recipient.getSubKey().getKeyID() != owner.getSubKey().getKeyID()) {
encryptedDataGenerator.addMethod(recipient.getSubKey().getPublicKey());
}
}
encryptedDataGenerator.addMethod(owner.getSubKey().getPublicKey());
return encryptedDataGenerator.open(out, new byte[BUFFER_SIZE]);
}
private OutputStream getCompressionWrapper(OutputStream out) throws Exception {
return new PGPCompressedDataGenerator(CompressionAlgorithm.DEFAULT.toInteger()).open(out);
}
private PGPSignatureGenerator getSignatureGenerator(UnlockedMasterKey owner) throws Exception {
final PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(
owner.getPublicKey().getAlgorithm(),
HashAlgorithm.DEFAULT.toInteger(),
"BC");
signatureGenerator.initSign(PGPSignature.BINARY_DOCUMENT, owner.getPrivateKey());
final PGPSignatureSubpacketGenerator signatureMetaData = new PGPSignatureSubpacketGenerator();
signatureMetaData.setSignerUserID(false, owner.getUserID());
signatureGenerator.setHashedSubpackets(signatureMetaData.generate());
return signatureGenerator;
}
private OutputStream getLiteralWrapper(OutputStream output) throws Exception {
return new PGPLiteralDataGenerator().open(output,
PGPLiteralData.BINARY,
PGPLiteralData.CONSOLE,
new DateTime(DateTimeZone.UTC).toDate(),
new byte[BUFFER_SIZE]
);
}
}