1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2025-12-10 06:11:08 +01:00

Introduce OpenPgpInputStream to distinguish between armored, binary and non-OpenPGP data

This commit is contained in:
Paul Schaub 2022-04-22 20:53:44 +02:00
parent 3309781b11
commit 46f69b9fa5
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
6 changed files with 884 additions and 157 deletions

View file

@ -4,8 +4,6 @@
package org.pgpainless.decryption_verification;
import java.io.BufferedInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
@ -66,8 +64,6 @@ import org.pgpainless.signature.SignatureUtils;
import org.pgpainless.signature.consumer.DetachedSignatureCheck;
import org.pgpainless.signature.consumer.OnePassSignatureCheck;
import org.pgpainless.util.ArmoredInputStreamFactory;
import org.pgpainless.util.CRCingArmoredInputStreamWrapper;
import org.pgpainless.util.PGPUtilWrapper;
import org.pgpainless.util.Passphrase;
import org.pgpainless.util.SessionKey;
import org.pgpainless.util.Tuple;
@ -81,9 +77,6 @@ public final class DecryptionStreamFactory {
// Maximum nesting depth of packets (e.g. compression, encryption...)
private static final int MAX_PACKET_NESTING_DEPTH = 16;
// Buffer Size for BufferedInputStreams
public static int BUFFER_SIZE = 4096;
private final ConsumerOptions options;
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
private final List<OnePassSignatureCheck> onePassSignatureChecks = new ArrayList<>();
@ -99,8 +92,8 @@ public final class DecryptionStreamFactory {
@Nonnull ConsumerOptions options)
throws PGPException, IOException {
DecryptionStreamFactory factory = new DecryptionStreamFactory(options);
BufferedInputStream bufferedIn = new BufferedInputStream(inputStream, BUFFER_SIZE);
return factory.parseOpenPGPDataAndCreateDecryptionStream(bufferedIn);
OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream);
return factory.parseOpenPGPDataAndCreateDecryptionStream(openPgpIn);
}
public DecryptionStreamFactory(ConsumerOptions options) {
@ -133,69 +126,52 @@ public final class DecryptionStreamFactory {
}
}
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(BufferedInputStream bufferedIn)
private DecryptionStream parseOpenPGPDataAndCreateDecryptionStream(OpenPgpInputStream openPgpIn)
throws IOException, PGPException {
InputStream pgpInStream;
InputStream outerDecodingStream;
PGPObjectFactory objectFactory;
try {
outerDecodingStream = PGPUtilWrapper.getDecoderStream(bufferedIn);
outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(outerDecodingStream);
if (outerDecodingStream instanceof ArmoredInputStream) {
ArmoredInputStream armor = (ArmoredInputStream) outerDecodingStream;
// Cleartext Signed Message
// Throw a WrongConsumingMethodException to delegate preparation (extraction of signatures)
// to the CleartextSignatureProcessor which will call us again (see comment above)
if (armor.isClearText()) {
bufferedIn.reset();
return parseCleartextSignedMessage(bufferedIn);
}
}
// Non-OpenPGP data. We are probably verifying detached signatures
if (openPgpIn.isNonOpenPgp()) {
outerDecodingStream = openPgpIn;
pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null);
return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream, null);
}
if (openPgpIn.isBinaryOpenPgp()) {
outerDecodingStream = openPgpIn;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream,
(outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null);
} catch (EOFException | FinalIOException e) {
// Broken message or invalid decryption session key
throw e;
} catch (MissingLiteralDataException e) {
// Not an OpenPGP message.
// Reset the buffered stream to parse the message as arbitrary binary data
// to allow for detached signature verification.
LOGGER.debug("The message appears to not be an OpenPGP message. This is probably data signed with detached signatures?");
bufferedIn.reset();
outerDecodingStream = bufferedIn;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
pgpInStream = wrapInVerifySignatureStream(bufferedIn, objectFactory);
} catch (IOException e) {
if (e.getMessage().contains("invalid armor") || e.getMessage().contains("invalid header encountered")) {
// We falsely assumed the data to be armored.
LOGGER.debug("The message is apparently not armored.");
bufferedIn.reset();
outerDecodingStream = CRCingArmoredInputStreamWrapper.possiblyWrap(bufferedIn);
pgpInStream = wrapInVerifySignatureStream(outerDecodingStream, null);
resultBuilder, integrityProtectedEncryptedInputStream, null);
}
if (openPgpIn.isAsciiArmored()) {
ArmoredInputStream armoredInputStream = ArmoredInputStreamFactory.get(openPgpIn);
if (armoredInputStream.isClearText()) {
return parseCleartextSignedMessage(armoredInputStream);
} else {
throw new FinalIOException(e);
outerDecodingStream = armoredInputStream;
objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(outerDecodingStream);
// Parse OpenPGP message
pgpInStream = processPGPPackets(objectFactory, 1);
return new DecryptionStream(pgpInStream,
resultBuilder, integrityProtectedEncryptedInputStream,
outerDecodingStream);
}
}
return new DecryptionStream(pgpInStream, resultBuilder, integrityProtectedEncryptedInputStream,
(outerDecodingStream instanceof ArmoredInputStream) ? outerDecodingStream : null);
throw new PGPException("Not sure how to handle the input stream.");
}
private DecryptionStream parseCleartextSignedMessage(BufferedInputStream in)
private DecryptionStream parseCleartextSignedMessage(ArmoredInputStream armorIn)
throws IOException, PGPException {
resultBuilder.setCompressionAlgorithm(CompressionAlgorithm.UNCOMPRESSED)
.setFileEncoding(StreamEncoding.TEXT);
ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(in);
MultiPassStrategy multiPassStrategy = options.getMultiPassStrategy();
PGPSignatureList signatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(armorIn, multiPassStrategy.getMessageOutputStream());

View file

@ -0,0 +1,144 @@
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.decryption_verification;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.pgpainless.implementation.ImplementationFactory;
public class OpenPgpInputStream extends BufferedInputStream {
private static final byte[] ARMOR_HEADER = "-----BEGIN PGP ".getBytes(Charset.forName("UTF8"));
// Buffer beginning bytes of the data
public static final int MAX_BUFFER_SIZE = 8192;
private final byte[] buffer;
private final int bufferLen;
private boolean containsArmorHeader;
private boolean containsOpenPgpPackets;
public OpenPgpInputStream(InputStream in) throws IOException {
super(in, MAX_BUFFER_SIZE);
mark(MAX_BUFFER_SIZE);
buffer = new byte[MAX_BUFFER_SIZE];
bufferLen = read(buffer);
reset();
inspectBuffer();
}
private void inspectBuffer() {
if (determineIsArmored()) {
return;
}
determineIsBinaryOpenPgp();
}
private boolean determineIsArmored() {
if (startsWithIgnoringWhitespace(buffer, ARMOR_HEADER, bufferLen)) {
containsArmorHeader = true;
return true;
}
return false;
}
private void determineIsBinaryOpenPgp() {
if (bufferLen == -1) {
// Empty data
return;
}
try {
ByteArrayInputStream bufferIn = new ByteArrayInputStream(buffer, 0, bufferLen);
PGPObjectFactory objectFactory = ImplementationFactory.getInstance().getPGPObjectFactory(bufferIn);
while (objectFactory.nextObject() != null) {
// read all packets in buffer
}
containsOpenPgpPackets = true;
} catch (IOException e) {
if (e.getMessage().contains("premature end of stream in PartialInputStream")) {
// We *probably* hit valid, but large OpenPGP data
// This is not an optimal way of determining the nature of data, but probably the best
// we can get from BC.
containsOpenPgpPackets = true;
}
// else: seemingly random, non-OpenPGP data
}
}
private boolean startsWith(byte[] bytes, byte[] subsequence, int bufferLen) {
return indexOfSubsequence(bytes, subsequence, bufferLen) == 0;
}
private int indexOfSubsequence(byte[] bytes, byte[] subsequence, int bufferLen) {
if (bufferLen == -1) {
return -1;
}
// Naive implementation
// TODO: Could be improved by using e.g. Knuth-Morris-Pratt algorithm.
for (int i = 0; i < bufferLen; i++) {
if ((i + subsequence.length) <= bytes.length) {
boolean found = true;
for (int j = 0; j < subsequence.length; j++) {
if (bytes[i + j] != subsequence[j]) {
found = false;
break;
}
}
if (found) {
return i;
}
}
}
return -1;
}
private boolean startsWithIgnoringWhitespace(byte[] bytes, byte[] subsequence, int bufferLen) {
if (bufferLen == -1) {
return false;
}
for (int i = 0; i < bufferLen; i++) {
// Working on bytes is not trivial with unicode data, but its good enough here
if (Character.isWhitespace(bytes[i])) {
continue;
}
if ((i + subsequence.length) > bytes.length) {
return false;
}
for (int j = 0; j < subsequence.length; j++) {
if (bytes[i + j] != subsequence[j]) {
return false;
}
}
return true;
}
return false;
}
public boolean isAsciiArmored() {
return containsArmorHeader;
}
public boolean isBinaryOpenPgp() {
return containsOpenPgpPackets;
}
public boolean isNonOpenPgp() {
return !isAsciiArmored() && !isBinaryOpenPgp();
}
}

View file

@ -4,7 +4,6 @@
package org.pgpainless.util;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -14,6 +13,8 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.regex.Pattern;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.bcpg.ArmoredOutputStream;
@ -29,11 +30,9 @@ import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.KeyFingerPrintCalculator;
import org.bouncycastle.util.io.Streams;
import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.decryption_verification.OpenPgpInputStream;
import org.pgpainless.key.OpenPgpFingerprint;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
public final class ArmorUtils {
// MessageIDs are 32 printable characters
@ -550,16 +549,12 @@ public final class ArmorUtils {
@Nonnull
public static InputStream getDecoderStream(@Nonnull InputStream inputStream)
throws IOException {
BufferedInputStream buf = new BufferedInputStream(inputStream, 512);
InputStream decoderStream = PGPUtilWrapper.getDecoderStream(buf);
// Data is not armored -> return
if (decoderStream instanceof BufferedInputStream) {
return decoderStream;
OpenPgpInputStream openPgpIn = new OpenPgpInputStream(inputStream);
if (openPgpIn.isAsciiArmored()) {
ArmoredInputStream armorIn = ArmoredInputStreamFactory.get(openPgpIn);
return PGPUtil.getDecoderStream(armorIn);
}
// Wrap armored input stream with fix for #159
decoderStream = CRCingArmoredInputStreamWrapper.possiblyWrap(decoderStream);
decoderStream = PGPUtil.getDecoderStream(decoderStream);
return decoderStream;
return openPgpIn;
}
}

View file

@ -1,40 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.util;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.bouncycastle.openpgp.PGPUtil;
public final class PGPUtilWrapper {
private PGPUtilWrapper() {
}
/**
* {@link PGPUtil#getDecoderStream(InputStream)} sometimes mistakens non-base64 data for base64 encoded data.
*
* This method expects a {@link BufferedInputStream} which is being reset in case an {@link IOException} is encountered.
* Therefore, we can properly handle non-base64 encoded data.
*
* @param buf buffered input stream
* @return input stream
* @throws IOException in case of an io error which is unrelated to base64 encoding
*/
public static InputStream getDecoderStream(BufferedInputStream buf) throws IOException {
try {
return PGPUtil.getDecoderStream(buf);
} catch (IOException e) {
if (e.getMessage().contains("invalid characters encountered at end of base64 data")) {
buf.reset();
return buf;
}
throw e;
}
}
}