001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.sop;
006
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.OutputStream;
010
011import org.bouncycastle.openpgp.PGPException;
012import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
013import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
014import org.bouncycastle.util.io.Streams;
015import org.pgpainless.PGPainless;
016import org.pgpainless.algorithm.DocumentSignatureType;
017import org.pgpainless.algorithm.StreamEncoding;
018import org.pgpainless.encryption_signing.EncryptionOptions;
019import org.pgpainless.encryption_signing.EncryptionStream;
020import org.pgpainless.encryption_signing.ProducerOptions;
021import org.pgpainless.encryption_signing.SigningOptions;
022import org.pgpainless.exception.WrongPassphraseException;
023import org.pgpainless.key.protection.SecretKeyRingProtector;
024import org.pgpainless.util.Passphrase;
025import sop.util.ProxyOutputStream;
026import sop.Ready;
027import sop.enums.EncryptAs;
028import sop.exception.SOPGPException;
029import sop.operation.Encrypt;
030
031public class EncryptImpl implements Encrypt {
032
033    EncryptionOptions encryptionOptions = new EncryptionOptions();
034    SigningOptions signingOptions = null;
035
036    private EncryptAs encryptAs = EncryptAs.Binary;
037    boolean armor = true;
038
039    @Override
040    public Encrypt noArmor() {
041        armor = false;
042        return this;
043    }
044
045    @Override
046    public Encrypt mode(EncryptAs mode) throws SOPGPException.UnsupportedOption {
047        this.encryptAs = mode;
048        return this;
049    }
050
051    @Override
052    public Encrypt signWith(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.CertCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
053        try {
054            PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
055            if (keys.size() != 1) {
056                throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size()));
057            }
058
059            if (signingOptions == null) {
060                signingOptions = SigningOptions.get();
061            }
062            try {
063                signingOptions.addInlineSignatures(SecretKeyRingProtector.unprotectedKeys(), keys, DocumentSignatureType.BINARY_DOCUMENT);
064            } catch (IllegalArgumentException e) {
065                throw new SOPGPException.CertCannotSign();
066            } catch (WrongPassphraseException e) {
067                throw new SOPGPException.KeyIsProtected();
068            }
069        } catch (IOException | PGPException e) {
070            throw new SOPGPException.BadData(e);
071        }
072        return this;
073    }
074
075    @Override
076    public Encrypt withPassword(String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption {
077        encryptionOptions.addPassphrase(Passphrase.fromPassword(password));
078        return this;
079    }
080
081    @Override
082    public Encrypt withCert(InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData {
083        try {
084            PGPPublicKeyRingCollection certificates = PGPainless.readKeyRing()
085                    .keyRingCollection(cert, false)
086                    .getPgpPublicKeyRingCollection();
087            encryptionOptions.addRecipients(certificates);
088        } catch (IOException | PGPException e) {
089            throw new SOPGPException.BadData(e);
090        }
091        return this;
092    }
093
094    @Override
095    public Ready plaintext(InputStream plaintext) throws IOException {
096        ProducerOptions producerOptions = signingOptions != null ?
097                ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions) :
098                ProducerOptions.encrypt(encryptionOptions);
099        producerOptions.setAsciiArmor(armor);
100        producerOptions.setEncoding(encryptAsToStreamEncoding(encryptAs));
101
102        try {
103            ProxyOutputStream proxy = new ProxyOutputStream();
104            EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
105                    .onOutputStream(proxy)
106                    .withOptions(producerOptions);
107
108            return new Ready() {
109                @Override
110                public void writeTo(OutputStream outputStream) throws IOException {
111                    proxy.replaceOutputStream(outputStream);
112                    Streams.pipeAll(plaintext, encryptionStream);
113                    encryptionStream.close();
114                }
115            };
116        } catch (PGPException e) {
117            throw new IOException();
118        }
119    }
120
121    private static StreamEncoding encryptAsToStreamEncoding(EncryptAs encryptAs) {
122        switch (encryptAs) {
123            case Binary:
124                return StreamEncoding.BINARY;
125            case Text:
126                return StreamEncoding.TEXT;
127            case MIME:
128                return StreamEncoding.UTF8;
129        }
130        throw new IllegalArgumentException("Invalid value encountered: " + encryptAs);
131    }
132}