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 317b8cb2..37b7288e 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 @@ -23,6 +23,7 @@ import org.pgpainless.algorithm.CompressionAlgorithm; import org.pgpainless.algorithm.SymmetricKeyAlgorithm; import org.pgpainless.implementation.ImplementationFactory; import org.pgpainless.key.SubkeyIdentifier; +import org.pgpainless.util.ArmorUtils; import org.pgpainless.util.ArmoredOutputStreamFactory; import org.pgpainless.util.StreamGeneratorWrapper; import org.slf4j.Logger; @@ -74,6 +75,9 @@ public final class EncryptionStream extends OutputStream { LOGGER.debug("Wrap encryption output in ASCII armor"); armorOutputStream = ArmoredOutputStreamFactory.get(outermostStream); + if (options.hasComment()) { + ArmorUtils.addCommentHeader(armorOutputStream, options.getComment()); + } 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 94b7f0ce..890acc6c 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 @@ -25,6 +25,7 @@ public final class ProducerOptions { private CompressionAlgorithm compressionAlgorithmOverride = PGPainless.getPolicy().getCompressionAlgorithmPolicy() .defaultCompressionAlgorithm(); private boolean asciiArmor = true; + private String comment = null; private ProducerOptions(EncryptionOptions encryptionOptions, SigningOptions signingOptions) { this.encryptionOptions = encryptionOptions; @@ -107,6 +108,40 @@ public final class ProducerOptions { return asciiArmor; } + /** + * set the comment for header in ascii armored output. + * The default value is null, which means no comment header is added. + * Multiline comments are possible using '\\n'. + * + * @param comment comment header text + * @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; + } + + /** + * Return comment set for header in ascii armored output. + * + * @return comment + */ + public String getComment() { + return comment; + } + + /** + * Return whether a comment was set (!= null). + * + * @return comment + */ + public boolean hasComment() { + return comment != null; + } + public ProducerOptions setCleartextSigned() { if (signingOptions == null) { throw new IllegalArgumentException("Signing Options cannot be null if cleartext signing is enabled."); diff --git a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java index d2f1113c..3a3888a1 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java +++ b/pgpainless-core/src/test/java/org/pgpainless/example/Encrypt.java @@ -129,4 +129,71 @@ public class Encrypt { assertEquals(message, plaintext.toString()); } + + /** + * In this example, Alice is sending a signed and encrypted message to Bob. + * She encrypts the message to both bobs certificate and her own. + * A comment header with the text "This comment was added using options." is added + * using the fluent ProducerOption syntax. + * + * Bob subsequently decrypts the message using his key. + */ + @Test + public void encryptWithCommentHeader() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException { + // Prepare keys + PGPSecretKeyRing keyAlice = PGPainless.generateKeyRing() + .modernKeyRing("alice@pgpainless.org", null); + PGPPublicKeyRing certificateAlice = KeyRingUtils.publicKeyRingFrom(keyAlice); + + PGPSecretKeyRing keyBob = PGPainless.generateKeyRing() + .modernKeyRing("bob@pgpainless.org", null); + PGPPublicKeyRing certificateBob = KeyRingUtils.publicKeyRingFrom(keyBob); + SecretKeyRingProtector protectorBob = SecretKeyRingProtector.unprotectedKeys(); + + // plaintext message to encrypt + String message = "Hello, World!\n"; + String comment = "This comment was added using options."; + ByteArrayOutputStream ciphertext = new ByteArrayOutputStream(); + // Encrypt and sign + EncryptionStream encryptor = PGPainless.encryptAndOrSign() + .onOutputStream(ciphertext) + .withOptions(ProducerOptions.encrypt( + // we want to encrypt communication (affects key selection based on key flags) + EncryptionOptions.encryptCommunications() + .addRecipient(certificateBob) + .addRecipient(certificateAlice) + ).setAsciiArmor(true) + .setComment(comment) + ); + + // Pipe data trough and CLOSE the stream (important) + Streams.pipeAll(new ByteArrayInputStream(message.getBytes(StandardCharsets.UTF_8)), encryptor); + encryptor.close(); + String encryptedMessage = ciphertext.toString(); + + // check that comment header was added after "BEGIN PGP" and "Version:" + assertEquals(encryptedMessage.split("\n")[2].trim(), "Comment: " + comment); + + // also test, that decryption still works... + + // Decrypt and verify signatures + DecryptionStream decryptor = PGPainless.decryptAndOrVerify() + .onInputStream(new ByteArrayInputStream(encryptedMessage.getBytes(StandardCharsets.UTF_8))) + .withOptions(new ConsumerOptions() + .addDecryptionKey(keyBob, protectorBob) + .addVerificationCert(certificateAlice) + ); + + ByteArrayOutputStream plaintext = new ByteArrayOutputStream(); + + Streams.pipeAll(decryptor, plaintext); + decryptor.close(); + + // Check the metadata to see how the message was encrypted/signed + OpenPgpMetadata metadata = decryptor.getResult(); + assertTrue(metadata.isEncrypted()); + assertEquals(message, plaintext.toString()); + } + + }