001/* 002 * Copyright 2018 Paul Schaub. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.pgpainless.encryption_signing; 017 018import javax.annotation.Nonnull; 019import java.io.IOException; 020import java.io.OutputStream; 021import java.util.ArrayList; 022import java.util.Collections; 023import java.util.Date; 024import java.util.HashSet; 025import java.util.List; 026import java.util.Set; 027import java.util.logging.Level; 028import java.util.logging.Logger; 029 030import org.bouncycastle.bcpg.ArmoredOutputStream; 031import org.bouncycastle.bcpg.BCPGOutputStream; 032import org.bouncycastle.openpgp.PGPCompressedDataGenerator; 033import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; 034import org.bouncycastle.openpgp.PGPException; 035import org.bouncycastle.openpgp.PGPLiteralData; 036import org.bouncycastle.openpgp.PGPLiteralDataGenerator; 037import org.bouncycastle.openpgp.PGPPrivateKey; 038import org.bouncycastle.openpgp.PGPPublicKey; 039import org.bouncycastle.openpgp.PGPSignature; 040import org.bouncycastle.openpgp.PGPSignatureGenerator; 041import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; 042import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; 043import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; 044import org.pgpainless.algorithm.CompressionAlgorithm; 045import org.pgpainless.algorithm.HashAlgorithm; 046import org.pgpainless.algorithm.SymmetricKeyAlgorithm; 047import org.pgpainless.decryption_verification.OpenPgpMetadata; 048 049/** 050 * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. 051 * @see <a href="https://github.com/neuhalje/bouncy-gpg/blob/master/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java">Source</a> 052 */ 053public final class EncryptionStream extends OutputStream { 054 055 private static final Logger LOGGER = Logger.getLogger(EncryptionStream.class.getName()); 056 private static final Level LEVEL = Level.FINE; 057 058 private static final int BUFFER_SIZE = 1 << 8; 059 060 private final OpenPgpMetadata result; 061 062 private List<PGPSignatureGenerator> signatureGenerators = new ArrayList<>(); 063 private boolean closed = false; 064 065 // ASCII Armor 066 private ArmoredOutputStream armorOutputStream = null; 067 068 // Public Key Encryption of Symmetric Session Key 069 private OutputStream publicKeyEncryptedStream = null; 070 071 // Data Compression 072 private PGPCompressedDataGenerator compressedDataGenerator; 073 private BCPGOutputStream basicCompressionStream; 074 075 // Literal Data 076 private PGPLiteralDataGenerator literalDataGenerator; 077 private OutputStream literalDataStream; 078 079 EncryptionStream(@Nonnull OutputStream targetOutputStream, 080 @Nonnull Set<PGPPublicKey> encryptionKeys, 081 @Nonnull Set<PGPPrivateKey> signingKeys, 082 @Nonnull SymmetricKeyAlgorithm symmetricKeyAlgorithm, 083 @Nonnull HashAlgorithm hashAlgorithm, 084 @Nonnull CompressionAlgorithm compressionAlgorithm, 085 boolean asciiArmor) 086 throws IOException, PGPException { 087 088 // Currently outermost Stream 089 OutputStream outerMostStream; 090 if (asciiArmor) { 091 LOGGER.log(LEVEL, "Wrap encryption output in ASCII armor"); 092 armorOutputStream = new ArmoredOutputStream(targetOutputStream); 093 outerMostStream = armorOutputStream; 094 } else { 095 LOGGER.log(LEVEL, "Encryption output will be binary"); 096 outerMostStream = targetOutputStream; 097 } 098 099 // If we want to encrypt 100 if (!encryptionKeys.isEmpty()) { 101 LOGGER.log(LEVEL, "At least one encryption key is available -> encrypt using " + symmetricKeyAlgorithm); 102 BcPGPDataEncryptorBuilder dataEncryptorBuilder = 103 new BcPGPDataEncryptorBuilder(symmetricKeyAlgorithm.getAlgorithmId()); 104 105 LOGGER.log(LEVEL, "Integrity protection enabled"); 106 dataEncryptorBuilder.setWithIntegrityPacket(true); 107 108 PGPEncryptedDataGenerator encryptedDataGenerator = 109 new PGPEncryptedDataGenerator(dataEncryptorBuilder); 110 111 for (PGPPublicKey key : encryptionKeys) { 112 LOGGER.log(LEVEL, "Encrypt for key " + Long.toHexString(key.getKeyID())); 113 encryptedDataGenerator.addMethod(new BcPublicKeyKeyEncryptionMethodGenerator(key)); 114 } 115 116 publicKeyEncryptedStream = encryptedDataGenerator.open(outerMostStream, new byte[BUFFER_SIZE]); 117 outerMostStream = publicKeyEncryptedStream; 118 } 119 120 // If we want to sign, prepare for signing 121 if (!signingKeys.isEmpty()) { 122 LOGGER.log(LEVEL, "At least one signing key is available -> sign " + hashAlgorithm + " hash of message"); 123 for (PGPPrivateKey privateKey : signingKeys) { 124 LOGGER.log(LEVEL, "Sign using key " + Long.toHexString(privateKey.getKeyID())); 125 BcPGPContentSignerBuilder contentSignerBuilder = new BcPGPContentSignerBuilder( 126 privateKey.getPublicKeyPacket().getAlgorithm(), hashAlgorithm.getAlgorithmId()); 127 128 PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(contentSignerBuilder); 129 signatureGenerator.init(PGPSignature.BINARY_DOCUMENT, privateKey); 130 signatureGenerators.add(signatureGenerator); 131 } 132 } 133 134 LOGGER.log(LEVEL, "Compress using " + compressionAlgorithm); 135 // Compression 136 compressedDataGenerator = new PGPCompressedDataGenerator( 137 compressionAlgorithm.getAlgorithmId()); 138 basicCompressionStream = new BCPGOutputStream(compressedDataGenerator.open(outerMostStream)); 139 140 // If we want to sign, sign! 141 for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { 142 signatureGenerator.generateOnePassVersion(false).encode(basicCompressionStream); 143 } 144 145 literalDataGenerator = new PGPLiteralDataGenerator(); 146 literalDataStream = literalDataGenerator.open(basicCompressionStream, 147 PGPLiteralData.BINARY, PGPLiteralData.CONSOLE, new Date(), new byte[BUFFER_SIZE]); 148 149 // Prepare result 150 Set<Long> recipientKeyIds = new HashSet<>(); 151 for (PGPPublicKey recipient : encryptionKeys) { 152 recipientKeyIds.add(recipient.getKeyID()); 153 } 154 155 Set<Long> signingKeyIds = new HashSet<>(); 156 for (PGPPrivateKey signer : signingKeys) { 157 signingKeyIds.add(signer.getKeyID()); 158 } 159 160 161 this.result = new OpenPgpMetadata(recipientKeyIds, 162 null, symmetricKeyAlgorithm, 163 compressionAlgorithm, true, 164 signingKeyIds, Collections.emptySet()); 165 } 166 167 @Override 168 public void write(int data) throws IOException { 169 literalDataStream.write(data); 170 171 for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { 172 byte asByte = (byte) (data & 0xff); 173 signatureGenerator.update(asByte); 174 } 175 } 176 177 @Override 178 public void write(byte[] buffer) throws IOException { 179 write(buffer, 0, buffer.length); 180 } 181 182 183 @Override 184 public void write(byte[] buffer, int off, int len) throws IOException { 185 literalDataStream.write(buffer, 0, len); 186 for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { 187 signatureGenerator.update(buffer, 0, len); 188 } 189 } 190 191 @Override 192 public void flush() throws IOException { 193 literalDataStream.flush(); 194 } 195 196 @Override 197 public void close() throws IOException { 198 if (!closed) { 199 200 // Literal Data 201 literalDataStream.flush(); 202 literalDataStream.close(); 203 literalDataGenerator.close(); 204 205 // Signing 206 for (PGPSignatureGenerator signatureGenerator : signatureGenerators) { 207 try { 208 signatureGenerator.generate().encode(basicCompressionStream); 209 } catch (PGPException e) { 210 throw new IOException(e); 211 } 212 } 213 214 // Compressed Data 215 compressedDataGenerator.close(); 216 217 // Public Key Encryption 218 if (publicKeyEncryptedStream != null) { 219 publicKeyEncryptedStream.flush(); 220 publicKeyEncryptedStream.close(); 221 } 222 223 // Armor 224 if (armorOutputStream != null) { 225 armorOutputStream.flush(); 226 armorOutputStream.close(); 227 } 228 closed = true; 229 } 230 } 231 232 public OpenPgpMetadata getResult() { 233 return result; 234 } 235}