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:
parent
3309781b11
commit
46f69b9fa5
6 changed files with 884 additions and 157 deletions
|
|
@ -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());
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue