diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java index 66fd1d24..3b737702 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/EncryptionStream.java @@ -21,6 +21,7 @@ import org.bouncycastle.openpgp.PGPSignatureGenerator; import org.bouncycastle.openpgp.operator.PGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator; import org.pgpainless.algorithm.CompressionAlgorithm; +import org.pgpainless.algorithm.StreamEncoding; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; @@ -70,6 +71,8 @@ public final class EncryptionStream extends OutputStream { prepareCompression(); prepareOnePassSignatures(); prepareLiteralDataProcessing(); + prepareSigningStream(); + prepareInputEncoding(); } private void prepareArmor() { @@ -174,20 +177,19 @@ public final class EncryptionStream extends OutputStream { .setFileEncoding(options.getEncoding()); } + public void prepareSigningStream() { + outermostStream = new SignatureGenerationStream(outermostStream, options.getSigningOptions()); + } + + public void prepareInputEncoding() { + CRLFGeneratorStream crlfGeneratorStream = new CRLFGeneratorStream(outermostStream, + options.isApplyCRLFEncoding() ? StreamEncoding.UTF8 : StreamEncoding.BINARY); + outermostStream = crlfGeneratorStream; + } + @Override public void write(int data) throws IOException { outermostStream.write(data); - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - byte asByte = (byte) (data & 0xff); - signatureGenerator.update(asByte); - } } @Override @@ -199,15 +201,6 @@ public final class EncryptionStream extends OutputStream { @Override public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { outermostStream.write(buffer, 0, len); - SigningOptions signingOptions = options.getSigningOptions(); - if (signingOptions == null || signingOptions.getSigningMethods().isEmpty()) { - return; - } - for (SubkeyIdentifier signingKey : signingOptions.getSigningMethods().keySet()) { - SigningOptions.SigningMethod signingMethod = signingOptions.getSigningMethods().get(signingKey); - PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); - signatureGenerator.update(buffer, 0, len); - } } @Override @@ -221,6 +214,8 @@ public final class EncryptionStream extends OutputStream { return; } + outermostStream.close(); + // Literal Data if (literalDataStream != null) { literalDataStream.flush(); diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java index 9ee3c03e..41d9ca85 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/ProducerOptions.java @@ -19,7 +19,8 @@ public final class ProducerOptions { private final SigningOptions signingOptions; private String fileName = ""; private Date modificationDate = PGPLiteralData.NOW; - private StreamEncoding streamEncoding = StreamEncoding.BINARY; + private StreamEncoding encodingField = StreamEncoding.BINARY; + private boolean applyCRLFEncoding = false; private boolean cleartextSigned = false; private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() @@ -223,9 +224,12 @@ public final class ProducerOptions { } /** - * Set the format of the literal data packet. + * Set format metadata field of the literal data packet. * Defaults to {@link StreamEncoding#BINARY}. * + * This does not change the encoding of the wrapped data itself. + * To apply CR/LF encoding to your input data before processing, use {@link #applyCRLFEncoding(boolean)} instead. + * * @see RFC4880 ยง5.9. Literal Data Packet * * @param encoding encoding @@ -235,12 +239,37 @@ public final class ProducerOptions { */ @Deprecated public ProducerOptions setEncoding(@Nonnull StreamEncoding encoding) { - this.streamEncoding = encoding; + this.encodingField = encoding; return this; } public StreamEncoding getEncoding() { - return streamEncoding; + return encodingField; + } + + /** + * Apply special encoding of line endings to the input data. + * By default, this is set to
false
, which means that the data is not altered. + * + * Setting it to
true
will change the line endings to CR/LF. + * Note: The encoding will not be reversed when decrypting, so applying CR/LF encoding will result in + * the identity "decrypt(encrypt(data)) == data == verify(sign(data))". + * + * @param applyCRLFEncoding apply crlf encoding + * @return this + */ + public ProducerOptions applyCRLFEncoding(boolean applyCRLFEncoding) { + this.applyCRLFEncoding = applyCRLFEncoding; + return this; + } + + /** + * Return the input encoding that will be applied before signing / encryption. + * + * @return input encoding + */ + public boolean isApplyCRLFEncoding() { + return applyCRLFEncoding; } /** diff --git a/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java new file mode 100644 index 00000000..69ae1346 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/encryption_signing/SignatureGenerationStream.java @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPSignatureGenerator; +import org.pgpainless.key.SubkeyIdentifier; + +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.OutputStream; + +class SignatureGenerationStream extends OutputStream { + + private final OutputStream wrapped; + private final SigningOptions options; + + SignatureGenerationStream(OutputStream wrapped, SigningOptions signingOptions) { + this.wrapped = wrapped; + this.options = signingOptions; + } + + @Override + public void write(int b) throws IOException { + wrapped.write(b); + if (options == null || options.getSigningMethods().isEmpty()) { + return; + } + + for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); + byte asByte = (byte) (b & 0xff); + signatureGenerator.update(asByte); + } + } + + @Override + public void write(@Nonnull byte[] buffer) throws IOException { + write(buffer, 0, buffer.length); + } + + @Override + public void write(@Nonnull byte[] buffer, int off, int len) throws IOException { + wrapped.write(buffer, 0, len); + if (options == null || options.getSigningMethods().isEmpty()) { + return; + } + for (SubkeyIdentifier signingKey : options.getSigningMethods().keySet()) { + SigningOptions.SigningMethod signingMethod = options.getSigningMethods().get(signingKey); + PGPSignatureGenerator signatureGenerator = signingMethod.getSignatureGenerator(); + signatureGenerator.update(buffer, 0, len); + } + } + + @Override + public void close() throws IOException { + wrapped.close(); + } +} diff --git a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java similarity index 75% rename from pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java rename to pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java index 7cae970e..e1722343 100644 --- a/pgpainless-core/src/test/java/investigations/CanonicalizedDataEncryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/CanonicalizedDataEncryptionTest.java @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package investigations; +package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -34,9 +34,6 @@ import org.pgpainless.PGPainless; import org.pgpainless.algorithm.DocumentSignatureType; import org.pgpainless.algorithm.HashAlgorithm; import org.pgpainless.algorithm.StreamEncoding; -import org.pgpainless.decryption_verification.ConsumerOptions; -import org.pgpainless.decryption_verification.DecryptionStream; -import org.pgpainless.decryption_verification.OpenPgpMetadata; import org.pgpainless.encryption_signing.CRLFGeneratorStream; import org.pgpainless.encryption_signing.EncryptionOptions; import org.pgpainless.encryption_signing.EncryptionStream; @@ -120,9 +117,11 @@ public class CanonicalizedDataEncryptionTest { // CHECKSTYLE:ON } + // NO CR/LF ENCODING PRIOR TO PROCESSING + @Test - public void binaryDataBinarySig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY); + public void noInputEncodingBinaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -135,8 +134,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void binaryDataTextSig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY); + public void noInputEncodingBinaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -149,8 +148,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void textDataBinarySig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT); + public void noInputEncodingTextDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -163,8 +162,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void textDataTextSig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT); + public void noInputEncodingTextDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -177,8 +176,8 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void utf8DataBinarySig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8); + public void noInputEncodingUtf8DataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, false); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -191,8 +190,23 @@ public class CanonicalizedDataEncryptionTest { } @Test - public void utf8DataTextSig() throws PGPException, IOException { - String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8); + public void noInputEncodingUtf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, false); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + // APPLY CR/LF ENCODING PRIOR TO PROCESSING + + @Test + public void inputEncodingBinaryDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.BINARY, true); OpenPgpMetadata metadata = decryptAndVerify(msg); if (!metadata.isVerified()) { @@ -204,7 +218,80 @@ public class CanonicalizedDataEncryptionTest { } } - private String encryptAndSign(String message, DocumentSignatureType sigType, StreamEncoding dataFormat) + @Test + public void inputEncodingBinaryDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.BINARY, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingTextDataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.TEXT, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingTextDataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.TEXT, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingUtf8DataBinarySig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.BINARY_DOCUMENT, StreamEncoding.UTF8, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + @Test + public void inputEncodingUtf8DataTextSig() throws PGPException, IOException { + String msg = encryptAndSign(message, DocumentSignatureType.CANONICAL_TEXT_DOCUMENT, StreamEncoding.UTF8, true); + OpenPgpMetadata metadata = decryptAndVerify(msg); + + if (!metadata.isVerified()) { + // CHECKSTYLE:OFF + System.out.println("Not verified. Session-Key: " + metadata.getSessionKey()); + System.out.println(msg); + // CHECKSTYLE:ON + fail(); + } + } + + private String encryptAndSign(String message, + DocumentSignatureType sigType, + StreamEncoding dataFormat, + boolean applyCRLFEncoding) throws PGPException, IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); @@ -218,6 +305,7 @@ public class CanonicalizedDataEncryptionTest { .addInlineSignature(SecretKeyRingProtector.unprotectedKeys(), secretKeys, sigType) ) .setEncoding(dataFormat) + .applyCRLFEncoding(applyCRLFEncoding) ); ByteArrayInputStream inputStream = new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8));