package org.bouncycastle.openpgp; import java.io.IOException; import java.io.OutputStream; import java.math.BigInteger; import java.security.DigestOutputStream; import java.security.Key; import java.security.MessageDigest; import java.security.NoSuchProviderException; import java.security.Provider; import java.security.SecureRandom; import java.security.Security; import java.util.ArrayList; import java.util.List; import javax.crypto.Cipher; import javax.crypto.CipherOutputStream; import javax.crypto.spec.IvParameterSpec; import org.bouncycastle.bcpg.BCPGOutputStream; import org.bouncycastle.bcpg.ContainedPacket; import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.bcpg.PacketTags; import org.bouncycastle.bcpg.PublicKeyEncSessionPacket; import org.bouncycastle.bcpg.S2K; import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags; import org.bouncycastle.bcpg.SymmetricKeyEncSessionPacket; /** * Generator for encrypted objects. */ public class PGPEncryptedDataGenerator implements SymmetricKeyAlgorithmTags, StreamGenerator { /** * Specifier for SHA-1 S2K PBE generator. */ public static final int S2K_SHA1 = HashAlgorithmTags.SHA1; /** * Specifier for SHA-224 S2K PBE generator. */ public static final int S2K_SHA224 = HashAlgorithmTags.SHA224; /** * Specifier for SHA-256 S2K PBE generator. */ public static final int S2K_SHA256 = HashAlgorithmTags.SHA256; /** * Specifier for SHA-384 S2K PBE generator. */ public static final int S2K_SHA384 = HashAlgorithmTags.SHA384; /** * Specifier for SHA-512 S2K PBE generator. */ public static final int S2K_SHA512 = HashAlgorithmTags.SHA512; private BCPGOutputStream pOut; private CipherOutputStream cOut; private Cipher c; private boolean withIntegrityPacket = false; private boolean oldFormat = false; private DigestOutputStream digestOut; private abstract class EncMethod extends ContainedPacket { protected byte[] sessionInfo; protected int encAlgorithm; protected Key key; public abstract void addSessionInfo( byte[] sessionInfo) throws Exception; } private class PBEMethod extends EncMethod { S2K s2k; PBEMethod( int encAlgorithm, S2K s2k, Key key) { this.encAlgorithm = encAlgorithm; this.s2k = s2k; this.key = key; } public Key getKey() { return key; } public void addSessionInfo( byte[] sessionInfo) throws Exception { String cName = PGPUtil.getSymmetricCipherName(encAlgorithm); Cipher c = Cipher.getInstance(cName + "/CFB/NoPadding", defProvider); c.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(new byte[c.getBlockSize()]), rand); this.sessionInfo = c.doFinal(sessionInfo, 0, sessionInfo.length - 2); } public void encode(BCPGOutputStream pOut) throws IOException { SymmetricKeyEncSessionPacket pk = new SymmetricKeyEncSessionPacket(encAlgorithm, s2k, sessionInfo); pOut.writePacket(pk); } } private class PubMethod extends EncMethod { PGPPublicKey pubKey; BigInteger[] data; PubMethod( PGPPublicKey pubKey) { this.pubKey = pubKey; } public void addSessionInfo( byte[] sessionInfo) throws Exception { Cipher c; switch (pubKey.getAlgorithm()) { case PGPPublicKey.RSA_ENCRYPT: case PGPPublicKey.RSA_GENERAL: c = Cipher.getInstance("RSA/ECB/PKCS1Padding", defProvider); break; case PGPPublicKey.ELGAMAL_ENCRYPT: case PGPPublicKey.ELGAMAL_GENERAL: c = Cipher.getInstance("ElGamal/ECB/PKCS1Padding", defProvider); break; case PGPPublicKey.DSA: throw new PGPException("Can't use DSA for encryption."); case PGPPublicKey.ECDSA: throw new PGPException("Can't use ECDSA for encryption."); default: throw new PGPException("unknown asymmetric algorithm: " + pubKey.getAlgorithm()); } Key key = pubKey.getKey(defProvider); c.init(Cipher.ENCRYPT_MODE, key, rand); byte[] encKey = c.doFinal(sessionInfo); switch (pubKey.getAlgorithm()) { case PGPPublicKey.RSA_ENCRYPT: case PGPPublicKey.RSA_GENERAL: data = new BigInteger[1]; data[0] = new BigInteger(1, encKey); break; case PGPPublicKey.ELGAMAL_ENCRYPT: case PGPPublicKey.ELGAMAL_GENERAL: byte[] b1 = new byte[encKey.length / 2]; byte[] b2 = new byte[encKey.length / 2]; System.arraycopy(encKey, 0, b1, 0, b1.length); System.arraycopy(encKey, b1.length, b2, 0, b2.length); data = new BigInteger[2]; data[0] = new BigInteger(1, b1); data[1] = new BigInteger(1, b2); break; default: throw new PGPException("unknown asymmetric algorithm: " + encAlgorithm); } } public void encode(BCPGOutputStream pOut) throws IOException { PublicKeyEncSessionPacket pk = new PublicKeyEncSessionPacket(pubKey.getKeyID(), pubKey.getAlgorithm(), data); pOut.writePacket(pk); } } private List methods = new ArrayList(); private int defAlgorithm; private SecureRandom rand; private Provider defProvider; /** * Base constructor. * * @param encAlgorithm the symmetric algorithm to use. * @param rand source of randomness * @param provider the provider to use for encryption algorithms. */ public PGPEncryptedDataGenerator( int encAlgorithm, SecureRandom rand, String provider) { this(encAlgorithm, rand, Security.getProvider(provider)); } public PGPEncryptedDataGenerator( int encAlgorithm, SecureRandom rand, Provider provider) { this.defAlgorithm = encAlgorithm; this.rand = rand; this.defProvider = provider; } /** * Creates a cipher stream which will have an integrity packet * associated with it. * * @param encAlgorithm * @param withIntegrityPacket * @param rand * @param provider */ public PGPEncryptedDataGenerator( int encAlgorithm, boolean withIntegrityPacket, SecureRandom rand, String provider) { this(encAlgorithm, withIntegrityPacket, rand, Security.getProvider(provider)); } public PGPEncryptedDataGenerator( int encAlgorithm, boolean withIntegrityPacket, SecureRandom rand, Provider provider) { this.defAlgorithm = encAlgorithm; this.rand = rand; this.defProvider = provider; this.withIntegrityPacket = withIntegrityPacket; } /** * Base constructor. * * @param encAlgorithm the symmetric algorithm to use. * @param rand source of randomness * @param oldFormat PGP 2.6.x compatability required. * @param provider the provider to use for encryption algorithms. */ public PGPEncryptedDataGenerator( int encAlgorithm, SecureRandom rand, boolean oldFormat, String provider) { this.defAlgorithm = encAlgorithm; this.rand = rand; this.defProvider = Security.getProvider(provider); this.oldFormat = oldFormat; } public PGPEncryptedDataGenerator( int encAlgorithm, SecureRandom rand, boolean oldFormat, Provider provider) { this.defAlgorithm = encAlgorithm; this.rand = rand; this.defProvider = provider; this.oldFormat = oldFormat; } /** * Add a PBE encryption method to the encrypted object using the default algorithm (S2K_SHA1). * * @param passPhrase * @throws NoSuchProviderException * @throws PGPException */ public void addMethod( char[] passPhrase) throws NoSuchProviderException, PGPException { addMethod(passPhrase, HashAlgorithmTags.SHA1); } /** * Add a PBE encryption method to the encrypted object. * * @param passPhrase passphrase to use to generate key. * @param s2kDigest digest algorithm to use for S2K calculation * @throws NoSuchProviderException * @throws PGPException */ public void addMethod( char[] passPhrase, int s2kDigest) throws NoSuchProviderException, PGPException { if (defProvider == null) { throw new NoSuchProviderException("unable to find provider."); } byte[] iv = new byte[8]; rand.nextBytes(iv); S2K s2k = new S2K(s2kDigest, iv, 0x60); methods.add(new PBEMethod(defAlgorithm, s2k, PGPUtil.makeKeyFromPassPhrase(defAlgorithm, s2k, passPhrase, defProvider))); } /** * Add a public key encrypted session key to the encrypted object. * * @param key * @throws NoSuchProviderException * @throws PGPException */ public void addMethod( PGPPublicKey key) throws NoSuchProviderException, PGPException { if (!key.isEncryptionKey()) { throw new IllegalArgumentException("passed in key not an encryption key!"); } if (defProvider == null) { throw new NoSuchProviderException("unable to find provider."); } methods.add(new PubMethod(key)); } private void addCheckSum( byte[] sessionInfo) { int check = 0; for (int i = 1; i != sessionInfo.length - 2; i++) { check += sessionInfo[i] & 0xff; } sessionInfo[sessionInfo.length - 2] = (byte)(check >> 8); sessionInfo[sessionInfo.length - 1] = (byte)(check); } private byte[] createSessionInfo( int algorithm, Key key) { byte[] keyBytes = key.getEncoded(); byte[] sessionInfo = new byte[keyBytes.length + 3]; sessionInfo[0] = (byte) algorithm; System.arraycopy(keyBytes, 0, sessionInfo, 1, keyBytes.length); addCheckSum(sessionInfo); return sessionInfo; } /** * If buffer is non null stream assumed to be partial, otherwise the * length will be used to output a fixed length packet. * <p> * The stream created can be closed off by either calling close() * on the stream or close() on the generator. Closing the returned * stream does not close off the OutputStream parameter out. * * @param out * @param length * @param buffer * @return * @throws IOException * @throws PGPException * @throws IllegalStateException */ private OutputStream open( OutputStream out, long length, byte[] buffer) throws IOException, PGPException, IllegalStateException { if (cOut != null) { throw new IllegalStateException("generator already in open state"); } if (methods.size() == 0) { throw new IllegalStateException("no encryption methods specified"); } if (defProvider == null) { throw new IllegalStateException("provider resolves to null"); } Key key = null; pOut = new BCPGOutputStream(out); if (methods.size() == 1) { if (methods.get(0) instanceof PBEMethod) { PBEMethod m = (PBEMethod)methods.get(0); key = m.getKey(); } else { key = PGPUtil.makeRandomKey(defAlgorithm, rand); byte[] sessionInfo = createSessionInfo(defAlgorithm, key); PubMethod m = (PubMethod)methods.get(0); try { m.addSessionInfo(sessionInfo); } catch (Exception e) { throw new PGPException("exception encrypting session key", e); } } pOut.writePacket((ContainedPacket)methods.get(0)); } else // multiple methods { key = PGPUtil.makeRandomKey(defAlgorithm, rand); byte[] sessionInfo = createSessionInfo(defAlgorithm, key); for (int i = 0; i != methods.size(); i++) { EncMethod m = (EncMethod)methods.get(i); try { m.addSessionInfo(sessionInfo); } catch (Exception e) { throw new PGPException("exception encrypting session key", e); } pOut.writePacket(m); } } String cName = PGPUtil.getSymmetricCipherName(defAlgorithm); if (cName == null) { throw new PGPException("null cipher specified"); } try { if (withIntegrityPacket) { c = Cipher.getInstance(cName + "/CFB/NoPadding", defProvider); } else { c = Cipher.getInstance(cName + "/OpenPGPCFB/NoPadding", defProvider); } byte[] iv = new byte[c.getBlockSize()]; c.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv), rand); if (buffer == null) { // // we have to add block size + 2 for the generated IV and + 1 + 22 if integrity protected // if (withIntegrityPacket) { pOut = new BCPGOutputStream(out, PacketTags.SYM_ENC_INTEGRITY_PRO, length + c.getBlockSize() + 2 + 1 + 22); pOut.write(1); // version number } else { pOut = new BCPGOutputStream(out, PacketTags.SYMMETRIC_KEY_ENC, length + c.getBlockSize() + 2, oldFormat); } } else { if (withIntegrityPacket) { pOut = new BCPGOutputStream(out, PacketTags.SYM_ENC_INTEGRITY_PRO, buffer); pOut.write(1); // version number } else { pOut = new BCPGOutputStream(out, PacketTags.SYMMETRIC_KEY_ENC, buffer); } } OutputStream genOut = cOut = new CipherOutputStream(pOut, c); if (withIntegrityPacket) { String digestName = PGPUtil.getDigestName(HashAlgorithmTags.SHA1); MessageDigest digest = MessageDigest.getInstance(digestName, defProvider); genOut = digestOut = new DigestOutputStream(cOut, digest); } byte[] inLineIv = new byte[c.getBlockSize() + 2]; rand.nextBytes(inLineIv); inLineIv[inLineIv.length - 1] = inLineIv[inLineIv.length - 3]; inLineIv[inLineIv.length - 2] = inLineIv[inLineIv.length - 4]; genOut.write(inLineIv); return new WrappedGeneratorStream(genOut, this); } catch (Exception e) { throw new PGPException("Exception creating cipher", e); } } /** * Return an outputstream which will encrypt the data as it is written * to it. * <p> * The stream created can be closed off by either calling close() * on the stream or close() on the generator. Closing the returned * stream does not close off the OutputStream parameter out. * * @param out * @param length * @return OutputStream * @throws IOException * @throws PGPException */ public OutputStream open( OutputStream out, long length) throws IOException, PGPException { return this.open(out, length, null); } /** * Return an outputstream which will encrypt the data as it is written * to it. The stream will be written out in chunks according to the size of the * passed in buffer. * <p> * The stream created can be closed off by either calling close() * on the stream or close() on the generator. Closing the returned * stream does not close off the OutputStream parameter out. * <p> * <b>Note</b>: if the buffer is not a power of 2 in length only the largest power of 2 * bytes worth of the buffer will be used. * * @param out * @param buffer the buffer to use. * @return OutputStream * @throws IOException * @throws PGPException */ public OutputStream open( OutputStream out, byte[] buffer) throws IOException, PGPException { return this.open(out, 0, buffer); } /** * Close off the encrypted object - this is equivalent to calling close on the stream * returned by the open() method. * <p> * <b>Note</b>: This does not close the underlying output stream, only the stream on top of it created by the open() method. * @throws IOException */ public void close() throws IOException { if (cOut != null) { if (digestOut != null) { // // hand code a mod detection packet // BCPGOutputStream bOut = new BCPGOutputStream(digestOut, PacketTags.MOD_DETECTION_CODE, 20); bOut.flush(); digestOut.flush(); byte[] dig = digestOut.getMessageDigest().digest(); cOut.write(dig); } cOut.flush(); try { pOut.write(c.doFinal()); pOut.finish(); } catch (Exception e) { throw new IOException(e.toString()); } cOut = null; pOut = null; } } }