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}