diff --git a/CHANGELOG.md b/CHANGELOG.md index 34bb254..c7e14e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## V 2.x.x (NEXT) +* Enh: Bump Bouncy Castle to 1.77 and jdk18on for security fixes +* New: Provide a way to change name and modification date in literal data packet + ## V 2.3.0 Bugfix Release This releases fixes a security issue (#50) where encrypted, but not signed archives could be modified. diff --git a/build.gradle b/build.gradle index 6ad763b..e8107bf 100644 --- a/build.gradle +++ b/build.gradle @@ -123,8 +123,8 @@ check.dependsOn integrationTest dependencies { - compile 'org.bouncycastle:bcprov-jdk15on:1.67' - compile 'org.bouncycastle:bcpg-jdk15on:1.67' + compile 'org.bouncycastle:bcprov-jdk18on:1.77' + compile 'org.bouncycastle:bcpg-jdk18on:1.77' compile 'org.slf4j:slf4j-api:1.7.30' diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/BuildEncryptionOutputStreamAPI.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/BuildEncryptionOutputStreamAPI.java index ff41e12..b72f1d1 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/BuildEncryptionOutputStreamAPI.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/BuildEncryptionOutputStreamAPI.java @@ -52,6 +52,9 @@ public final class BuildEncryptionOutputStreamAPI { private Set recipients; private boolean armorOutput; + private NameProvider nameProvider; + private ModificationDateProvider modificationDateProvider; + // Signature @@ -95,6 +98,21 @@ private KeySelectionStrategy getKeySelectionStrategy() { public interface Build { + /** + * Set name a modification date providers which will be used to set name and modification date + * of literal data (literal packet in the output data). + *

+ * This allows acts like we are actually working with a file, not just unnamed, just-in-time stream of data. + *

+ * If not provided ({@code null}), default values will be used - empty string ({@code ""}) as a name + * anc current date ({@code new Date()}) as a modification date. + * + * @param nameProvider literal data name provider (nullable) + * @param modificationDateProvider literal data modification date provider (nullable) + * @return this + */ + Build withProviders(NameProvider nameProvider, ModificationDateProvider modificationDateProvider); + OutputStream andWriteTo(OutputStream sinkForEncryptedData) throws PGPException, SignatureException, NoSuchAlgorithmException, NoSuchProviderException, IOException; } @@ -502,6 +520,13 @@ public Build armorAsciiOutput() { public final class Builder implements Build { + @Override + public Build withProviders(NameProvider nameProvider, ModificationDateProvider modificationDateProvider) { + BuildEncryptionOutputStreamAPI.this.nameProvider = nameProvider; + BuildEncryptionOutputStreamAPI.this.modificationDateProvider = modificationDateProvider; + return this; + } + @Override public OutputStream andWriteTo(OutputStream sinkForEncryptedData) throws PGPException, SignatureException, NoSuchAlgorithmException, NoSuchProviderException, IOException { @@ -513,7 +538,9 @@ public OutputStream andWriteTo(OutputStream sinkForEncryptedData) BuildEncryptionOutputStreamAPI.this.sinkForEncryptedData, getKeySelectionStrategy(), BuildEncryptionOutputStreamAPI.this.armorOutput, - BuildEncryptionOutputStreamAPI.this.recipients); + BuildEncryptionOutputStreamAPI.this.recipients, + BuildEncryptionOutputStreamAPI.this.nameProvider, + BuildEncryptionOutputStreamAPI.this.modificationDateProvider); } } diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/ModificationDateProvider.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/ModificationDateProvider.java new file mode 100644 index 0000000..5e9326b --- /dev/null +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/ModificationDateProvider.java @@ -0,0 +1,24 @@ +package name.neuhalfen.projects.crypto.bouncycastle.openpgp; + +import java.io.OutputStream; +import java.util.Date; + +/** + * Modification date provider. + * + * @see org.bouncycastle.openpgp.PGPLiteralDataGenerator#open(OutputStream, char, String, Date, byte[]) + */ +public interface ModificationDateProvider { + + /** + * Default instance which provides modification date as a current date. + */ + ModificationDateProvider DEFAULT_INSTANCE = Date::new; + + /** + * Provide desired modification date. + * + * @return modification date (never {@code null}) + */ + Date getModificationDate(); +} diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/NameProvider.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/NameProvider.java new file mode 100644 index 0000000..db94d44 --- /dev/null +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/NameProvider.java @@ -0,0 +1,26 @@ +package name.neuhalfen.projects.crypto.bouncycastle.openpgp; + +import java.io.OutputStream; +import java.util.Date; + +/** + * Name provider. + * + * @see org.bouncycastle.openpgp.PGPLiteralDataGenerator#open(OutputStream, char, String, Date, byte[]) + */ +public interface NameProvider { + + /** + * Default instance which provides an empty string ({@code ""}) as a name. + */ + NameProvider DEFAULT_INSTANCE = () -> ""; + + /** + * Provide desired name. + * + * @return name (never never {@code null}) + */ + String getName(); +} + + diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java index 8ddc794..14e3413 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStream.java @@ -7,11 +7,13 @@ import java.io.OutputStream; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; -import java.util.Date; import java.util.Iterator; +import java.util.Optional; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import name.neuhalfen.projects.crypto.bouncycastle.openpgp.ModificationDateProvider; +import name.neuhalfen.projects.crypto.bouncycastle.openpgp.NameProvider; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PGPAlgorithmSuite; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.PGPUtilities; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy; @@ -95,6 +97,40 @@ public static OutputStream create(final KeyringConfig config, final boolean armor, final Set encryptTo) throws IOException, PGPException, NoSuchAlgorithmException, NoSuchProviderException { + return create(config, algorithmSuite, signingUid, cipherTextSink, keySelectionStrategy, armor, encryptTo, null, null); + } + + /** + * Return a stream that, when written plaintext into, writes the ciphertext into cipherTextSink. + * + * @param config key configuration + * @param algorithmSuite algorithm suite to use. + * @param signingUid sign with this uid (optionally) + * @param cipherTextSink write the ciphertext in here + * @param keySelectionStrategy selection strategy + * @param armor armor the file (true) or use binary. + * @param encryptTo encrypt to + * @param nameProvider a name provider for literal data packet (default to {@code ""}) + * @param modificationDateProvider a modification date for literal data packet (default to current date) + * + * @return stream where plaintext gets written into + * + * @throws IOException streams, IO, ... + * @throws PGPException pgp error + * @throws NoSuchAlgorithmException algorithmSuite not supported + * @throws NoSuchProviderException bouncy castle not registered + * @see name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.DefaultPGPAlgorithmSuites + */ + public static OutputStream create(final KeyringConfig config, + final PGPAlgorithmSuite algorithmSuite, + @Nullable final String signingUid, + final OutputStream cipherTextSink, + final KeySelectionStrategy keySelectionStrategy, + final boolean armor, + final Set encryptTo, + final NameProvider nameProvider, + final ModificationDateProvider modificationDateProvider) + throws IOException, PGPException, NoSuchAlgorithmException, NoSuchProviderException { requireNonNull(config, "callback must not be null"); requireNonNull(cipherTextSink, "cipherTextSink must not be null"); @@ -109,7 +145,7 @@ public static OutputStream create(final KeyringConfig config, } final PGPEncryptingStream encryptingStream = new PGPEncryptingStream(config, algorithmSuite); - encryptingStream.setup(cipherTextSink, signingUid, encryptTo, keySelectionStrategy, armor); + encryptingStream.setup(cipherTextSink, signingUid, encryptTo, keySelectionStrategy, armor, nameProvider, modificationDateProvider); return encryptingStream; } @@ -134,7 +170,9 @@ private void setup(final OutputStream cipherTextSink, @Nullable final String signingUid, final Set pubEncKeys, final KeySelectionStrategy keySelectionStrategy, - final boolean armor) throws + final boolean armor, + final NameProvider nameProvider, + final ModificationDateProvider modificationDateProvider) throws IOException, PGPException { isDoSign = signingUid != null; @@ -192,13 +230,13 @@ private void setup(final OutputStream cipherTextSink, if (userIDs.hasNext()) { final PGPSignatureSubpacketGenerator spGen = new PGPSignatureSubpacketGenerator(); - spGen.setSignerUserID(false, (String) userIDs.next()); + spGen.addSignerUserID(false, (String) userIDs.next()); signatureGenerator.setHashedSubpackets(spGen.generate()); } } compressionStreamGenerator = new PGPCompressedDataGenerator( - algorithmSuite.getCompressionEncryptionAlgorithmCode().getAlgorithmId()); + algorithmSuite.getCompressionAlgorithmCode().getAlgorithmId()); compressionStream = new BCPGOutputStream( compressionStreamGenerator.open(outerEncryptionStream)); @@ -208,7 +246,11 @@ private void setup(final OutputStream cipherTextSink, encryptionDataStreamGenerator = new PGPLiteralDataGenerator(); encryptionDataStream = encryptionDataStreamGenerator - .open(compressionStream, PGPLiteralData.BINARY, "", new Date(), new byte[1 << 16]); + .open(compressionStream, + PGPLiteralData.BINARY, + Optional.ofNullable(nameProvider).orElse(NameProvider.DEFAULT_INSTANCE).getName(), + Optional.ofNullable(modificationDateProvider).orElse(ModificationDateProvider.DEFAULT_INSTANCE).getModificationDate(), + new byte[1 << 16]); } @Override diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/Rfc4880KeySelectionStrategy.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/Rfc4880KeySelectionStrategy.java index e2e7613..1b1c679 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/Rfc4880KeySelectionStrategy.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/Rfc4880KeySelectionStrategy.java @@ -182,21 +182,14 @@ protected Predicate hasPrivateKey(final PGPSecretKeyRingCollection return pubKey -> { requireNonNull(pubKey, "pubKey must not be null"); - try { - final boolean hasPrivateKey = secretKeyRings.contains(pubKey.getKeyID()); - - if (!hasPrivateKey) { - LOGGER.trace("Skipping pubkey {} (no private key found)", - Long.toHexString(pubKey.getKeyID())); - } - - return hasPrivateKey; - } catch (PGPException e) { - // ignore this for filtering - LOGGER.debug("Failed to test for private key for pubkey " + pubKey//NOPMD:GuardLogStatement - .getKeyID()); - return false; + final boolean hasPrivateKey = secretKeyRings.contains(pubKey.getKeyID()); + + if (!hasPrivateKey) { + LOGGER.trace("Skipping pubkey {} (no private key found)", + Long.toHexString(pubKey.getKeyID())); } + + return hasPrivateKey; }; } diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStreamTest.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStreamTest.java index 82aa746..877895c 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStreamTest.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/PGPEncryptingStreamTest.java @@ -4,7 +4,6 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.charset.StandardCharsets; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.SignatureException;