001// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org> 002// 003// SPDX-License-Identifier: Apache-2.0 004 005package org.pgpainless.encryption_signing; 006 007import java.io.IOException; 008import java.io.OutputStream; 009import java.util.ArrayList; 010import java.util.List; 011import javax.annotation.Nonnull; 012 013import org.bouncycastle.bcpg.ArmoredOutputStream; 014import org.bouncycastle.bcpg.BCPGOutputStream; 015import org.bouncycastle.openpgp.PGPCompressedDataGenerator; 016import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; 017import org.bouncycastle.openpgp.PGPException; 018import org.bouncycastle.openpgp.PGPSignature; 019import org.bouncycastle.openpgp.PGPSignatureGenerator; 020import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; 021import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; 022import org.pgpainless.algorithm.CompressionAlgorithm; 023import org.pgpainless.algorithm.SymmetricKeyAlgorithm; 024import org.pgpainless.implementation.ImplementationFactory; 025import org.pgpainless.key.SubkeyIdentifier; 026import org.pgpainless.util.ArmoredOutputStreamFactory; 027import org.pgpainless.util.StreamGeneratorWrapper; 028import org.slf4j.Logger; 029import org.slf4j.LoggerFactory; 030 031/** 032 * This class is based upon Jens Neuhalfen's Bouncy-GPG PGPEncryptingStream. 033 * @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> 034 */ 035public final class EncryptionStream extends OutputStream { 036 037 private static final Logger LOGGER = LoggerFactory.getLogger(EncryptionStream.class); 038 039 private final ProducerOptions options; 040 private final EncryptionResult.Builder resultBuilder = EncryptionResult.builder(); 041 042 private boolean closed = false; 043 // 1 << 8 causes wrong partial body length encoding 044 // 1 << 9 fixes this. 045 // see https://github.com/pgpainless/pgpainless/issues/160 046 private static final int BUFFER_SIZE = 1 << 9; 047 048 OutputStream outermostStream; 049 private ArmoredOutputStream armorOutputStream = null; 050 private OutputStream publicKeyEncryptedStream = null; 051 private PGPCompressedDataGenerator compressedDataGenerator; 052 private BCPGOutputStream basicCompressionStream; 053 private StreamGeneratorWrapper streamGeneratorWrapper; 054 private OutputStream literalDataStream; 055 056 EncryptionStream(@Nonnull OutputStream targetOutputStream, 057 @Nonnull ProducerOptions options) 058 throws IOException, PGPException { 059 this.options = options; 060 outermostStream = targetOutputStream; 061 062 prepareArmor(); 063 prepareEncryption(); 064 prepareCompression(); 065 prepareOnePassSignatures(); 066 prepareLiteralDataProcessing(); 067 } 068 069 private void prepareArmor() { 070 if (!options.isAsciiArmor()) { 071 LOGGER.debug("Output will be unarmored"); 072 return; 073 } 074 075 LOGGER.debug("Wrap encryption output in ASCII armor"); 076 armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); 077 outermostStream = armorOutputStream; 078 } 079 080 private void prepareEncryption() throws IOException, PGPException { 081 EncryptionOptions encryptionOptions = options.getEncryptionOptions(); 082 if (encryptionOptions == null || encryptionOptions.getEncryptionMethods().isEmpty()) { 083 // No encryption options/methods -> no encryption 084 resultBuilder.setEncryptionAlgorithm(SymmetricKeyAlgorithm.NULL); 085 return; 086 } 087 088 SymmetricKeyAlgorithm encryptionAlgorithm = EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(encryptionOptions); 089 resultBuilder.setEncryptionAlgorithm(encryptionAlgorithm); 090 LOGGER.debug("Encrypt message using {}", encryptionAlgorithm); 091 PGPDataEncryptorBuilder dataEncryptorBuilder = 092 ImplementationFactory.getInstance().getPGPDataEncryptorBuilder(encryptionAlgorithm); 093 dataEncryptorBuilder.setWithIntegrityPacket(true); 094 095 PGPEncryptedDataGenerator encryptedDataGenerator = 096 new PGPEncryptedDataGenerator(dataEncryptorBuilder); 097 for (PGPKeyEncryptionMethodGenerator encryptionMethod : encryptionOptions.getEncryptionMethods()) { 098 encryptedDataGenerator.addMethod(encryptionMethod); 099 } 100 101 for (SubkeyIdentifier recipientSubkeyIdentifier : encryptionOptions.getEncryptionKeyIdentifiers()) { 102 resultBuilder.addRecipient(recipientSubkeyIdentifier); 103 } 104 105 publicKeyEncryptedStream = encryptedDataGenerator.open(outermostStream, new byte[BUFFER_SIZE]); 106 outermostStream = publicKeyEncryptedStream; 107 } 108 109 private void prepareCompression() throws IOException { 110 CompressionAlgorithm compressionAlgorithm = EncryptionBuilder.negotiateCompressionAlgorithm(options); 111 resultBuilder.setCompressionAlgorithm(compressionAlgorithm); 112 compressedDataGenerator = new PGPCompressedDataGenerator( 113 compressionAlgorithm.getAlgorithmId()); 114 if (compressionAlgorithm == CompressionAlgorithm.UNCOMPRESSED) { 115 return; 116 } 117 118 LOGGER.debug("Compress using {}", compressionAlgorithm); 119 basicCompressionStream = new BCPGOutputStream(compressedDataGenerator.open(outermostStream)); 120 outermostStream = basicCompressionStream; 121 } 122 123 private void prepareOnePassSignatures() throws IOException, PGPException { 124 SigningOptions signingOptions = options.getSigningOptions(); 125 if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { 126 // No singing options/methods -> no signing 127 return; 128 } 129 130 int sigIndex = 0; 131 for (SubkeyIdentifier identifier : signingOptions.getSigningMethods().keySet()) { 132 sigIndex++; 133 SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(identifier); 134 135 if (!signingMethod.isDetached()) { 136 PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); 137 // The last sig is not nested, all others are 138 boolean nested = sigIndex != signingOptions.getSigningMethods().size(); 139 signatureGenerator.generateOnePassVersion(nested).encode(outermostStream); 140 } 141 } 142 } 143 144 private void prepareLiteralDataProcessing() throws IOException { 145 if (options.isCleartextSigned()) { 146 SigningOptions.SigningMethod firstMethod = options.getSigningOptions().getSigningMethods().values().iterator().next(); 147 armorOutputStream.beginClearText(firstMethod.getHashAlgorithm().getAlgorithmId()); 148 return; 149 } 150 151 streamGeneratorWrapper = StreamGeneratorWrapper.forStreamEncoding(options.getEncoding()); 152 literalDataStream = streamGeneratorWrapper.open(outermostStream, 153 options.getFileName(), options.getModificationDate(), new byte[BUFFER_SIZE]); 154 outermostStream = literalDataStream; 155 156 resultBuilder.setFileName(options.getFileName()) 157 .setModificationDate(options.getModificationDate()) 158 .setFileEncoding(options.getEncoding()); 159 } 160 161 @Override 162 public void write(int data) throws IOException { 163 outermostStream.write(data); 164 SigningOptions signingOptions = options.getSigningOptions(); 165 if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { 166 return; 167 } 168 169 for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { 170 SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); 171 PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); 172 byte asByte = (byte) (data & 0xff); 173 signatureGenerator.update(asByte); 174 } 175 } 176 177 @Override 178 public void write(@Nonnull byte[] buffer) throws IOException { 179 write(buffer, 0, buffer.length); 180 } 181 182 183 @Override 184 public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { 185 outermostStream.write(buffer, 0, len); 186 SigningOptions signingOptions = options.getSigningOptions(); 187 if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { 188 return; 189 } 190 for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { 191 SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); 192 PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); 193 signatureGenerator.update(buffer, 0, len); 194 } 195 } 196 197 @Override 198 public void flush() throws IOException { 199 outermostStream.flush(); 200 } 201 202 @Override 203 public void close() throws IOException { 204 if (closed) { 205 return; 206 } 207 208 // Literal Data 209 if (literalDataStream != null) { 210 literalDataStream.flush(); 211 literalDataStream.close(); 212 } 213 if (streamGeneratorWrapper != null) { 214 streamGeneratorWrapper.close(); 215 } 216 217 if (options.isCleartextSigned()) { 218 // Add linebreak between body and signatures 219 // TODO: We should only add this line if required. 220 // I.e. if the message already ends with \n, don't add another linebreak. 221 armorOutputStream.write('\r'); 222 armorOutputStream.write('\n'); 223 armorOutputStream.endClearText(); 224 } 225 226 try { 227 writeSignatures(); 228 } catch (PGPException e) { 229 throw new IOException("Exception while writing signatures.", e); 230 } 231 232 // Compressed Data 233 compressedDataGenerator.close(); 234 235 // Public Key Encryption 236 if (publicKeyEncryptedStream != null) { 237 publicKeyEncryptedStream.flush(); 238 publicKeyEncryptedStream.close(); 239 } 240 241 // Armor 242 if (armorOutputStream != null) { 243 armorOutputStream.flush(); 244 armorOutputStream.close(); 245 } 246 closed = true; 247 } 248 249 private void writeSignatures() throws PGPException, IOException { 250 SigningOptions signingOptions = options.getSigningOptions(); 251 if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { 252 return; 253 } 254 255 // One-Pass-Signatures are bracketed. That means we have to append the signatures in reverse order 256 // compared to the one-pass-signature packets. 257 List<SubkeyIdentifier> signingKeys = new ArrayList<>(signingOptions.getSigningMethods().keySet()); 258 for (int i = signingKeys.size() - 1; i >= 0; i--) { 259 SubkeyIdentifier signingKey = signingKeys.get(i); 260 SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); 261 PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); 262 PGPSignature signature = signatureGenerator.generate(); 263 if (signingMethod.isDetached()) { 264 resultBuilder.addDetachedSignature(signingKey, signature); 265 } 266 if (!signingMethod.isDetached() || options.isCleartextSigned()) { 267 signature.encode(outermostStream); 268 } 269 } 270 } 271 272 public EncryptionResult getResult() { 273 if (!closed) { 274 throw new IllegalStateException("EncryptionStream must be closed before accessing the Result."); 275 } 276 return resultBuilder.build(); 277 } 278 279 public boolean isClosed() { 280 return closed; 281 } 282}