From 5ac95025f852c482e3b13332f7dacdc2e775f4b8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 30 Aug 2023 13:50:49 +0200 Subject: [PATCH] WIP: Kotlin conversion: ConsumerOptions --- .../ConsumerOptions.java | 508 ------------------ .../ConsumerOptions.kt | 394 ++++++++++++++ .../OpenPgpMessageInputStream.kt | 42 +- .../pgpainless/signature/SignatureUtils.kt | 2 + .../MissingPassphraseForDecryptionTest.java | 24 - 5 files changed, 417 insertions(+), 553 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java b/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java deleted file mode 100644 index d0d9230b..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/decryption_verification/ConsumerOptions.java +++ /dev/null @@ -1,508 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.decryption_verification; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; -import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy; -import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy; -import org.pgpainless.key.SubkeyIdentifier; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.signature.SignatureUtils; -import org.pgpainless.util.Passphrase; -import org.pgpainless.util.SessionKey; - -/** - * Options for decryption and signature verification. - */ -public class ConsumerOptions { - - private boolean ignoreMDCErrors = false; - private boolean forceNonOpenPgpData = false; - - private Date verifyNotBefore = null; - private Date verifyNotAfter = new Date(); - - private final CertificateSource certificates = new CertificateSource(); - private final Set detachedSignatures = new HashSet<>(); - private MissingPublicKeyCallback missingCertificateCallback = null; - - // Session key for decryption without passphrase/key - private SessionKey sessionKey = null; - private final Map customPublicKeyDataDecryptorFactories = - new HashMap<>(); - - private final Map decryptionKeys = new HashMap<>(); - private final Set decryptionPassphrases = new HashSet<>(); - private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE; - - private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy(); - - public static ConsumerOptions get() { - return new ConsumerOptions(); - } - - /** - * Consider signatures on the message made before the given timestamp invalid. - * Null means no limitation. - * - * @param timestamp timestamp - * @return options - */ - public ConsumerOptions verifyNotBefore(Date timestamp) { - this.verifyNotBefore = timestamp; - return this; - } - - /** - * Return the earliest creation date on which signatures on the message are considered valid. - * Signatures made earlier than this date are considered invalid. - * - * @return earliest allowed signature creation date or null - */ - public @Nullable Date getVerifyNotBefore() { - return verifyNotBefore; - } - - /** - * Consider signatures on the message made after the given timestamp invalid. - * Null means no limitation. - * - * @param timestamp timestamp - * @return options - */ - public ConsumerOptions verifyNotAfter(Date timestamp) { - this.verifyNotAfter = timestamp; - return this; - } - - /** - * Return the latest possible creation date on which signatures made on the message are considered valid. - * Signatures made later than this date are considered invalid. - * - * @return Latest possible creation date or null. - */ - public Date getVerifyNotAfter() { - return verifyNotAfter; - } - - /** - * Add a certificate (public key ring) for signature verification. - * - * @param verificationCert certificate for signature verification - * @return options - */ - public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) { - this.certificates.addCertificate(verificationCert); - return this; - } - - /** - * Add a set of certificates (public key rings) for signature verification. - * - * @param verificationCerts certificates for signature verification - * @return options - */ - public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) { - for (PGPPublicKeyRing certificate : verificationCerts) { - addVerificationCert(certificate); - } - return this; - } - - /** - * Add some detached signatures from the given {@link InputStream} for verification. - * - * @param signatureInputStream input stream of detached signatures - * @return options - * - * @throws IOException in case of an IO error - * @throws PGPException in case of an OpenPGP error - */ - public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) - throws IOException, PGPException { - List signatures = SignatureUtils.readSignatures(signatureInputStream); - return addVerificationOfDetachedSignatures(signatures); - } - - /** - * Add some detached signatures for verification. - * - * @param detachedSignatures detached signatures - * @return options - */ - public ConsumerOptions addVerificationOfDetachedSignatures(List detachedSignatures) { - for (PGPSignature signature : detachedSignatures) { - addVerificationOfDetachedSignature(signature); - } - return this; - } - - /** - * Add a detached signature for the signature verification process. - * - * @param detachedSignature detached signature - * @return options - */ - public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) { - detachedSignatures.add(detachedSignature); - return this; - } - - /** - * Set a callback that's used when a certificate (public key) is missing for signature verification. - * - * @param callback callback - * @return options - */ - public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) { - this.missingCertificateCallback = callback; - return this; - } - - - /** - * Attempt decryption using a session key. - * - * Note: PGPainless does not yet support decryption with session keys. - * - * @see RFC4880 on Session Keys - * - * @param sessionKey session key - * @return options - */ - public ConsumerOptions setSessionKey(@Nonnull SessionKey sessionKey) { - this.sessionKey = sessionKey; - return this; - } - - /** - * Return the session key. - * - * @return session key or null - */ - public @Nullable SessionKey getSessionKey() { - return sessionKey; - } - - /** - * Add a key for message decryption. - * The key is expected to be unencrypted. - * - * @param key unencrypted key - * @return options - */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) { - return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys()); - } - - /** - * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} - * is used to decrypt it when needed. - * - * @param key key - * @param keyRingProtector protector for the secret key - * @return options - */ - public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, - @Nonnull SecretKeyRingProtector keyRingProtector) { - decryptionKeys.put(key, keyRingProtector); - return this; - } - - /** - * Add the keys in the provided key collection for message decryption. - * - * @param keys key collection - * @param keyRingProtector protector for encrypted secret keys - * @return options - */ - public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, - @Nonnull SecretKeyRingProtector keyRingProtector) { - for (PGPSecretKeyRing key : keys) { - addDecryptionKey(key, keyRingProtector); - } - return this; - } - - /** - * Add a passphrase for message decryption. - * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase. - * - * @see Symmetrically Encrypted Data Packet - * - * @param passphrase passphrase - * @return options - */ - public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) { - decryptionPassphrases.add(passphrase); - return this; - } - - /** - * Add a custom {@link PublicKeyDataDecryptorFactory} which enable decryption of messages, e.g. using - * hardware-backed secret keys. - * (See e.g. {@link org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory}). - * - * @param factory decryptor factory - * @return options - */ - public ConsumerOptions addCustomDecryptorFactory(@Nonnull CustomPublicKeyDataDecryptorFactory factory) { - this.customPublicKeyDataDecryptorFactories.put(factory.getSubkeyIdentifier(), factory); - return this; - } - - /** - * Return the custom {@link PublicKeyDataDecryptorFactory PublicKeyDataDecryptorFactories} that were - * set by the user. - * These factories can be used to decrypt session keys using a custom logic. - * - * @return custom decryptor factories - */ - Map getCustomDecryptorFactories() { - return new HashMap<>(customPublicKeyDataDecryptorFactories); - } - - /** - * Return the set of available decryption keys. - * - * @return decryption keys - */ - public @Nonnull Set getDecryptionKeys() { - return Collections.unmodifiableSet(decryptionKeys.keySet()); - } - - /** - * Return the set of available message decryption passphrases. - * - * @return decryption passphrases - */ - public @Nonnull Set getDecryptionPassphrases() { - return Collections.unmodifiableSet(decryptionPassphrases); - } - - /** - * Return the explicitly set verification certificates. - * - * @deprecated use {@link #getCertificateSource()} instead. - * @return verification certs - */ - @Deprecated - public @Nonnull Set getCertificates() { - return certificates.getExplicitCertificates(); - } - - /** - * Return an object holding available certificates for signature verification. - * - * @return certificate source - */ - public @Nonnull CertificateSource getCertificateSource() { - return certificates; - } - - /** - * Return the callback that gets called when a certificate for signature verification is missing. - * This method might return
null
if the users hasn't set a callback. - * - * @return missing public key callback - */ - public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() { - return missingCertificateCallback; - } - - /** - * Return the {@link SecretKeyRingProtector} for the given {@link PGPSecretKeyRing}. - * - * @param decryptionKeyRing secret key - * @return protector for that particular secret key - */ - public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) { - return decryptionKeys.get(decryptionKeyRing); - } - - /** - * Return the set of detached signatures the user provided. - * - * @return detached signatures - */ - public @Nonnull Set getDetachedSignatures() { - return Collections.unmodifiableSet(detachedSignatures); - } - - /** - * By default, PGPainless will require encrypted messages to make use of SEIP data packets. - * Those are Symmetrically Encrypted Integrity Protected Data packets. - * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. - * Furthermore, PGPainless will throw an exception if verification of the MDC error detection - * code of the SEIP packet fails. - * - * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an - * attack or data corruption. - * - * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data - * without integrity protection. - * If the flag
ignoreMDCErrors
is set to true, PGPainless will - *
    - *
  • not throw exceptions for SEIP packets with tampered ciphertext
  • - *
  • not throw exceptions for SEIP packets with tampered MDC
  • - *
  • not throw exceptions for MDCs with bad CTB
  • - *
  • not throw exceptions for MDCs with bad length
  • - *
- * - * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC - * - * @see - * Sym. Encrypted Integrity Protected Data Packet - * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. - * @return options - */ - @Deprecated - public ConsumerOptions setIgnoreMDCErrors(boolean ignoreMDCErrors) { - this.ignoreMDCErrors = ignoreMDCErrors; - return this; - } - - /** - * Return true, if PGPainless is ignoring MDC errors. - * - * @return ignore mdc errors - */ - boolean isIgnoreMDCErrors() { - return ignoreMDCErrors; - } - - /** - * Force PGPainless to handle the data provided by the {@link InputStream} as non-OpenPGP data. - * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. - * - * @return options - */ - public ConsumerOptions forceNonOpenPgpData() { - this.forceNonOpenPgpData = true; - return this; - } - - /** - * Return true, if the ciphertext should be handled as binary non-OpenPGP data. - * - * @return true if non-OpenPGP data is forced - */ - boolean isForceNonOpenPgpData() { - return forceNonOpenPgpData; - } - - /** - * Specify the {@link MissingKeyPassphraseStrategy}. - * This strategy defines, how missing passphrases for unlocking secret keys are handled. - * In interactive mode ({@link MissingKeyPassphraseStrategy#INTERACTIVE}) PGPainless will try to obtain missing - * passphrases for secret keys via the {@link SecretKeyRingProtector SecretKeyRingProtectors} - * {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider} callback. - * - * In non-interactice mode ({@link MissingKeyPassphraseStrategy#THROW_EXCEPTION}, PGPainless will instead - * throw a {@link org.pgpainless.exception.MissingPassphraseException} containing the ids of all keys for which - * there are missing passphrases. - * - * @param strategy strategy - * @return options - */ - public ConsumerOptions setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy strategy) { - this.missingKeyPassphraseStrategy = strategy; - return this; - } - - /** - * Return the currently configured {@link MissingKeyPassphraseStrategy}. - * - * @return missing key passphrase strategy - */ - MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() { - return missingKeyPassphraseStrategy; - } - - /** - * Set a custom multi-pass strategy for processing cleartext-signed messages. - * Uses {@link InMemoryMultiPassStrategy} by default. - * - * @param multiPassStrategy multi-pass caching strategy - * @return builder - */ - public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) { - this.multiPassStrategy = multiPassStrategy; - return this; - } - - /** - * Return the currently configured {@link MultiPassStrategy}. - * Defaults to {@link InMemoryMultiPassStrategy}. - * - * @return multi-pass strategy - */ - public MultiPassStrategy getMultiPassStrategy() { - return multiPassStrategy; - } - - /** - * Source for OpenPGP certificates. - * When verifying signatures on a message, this object holds available signer certificates. - */ - public static class CertificateSource { - - private Set explicitCertificates = new HashSet<>(); - - /** - * Add a certificate as verification cert explicitly. - * - * @param certificate certificate - */ - public void addCertificate(PGPPublicKeyRing certificate) { - this.explicitCertificates.add(certificate); - } - - /** - * Return the set of explicitly set verification certificates. - * @return explicitly set verification certs - */ - public Set getExplicitCertificates() { - return Collections.unmodifiableSet(explicitCertificates); - } - - /** - * Return a certificate which contains a subkey with the given keyId. - * This method first checks all explicitly set verification certs and if no cert is found it consults - * the certificate stores. - * - * @param keyId key id - * @return certificate - */ - public PGPPublicKeyRing getCertificate(long keyId) { - - for (PGPPublicKeyRing cert : explicitCertificates) { - if (cert.getPublicKey(keyId) != null) { - return cert; - } - } - - return null; - } - } -} diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt new file mode 100644 index 00000000..5bbe098e --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -0,0 +1,394 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.decryption_verification + +import org.bouncycastle.openpgp.* +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy +import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy +import org.pgpainless.key.SubkeyIdentifier +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.signature.SignatureUtils +import org.pgpainless.util.Passphrase +import org.pgpainless.util.SessionKey +import java.io.IOException +import java.io.InputStream +import java.util.* + +/** + * Options for decryption and signature verification. + */ +class ConsumerOptions { + + private var ignoreMDCErrors = false + private var forceNonOpenPgpData = false + private var verifyNotBefore: Date? = null + private var verifyNotAfter: Date? = Date() + + private val certificates = CertificateSource() + private val detachedSignatures = mutableSetOf() + private var missingCertificateCallback: MissingPublicKeyCallback? = null + + private var sessionKey: SessionKey? = null + private val customDecryptorFactories = mutableMapOf() + private val decryptionKeys = mutableMapOf() + private val decryptionPassphrases = mutableSetOf() + private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE + private var multiPassStrategy: MultiPassStrategy = InMemoryMultiPassStrategy() + + /** + * Consider signatures on the message made before the given timestamp invalid. + * Null means no limitation. + * + * @param timestamp timestamp + * @return options + */ + fun verifyNotBefore(timestamp: Date?): ConsumerOptions = apply { + this.verifyNotBefore = timestamp + } + + fun getVerifyNotBefore() = verifyNotBefore + + /** + * Consider signatures on the message made after the given timestamp invalid. + * Null means no limitation. + * + * @param timestamp timestamp + * @return options + */ + fun verifyNotAfter(timestamp: Date?): ConsumerOptions = apply { + this.verifyNotAfter = timestamp + } + + fun getVerifyNotAfter() = verifyNotAfter + + /** + * Add a certificate (public key ring) for signature verification. + * + * @param verificationCert certificate for signature verification + * @return options + */ + fun addVerificationCert(verificationCert: PGPPublicKeyRing): ConsumerOptions = apply { + this.certificates.addCertificate(verificationCert) + } + + /** + * Add a set of certificates (public key rings) for signature verification. + * + * @param verificationCerts certificates for signature verification + * @return options + */ + fun addVerificationCerts(verificationCerts: PGPPublicKeyRingCollection): ConsumerOptions = apply { + for (cert in verificationCerts) { + addVerificationCert(cert) + } + } + + /** + * Add some detached signatures from the given [InputStream] for verification. + * + * @param signatureInputStream input stream of detached signatures + * @return options + * + * @throws IOException in case of an IO error + * @throws PGPException in case of an OpenPGP error + */ + @Throws(IOException::class, PGPException::class) + fun addVerificationOfDetachedSignatures(signatureInputStream: InputStream): ConsumerOptions = apply { + val signatures = SignatureUtils.readSignatures(signatureInputStream) + addVerificationOfDetachedSignatures(signatures) + } + + /** + * Add some detached signatures for verification. + * + * @param detachedSignatures detached signatures + * @return options + */ + fun addVerificationOfDetachedSignatures(detachedSignatures: List): ConsumerOptions = apply { + for (signature in detachedSignatures) { + addVerificationOfDetachedSignature(signature) + } + } + + /** + * Add a detached signature for the signature verification process. + * + * @param detachedSignature detached signature + * @return options + */ + fun addVerificationOfDetachedSignature(detachedSignature: PGPSignature): ConsumerOptions = apply { + detachedSignatures.add(detachedSignature) + } + + fun getDetachedSignatures() = detachedSignatures.toList() + + /** + * Set a callback that's used when a certificate (public key) is missing for signature verification. + * + * @param callback callback + * @return options + */ + fun setMissingCertificateCallback(callback: MissingPublicKeyCallback): ConsumerOptions = apply { + this.missingCertificateCallback = callback + } + + /** + * Attempt decryption using a session key. + * + * Note: PGPainless does not yet support decryption with session keys. + * + * See [RFC4880 on Session Keys](https://datatracker.ietf.org/doc/html/rfc4880#section-2.1) + * + * @param sessionKey session key + * @return options + */ + fun setSessionKey(sessionKey: SessionKey) = apply { this.sessionKey = sessionKey } + + fun getSessionKey() = sessionKey + + /** + * Add a key for message decryption. If the key is encrypted, the [SecretKeyRingProtector] + * is used to decrypt it when needed. + * + * @param key key + * @param keyRingProtector protector for the secret key + * @return options + */ + @JvmOverloads + fun addDecryptionKey(key: PGPSecretKeyRing, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()) = apply { + decryptionKeys[key] = protector + } + + /** + * Add the keys in the provided key collection for message decryption. + * + * @param keys key collection + * @param keyRingProtector protector for encrypted secret keys + * @return options + */ + @JvmOverloads + fun addDecryptionKeys(keys: PGPSecretKeyRingCollection, + protector: SecretKeyRingProtector = SecretKeyRingProtector.unprotectedKeys()) = apply { + for (key in keys) { + addDecryptionKey(key, protector) + } + } + + /** + * Add a passphrase for message decryption. + * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase. + * + * See [Symmetrically Encrypted Data Packet](https://datatracker.ietf.org/doc/html/rfc4880#section-5.7) + * + * @param passphrase passphrase + * @return options + */ + fun addDecryptionPassphrase(passphrase: Passphrase) = apply { + decryptionPassphrases.add(passphrase) + } + + /** + * Add a custom [PublicKeyDataDecryptorFactory] which enable decryption of messages, e.g. using + * hardware-backed secret keys. + * (See e.g. [org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory]). + * + * @param factory decryptor factory + * @return options + */ + fun addCustomDecryptorFactory(factory: CustomPublicKeyDataDecryptorFactory) = apply { + customDecryptorFactories[factory.subkeyIdentifier] = factory + } + + /** + * Return the custom [PublicKeyDataDecryptorFactory] that were + * set by the user. + * These factories can be used to decrypt session keys using a custom logic. + * + * @return custom decryptor factories + */ + fun getCustomDecryptorFactories() = customDecryptorFactories.toMap() + + /** + * Return the set of available decryption keys. + * + * @return decryption keys + */ + fun getDecryptionKeys() = decryptionKeys.keys.toSet() + + /** + * Return the set of available message decryption passphrases. + * + * @return decryption passphrases + */ + fun getDecryptionPassphrases() = decryptionPassphrases.toSet() + + /** + * Return an object holding available certificates for signature verification. + * + * @return certificate source + */ + fun getCertificateSource() = certificates + + /** + * Return the callback that gets called when a certificate for signature verification is missing. + * This method might return `null` if the users hasn't set a callback. + * + * @return missing public key callback + */ + fun getMissingCertificateCallback() = missingCertificateCallback + + /** + * Return the [SecretKeyRingProtector] for the given [PGPSecretKeyRing]. + * + * @param decryptionKeyRing secret key + * @return protector for that particular secret key + */ + fun getSecretKeyProtector(decryptionKeyRing: PGPSecretKeyRing): SecretKeyRingProtector? { + return decryptionKeys[decryptionKeyRing] + } + + /** + * By default, PGPainless will require encrypted messages to make use of SEIP data packets. + * Those are Symmetrically Encrypted Integrity Protected Data packets. + * Symmetrically Encrypted Data Packets without integrity protection are rejected by default. + * Furthermore, PGPainless will throw an exception if verification of the MDC error detection + * code of the SEIP packet fails. + * + * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an + * attack or data corruption. + * + * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data + * without integrity protection. + * If the flag
ignoreMDCErrors
is set to true, PGPainless will + * + * * not throw exceptions for SEIP packets with tampered ciphertext + * * not throw exceptions for SEIP packets with tampered MDC + * * not throw exceptions for MDCs with bad CTB + * * not throw exceptions for MDCs with bad length + * + * + * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC + * + * See [Sym. Encrypted Integrity Protected Data Packet](https://datatracker.ietf.org/doc/html/rfc4880.section-5.13) + * + * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise. + * @return options + */ + @Deprecated("Ignoring non-integrity-protected packets is discouraged.") + fun setIgnoreMDCErrors(ignoreMDCErrors: Boolean): ConsumerOptions = apply { this.ignoreMDCErrors = ignoreMDCErrors } + + fun isIgnoreMDCErrors() = ignoreMDCErrors + + /** + * Force PGPainless to handle the data provided by the [InputStream] as non-OpenPGP data. + * This workaround might come in handy if PGPainless accidentally mistakes the data for binary OpenPGP data. + * + * @return options + */ + fun forceNonOpenPgpData(): ConsumerOptions = apply { + this.forceNonOpenPgpData = true + } + + /** + * Return true, if the ciphertext should be handled as binary non-OpenPGP data. + * + * @return true if non-OpenPGP data is forced + */ + fun isForceNonOpenPgpData() = forceNonOpenPgpData + + /** + * Specify the [MissingKeyPassphraseStrategy]. + * This strategy defines, how missing passphrases for unlocking secret keys are handled. + * In interactive mode ([MissingKeyPassphraseStrategy.INTERACTIVE]) PGPainless will try to obtain missing + * passphrases for secret keys via the [SecretKeyRingProtector] + * [org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider] callback. + * + * In non-interactice mode ([MissingKeyPassphraseStrategy.THROW_EXCEPTION]), PGPainless will instead + * throw a [org.pgpainless.exception.MissingPassphraseException] containing the ids of all keys for which + * there are missing passphrases. + * + * @param strategy strategy + * @return options + */ + fun setMissingKeyPassphraseStrategy(strategy: MissingKeyPassphraseStrategy): ConsumerOptions { + this.missingKeyPassphraseStrategy = strategy + return this + } + + /** + * Return the currently configured [MissingKeyPassphraseStrategy]. + * + * @return missing key passphrase strategy + */ + fun getMissingKeyPassphraseStrategy(): MissingKeyPassphraseStrategy { + return missingKeyPassphraseStrategy + } + + /** + * Set a custom multi-pass strategy for processing cleartext-signed messages. + * Uses [InMemoryMultiPassStrategy] by default. + * + * @param multiPassStrategy multi-pass caching strategy + * @return builder + */ + fun setMultiPassStrategy(multiPassStrategy: MultiPassStrategy): ConsumerOptions { + this.multiPassStrategy = multiPassStrategy + return this + } + + /** + * Return the currently configured [MultiPassStrategy]. + * Defaults to [InMemoryMultiPassStrategy]. + * + * @return multi-pass strategy + */ + fun getMultiPassStrategy(): MultiPassStrategy { + return multiPassStrategy + } + + /** + * Source for OpenPGP certificates. + * When verifying signatures on a message, this object holds available signer certificates. + */ + class CertificateSource { + private val explicitCertificates: MutableSet = mutableSetOf() + + /** + * Add a certificate as verification cert explicitly. + * + * @param certificate certificate + */ + fun addCertificate(certificate: PGPPublicKeyRing) { + explicitCertificates.add(certificate) + } + + /** + * Return the set of explicitly set verification certificates. + * @return explicitly set verification certs + */ + fun getExplicitCertificates(): Set { + return explicitCertificates.toSet() + } + + /** + * Return a certificate which contains a subkey with the given keyId. + * This method first checks all explicitly set verification certs and if no cert is found it consults + * the certificate stores. + * + * @param keyId key id + * @return certificate + */ + fun getCertificate(keyId: Long): PGPPublicKeyRing? { + return explicitCertificates.firstOrNull { it.getPublicKey(keyId) != null } + } + } + + companion object { + @JvmStatic + fun get() = ConsumerOptions() + } +} \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 25b29100..49a6edf5 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -53,7 +53,7 @@ class OpenPgpMessageInputStream( // Add detached signatures only on the outermost OpenPgpMessageInputStream if (metadata is Message) { - signatures.addDetachedSignatures(options.detachedSignatures) + signatures.addDetachedSignatures(options.getDetachedSignatures()) } when(type) { @@ -67,7 +67,7 @@ class OpenPgpMessageInputStream( } Type.cleartext_signed -> { - val multiPassStrategy = options.multiPassStrategy + val multiPassStrategy = options.getMultiPassStrategy() val detachedSignatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( inputStream, multiPassStrategy.messageOutputStream) @@ -75,7 +75,7 @@ class OpenPgpMessageInputStream( signatures.addDetachedSignature(signature) } - options.forceNonOpenPgpData() + options.isForceNonOpenPgpData() nestedInputStream = TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) } @@ -212,7 +212,7 @@ class OpenPgpMessageInputStream( val encDataList = packetInputStream!!.readEncryptedDataList() if (!encDataList.isIntegrityProtected) { LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") - if (!options.isIgnoreMDCErrors) { + if (!options.isIgnoreMDCErrors()) { throw MessageNotIntegrityProtectedException() } } @@ -223,7 +223,7 @@ class OpenPgpMessageInputStream( " have an anonymous recipient.") // try custom decryptor factories - for ((key, decryptorFactory) in options.customDecryptorFactories) { + for ((key, decryptorFactory) in options.getCustomDecryptorFactories()) { LOGGER.debug("Attempt decryption with custom decryptor factory with key $key.") esks.pkesks.filter { // find matching PKESK @@ -237,8 +237,8 @@ class OpenPgpMessageInputStream( } // try provided session key - if (options.sessionKey != null) { - val sk = options.sessionKey!! + if (options.getSessionKey() != null) { + val sk = options.getSessionKey()!! LOGGER.debug("Attempt decryption with provided session key.") throwIfUnacceptable(sk.algorithm) @@ -260,7 +260,7 @@ class OpenPgpMessageInputStream( } // try passwords - for (passphrase in options.decryptionPassphrases) { + for (passphrase in options.getDecryptionPassphrases()) { for (skesk in esks.skesks) { LOGGER.debug("Attempt decryption with provided passphrase") val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm) @@ -295,7 +295,7 @@ class OpenPgpMessageInputStream( } LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") - val protector = options.getSecretKeyProtector(decryptionKeys) + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue if (!protector.hasPassphraseFor(keyId)) { LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") postponedDueToMissingPassphrase.add(secretKey to pkesk) @@ -317,7 +317,7 @@ class OpenPgpMessageInputStream( } LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") - val protector = options.getSecretKeyProtector(decryptionKeys) + val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue if (!protector.hasPassphraseFor(secretKey.keyID)) { LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") @@ -332,7 +332,7 @@ class OpenPgpMessageInputStream( } } - if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { + if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.THROW_EXCEPTION) { // Non-interactive mode: Throw an exception with all locked decryption keys postponedDueToMissingPassphrase.map { SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) @@ -340,7 +340,7 @@ class OpenPgpMessageInputStream( if (it.isNotEmpty()) throw MissingPassphraseException(it.toSet()) } - } else if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.INTERACTIVE) { + } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { val keyId = secretKey.keyID val decryptionKeys = getDecryptionKey(keyId)!! @@ -532,7 +532,7 @@ class OpenPgpMessageInputStream( return MessageMetadata((metadata as Message)) } - private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.decryptionKeys.firstOrNull { + private fun getDecryptionKey(keyId: Long): PGPSecretKeyRing? = options.getDecryptionKeys().firstOrNull { it.any { k -> k.keyID == keyId }.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { @@ -543,7 +543,7 @@ class OpenPgpMessageInputStream( private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List> { val algorithm = pkesk.algorithm val candidates = mutableListOf>() - options.decryptionKeys.forEach { + options.getDecryptionKeys().forEach { val info = PGPainless.inspectKeyRing(it) for (key in info.decryptionSubkeys) { if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { @@ -679,7 +679,7 @@ class OpenPgpMessageInputStream( SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) try { - SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(signature) CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) LOGGER.debug("Acceptable signature by key ${verification.signingKey}") @@ -713,13 +713,13 @@ class OpenPgpMessageInputStream( } fun findCertificate(keyId: Long): PGPPublicKeyRing? { - val cert = options.certificateSource.getCertificate(keyId) + val cert = options.getCertificateSource().getCertificate(keyId) if (cert != null) { return cert } - if (options.missingCertificateCallback != null) { - return options.missingCertificateCallback!!.onMissingPublicKeyEncountered(keyId) + if (options.getMissingCertificateCallback() != null) { + return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(keyId) } return null // TODO: Missing cert for sig } @@ -772,7 +772,7 @@ class OpenPgpMessageInputStream( for (detached in detachedSignatures) { val verification = SignatureVerification(detached.signature, detached.signingKeyIdentifier) try { - SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(detached.signature) CertificateValidator.validateCertificateAndVerifyInitializedSignature( detached.signature, KeyRingUtils.publicKeys(detached.signingKeyRing), policy) @@ -787,7 +787,7 @@ class OpenPgpMessageInputStream( for (prepended in prependedSignatures) { val verification = SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) try { - SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) + SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter()) .verify(prepended.signature) CertificateValidator.validateCertificateAndVerifyInitializedSignature( prepended.signature, KeyRingUtils.publicKeys(prepended.signingKeyRing), policy) @@ -877,7 +877,7 @@ class OpenPgpMessageInputStream( val openPgpIn = OpenPgpInputStream(inputStream) openPgpIn.reset() - if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData) { + if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) { return OpenPgpMessageInputStream(Type.non_openpgp, openPgpIn, options, metadata, policy) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt index b9b80f8f..4ca30e86 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/SignatureUtils.kt @@ -14,6 +14,7 @@ import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.util.ArmorUtils +import java.io.IOException import java.io.InputStream import java.util.* @@ -127,6 +128,7 @@ class SignatureUtils { } @JvmStatic + @Throws(IOException::class, PGPException::class) fun readSignatures(inputStream: InputStream): List { return readSignatures(inputStream, MAX_ITERATIONS) } diff --git a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java index f7d36aba..42562713 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/decryption_verification/MissingPassphraseForDecryptionTest.java @@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -59,29 +58,6 @@ public class MissingPassphraseForDecryptionTest { message = out.toByteArray(); } - @Test - public void invalidPostponedKeysStrategyTest() { - SecretKeyPassphraseProvider callback = new SecretKeyPassphraseProvider() { - @Override - public Passphrase getPassphraseFor(Long keyId) { - fail("MUST NOT get called in if postponed key strategy is invalid."); - return null; - } - - @Override - public boolean hasPassphrase(Long keyId) { - return true; - } - }; - ConsumerOptions options = new ConsumerOptions() - .setMissingKeyPassphraseStrategy(null) // illegal - .addDecryptionKey(secretKeys, SecretKeyRingProtector.defaultSecretKeyRingProtector(callback)); - - assertThrows(IllegalStateException.class, () -> PGPainless.decryptAndOrVerify() - .onInputStream(new ByteArrayInputStream(message)) - .withOptions(options)); - } - @Test public void interactiveStrategy() throws PGPException, IOException { // interactive callback