001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.decryption_verification.cleartext_signatures;
006
007import java.io.BufferedOutputStream;
008import java.io.ByteArrayOutputStream;
009import java.io.IOException;
010import java.io.InputStream;
011import java.io.OutputStream;
012
013import org.bouncycastle.bcpg.ArmoredInputStream;
014import org.bouncycastle.openpgp.PGPObjectFactory;
015import org.bouncycastle.openpgp.PGPSignatureList;
016import org.bouncycastle.util.Strings;
017import org.pgpainless.exception.WrongConsumingMethodException;
018import org.pgpainless.implementation.ImplementationFactory;
019import org.pgpainless.util.ArmoredInputStreamFactory;
020
021/**
022 * Utility class to deal with cleartext-signed messages.
023 * Based on Bouncycastle's {@link org.bouncycastle.openpgp.examples.ClearSignedFileProcessor}.
024 */
025public final class ClearsignedMessageUtil {
026
027    private ClearsignedMessageUtil() {
028
029    }
030
031    /**
032     * Dearmor a clearsigned message, detach the inband signatures and write the plaintext message to the provided
033     * messageOutputStream.
034     *
035     * @param clearsignedInputStream input stream containing a clearsigned message
036     * @param messageOutputStream output stream to which the dearmored message shall be written
037     * @return signatures
038     * @throws IOException if the message is not clearsigned or some other IO error happens
039     */
040    public static PGPSignatureList detachSignaturesFromInbandClearsignedMessage(InputStream clearsignedInputStream,
041                                                                                OutputStream messageOutputStream)
042            throws IOException, WrongConsumingMethodException {
043        ArmoredInputStream in = ArmoredInputStreamFactory.get(clearsignedInputStream);
044        if (!in.isClearText()) {
045            throw new WrongConsumingMethodException("Message is not using the Cleartext Signature Framework.");
046        }
047
048        OutputStream out = new BufferedOutputStream(messageOutputStream);
049        try {
050            ByteArrayOutputStream lineOut = new ByteArrayOutputStream();
051            int lookAhead = readInputLine(lineOut, in);
052            byte[] lineSep = getLineSeparator();
053
054            if (lookAhead != -1 && in.isClearText()) {
055                byte[] line = lineOut.toByteArray();
056                out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line));
057
058                while (lookAhead != -1 && in.isClearText()) {
059                    lookAhead = readInputLine(lineOut, lookAhead, in);
060                    line = lineOut.toByteArray();
061                    out.write(lineSep);
062                    out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line));
063                }
064            } else {
065                if (lookAhead != -1) {
066                    byte[] line = lineOut.toByteArray();
067                    out.write(line, 0, getLengthWithoutSeparatorOrTrailingWhitespace(line));
068                }
069            }
070        } finally {
071            out.close();
072        }
073
074        PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(in);
075        PGPSignatureList signatures = (PGPSignatureList) objectFactory.nextObject();
076
077        return signatures;
078    }
079
080    public static int readInputLine(ByteArrayOutputStream bOut, InputStream fIn)
081            throws IOException {
082        bOut.reset();
083
084        int lookAhead = -1;
085        int ch;
086
087        while ((ch = fIn.read()) >= 0) {
088            bOut.write(ch);
089            if (ch == '\r' || ch == '\n') {
090                lookAhead = readPassedEOL(bOut, ch, fIn);
091                break;
092            }
093        }
094
095        return lookAhead;
096    }
097
098    public static int readInputLine(ByteArrayOutputStream bOut, int lookAhead, InputStream fIn)
099            throws IOException {
100        bOut.reset();
101
102        int ch = lookAhead;
103
104        do {
105            bOut.write(ch);
106            if (ch == '\r' || ch == '\n') {
107                lookAhead = readPassedEOL(bOut, ch, fIn);
108                break;
109            }
110        }
111        while ((ch = fIn.read()) >= 0);
112
113        if (ch < 0) {
114            lookAhead = -1;
115        }
116
117        return lookAhead;
118    }
119
120    private static int readPassedEOL(ByteArrayOutputStream bOut, int lastCh, InputStream fIn)
121            throws IOException {
122        int lookAhead = fIn.read();
123
124        if (lastCh == '\r' && lookAhead == '\n') {
125            bOut.write(lookAhead);
126            lookAhead = fIn.read();
127        }
128
129        return lookAhead;
130    }
131
132
133    private static byte[] getLineSeparator() {
134        String nl = Strings.lineSeparator();
135        byte[] nlBytes = new byte[nl.length()];
136
137        for (int i = 0; i != nlBytes.length; i++) {
138            nlBytes[i] = (byte) nl.charAt(i);
139        }
140
141        return nlBytes;
142    }
143
144    private static int getLengthWithoutSeparatorOrTrailingWhitespace(byte[] line) {
145        int    end = line.length - 1;
146
147        while (end >= 0 && isWhiteSpace(line[end])) {
148            end--;
149        }
150
151        return end + 1;
152    }
153
154    private static boolean isLineEnding(byte b) {
155        return b == '\r' || b == '\n';
156    }
157
158    private static boolean isWhiteSpace(byte b) {
159        return isLineEnding(b) || b == '\t' || b == ' ';
160    }
161}