Skip to content

Commit

Permalink
Added capability to set armored headers on a per-file basis
Browse files Browse the repository at this point in the history
  • Loading branch information
Lyor Goldstein committed Feb 13, 2020
1 parent 398474b commit 8b2a8bb
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 30 deletions.
2 changes: 0 additions & 2 deletions src/main/java/org/c02e/jpgpj/DecryptionResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,4 @@ public String toString() {
+ ", numArmorHeaders=" + getArmorHeaders().size()
+ "]";
}


}
5 changes: 3 additions & 2 deletions src/main/java/org/c02e/jpgpj/Decryptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,9 @@ public FileMetadata decrypt(InputStream ciphertext, OutputStream plaintext)

/**
* Decrypts the specified PGP message into the specified output stream,
* and (if {@link #isVerificationRequired}) verifies the message
* signatures. Does not close or flush the streams.
* including the armored headers (if stream was armored and contained
* any such headers). If {@link #isVerificationRequired} also verifies
* the message signatures. <B>Note:</B> does not close or flush the streams.
*
* @param ciphertext PGP message, in binary or ASCII Armor format.
* @param plaintext Decrypted content target {@link OutputStream}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.c02e.jpgpj;

/**
* Used by the encryptor to allow users to configure per-file
* armored headers instead/in addition to the global ones that
* are set by the encryptor
*/
@FunctionalInterface
public interface EncryptedAsciiArmorHeadersCallback {
/**
* Invoked by the encryptor <U>after</U> updating the
* settings with the configured global headers.
*
* @param encryptor The {@link Encryptor} that is handling the encryption request
* @param meta The input plaintext {@link FileMetadata} - might be empty
* (but not {@code null}).
* @param manipulator The manipulator that can be used to update the headers
*/
void prepareAsciiArmoredHeaders(
Encryptor encryptor, FileMetadata meta, EncryptedAsciiArmorHeadersManipulator manipulator);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.c02e.jpgpj;

import java.util.Map;

import org.bouncycastle.bcpg.ArmoredOutputStream;

@FunctionalInterface
public interface EncryptedAsciiArmorHeadersManipulator {
/**
* A manipulator that ignores all headers manipulations
*/
EncryptedAsciiArmorHeadersManipulator EMPTY = (name, value) -> { /* do nothing */ };

/**
* Set the specified header value - replace any previous value
*
* @param name Case <U>sensitive</U> name of header to set. <B>Note:</B> this
* method can be used to <U>override</U> the default version header value.
* @param value Value to set - if {@code null} then equivalent to header removal
*/
void setHeader(String name, String value);

/**
* Removes specified header - no-op if header not set anyway
*
* @param name Case <U>sensitive</U> name of header to set. <B>Note:</B> this
* method can be used to <U>remove</U> the default version header value.
*/
default void removeHeader(String name) {
setHeader(name, null);
}

/**
* Replaces existing headers and adds missing ones
*
* @param headers The headers to update - ignored if {@code null}.
* <B>Note:</B> header name is case <U>sensitive</U>
*/
default void updateHeaders(Map<String, String> headers) {
if (headers != null) {
headers.forEach((name, value) -> setHeader(name, value));
}
}

/**
* Wraps an {@link ArmoredOutputStream}
*
* @param aos The stream to wrap - ignored if {@code null}
* @return The manipulator wrapping
*/
static EncryptedAsciiArmorHeadersManipulator wrap(ArmoredOutputStream aos) {
return (aos == null) ? EMPTY : (name, value) -> aos.setHeader(name, value);
}
}
79 changes: 59 additions & 20 deletions src/main/java/org/c02e/jpgpj/Encryptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;

import org.bouncycastle.bcpg.ArmoredOutputStream;
Expand Down Expand Up @@ -79,7 +78,8 @@ public class Encryptor {

protected boolean asciiArmored;
protected boolean removeDefaultArmoredVersionHeader;
protected final NavigableMap<String, String> armoredHeaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
protected final Map<String, String> armoredHeaders = new HashMap<>();
protected EncryptedAsciiArmorHeadersCallback armorHeadersCallback;

protected int compressionLevel;
protected CompressionAlgorithm compressionAlgorithm;
Expand Down Expand Up @@ -131,12 +131,37 @@ public void setAsciiArmored(boolean x) {
asciiArmored = x;
}

/**
* @return The last set {@link EncryptedAsciiArmorHeadersCallback}
* @see #setArmorHeadersCallback(EncryptedAsciiArmorHeadersCallback)
*/
public EncryptedAsciiArmorHeadersCallback getArmorHeadersCallback() {
return armorHeadersCallback;
}

/**
* Allows users to provide a callback that will be invoked for each
* encrypted <U>armored</U> output in order to allow them to set specified
* headers besides the global ones set by the encryptor. <B>Note:</B>
* affects the output only if {@link #isAsciiArmored() armored} setting is used.
*
* @param armorHeadersCallback The callback to invoke - {@code null} if none
* @see #isAsciiArmored()
* @see #isRemoveDefaultArmoredVersionHeader()
* @see #setArmoredHeaders(Map) setArmoredHeaders
* @see #addArmoredHeaders(Map) addArmoredHeaders
* @see #updateArmoredHeader(String, String) updateArmoredHeader
*/
public void setArmorHeadersCallback(EncryptedAsciiArmorHeadersCallback armorHeadersCallback) {
this.armorHeadersCallback = armorHeadersCallback;
}

/**
* By default the {@link ArmoredOutputStream} adds a &quot;Version&quot;
* header - this setting allows users to remove this header (and perhaps
* replace it and/or add others - see headers manipulation methods).
*
* @return {@code true} if &quot;Version&quot; should be removed - default={@code false)
* @return {@code true} if &quot;Version&quot; should be removed - default={@code false}
*/
public boolean isRemoveDefaultArmoredVersionHeader() {
return removeDefaultArmoredVersionHeader;
Expand All @@ -145,10 +170,11 @@ public boolean isRemoveDefaultArmoredVersionHeader() {
/**
* By default the {@link ArmoredOutputStream} adds a &quot;Version&quot;
* header - this setting allows users to remove this header (and perhaps
* replace it and/or add others - see headers manipulation methods).
* replace it and/or add others - see headers manipulation methods). <B>Note:</B>
* affects the output only if {@link #isAsciiArmored() armored} setting is used.
*
* @param removeDefaultarmoredVersionHeader {@code true} if &quot;Version&quot;
* should be removed - default={@code false). <B>Note:</B> relevant only if
* should be removed - default={@code false}. <B>Note:</B> relevant only if
* {@link #setAsciiArmored(boolean) armored} setting was also set.
*/
public void setRemoveDefaultArmoredVersionHeader(boolean removeDefaultArmoredVersionHeader) {
Expand All @@ -158,7 +184,7 @@ public void setRemoveDefaultArmoredVersionHeader(boolean removeDefaultArmoredVer
/**
* Retrieves the value for the specified armored header.
*
* @param name Case <U>insensitive</U> name of header to get
* @param name Case <U>sensitive</U> name of header to get
* @return The header value - {@code null} if header not set
* @throws NullPointerException If no header name provided
*/
Expand All @@ -168,16 +194,16 @@ public String getArmoredHeader(String name) {
}

/**
* @return An <U>unmodifiable</U> {@link NavigableMap} of
* @return An <U>unmodifiable</U> {@link Map} of
* the current armored headers - <B>Note:</B> header name
* access is case <U>insensitive</U>
* access is case <U>sensitive</U>
*/
public NavigableMap<String, String> getArmoredHeaders() {
public Map<String, String> getArmoredHeaders() {
if (armoredHeaders.isEmpty()) {
return Collections.emptyNavigableMap();
return Collections.emptyMap();
}

return Collections.unmodifiableNavigableMap(armoredHeaders);
return Collections.unmodifiableMap(armoredHeaders);
}

/**
Expand All @@ -186,7 +212,7 @@ public NavigableMap<String, String> getArmoredHeaders() {
*
* @param headers The new headers to set - may be {@code null}/empty. <B>Note:</B>
* <UL>
* <LI>Header names are case <U>insensitive</U></LI>
* <LI>Header names are case <U>sensitive</U></LI>
*
* <LI>
* In order to clear all headers need to also use
Expand All @@ -205,7 +231,7 @@ public void setArmoredHeaders(Map<String, String> headers) {
* setting is used.
*
* @param headers The headers to add - may be {@code null}/empty. <B>Note:</B>
* header names are case <U>insensitive</U>.
* header names are case <U>sensitive</U>.
*/
public void addArmoredHeaders(Map<String, String> headers) {
if (headers != null) {
Expand All @@ -217,7 +243,7 @@ public void addArmoredHeaders(Map<String, String> headers) {
* Sets the specified header value - replaces it if already set. <B>Note:</B>
* affects the output only if {@link #isAsciiArmored() armored} setting is used.
*
* @param name Case <U>insensitive</U> name of header to set. <B>Note:</B> this
* @param name Case <U>sensitive</U> name of header to set. <B>Note:</B> this
* method can be used to <U>override</U> the default version header value.
* @param value Value to set - if {@code null} then equivalent to
* {@link #removeArmoredHeader(String) header removal}
Expand All @@ -238,7 +264,7 @@ public String updateArmoredHeader(String name, String value) {
* Removes the specified armored header <B>Note:</B> affects the output only
* if {@link #isAsciiArmored() armored} setting is used.
*
* @param name Case <U>insensitive</U> name of header to remove - <B>Note:</B>
* @param name Case <U>sensitive</U> name of header to remove - <B>Note:</B>
* in order to remove the version header must use {@link #setRemoveDefaultArmoredVersionHeader(boolean)}.
* @return The removed value - {@code null} if header was not set
* @throws NullPointerException If no header name provided
Expand Down Expand Up @@ -621,7 +647,7 @@ public OutputStream prepareCiphertextOutputStream(
stack.add(ciphertext);

// setup encryption pipeline
ciphertext = pipeline(armor(ciphertext), stack);
ciphertext = pipeline(armor(ciphertext, meta), stack);
ciphertext = pipeline(encrypt(ciphertext, meta), stack);
ciphertext = pipeline(compress(ciphertext, meta), stack);
SigningOutputStream signingstream = sign(ciphertext, meta);
Expand Down Expand Up @@ -713,18 +739,21 @@ protected OutputStream pipeline(OutputStream out, List<OutputStream> stack) {
}

/**
* Wraps with stream that outputs ASCII-armored text - including configuring its
* armor headers.
* Wraps with stream that outputs ASCII-armored text - including configuring
* its armor headers.
*
* @param meta The input plaintext {@link FileMetadata} - might be empty
* (but not {@code null}).
* @param out The {@link OutputStream} to wrap
* @return The wrapped output stream - {@code null} if no wrapping.
* @see #isAsciiArmored()
* @see #isRemoveDefaultArmoredVersionHeader()
* @see #setArmoredHeaders(Map) setArmoredHeaders
* @see #addArmoredHeaders(Map) addArmoredHeaders
* @see #updateArmoredHeader(String, String) updateArmoredHeader
* @see #setArmorHeadersCallback(EncryptedAsciiArmorHeadersCallback)
*/
protected OutputStream armor(OutputStream out) {
protected OutputStream armor(OutputStream out, FileMetadata meta) {
if (!isAsciiArmored()) {
return null;
}
Expand All @@ -734,7 +763,17 @@ protected OutputStream armor(OutputStream out) {
aos.setHeader(ArmoredOutputStream.VERSION_HDR, null);
}

// add the global headers - if any
armoredHeaders.forEach((name, value) -> aos.setHeader(name, value));

// see if user wants to manipulate the headers
EncryptedAsciiArmorHeadersCallback callback = getArmorHeadersCallback();
if (callback != null) {
EncryptedAsciiArmorHeadersManipulator manipulator =
EncryptedAsciiArmorHeadersManipulator.wrap(aos);
callback.prepareAsciiArmoredHeaders(this, meta, manipulator);
}

return aos;
}

Expand Down
23 changes: 17 additions & 6 deletions src/test/groovy/org/c02e/jpgpj/EncryptorSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -292,19 +292,30 @@ hQEMAyne546XDHBhAQ...
encryptor.ring.keys*.passphrase = 'c02e'
encryptor.updateArmoredHeader("Version", "3.14")
encryptor.updateArmoredHeader("Encryptor", "c02e")
encryptor.armorHeadersCallback = new EncryptedAsciiArmorHeadersCallback() {
@Override
public void prepareAsciiArmoredHeaders(
Encryptor enc, FileMetadata meta, EncryptedAsciiArmorHeadersManipulator manipulator) {
manipulator.setHeader("Version", "2.71") // override
manipulator.setHeader("Callback", "true") // add new
}
}
encryptor.encrypt plainIn, cipherOut

def decryptor = new Decryptor(new Ring(stream('test-key-1.asc')))
decryptor.ring.keys*.passphrase = 'c02e'
def result = decryptor.decryptWithFullDetails cipherIn, plainOut
def armorHeaders = result.armorHeaders

// result headers list is unmodifiable and we want to sort it
def armorHeaders = new ArrayList<>(result.armorHeaders)
armorHeaders.sort(Comparator.naturalOrder())

then:
result.asciiArmored == true
armorHeaders.size() == 2
armorHeaders[0].equals("Version: 3.14")
armorHeaders[1].equals("Encryptor: c02e")

armorHeaders.size() == 3
armorHeaders[0].equals("Callback: true") // added by callback
armorHeaders[1].equals("Encryptor: c02e") // global setting in encryptor
armorHeaders[2].equals("Version: 2.71") // overwritten by the callback

plainOut.toString() == plainText
}

Expand Down

0 comments on commit 8b2a8bb

Please sign in to comment.