package com.wesabe.grendel.openpgp;
import com.wesabe.grendel.util.IntegerEquivalents;
import org.bouncycastle.openpgp.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;
import java.security.NoSuchProviderException;
/**
* A reader class capable of decrypting OpenPGP messages created by
* {@link MessageWriter}.
* <p>
* For security reasons, this class enforces the following constraints:
* <ul>
* <li>Uncompressed data is not accepted, due to adaptive-chosen plaintext
* attacks.
* <li>Integrity-protected data is required, and modification detection code
* packets are always verified.
* <li>OpenPGP/CFB mode's "quick check" is disabled, due to adaptive
* chosen-ciphertext oracle attacks.
* <li>Weak algorithms are not accepted.
* </ul>
*
* Any deviation from the format described by {@link MessageWriter} is
* considered an unrecoverable error.
*
* @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 MessageWriter
* @see HashAlgorithm#ACCEPTABLE_ALGORITHMS
* @see SymmetricAlgorithm#ACCEPTABLE_ALGORITHMS
* @author coda
*/
public class MessageReader {
private static final int BUFFER_SIZE = 1024 * 16; // 16KB
private final KeySet signer;
private final UnlockedKeySet recipient;
/**
* Creates a new reader for a encrypted+signed message.
*
* @param signer
* the {@link KeySet} belonging to the user who signed the
* message
* @param recipient
* the {@link UnlockedKeySet} belonging to the user whose public
* key the message is encrypted with
*/
public MessageReader(KeySet signer, UnlockedKeySet recipient) {
this.signer = signer;
this.recipient = recipient;
}
/**
* Decrypts the message and verifies its signature and integrity packet.
*
* @param encrypted
* the encrypted message body
* @return the decrypted message body
* @throws CryptographicException
* if any error occurs while processing the message. This should
* be taken as an indicator that the message has been tampered
* with or is invalid, and that retrying the operation would be
* pointless.
*/
public byte[] read(byte[] encrypted) throws CryptographicException {
try {
final PGPPublicKeyEncryptedData encryptedData = getEncryptedData(new ByteArrayInputStream(encrypted));
final InputStream decryptedData = encryptedData.getDataStream(recipient.getUnlockedSubKey().getPrivateKey(), "BC");
final InputStream decompressedData = getCompressedData(decryptedData);
final PGPObjectFactory factory = getFactory(decompressedData);
final PGPOnePassSignature signature = getOnePassSignature(signer, factory);
signature.initVerify(signer.getMasterKey().getPublicKey(), "BC");
final InputStream body = getLiteralData(factory);
final ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] b = new byte[BUFFER_SIZE];
int r = 0;
while ((r = body.read(b)) >= 0) {
output.write(b, 0, r);
signature.update(b, 0, r);
}
if (!signature.verify(getSignature(signer, factory))) {
throw new CryptographicException("Invalid signature");
}
if (!encryptedData.verify()) {
throw new CryptographicException("Integrity check failed");
}
return output.toByteArray();
} catch (IOException e) {
throw new CryptographicException(e);
} catch (ClassCastException e) {
throw new CryptographicException(e);
} catch (GeneralSecurityException e) {
throw new CryptographicException(e);
} catch (PGPException e) {
throw new CryptographicException(e);
}
}
private PGPSignature getSignature(KeySet owner, PGPObjectFactory factory) throws CryptographicException, IOException {
final PGPSignatureList signatures = (PGPSignatureList) factory.nextObject();
for (int i = 0, size = signatures.size(); i < size; i++) {
final PGPSignature signature = signatures.get(i);
if (signature.getKeyID() == owner.getMasterKey().getKeyID()) {
final HashAlgorithm hashAlgorithm = IntegerEquivalents.fromInt(
HashAlgorithm.class,
signature.getHashAlgorithm()
);
if (!HashAlgorithm.ACCEPTABLE_ALGORITHMS.contains(hashAlgorithm)) {
throw new CryptographicException("data was signed with " + hashAlgorithm + " which is unacceptable");
}
return signature;
}
}
throw new CryptographicException("couldn't find a signature by " + owner);
}
private InputStream getLiteralData(PGPObjectFactory factory) throws IOException {
return ((PGPLiteralData) factory.nextObject()).getDataStream();
}
private PGPOnePassSignature getOnePassSignature(KeySet owner, PGPObjectFactory factory) throws CryptographicException, IOException {
final PGPOnePassSignatureList signatures = (PGPOnePassSignatureList) factory.nextObject();
for (int i = 0, size = signatures.size(); i < size; i++) {
final PGPOnePassSignature signature = signatures.get(i);
if (signature.getKeyID() == owner.getMasterKey().getKeyID()) {
return signature;
}
}
throw new CryptographicException("couldn't find a one-pass signature by " + owner);
}
@SuppressWarnings("deprecation")
private InputStream getCompressedData(InputStream decryptedData) throws PGPException, IOException, CryptographicException {
final PGPObjectFactory factory = getFactory(decryptedData);
final PGPCompressedData compressedData = (PGPCompressedData) factory.nextObject();
if (compressedData.getAlgorithm() == CompressionAlgorithm.NONE.toInteger()) {
throw new CryptographicException("encrypted data is uncompressed");
}
return compressedData.getDataStream();
}
private PGPPublicKeyEncryptedData getEncryptedData(InputStream input) throws IOException,
CryptographicException, IllegalArgumentException, NoSuchProviderException, PGPException {
final PGPObjectFactory factory = getFactory(input);
final PGPEncryptedDataList encryptedDataList = (PGPEncryptedDataList) factory.nextObject();
for (int i = 0, size = encryptedDataList.size(); i < size; i++) {
final PGPEncryptedData encryptedData = (PGPEncryptedData) encryptedDataList.get(i);
if (encryptedData instanceof PGPPublicKeyEncryptedData) {
final PGPPublicKeyEncryptedData pkEncryptedData = (PGPPublicKeyEncryptedData) encryptedData;
if (pkEncryptedData.getKeyID() == recipient.getSubKey().getKeyID()) {
final SymmetricAlgorithm symmetricAlgorithm = IntegerEquivalents.fromInt(
SymmetricAlgorithm.class,
pkEncryptedData.getSymmetricAlgorithm(recipient.getUnlockedSubKey().getPrivateKey(), "BC")
);
if (!SymmetricAlgorithm.ACCEPTABLE_ALGORITHMS.contains(symmetricAlgorithm)) {
throw new CryptographicException("data is encrypted with " + symmetricAlgorithm + " which is unacceptable");
}
if (!pkEncryptedData.isIntegrityProtected()) {
throw new CryptographicException("missing integrity packet");
}
return pkEncryptedData;
}
}
}
throw new CryptographicException("no encrypted data for " + recipient + " found");
}
private PGPObjectFactory getFactory(InputStream input) throws IOException {
return new PGPObjectFactory(PGPUtil.getDecoderStream(input));
}
}