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));