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}