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.ByteArrayOutputStream;
008import java.io.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.util.ArrayList;
012import java.util.List;
013
014import org.bouncycastle.openpgp.PGPException;
015import org.bouncycastle.openpgp.PGPSecretKeyRing;
016import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
017import org.bouncycastle.openpgp.PGPSignature;
018import org.bouncycastle.util.io.Streams;
019import org.pgpainless.PGPainless;
020import org.pgpainless.algorithm.DocumentSignatureType;
021import org.pgpainless.encryption_signing.EncryptionResult;
022import org.pgpainless.encryption_signing.EncryptionStream;
023import org.pgpainless.encryption_signing.ProducerOptions;
024import org.pgpainless.encryption_signing.SigningOptions;
025import org.pgpainless.key.SubkeyIdentifier;
026import org.pgpainless.key.info.KeyRingInfo;
027import org.pgpainless.key.protection.SecretKeyRingProtector;
028import org.pgpainless.util.ArmoredOutputStreamFactory;
029import sop.Ready;
030import sop.enums.SignAs;
031import sop.exception.SOPGPException;
032import sop.operation.Sign;
033
034public class SignImpl implements Sign {
035
036    private boolean armor = true;
037    private SignAs mode = SignAs.Binary;
038    private final SigningOptions signingOptions = new SigningOptions();
039
040    @Override
041    public Sign noArmor() {
042        armor = false;
043        return this;
044    }
045
046    @Override
047    public Sign mode(SignAs mode) {
048        this.mode = mode;
049        return this;
050    }
051
052    @Override
053    public Sign key(InputStream keyIn) throws SOPGPException.KeyIsProtected, SOPGPException.BadData, IOException {
054        try {
055            PGPSecretKeyRingCollection keys = PGPainless.readKeyRing().secretKeyRingCollection(keyIn);
056            if (keys.size() != 1) {
057                throw new SOPGPException.BadData(new AssertionError("Exactly one secret key at a time expected. Got " + keys.size()));
058            }
059
060            PGPSecretKeyRing key = keys.iterator().next();
061            KeyRingInfo info = new KeyRingInfo(key);
062            if (!info.isFullyDecrypted()) {
063                throw new SOPGPException.KeyIsProtected();
064            }
065            signingOptions.addDetachedSignature(SecretKeyRingProtector.unprotectedKeys(), key, modeToSigType(mode));
066        } catch (PGPException e) {
067            throw new SOPGPException.BadData(e);
068        }
069        return this;
070    }
071
072    @Override
073    public Ready data(InputStream data) throws IOException {
074        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
075        try {
076            EncryptionStream signingStream = PGPainless.encryptAndOrSign()
077                    .onOutputStream(buffer)
078                    .withOptions(ProducerOptions.sign(signingOptions)
079                            .setAsciiArmor(armor));
080
081            return new Ready() {
082                @Override
083                public void writeTo(OutputStream outputStream) throws IOException {
084
085                    if (signingStream.isClosed()) {
086                        throw new IllegalStateException("EncryptionStream is already closed.");
087                    }
088
089                    Streams.pipeAll(data, signingStream);
090                    signingStream.close();
091                    EncryptionResult encryptionResult = signingStream.getResult();
092
093                    List<PGPSignature> signatures = new ArrayList<>();
094                    for (SubkeyIdentifier key : encryptionResult.getDetachedSignatures().keySet()) {
095                        signatures.addAll(encryptionResult.getDetachedSignatures().get(key));
096                    }
097
098                    OutputStream out;
099                    if (armor) {
100                        out = ArmoredOutputStreamFactory.get(outputStream);
101                    } else {
102                        out = outputStream;
103                    }
104                    for (PGPSignature sig : signatures) {
105                        sig.encode(out);
106                    }
107                    out.close();
108                    outputStream.close(); // armor out does not close underlying stream
109                }
110            };
111
112        } catch (PGPException e) {
113            throw new RuntimeException(e);
114        }
115
116    }
117
118    private static DocumentSignatureType modeToSigType(SignAs mode) {
119        return mode == SignAs.Binary ? DocumentSignatureType.BINARY_DOCUMENT
120                : DocumentSignatureType.CANONICAL_TEXT_DOCUMENT;
121    }
122}