From 41e663e25b3b868a6b56e2246c14b0544e540a0e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 5 Jun 2023 20:18:06 +0200 Subject: [PATCH] Allow setting custom version header when encrypting/signing message --- .../encryption_signing/EncryptionStream.java | 6 +++ .../encryption_signing/ProducerOptions.java | 45 ++++++++++++++--- .../java/org/pgpainless/util/ArmorUtils.java | 16 ++++++ .../encryption_signing/AsciiArmorTest.java | 49 +++++++++++++++++++ 4 files changed, 109 insertions(+), 7 deletions(-) create mode 100644 pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java 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 2e370b94..d2a16e08 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 @@ -99,6 +99,12 @@ public final class EncryptionStream extends OutputStream { } } } + if (options.hasVersion()) { + String version = options.getVersion().trim(); + if (!version.isEmpty()) { + ArmorUtils.setVersionHeader(armorOutputStream, version); + } + } outermostStream = armorOutputStream; } 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 4948a7fe..00fbb10a 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 @@ -28,6 +28,7 @@ public final class ProducerOptions { .defaultCompressionAlgorithm(); private boolean asciiArmor = true; private String comment = null; + private String version = null; private ProducerOptions(EncryptionOptions encryptionOptions, SigningOptions signingOptions) { this.encryptionOptions = encryptionOptions; @@ -120,7 +121,7 @@ public final class ProducerOptions { * Set the comment header in ASCII armored output. * The default value is null, which means no comment header is added. * Multiline comments are possible using '\\n'. - * + *
* Note: If a default header comment is set using {@link org.pgpainless.util.ArmoredOutputStreamFactory#setComment(String)}, * then both comments will be written to the produced ASCII armor. * @@ -128,13 +129,25 @@ public final class ProducerOptions { * @return builder */ public ProducerOptions setComment(String comment) { - if (!asciiArmor) { - throw new IllegalArgumentException("Comment can only be set when ASCII armoring is enabled."); - } this.comment = comment; return this; } + /** + * Set the version header in ASCII armored output. + * The default value is null, which means no version header is added. + *
+ * Note: If the value is non-null, then this method overrides the default version header set using + * {@link org.pgpainless.util.ArmoredOutputStreamFactory#setVersionInfo(String)}. + * + * @param version version header, or null for no version info. + * @return builder + */ + public ProducerOptions setVersion(String version) { + this.version = version; + return this; + } + /** * Return comment set for header in ascii armored output. * @@ -144,15 +157,33 @@ public final class ProducerOptions { return comment; } + /** + * Return the version info header in ascii armored output. + * + * @return version info + */ + public String getVersion() { + return version; + } + /** * Return whether a comment was set (!= null). * - * @return comment + * @return true if commend is set */ public boolean hasComment() { return comment != null; } + /** + * Return whether a version header was set (!= null). + * + * @return true if version header is set + */ + public boolean hasVersion() { + return version != null; + } + public ProducerOptions setCleartextSigned() { if (signingOptions == null) { throw new IllegalArgumentException("Signing Options cannot be null if cleartext signing is enabled."); @@ -233,7 +264,7 @@ public final class ProducerOptions { /** * 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()} instead. * @@ -257,7 +288,7 @@ public final class ProducerOptions { /** * Apply special encoding of line endings to the input data. * By default, this is disabled, which means that the data is not altered. - * + *
* Enabling it 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))". diff --git a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java index 87170638..cf19d273 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java +++ b/pgpainless-core/src/main/java/org/pgpainless/util/ArmorUtils.java @@ -417,6 +417,22 @@ public final class ArmorUtils { return new Tuple<>(printed, countIdentities); } + /** + * Set the version header entry in the ASCII armor. + * If the version info is null or only contains whitespace characters, then the version header will be removed. + * + * @param armor armored output stream + * @param version version header. + */ + public static void setVersionHeader(@Nonnull ArmoredOutputStream armor, + @Nullable String version) { + if (version == null || version.trim().isEmpty()) { + armor.setHeader(HEADER_VERSION, null); + } else { + armor.setHeader(HEADER_VERSION, version); + } + } + /** * Add an ASCII armor header entry about the used hash algorithm into the {@link ArmoredOutputStream}. * diff --git a/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java new file mode 100644 index 00000000..b6f8bf24 --- /dev/null +++ b/pgpainless-core/src/test/java/org/pgpainless/encryption_signing/AsciiArmorTest.java @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.encryption_signing; + +import org.bouncycastle.openpgp.PGPException; +import org.junit.jupiter.api.Test; +import org.pgpainless.PGPainless; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AsciiArmorTest { + + @Test + public void testCustomAsciiArmorComments() throws PGPException, IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .setAsciiArmor(true) + .setComment("This is a comment.\nThis is another comment.")); + encryptionStream.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + String asciiArmored = out.toString(); + assertTrue(asciiArmored.contains("Comment: This is a comment.")); + assertTrue(asciiArmored.contains("Comment: This is another comment.")); + } + + @Test + public void testCustomAsciiArmorVersion() throws IOException, PGPException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + EncryptionStream encryptionStream = PGPainless.encryptAndOrSign() + .onOutputStream(out) + .withOptions(ProducerOptions.noEncryptionNoSigning() + .setAsciiArmor(true) + .setVersion("Custom-PGP 1.2.3")); + encryptionStream.write("Hello, World!".getBytes(StandardCharsets.UTF_8)); + encryptionStream.close(); + + String asciiArmored = out.toString(); + assertTrue(asciiArmored.contains("Version: Custom-PGP 1.2.3")); + } +}