Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Armour headers support in encryptor and decryptor #27

Merged
merged 4 commits into from
Feb 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions src/main/java/org/c02e/jpgpj/DecryptionResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package org.c02e.jpgpj;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
* Holds the most detailed information about a decrypted file
*/
public class DecryptionResult {
private final FileMetadata fileMetadata;
private final boolean armored;
private final List<String> armorHeaders;

public DecryptionResult(
FileMetadata fileMetadata, boolean armored, Collection<String> armorHeaders) {
this.fileMetadata = fileMetadata;
this.armored = armored;
this.armorHeaders = ((armorHeaders == null) || armorHeaders.isEmpty())
? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList<>(armorHeaders));
}

/**
* @return The decrypted {@link FileMetadata} - may be {@code null} if none was provided
*/
public FileMetadata getFileMetadata() {
return fileMetadata;
}

/**
* @return {@code true} if the encrypted data was armored
*/
public boolean isAsciiArmored() {
return armored;
}

/**
* @return An <U>unmodifiable</U> {@link List} of extracted armored
* headers - is valid only if {@link #isAsciiArmored() armored}. <B>Note:</B>
* might be empty if the encrypted data was armored but contained no headers.
*/
public List<String> getArmorHeaders() {
return armorHeaders;
}

@Override
public String toString() {
return getClass().getSimpleName()
+ "[metadata=" + getFileMetadata()
+ ", armored=" + isAsciiArmored()
+ ", numArmorHeaders=" + getArmorHeaders().size()
+ "]";
}
}
71 changes: 54 additions & 17 deletions src/main/java/org/c02e/jpgpj/Decryptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

Expand Down Expand Up @@ -78,7 +79,7 @@ public class Decryptor {
protected String symmetricPassphrase;
protected int maxFileBufferSize = 0x100000; //1MB
protected Ring ring;
protected Logger log = LoggerFactory.getLogger(Decryptor.class.getName());
protected final Logger log = LoggerFactory.getLogger(Decryptor.class.getName());


/** Constructs a decryptor with an empty key ring. */
Expand Down Expand Up @@ -226,7 +227,7 @@ public void clearSecrets() {
* the message was not signed by any of the keys supplied for verification.
*/
public FileMetadata decrypt(File ciphertext, File plaintext)
throws IOException, PGPException {
throws IOException, PGPException {
if (ciphertext.equals(plaintext))
throw new IOException("cannot decrypt " + ciphertext +
" over itself");
Expand Down Expand Up @@ -284,23 +285,63 @@ public FileMetadata decrypt(File ciphertext, File plaintext)
* of the keys supplied for decryption.
* @throws VerificationException if {@link #isVerificationRequired} and
* the message was not signed by any of the keys supplied for verification.
* @see #decryptWithFullDetails(InputStream, OutputStream) decryptWithFullDetails
*/
public FileMetadata decrypt(InputStream ciphertext, OutputStream plaintext)
throws IOException, PGPException {
List<FileMetadata> meta = unpack(parse(unarmor(ciphertext)), plaintext);
throws IOException, PGPException {
DecryptionResult result = decryptWithFullDetails(ciphertext, plaintext);
return result.getFileMetadata();
}

/**
* Decrypts the specified PGP message into the specified output stream,
* 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}
* @return The {@link DecryptionResult} containing all relevant
* information that could be extracted from the encrypted data - including
* metadata, armored headers (if any), etc...
* @throws IOException if an IO error occurs reading from or writing to
* the underlying input or output streams.
* @throws PGPException if the PGP message is not formatted correctly.
* @throws PassphraseException if an incorrect passphrase was supplied
* for one of the decryption keys, or as the
* {@link #getSymmetricPassphrase()}.
* @throws DecryptionException if the message was not encrypted for any
* of the keys supplied for decryption.
* @throws VerificationException if {@link #isVerificationRequired} and
* the message was not signed by any of the keys supplied for verification.
*/
public DecryptionResult decryptWithFullDetails(
InputStream ciphertext, OutputStream plaintext)
throws IOException, PGPException {
InputStream unarmoredStream = unarmor(ciphertext);
List<FileMetadata> meta = unpack(parse(unarmoredStream), plaintext);
if (meta.size() > 1)
throw new PGPException("content contained more than one file");
if (meta.size() < 1)
return new FileMetadata();
return meta.get(0);
}

FileMetadata metadata = (meta.size() < 1) ? new FileMetadata() : meta.get(0);
if (unarmoredStream instanceof ArmoredInputStream) {
ArmoredInputStream ais = (ArmoredInputStream) unarmoredStream;
String[] headers = ais.getArmorHeaders();
return new DecryptionResult(metadata, true,
((headers == null) || (headers.length == 0))
? Collections.emptyList()
: Arrays.asList(headers));
} else {
return new DecryptionResult(metadata, false, Collections.emptyList());
}
}

/**
* Recursively unpacks the pgp message packets,
* writing the decrypted message content into the output stream.
*/
protected List<FileMetadata> unpack(Iterator<?> packets,
OutputStream plaintext) throws IOException, PGPException {
protected List<FileMetadata> unpack(Iterator<?> packets, OutputStream plaintext)
throws IOException, PGPException {
List<FileMetadata> meta = new ArrayList<FileMetadata>();
List<Verifier> verifiers = new ArrayList<Verifier>();

Expand All @@ -326,17 +367,14 @@ protected List<FileMetadata> unpack(Iterator<?> packets,
// in already initialized verifiers
else
matchSignatures(list.iterator(), verifiers);

} else if (packet instanceof PGPEncryptedDataList) {
PGPEncryptedDataList list = (PGPEncryptedDataList) packet;
// decrypt and unpack encrypted content
meta.addAll(unpack(parse(decrypt(list.iterator())), plaintext));

} else if (packet instanceof PGPCompressedData) {
InputStream i = ((PGPCompressedData) packet).getDataStream();
// unpack compressed content
meta.addAll(unpack(parse(i), plaintext));

} else if (packet instanceof PGPLiteralData) {
PGPLiteralData data = (PGPLiteralData) packet;
FileMetadata file = new FileMetadata(data);
Expand All @@ -345,7 +383,6 @@ protected List<FileMetadata> unpack(Iterator<?> packets,
// while also passing input bytes into verifiers
file.setLength(copy(i, plaintext, verifiers));
meta.add(file);

} else {
throw new PGPException("unexpected packet: " + packet.getClass());
}
Expand All @@ -362,7 +399,7 @@ protected List<FileMetadata> unpack(Iterator<?> packets,
* for which a verification key is available.
*/
protected List<Verifier> buildVerifiers(Iterator<?> signatures)
throws PGPException {
throws PGPException {
List<Verifier> verifiers = new ArrayList<Verifier>();
while (signatures.hasNext()) {
Verifier verifier = null;
Expand Down Expand Up @@ -517,10 +554,10 @@ protected void verify(List<Verifier> verifiers, List<FileMetadata> meta)

/**
* Wraps stream with ArmoredInputStream if necessary
* (to convert ascii-armored content back into binary data).
* (to convert ASCII-armored content back into binary data).
*/
protected InputStream unarmor(InputStream stream)
throws IOException, PGPException {
throws IOException, PGPException {
DetectionResult result = FileDetection.detectContainer(stream,
getMaxFileBufferSize());
switch (result.type) {
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);
}
}
Loading