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}