diff --git a/.gitignore b/.gitignore
index 5438f006..1e246039 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@ build/
out/
gradle.properties
*.iml
+bin/
diff --git a/.travis.yml b/.travis.yml
index 6126fd7e..14fac53f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -11,9 +11,10 @@ cache:
jdk:
- openjdk8
- oraclejdk8
-# - openjdk10
- - openjdk11
- - oraclejdk11
+ - openjdk13
+ - oraclejdk13
+ - openjdk14
+ - oraclejdk14
script:
- sudo apt-get update
@@ -24,7 +25,7 @@ script:
- # sudo snap install hugo
- # export PATH
- # /snap/bin/hugo version
- - ./gradlew -PHUGO_EXEC="/snap/bin/hugo" --info --stacktrace check jacocoTestReport # generateWebsite
+ - ./gradlew -PHUGO_EXEC="/snap/bin/hugo" -info --stacktrace clean check test integrationTest jacocoTestReport
after_success:
- bash <(curl -s https://codecov.io/bash)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5b3bc77..34bb254f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,18 @@
## V 2.x.x (NEXT)
+## V 2.3.0 Bugfix Release
+
+This releases fixes a security issue (#50) where encrypted, but not signed archives could be modified.
+Some background on MDC and why it's important security-wise: https://gpgtools.tenderapp.com/kb/faq/modification-detection-code-mdc-errors
+
+* Fix: Do not expose logback as compile-time dependency (#41)
+* Fix: java.io.EOFException: Unexpected end of ZIP input stream using 2.2.0 version for PGP file (#46)
+* Fix: KeyFlag#extractPublicKeyFlags throws NullPointerException if called on an older public key with no hashed subpackets (#48)
+* Fix: Encrypting with keys that don't have a KeyFlags subpacket (#50)
+* Fix: MDC (integrity checksum) is not verified when decrypting (#45)
+* Enh: Bump Bouncy Castle to 1.67
+
+
## V 2.2.0 Key generation
* new: Add key generation (initial version by Paul Schaub [@vanitasvitae])
diff --git a/README.md b/README.md
index 4ccd591b..96e9454e 100644
--- a/README.md
+++ b/README.md
@@ -202,8 +202,8 @@ repositories {
// ...
dependencies {
- compile 'org.bouncycastle:bcprov-jdk15on:1.64'
- compile 'org.bouncycastle:bcpg-jdk15on:1.64'
+ compile 'org.bouncycastle:bcprov-jdk15on:1.67'
+ compile 'org.bouncycastle:bcpg-jdk15on:1.67'
// ...
compile 'name.neuhalfen.projects.crypto.bouncycastle.openpgp:bouncy-gpg:2.+'
// ...
@@ -215,10 +215,10 @@ dependencies {
* Decryption will enforce that the ciphertext has been signed by ALL of the public key ids * passed. - * + *
+ ** Given the following keyring: - * + *
*{@code * $ gpg -k --keyid-format=0xlong * diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/DefaultPGPAlgorithmSuites.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/DefaultPGPAlgorithmSuites.java index ad9a9e67..48a850af 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/DefaultPGPAlgorithmSuites.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/DefaultPGPAlgorithmSuites.java @@ -6,14 +6,15 @@ public final class DefaultPGPAlgorithmSuites { /** * GPG default algorithms. */ - private final static PGPAlgorithmSuite DEFAULT_GPG = new PGPAlgorithmSuite( + private static final PGPAlgorithmSuite DEFAULT_GPG = new PGPAlgorithmSuite( PGPHashAlgorithms.SHA1, PGPSymmetricEncryptionAlgorithms.CAST5, PGPCompressionAlgorithms.ZLIB); + /** * GPG strong crypto algorithms. */ - private final static PGPAlgorithmSuite STRONG_GPG = new PGPAlgorithmSuite( + private static final PGPAlgorithmSuite STRONG_GPG = new PGPAlgorithmSuite( PGPHashAlgorithms.SHA_256, PGPSymmetricEncryptionAlgorithms.AES_128, PGPCompressionAlgorithms.ZLIB); @@ -21,7 +22,7 @@ public final class DefaultPGPAlgorithmSuites { /** * Algorithm suite for XEP-0373: OpenPGP for XMPP. */ - private final static PGPAlgorithmSuite DEFAULT_OX = new PGPAlgorithmSuite( + private static final PGPAlgorithmSuite DEFAULT_OX = new PGPAlgorithmSuite( PGPHashAlgorithms.SHA_256, PGPSymmetricEncryptionAlgorithms.AES_128, PGPCompressionAlgorithms.UNCOMPRESSED); @@ -48,6 +49,9 @@ public static PGPAlgorithmSuite strongSuite() { return STRONG_GPG; } + /** + * Algorithm suite for XEP-0373: OpenPGP for XMPP. + */ public static PGPAlgorithmSuite oxSuite() { return DEFAULT_OX; } diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/Feature.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/Feature.java index be614d95..3ce0c2a7 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/Feature.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/Feature.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms; import java.util.HashMap; diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPCompressionAlgorithms.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPCompressionAlgorithms.java index 7d1b2371..d146a415 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPCompressionAlgorithms.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPCompressionAlgorithms.java @@ -30,9 +30,9 @@ public enum PGPCompressionAlgorithms { */ BZIP2(CompressionAlgorithmTags.BZIP2); - private final static SetRECOMMENDED_ALGORITHMS = SetUtils + private static final Set RECOMMENDED_ALGORITHMS = SetUtils .unmodifiableSet(BZIP2, ZLIB, ZIP, UNCOMPRESSED); - private final static int[] RECOMMENDED_ALGORITHM_IDS = + private static final int[] RECOMMENDED_ALGORITHM_IDS = RECOMMENDED_ALGORITHMS.stream().mapToInt(algorithm -> algorithm.algorithmId).toArray(); private final int algorithmId; diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPHashAlgorithms.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPHashAlgorithms.java index 5fcc8e3f..ec18a422 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPHashAlgorithms.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPHashAlgorithms.java @@ -58,12 +58,12 @@ public enum PGPHashAlgorithms { */ HAVAL_5_160(HashAlgorithmTags.HAVAL_5_160, true,true); - private final static Set RECOMMENDED_ALGORITHMS = Collections + private static final Set RECOMMENDED_ALGORITHMS = Collections .unmodifiableSet( Arrays.stream( - PGPHashAlgorithms.values()).filter(alg -> !alg.insecure && alg.supportedInGPG ) + PGPHashAlgorithms.values()).filter(alg -> !alg.insecure && alg.supportedInGPG) .collect(Collectors.toSet())); - private final static int[] RECOMMENDED_ALGORITHM_IDS = + private static final int[] RECOMMENDED_ALGORITHM_IDS = RECOMMENDED_ALGORITHMS.stream().mapToInt(algorithm -> algorithm.algorithmId).toArray(); private final int algorithmId; private final boolean insecure; diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPSymmetricEncryptionAlgorithms.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPSymmetricEncryptionAlgorithms.java index be5126a0..dcf2cdc3 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPSymmetricEncryptionAlgorithms.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PGPSymmetricEncryptionAlgorithms.java @@ -40,7 +40,7 @@ public enum PGPSymmetricEncryptionAlgorithms { BLOWFISH(SymmetricKeyAlgorithmTags.BLOWFISH, true), /** - * SAFER-SK128 (13 rounds) [SAFER] Insecure: 64 bit blocksize. + * SAFER-SK128 (13 rounds) [SAFER] [INSECURE]: 64 bit blocksize. */ SAFER(SymmetricKeyAlgorithmTags.SAFER, true), @@ -85,13 +85,13 @@ public enum PGPSymmetricEncryptionAlgorithms { CAMELLIA_256(SymmetricKeyAlgorithmTags.CAMELLIA_256, false); - private final static Set
RECOMMENDED_ALGORITHMS = Collections + private static final Set RECOMMENDED_ALGORITHMS = Collections .unmodifiableSet( Arrays.stream( PGPSymmetricEncryptionAlgorithms.values()) .filter(alg -> !alg.insecure) .collect(Collectors.toSet())); - private final static int[] RECOMMENDED_ALGORITHM_IDS = + private static final int[] RECOMMENDED_ALGORITHM_IDS = RECOMMENDED_ALGORITHMS.stream().mapToInt(algorithm -> algorithm.algorithmId).toArray(); private final int algorithmId; private final boolean insecure; diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PublicKeyAlgorithm.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PublicKeyAlgorithm.java index fa63c1f5..94e14e39 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PublicKeyAlgorithm.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/algorithms/PublicKeyAlgorithm.java @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms; import java.util.HashMap; diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactory.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactory.java index f67dc8ed..161cd938 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactory.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactory.java @@ -40,6 +40,7 @@ public final class DecryptionStreamFactory { private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory .getLogger(DecryptionStreamFactory.class); + private PGPPublicKeyEncryptedData pbe; @Nonnull private final PGPContentVerifierBuilderProvider pgpContentVerifierBuilderProvider = @@ -116,6 +117,7 @@ public InputStream wrapWithDecryptAndVerify(InputStream inputStream) * @throws PGPException the pGP exception * @throws IOException Signals that an I/O exception has occurred. */ + @SuppressWarnings({"PMD.ExcessiveMethodLength", "PMD.OnlyOneReturn", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.CyclomaticComplexity"}) private InputStream nextDecryptedStream(PGPObjectFactory factory, @@ -124,7 +126,6 @@ private InputStream nextDecryptedStream(PGPObjectFactory factory, Object pgpObj; - // while ((pgpObj = factory.nextObject()) != null) { //NOPMD if (pgpObj instanceof PGPEncryptedDataList) { @@ -139,7 +140,7 @@ private InputStream nextDecryptedStream(PGPObjectFactory factory, // find the secret key // PGPPrivateKey privateKey = null; - PGPPublicKeyEncryptedData pbe = null; // NOPMD: must initialize pbe + while (privateKey == null && encryptedDataObjects.hasNext()) { pbe = (PGPPublicKeyEncryptedData) encryptedDataObjects.next(); privateKey = PGPUtilities.findSecretKey(config.getSecretKeyRings(), pbe.getKeyID(), @@ -153,18 +154,14 @@ private InputStream nextDecryptedStream(PGPObjectFactory factory, + " used to encrypt the file, aborting"); } - // decrypt the data - try( - InputStream plainText = pbe - .getDataStream(new BcPublicKeyDataDecryptorFactory( - privateKey)) // NOPMD: AvoidInstantiatingObjectsInLoops - ) - { - final PGPObjectFactory nextFactory = new PGPObjectFactory(plainText, - new BcKeyFingerprintCalculator());// NOPMD: AvoidInstantiatingObjectsInLoops - return nextDecryptedStream(nextFactory, state); // NOPMD: OnlyOneReturn - } + + // decrypt the data + final InputStream plainText = pbe // NOPMD: CloseResource + .getDataStream(new BcPublicKeyDataDecryptorFactory(privateKey)); // NOPMD: AvoidInstantiatingObjectsInLoops + final PGPObjectFactory nextFactory = new PGPObjectFactory(plainText, + new BcKeyFingerprintCalculator());// NOPMD: AvoidInstantiatingObjectsInLoops + return nextDecryptedStream(nextFactory, state); // NOPMD: OnlyOneReturn } else if (pgpObj instanceof PGPCompressedData) { LOGGER.trace("Found instance of PGPCompressedData"); final PGPObjectFactory nextFactory = new PGPObjectFactory( @@ -215,12 +212,12 @@ private InputStream nextDecryptedStream(PGPObjectFactory factory, if (!state.hasVerifiableSignatures()) { throw new PGPException("Signature checking is required but message was not signed!"); } - return new SignatureValidatingInputStream( + return new MDCValidatingInputStream(new SignatureValidatingInputStream( literalDataInputStream, - state, signatureValidationStrategy); // NOPMD: OnlyOneReturn + state, signatureValidationStrategy), pbe); // NOPMD: OnlyOneReturn } else { - return literalDataInputStream; // NOPMD: OnlyOneReturn + return new MDCValidatingInputStream(literalDataInputStream, pbe); // NOPMD: OnlyOneReturn } } else { // keep on searching... if (LOGGER.isTraceEnabled()) { @@ -230,4 +227,4 @@ private InputStream nextDecryptedStream(PGPObjectFactory factory, } throw new PGPException("No data found"); } -} \ No newline at end of file +} diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/MDCValidatingInputStream.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/MDCValidatingInputStream.java new file mode 100644 index 00000000..0158329d --- /dev/null +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/MDCValidatingInputStream.java @@ -0,0 +1,119 @@ +package name.neuhalfen.projects.crypto.bouncycastle.openpgp.decrypting; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import javax.annotation.Nonnull; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; + +@SuppressWarnings("PMD.ShortVariable") +final class MDCValidatingInputStream extends FilterInputStream { + + private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory + .getLogger(MDCValidatingInputStream.class); + + /** + * Creates a MDCValidatingInputStream
by assigning the argument + *inputStream
to the fieldthis.inputStream
andpbe
tothis.pbe
so as to remember it for + * later use. + * + * @param inputStream the underlying input stream + * @param pbe the pgp public key encrypted data to verify message integrity + */ + + private final PGPPublicKeyEncryptedData pbe; + + MDCValidatingInputStream(InputStream inputStream, PGPPublicKeyEncryptedData pbe) { + super(inputStream); + this.pbe = pbe; + } + + @Override + public int read() throws IOException { + final int data = super.read(); + final boolean endOfStream = data == -1; + if (endOfStream) { + validateMDC(); + } + return data; + } + + @Override + public int read(@Nonnull byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(@Nonnull byte[] b, int off, int len) throws IOException { + final int read = super.read(b, off, len); + + final boolean endOfStream = read == -1; + if (endOfStream) { + validateMDC(); + } + return read; + } + + /** + * Checks MDC if present. + * + * @throws IOException Error while reading input stream or if MDC fails + */ + private void validateMDC() throws IOException { + try { + if (pbe.isIntegrityProtected()) { + if (!pbe.verify()) { + throw new PGPException("Data is integrity protected but integrity check failed"); + } + } else { + LOGGER.trace("Data integrity is not checked"); + } + } catch (PGPException ex) { + throw new IOException("Error while validating MDC", ex); + } + + } + + // NOTE: We cannot simply delegate to super.skip, since we need to ensure our own read +// impl, which updates the one-pass signatures, is used to read the bytes being +// skipped. + @Override + public long skip(long n) throws IOException { + if (n <= 0) { + return 0; + } + + // buffer to be reused repeatedly + final byte[] buffer = new byte[(int) Math.min(4096, n)]; + + long remaining = n; + while (remaining > 0) { + final int read = read(buffer, 0, (int) Math.min(buffer.length, remaining)); + final boolean endOfStream = read == -1; + if (endOfStream) { + break; + } + remaining -= read; + } + + return n - remaining; + } + + @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel") + @Override + public synchronized void mark(int readlimit) { + throw new UnsupportedOperationException("mark not supported"); + } + + @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel") + @Override + public synchronized void reset() throws IOException { + throw new UnsupportedOperationException("reset not supported"); + } + + @Override + public boolean markSupported() { + return false; + } +} diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/SignatureValidatingInputStream.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/SignatureValidatingInputStream.java index 3b0a8a91..d7912d15 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/SignatureValidatingInputStream.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/SignatureValidatingInputStream.java @@ -85,9 +85,29 @@ private void validateSignature() throws IOException { } + // NOTE: We cannot simply delegate to super.skip, since we need to ensure our own read +// impl, which updates the one-pass signatures, is used to read the bytes being +// skipped. @Override public long skip(long n) throws IOException { - throw new UnsupportedOperationException("Skipping not supported"); + if (n <= 0) { + return 0; + } + + // buffer to be reused repeatedly + final byte[] buffer = new byte[(int) Math.min(4096, n)]; + + long remaining = n; + while (remaining > 0) { + final int read = read(buffer, 0, (int) Math.min(buffer.length, remaining)); + final boolean endOfStream = read == -1; + if (endOfStream) { + break; + } + remaining -= read; + } + + return n - remaining; } @SuppressWarnings("PMD.AvoidSynchronizedAtMethodLevel") 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 6df15e1a..8ddc794e 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 @@ -115,6 +115,8 @@ public static OutputStream create(final KeyringConfig config, /** + * Internal method to set up the stream. + * * @param cipherTextSink Where the ciphertext goes * @param signingUid Sign with this uid. null: do not sign * @param pubEncKeys the pub enc keys diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/PGPUtilities.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/PGPUtilities.java index 5359b764..29d38fb9 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/PGPUtilities.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/PGPUtilities.java @@ -19,7 +19,7 @@ /** - * FIXME: Cleanup code, throw out duplicates etc + * FIXME: Cleanup code, throw out duplicates etc. */ @SuppressWarnings({"PMD.ClassNamingConventions"}) public final class PGPUtilities { diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/ByEMailKeySelectionStrategy.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/ByEMailKeySelectionStrategy.java index f5b3986f..ec6bde8b 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/ByEMailKeySelectionStrategy.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/ByEMailKeySelectionStrategy.java @@ -10,11 +10,11 @@ import org.bouncycastle.openpgp.PGPPublicKeyRing; /** - * This implements the key selection strategy for BouncyGPG and selects keys based on - * email addresses. - * + *This implements the key selection strategy for BouncyGPG and selects keys based on + * email addresses.
+ ** For this it wraps the given addresses in </>. - * + *
* https://tools.ietf.org/html/rfc4880#section-5.2.3.21 */ public class ByEMailKeySelectionStrategy extends Rfc4880KeySelectionStrategy implements diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeySelectionStrategy.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeySelectionStrategy.java index 649c0b9a..6cb2cedb 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeySelectionStrategy.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeySelectionStrategy.java @@ -10,9 +10,12 @@ public interface KeySelectionStrategy { /** + ** Extract a signing/encryption key from the keyrings. - * + *
+ ** The implementation should try to find the best matching key. + *
* * @param purpose Return a singing or encryption key? * @param uid The recipient ("FOR_ENCRYPTION") or the sender ("FOR_SIGNING") diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeyringConfigCallbacks.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeyringConfigCallbacks.java index 125673d0..8c4de7d8 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeyringConfigCallbacks.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/callbacks/KeyringConfigCallbacks.java @@ -1,13 +1,12 @@ package name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks; - import static java.util.Objects.requireNonNull; import java.util.Map; /** - * Factory for convenience implementations of KeyringConfigCallback. . {@link - * KeyringConfigCallback} + * Factory for convenience implementations of KeyringConfigCallback. + * {@link KeyringConfigCallback} */ @SuppressWarnings({"PMD.ClassNamingConventions"}) public final class KeyringConfigCallbacks { @@ -16,21 +15,40 @@ public final class KeyringConfigCallbacks { private KeyringConfigCallbacks() { } + /** + * Use the passed passphrase to decrypt every key encountered. + * @param passphrase passphrase to use + * @return a prebuild instance + */ @SuppressWarnings("PMD.UseVarargs") public static KeyringConfigCallback withPassword(char[] passphrase) { return new StaticPasswordKeyringConfigCallback(passphrase); } - + /** + * Use the passed passphrase to decrypt every key encountered. + * @param passphrase passphrase to use + * @return a prebuild instance + */ public static KeyringConfigCallback withPassword(String passphrase) { requireNonNull(passphrase, "passphrase must not be null"); return withPassword(passphrase.toCharArray()); } + /** + * Assume that all keys are unprotected. + * + * @return a prebuild instance + */ public static KeyringConfigCallback withUnprotectedKeys() { return new UnprotectedKeysKeyringConfigCallback(); } + /** + * Use the passed map that contains the passphrase per keyID. + * + * @return a prebuild instance + */ public static KeyringConfigCallback withPasswordsFromMap( MapmapSourceKeyIdToPassphrase) { requireNonNull(mapSourceKeyIdToPassphrase, "mapSourceKeyIdToPassphrase must not be null"); 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 8e246b50..e2e7613c 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 @@ -5,23 +5,22 @@ import java.io.IOException; import java.time.Instant; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import javax.annotation.Nullable; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.generation.KeyFlag; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfig; +import org.bouncycastle.bcpg.PublicKeyAlgorithmTags; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; /** - * This implements the key selection strategy for BouncyGPG . - * This strategy is tries to implement rfc4880 section-5.2.3.21. + * This implements the key selection strategy for BouncyGPG. + * * This strategy is tries to implement rfc4880 section-5.2.3.21. * https://tools.ietf.org/html/rfc4880#section-5.2.3.21 */ public class Rfc4880KeySelectionStrategy implements KeySelectionStrategy { @@ -33,6 +32,22 @@ public class Rfc4880KeySelectionStrategy implements KeySelectionStrategy { private final boolean ignoreCase; private final boolean matchPartial; + // list of algorithms that can be used for encryption + private final List encryptionAlgorithms = Arrays.asList( + PublicKeyAlgorithmTags.RSA_GENERAL, + PublicKeyAlgorithmTags.RSA_ENCRYPT, + PublicKeyAlgorithmTags.ECDH, + PublicKeyAlgorithmTags.ELGAMAL_ENCRYPT, + PublicKeyAlgorithmTags.ELGAMAL_GENERAL); + + // list of algorithms that can be used for signing + private final List signatureAlgorithms = Arrays.asList( + PublicKeyAlgorithmTags.RSA_GENERAL, + PublicKeyAlgorithmTags.RSA_SIGN, + PublicKeyAlgorithmTags.DSA, + PublicKeyAlgorithmTags.ECDSA, + PublicKeyAlgorithmTags.EDDSA); + /** * Construct an instance with matchPartial and ignoreCase set to true. * @@ -221,39 +236,72 @@ protected boolean isExpired(PGPPublicKey pubKey) { return isExpired; } - + /** + * Checks if a public key may be used for encryption. This uses the key KeyFlags subpacket content by default, + * falling back to the key algorithm if there isn't any KeyFlags subpacket + * @param publicKey public key to examine + * @return true if the key can be used for encryption + */ protected boolean isEncryptionKey(PGPPublicKey publicKey) { requireNonNull(publicKey, "publicKey must not be null"); + boolean isEncryptionKey = false; + + final Optional > optionalKeyFlags = extractPublicKeyFlags(publicKey); + + /* If the key contains a KeyFlag subpacket, we extract its flags to determine if the + key can be used for encryption + */ + if (optionalKeyFlags.isPresent()) { // NOPMD:LawOfDemeter + final Set keyFlags = optionalKeyFlags.get(); + final boolean canEncryptCommunication = keyFlags // NOPMD:LawOfDemeter + .contains(KeyFlag.ENCRYPT_COMMS); + final boolean canEncryptStorage = keyFlags // NOPMD:LawOfDemeter + .contains(KeyFlag.ENCRYPT_STORAGE); + isEncryptionKey = canEncryptCommunication || canEncryptStorage; + } else { + /* If the key doesn't contain any KeyFlag subpacket, check the key algorithm. + This is what GPG does (g10/misc.c) and lets us encrypt with keys that don't contain a KeyFlag subpacket + */ + isEncryptionKey = encryptionAlgorithms.contains(publicKey.getAlgorithm()); + } - final Set keyFlags = extractPublicKeyFlags(publicKey); + return isEncryptionKey; + } - final boolean canEncryptCommunication = keyFlags // NOPMD:LawOfDemeter - .contains(KeyFlag.ENCRYPT_COMMS); - final boolean canEncryptStorage = keyFlags // NOPMD:LawOfDemeter - .contains(KeyFlag.ENCRYPT_STORAGE); + protected boolean isVerificationKey(PGPPublicKey publicKey) { + requireNonNull(publicKey, "publicKey must not be null"); - return canEncryptCommunication || canEncryptStorage; - } + final Optional > optionalKeyFlags = extractPublicKeyFlags(publicKey); + boolean isVerificationKey; - protected boolean isVerificationKey(PGPPublicKey pubKey) { - final boolean isVerficationKey = - extractPublicKeyFlags(pubKey).contains(KeyFlag.SIGN_DATA); // NOPMD:LawOfDemeter + /* If the key contains a KeyFlag subpacket, we extract its flags to determine if the + key can be used for signing + */ + if (optionalKeyFlags.isPresent()) { // NOPMD:LawOfDemeter + isVerificationKey = optionalKeyFlags.get().contains(KeyFlag.SIGN_DATA); // NOPMD:LawOfDemeter + } else { + /* If the key doesn't contain any KeyFlag subpacket, check the key algorithm. + This is what GPG does (g10/misc.c) and lets us signing with keys that don't contain a KeyFlag subpacket + */ + isVerificationKey =signatureAlgorithms.contains(publicKey.getAlgorithm()); + } - if (!isVerficationKey) { + if (!isVerificationKey) { LOGGER.trace("Skipping pubkey {} (no signing key)", - Long.toHexString(pubKey.getKeyID())); + Long.toHexString(publicKey.getKeyID())); } - return isVerficationKey; + + return isVerificationKey; } - protected boolean isRevoked(PGPPublicKey pubKey) { - requireNonNull(pubKey, "pubKey must not be null"); + protected boolean isRevoked(PGPPublicKey publicKey) { + requireNonNull(publicKey, "pubKey must not be null"); - final boolean hasRevocation = pubKey.hasRevocation(); + final boolean hasRevocation = publicKey.hasRevocation(); if (hasRevocation) { LOGGER.trace("Skipping pubkey {} (revoked)", - Long.toHexString(pubKey.getKeyID())); + Long.toHexString(publicKey.getKeyID())); } return hasRevocation; } diff --git a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/generation/KeyFlag.java b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/generation/KeyFlag.java index b2eae0c8..b88f3a94 100644 --- a/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/generation/KeyFlag.java +++ b/src/main/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/keys/generation/KeyFlag.java @@ -17,9 +17,13 @@ import static java.util.Objects.requireNonNull; +import java.util.Collections; import java.util.EnumSet; import java.util.Iterator; +import java.util.Optional; import java.util.Set; + +import org.bouncycastle.bcpg.SignatureSubpacketTags; import org.bouncycastle.bcpg.sig.KeyFlags; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSignature; @@ -72,7 +76,12 @@ public enum KeyFlag { this.flag = flag; } + @SuppressWarnings("PMD.OnlyOneReturn") public static Set fromInteger(int bitmask) { + if (bitmask == 0) { + return Collections.emptySet(); + } + final Set flags = EnumSet.noneOf(KeyFlag.class); int identifiedFlags = 0; @@ -88,14 +97,22 @@ public static Set fromInteger(int bitmask) { throw new IllegalArgumentException( "Could not identify the following KeyFlags: 0b" + Long.toBinaryString(unknownFlags)); } - return flags; + return Collections.unmodifiableSet(flags); } + /** + * Returns the list of key flags (ie whether the key can be used to encrypt, sign, etc) based on the analysis of the + * KeyFlags subpacket. Returns an empty Optional object if the key does not contain a KeyFlags subpacket + * @param publicKey the key to analyse + * @return a list of key flags, or an empty Optional if the key doesn't contain a KeyFlags subpacket + */ @SuppressWarnings({"PMD.LawOfDemeter"}) - public static Set extractPublicKeyFlags(PGPPublicKey publicKey) { + public static Optional > extractPublicKeyFlags(PGPPublicKey publicKey) { requireNonNull(publicKey, "publicKey must not be null"); int aggregatedKeyFlags = 0; + boolean hasKeyFlags = false; + Optional > publicKeyFlags; final Iterator directKeySignatures = publicKey.getSignatures(); @@ -103,10 +120,21 @@ public static Set extractPublicKeyFlags(PGPPublicKey publicKey) { final PGPSignature signature = directKeySignatures.next(); final PGPSignatureSubpacketVector hashedSubPackets = signature.getHashedSubPackets(); - final int keyFlags = hashedSubPackets.getKeyFlags(); - aggregatedKeyFlags |= keyFlags; + if (hashedSubPackets != null && hashedSubPackets.hasSubpacket(SignatureSubpacketTags.KEY_FLAGS)) { + hasKeyFlags = true; + // hashedSubPackets is null for PGP v3 and earlier. + final int keyFlags = hashedSubPackets.getKeyFlags(); + aggregatedKeyFlags |= keyFlags; + } } - return fromInteger(aggregatedKeyFlags); + + if (hasKeyFlags) { + publicKeyFlags = Optional.of(fromInteger(aggregatedKeyFlags)); + } else { + publicKeyFlags = Optional.empty(); + } + + return publicKeyFlags; } public int getFlag() { diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactoryTest.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactoryTest.java index d2f4812a..262e9abe 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactoryTest.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/decrypting/DecryptionStreamFactoryTest.java @@ -331,4 +331,21 @@ public void decryptingSignedMessageWithMultipleSignaturesUnknownSignatureFirst_a Assert.assertThat(decryptedQuote, equalTo(IMPORTANT_QUOTE_TEXT)); } -} \ No newline at end of file + @Test(expected = IOException.class) + public void decryptingTamperedUnSignedCiphertextWithMDC_fails() + throws IOException, NoSuchAlgorithmException, NoSuchProviderException { + + final KeyringConfig config = Configs.keyringConfigFromFilesForRecipient(); + + byte[] buf = IMPORTANT_QUOTE_NOT_SIGNED_NOT_COMPRESSED.getBytes("US-ASCII"); + + // Tampered MDC bit to cause Verification failure, + // Figured out using trial and error (change in random bits can cause protocol failuree during decryption) + buf[595]++; + + decrypt( + buf, config, + SignatureValidationStrategies.ignoreSignatures()); + } + +} diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptWithOpenPGPTestDriverTest.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptWithOpenPGPTestDriverTest.java index 98799e1b..8ddb40d5 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptWithOpenPGPTestDriverTest.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptWithOpenPGPTestDriverTest.java @@ -16,12 +16,12 @@ import java.security.SignatureException; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.BouncyGPG; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.DefaultPGPAlgorithmSuites; +import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PGPHashAlgorithms; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfig; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.Configs; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.DevNullOutputStream; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.RandomDataInputStream; -import org.bouncycastle.crypto.tls.HashAlgorithm; import org.bouncycastle.openpgp.PGPException; import org.junit.Before; import org.junit.Ignore; @@ -130,7 +130,7 @@ public void encryption_toSignOnlyKey_throws() EncryptionConfig encryptAndSignConfig = new EncryptionConfig( "sender@example.com", "sender.signonly@example.com", - HashAlgorithm.sha1, + PGPHashAlgorithms.SHA1, keyringConfig); EncryptWithOpenPGPTestDriver sut = new EncryptWithOpenPGPTestDriver(encryptAndSignConfig, DefaultPGPAlgorithmSuites.defaultSuiteForGnuPG()); @@ -171,7 +171,7 @@ public void encryptionRSAAndSigningWithDSA_smallAmountsOfData_doesNotCrash() EncryptionConfig encryptAndSignConfig = new EncryptionConfig( "sender.signonly@example.com", "sender@example.com", - HashAlgorithm.sha1, + PGPHashAlgorithms.SHA1, keyringConfig); EncryptWithOpenPGPTestDriver sut = new EncryptWithOpenPGPTestDriver(encryptAndSignConfig, DefaultPGPAlgorithmSuites.defaultSuiteForGnuPG()); diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionConfig.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionConfig.java index eb131641..c2cd746e 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionConfig.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionConfig.java @@ -1,10 +1,7 @@ package name.neuhalfen.projects.crypto.bouncycastle.openpgp.encrypting; -import java.io.IOException; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; +import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PGPHashAlgorithms; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeySelectionStrategy.PURPOSE; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.Rfc4880KeySelectionStrategy; @@ -14,6 +11,11 @@ import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; +import java.io.IOException; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + /** * TODO: This class now only serves as a vehicle to old test drivers and should be factored into @@ -21,65 +23,65 @@ */ public class EncryptionConfig { - private final String signatureSecretKeyId; - private final String encryptionPublicKeyId; - private final int pgpHashAlgorithmCode; - private final KeyringConfig keyringConfig; + private final String signatureSecretKeyId; + private final String encryptionPublicKeyId; + private final PGPHashAlgorithms pgpHashAlgorithmCode; + private final KeyringConfig keyringConfig; - private final KeySelectionStrategy keySelectionStrategy = new Rfc4880KeySelectionStrategy( - Instant.MAX); + private final KeySelectionStrategy keySelectionStrategy = new Rfc4880KeySelectionStrategy( + Instant.MAX); - public EncryptionConfig(String signatureSecretKeyId, - String encryptionPublicKeyId, - int pgpHashAlgorithmCode, - KeyringConfig keyringConfig) { - this.keyringConfig = keyringConfig; - this.signatureSecretKeyId = signatureSecretKeyId; - this.encryptionPublicKeyId = encryptionPublicKeyId; - this.pgpHashAlgorithmCode = pgpHashAlgorithmCode; - } + public EncryptionConfig(String signatureSecretKeyId, + String encryptionPublicKeyId, + PGPHashAlgorithms pgpHashAlgorithmCode, + KeyringConfig keyringConfig) { + this.keyringConfig = keyringConfig; + this.signatureSecretKeyId = signatureSecretKeyId; + this.encryptionPublicKeyId = encryptionPublicKeyId; + this.pgpHashAlgorithmCode = pgpHashAlgorithmCode; + } - public PGPPublicKeyRingCollection getPublicKeyRings() throws IOException, PGPException { + public PGPPublicKeyRingCollection getPublicKeyRings() throws IOException, PGPException { - return keyringConfig.getPublicKeyRings(); - } + return keyringConfig.getPublicKeyRings(); + } - public PGPSecretKeyRingCollection getSecretKeyRings() throws IOException, PGPException { + public PGPSecretKeyRingCollection getSecretKeyRings() throws IOException, PGPException { - return keyringConfig.getSecretKeyRings(); - } + return keyringConfig.getSecretKeyRings(); + } - public String getSignatureSecretKeyId() { - return signatureSecretKeyId; - } + public String getSignatureSecretKeyId() { + return signatureSecretKeyId; + } - public String getEncryptionPublicKeyId() { - return encryptionPublicKeyId; - } + public String getEncryptionPublicKeyId() { + return encryptionPublicKeyId; + } - public int getPgpHashAlgorithmCode() { - return pgpHashAlgorithmCode; - } + public int getPgpHashAlgorithmCode() { + return pgpHashAlgorithmCode.getAlgorithmId(); + } - public KeyringConfig getConfig() { - return keyringConfig; - } + public KeyringConfig getConfig() { + return keyringConfig; + } - public Set getEncryptionPublicKeys() throws PGPException, IOException { - Set keys = new HashSet<>(); - keys.add(keySelectionStrategy - .selectPublicKey(PURPOSE.FOR_ENCRYPTION, getEncryptionPublicKeyId(), keyringConfig)); + public Set getEncryptionPublicKeys() throws PGPException, IOException { + Set keys = new HashSet<>(); + keys.add(keySelectionStrategy + .selectPublicKey(PURPOSE.FOR_ENCRYPTION, getEncryptionPublicKeyId(), keyringConfig)); - return keys; - } + return keys; + } - public Set getEncryptionPublicKeysNoValidation() throws IOException, PGPException { - Set keys = new HashSet<>(); - keys.add( - keyringConfig.getPublicKeyRings().getKeyRings(getEncryptionPublicKeyId(), true, true).next() - .getPublicKey()); + public Set getEncryptionPublicKeysNoValidation() throws IOException, PGPException { + Set keys = new HashSet<>(); + keys.add( + keyringConfig.getPublicKeyRings().getKeyRings(getEncryptionPublicKeyId(), true, true).next() + .getPublicKey()); - return keys; - } + return keys; + } } diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionScenariosTest.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionScenariosTest.java index 195d461d..91fb6144 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionScenariosTest.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/EncryptionScenariosTest.java @@ -2,11 +2,8 @@ import name.neuhalfen.projects.crypto.bouncycastle.openpgp.BouncyGPG; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.DefaultPGPAlgorithmSuites; -import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring; -import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfig; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfigs; -import name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.Configs; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.ExampleMessages; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.util.io.Streams; @@ -19,6 +16,7 @@ import java.time.Instant; import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNotNull; /** * Tests several encryption scenarios. As usual, most are integration style tests. @@ -75,4 +73,47 @@ public void encrypt_masterKeyWithoutSubkeys_works() assertArrayEquals(ExampleMessages.IMPORTANT_QUOTE_TEXT.getBytes(), plainBA.toByteArray()); } + @Test + public void encrypt_masterKeyWithoutSubkeysOrKeyFlags_works() + throws IOException, PGPException, NoSuchAlgorithmException, SignatureException, NoSuchProviderException { + + // Reported in https://github.com/neuhalje/bouncy-gpg/issues/50 + + /* We test that a key that doesn't contain any KeyFlags subpacket can still be used for encrypting, + by checking the key algorithm, like GPG does + */ + + // keyring + InMemoryKeyring sendersKeyring = KeyringConfigs.forGpgExportedKeys(keyId -> null); + sendersKeyring.addPublicKey(ExampleMessages.ONLY_MASTER_KEY_PUBKEY_NO_KEY_FLAGS.getBytes()); + + // encrypt + ByteArrayOutputStream result = new ByteArrayOutputStream(); + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(result); + + final OutputStream outputStream = BouncyGPG + .encryptToStream() + .withConfig(sendersKeyring) + .setReferenceDateForKeyValidityTo(ExampleMessages.ONLY_MASTER_KEY_EXPIRY_DATE.minusSeconds(1)) + .withAlgorithms(DefaultPGPAlgorithmSuites.strongSuite()) + .toRecipient(ExampleMessages.ONLY_MASTER_KEY_UID_NO_KEY_FLAGS) + .andDoNotSign() + .binaryOutput() + .andWriteTo(bufferedOutputStream); + + final InputStream is = new ByteArrayInputStream( + ExampleMessages.IMPORTANT_QUOTE_TEXT.getBytes()); + Streams.pipeAll(is, outputStream); + outputStream.close(); + bufferedOutputStream.close(); + is.close(); + + /* test that the data can be encrypted. + If the test fails, Bouncy-PGP would complain that it can't find a public key to encrypt with + */ + + final byte[] ciphertext = result.toByteArray(); + result.close(); + assertNotNull(ciphertext); + } } diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/keyrings/EncryptionConfigTest.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/keyrings/EncryptionConfigTest.java index 1b368b99..ae38fb00 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/keyrings/EncryptionConfigTest.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/encrypting/keyrings/EncryptionConfigTest.java @@ -8,10 +8,10 @@ import static org.junit.Assert.assertThat; import java.io.IOException; + import name.neuhalfen.projects.crypto.bouncycastle.openpgp.encrypting.EncryptionConfig; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallbacks; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.Configs; -import org.bouncycastle.crypto.tls.HashAlgorithm; import org.bouncycastle.openpgp.PGPException; import org.hamcrest.text.IsEmptyString; import org.junit.Test; @@ -32,7 +32,7 @@ public void loadFromFiles_works() throws IOException, PGPException { assertThat(encryptionConfig.getEncryptionPublicKeyId(), is(not(isEmptyOrNullString()))); assertThat(encryptionConfig.getPgpHashAlgorithmCode(), - is(not(equalTo((int) HashAlgorithm.none)))); + is(not(equalTo((int) 0)))); assertThat(encryptionConfig.getPublicKeyRings(), is(notNullValue())); assertThat(encryptionConfig.getSecretKeyRings(), is(notNullValue())); diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/roundtrip/EncryptionDecryptionRoundtripIntegrationTest.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/roundtrip/EncryptionDecryptionRoundtripIntegrationTest.java index c9a48d27..2f8c632c 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/roundtrip/EncryptionDecryptionRoundtripIntegrationTest.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/roundtrip/EncryptionDecryptionRoundtripIntegrationTest.java @@ -2,7 +2,7 @@ import static name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.ExampleMessages.FULL_USER_ID_SENDER; -import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.*; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; @@ -91,6 +91,44 @@ public void encryptAndSignArmored_thenDecryptAndVerify_yieldsOriginalPlaintext() assertArrayEquals(expectedPlaintext, decryptedPlaintext); } + @Test + public void encryptAndSignArmored_thenDecryptAndVerifyWithSkip_yieldsOriginalPlaintext() + throws IOException, PGPException, NoSuchAlgorithmException, NoSuchProviderException, SignatureException { + final byte[] expectedPlaintext = ExampleMessages.IMPORTANT_QUOTE_TEXT.getBytes( + "US-ASCII"); + + ByteArrayOutputStream cipherText = new ByteArrayOutputStream(); + + final OutputStream encryptionStream = BouncyGPG + .encryptToStream() + .withConfig(Configs.keyringConfigFromFilesForSender()) + .withAlgorithms(algorithmSuite) + .toRecipient("recipient@example.com") + .andSignWith("sender@example.com") + .armorAsciiOutput() + .andWriteTo(cipherText); + + encryptionStream.write(expectedPlaintext); + encryptionStream.close(); + cipherText.close(); + + ByteArrayInputStream cipherTextAsSource = new ByteArrayInputStream(cipherText.toByteArray()); + + // Decrypt + final InputStream decryptedPlaintextStream = BouncyGPG + .decryptAndVerifyStream() + .withConfig(Configs.keyringConfigFromResourceForRecipient()) + .andRequireSignatureFromAllKeys("sender@example.com") + .fromEncryptedInputStream(cipherTextAsSource); + + // Skip 5 bytes of plaintext + decryptedPlaintextStream.skip(5); + + final byte[] decryptedPlaintext = Streams.readAll(decryptedPlaintextStream); + // And expect the rest of the plaintext to be identical to the input with the first 5 chars (==bytes in this case) skipped + assertEquals(new String(decryptedPlaintext, "US-ASCII"), ExampleMessages.IMPORTANT_QUOTE_TEXT.substring(5)); + } + @Test public void encryptAndSignBinary_thenDecryptAndVerify_yieldsOriginalPlaintext() diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/Configs.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/Configs.java index 4546d8a5..176481cc 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/Configs.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/Configs.java @@ -4,6 +4,8 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; + +import name.neuhalfen.projects.crypto.bouncycastle.openpgp.algorithms.PGPHashAlgorithms; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.encrypting.EncryptWithOpenPGPTestDriverTest; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.encrypting.EncryptionConfig; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.callbacks.KeyringConfigCallback; @@ -11,7 +13,7 @@ import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.InMemoryKeyring; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfig; import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.keyrings.KeyringConfigs; -import org.bouncycastle.crypto.tls.HashAlgorithm; +import org.bouncycastle.bcpg.HashAlgorithmTags; import org.bouncycastle.openpgp.PGPException; /** @@ -32,7 +34,7 @@ public static EncryptionConfig buildConfigForEncryptionFromFiles(KeyringConfigCa EncryptionConfig encryptAndSignConfig = new EncryptionConfig( "recipient@example.com", "recipient@example.com", - HashAlgorithm.sha1, + PGPHashAlgorithms.SHA1, keyringConfig); return encryptAndSignConfig; @@ -47,7 +49,7 @@ public static EncryptionConfig buildConfigForEncryptionFromResources(String sign EncryptionConfig encryptAndSignConfig = new EncryptionConfig( "sender@example.com", "recipient@example.com", - HashAlgorithm.sha1, + PGPHashAlgorithms.SHA1, keyringConfig); return encryptAndSignConfig; diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/ExampleMessages.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/ExampleMessages.java index cbc48271..987f7f52 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/ExampleMessages.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/ExampleMessages.java @@ -1,6 +1,7 @@ package name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling; +import java.time.Instant; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -483,6 +484,36 @@ public class ExampleMessages { "=8wl4\n" + "-----END PGP PRIVATE KEY BLOCK-----\n"; + public final static long ONLY_MASTER_KEY_ID_NO_KEY_FLAGS = 0x9E698FBA9F857349L; + public final static String ONLY_MASTER_KEY_UID_NO_KEY_FLAGS = "support@anzgcis.com"; + public final static Instant ONLY_MASTER_KEY_EXPIRY_DATE = Instant.parse("2028-08-06T00:00:00Z"); + + public final static String ONLY_MASTER_KEY_PASSPHRASE_NO_KEY_FLAGS = ""; + public final static String ONLY_MASTER_KEY_PUBKEY_NO_KEY_FLAGS= "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Version: SecureBlackbox 15\n" + + "\n" + + "xsBNBFtrhAABCACbJgPWFmodLq0cqIwiZG8hnHusSBA1nan8OQk2Dr+l37smSjO7\n" + + "TT0N/G8er79lgVyynYXXHH7EfY7tvvWPS5N55NvYiLxRYRPgFgy+cD6TsSn8W0qD\n" + + "zwCf5Jw5ZeU4pdRo6hN1Qgl6IcETQWZOsob3BWmg2aLnl+1SyY3zh0jW6z6Wn4En\n" + + "tpPj4iELe6o1SNcmC2A2fIVUoMlBHa+Mcumw+kqv4vsA6xJmpDYniBaW/asVaIBl\n" + + "4bA0Iux5Osr83pNJOBHpLk9ozyMF0rtqyyvA8l3SiLA2jyeXoBEQ9mEJi+G0/PBI\n" + + "mAoMs3FXlu4Dqdh6jk0vHDk261tMq8wqas7nABEBAAHCwGIEHwEIABYFCRLMAwAF\n" + + "AltreqYJEJ5pj7qfhXNJAACwjQgAktbLedTzTJmRgdgQsH3uakoFmi7Pc2Q/N9J7\n" + + "q8wJhBcxUUILQQck9fmzM4XOgAJmX0Sn+3xnkUvdnSP8EGd5SPezoDo29Udrm99z\n" + + "LV//9N5p4febzQwAhQJg3rGz5oDt5VMwTdSxhxPAKvkIV1QAQFqeZn6wO26Voz3h\n" + + "wfLt3xX904pomtCeakdRu+akqTBKS28Q6atHauHAAzlr1cfjLcLHdqI1y1zUbAyE\n" + + "dmYhpiIM3sr1ZvjaSRQv+vF/l5z88EVE5Ag4U5Mdw8X7R9WCnEk5ojOOveNYIsq2\n" + + "rjh9rAvD6aNBK4v+1wx9P1zSv227SojxX+AB103GSYitBSLTUs0oRmlsZWFjdGl2\n" + + "ZSBTdXBwb3J0IDxzdXBwb3J0QGFuemdjaXMuY29tPsLAXwQQAQgAEwUCW2wHRgIZ\n" + + "AQkQnmmPup+Fc0kAAGN8CACR82NV7nMGOm/crnu7k2dCHCoiTsucerG1tDFyCNYx\n" + + "3w0WATvSBS6XRwLfl6IaVbjpauZk+bH6NVgo9tpwKwGn7Wh9takHMsuRPoNiudqf\n" + + "S/duYt/ugOhlajfe3EFuGlt62j8pVT5o9bgCZQNEUgkGzwwz+j/saWHsI9fW26Ri\n" + + "W/tyJk0IICLf2aEJOi63PsllRQ+CtimvZkRalvcCuS19JNcYZlz3GyM/+z3LiYMi\n" + + "b1s+vDO4ZJgka+nTWHL1VrbctH79n23hmV1pK7LXAl8mZlo0wUU+R+01WumSVSQZ\n" + + "XGs5vAe6nG216ghBcNNd0UbQMZXwuIQlOtN5CSwoHdp6\n" + + "=qWOE\n" + + "-----END PGP PUBLIC KEY BLOCK-----\n"; + public final static Map ALL_KEYRINGS_PASSWORDS = BUILD_ALL_KEYRINGS_PASSWORDS(); /** * Encrypted-To: recipient@example.com Signed-By: sender@example.com Compressed: false @@ -738,6 +769,7 @@ private static Map BUILD_ALL_KEYRINGS_PASSWORDS() { m.put(ExampleMessages.PUBKEY_ID_RECIPIENT, "recipient".toCharArray()); m.put(ExampleMessages.ONLY_MASTER_KEY_ID, null); + m.put(ExampleMessages.ONLY_MASTER_KEY_ID_NO_KEY_FLAGS, null); return Collections.unmodifiableMap(m); } diff --git a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/matcher/SecretKeyringKeyRoleMatcher.java b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/matcher/SecretKeyringKeyRoleMatcher.java index 9de5453c..a65b3472 100644 --- a/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/matcher/SecretKeyringKeyRoleMatcher.java +++ b/src/test/java/name/neuhalfen/projects/crypto/bouncycastle/openpgp/testtooling/matcher/SecretKeyringKeyRoleMatcher.java @@ -1,9 +1,7 @@ package name.neuhalfen.projects.crypto.bouncycastle.openpgp.testtooling.matcher; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.Set; +import java.util.*; + import name.neuhalfen.projects.crypto.bouncycastle.openpgp.keys.generation.KeyFlag; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPSecretKey; @@ -54,21 +52,22 @@ EnumSet parseKeyRing(final PGPSecretKeyRing ring) { EnumSet parseKey(final PGPSecretKey item) { final EnumSet found = EnumSet.noneOf(KeyRole.class); final PGPPublicKey publicKey = item.getPublicKey(); - final Set keyFlags = KeyFlag.extractPublicKeyFlags(publicKey); + final Optional > keyFlags = KeyFlag.extractPublicKeyFlags(publicKey); if (item.isMasterKey()) { found.add(KeyRole.MASTER); } if (item.isSigningKey()) { - if (keyFlags.contains(KeyFlag.SIGN_DATA)) { + if (keyFlags.isPresent() && keyFlags.get().contains(KeyFlag.SIGN_DATA)) { found.add(KeyRole.SIGNING); } } if (item.getPublicKey().isEncryptionKey()) { final boolean hasEncryptionFlags = - keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) || keyFlags.contains(KeyFlag.ENCRYPT_COMMS); + keyFlags.isPresent() && keyFlags.get().contains(KeyFlag.ENCRYPT_STORAGE) || + keyFlags.isPresent() && keyFlags.get().contains(KeyFlag.ENCRYPT_COMMS); if (hasEncryptionFlags) { found.add(KeyRole.ENCRYPTION); } diff --git a/website/content/_index.md b/website/content/_index.md index db0cd06d..715ee613 100644 --- a/website/content/_index.md +++ b/website/content/_index.md @@ -183,8 +183,8 @@ repositories { // ... dependencies { - compile 'org.bouncycastle:bcprov-jdk15on:1.60' - compile 'org.bouncycastle:bcpg-jdk15on:1.60' + compile 'org.bouncycastle:bcprov-jdk15on:1.67' + compile 'org.bouncycastle:bcpg-jdk15on:1.67' // ... compile 'name.neuhalfen.projects.crypto.bouncycastle.openpgp:bouncy-gpg:2.+' // ...