1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-25 04:17:59 +01:00

WIP: Kotlin conversion: ConsumerOptions

This commit is contained in:
Paul Schaub 2023-08-30 13:50:49 +02:00
parent 4dfcd1d822
commit 5ac95025f8
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
5 changed files with 417 additions and 553 deletions

View file

@ -1,508 +0,0 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSignature> detachedSignatures = new HashSet<>();
private MissingPublicKeyCallback missingCertificateCallback = null;
// Session key for decryption without passphrase/key
private SessionKey sessionKey = null;
private final Map<SubkeyIdentifier, PublicKeyDataDecryptorFactory> customPublicKeyDataDecryptorFactories =
new HashMap<>();
private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
private final Set<Passphrase> 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<PGPSignature> signatures = SignatureUtils.readSignatures(signatureInputStream);
return addVerificationOfDetachedSignatures(signatures);
}
/**
* Add some detached signatures for verification.
*
* @param detachedSignatures detached signatures
* @return options
*/
public ConsumerOptions addVerificationOfDetachedSignatures(List<PGPSignature> 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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-2.1">RFC4880 on Session Keys</a>
*
* @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 <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.7">Symmetrically Encrypted Data Packet</a>
*
* @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<SubkeyIdentifier, PublicKeyDataDecryptorFactory> getCustomDecryptorFactories() {
return new HashMap<>(customPublicKeyDataDecryptorFactories);
}
/**
* Return the set of available decryption keys.
*
* @return decryption keys
*/
public @Nonnull Set<PGPSecretKeyRing> getDecryptionKeys() {
return Collections.unmodifiableSet(decryptionKeys.keySet());
}
/**
* Return the set of available message decryption passphrases.
*
* @return decryption passphrases
*/
public @Nonnull Set<Passphrase> getDecryptionPassphrases() {
return Collections.unmodifiableSet(decryptionPassphrases);
}
/**
* Return the explicitly set verification certificates.
*
* @deprecated use {@link #getCertificateSource()} instead.
* @return verification certs
*/
@Deprecated
public @Nonnull Set<PGPPublicKeyRing> 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 <pre>null</pre> 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<PGPSignature> 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 <pre>ignoreMDCErrors</pre> is set to true, PGPainless will
* <ul>
* <li>not throw exceptions for SEIP packets with tampered ciphertext</li>
* <li>not throw exceptions for SEIP packets with tampered MDC</li>
* <li>not throw exceptions for MDCs with bad CTB</li>
* <li>not throw exceptions for MDCs with bad length</li>
* </ul>
*
* It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC
*
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.13">
* Sym. Encrypted Integrity Protected Data Packet</a>
* @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<PGPPublicKeyRing> 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<PGPPublicKeyRing> 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;
}
}
}

View file

@ -0,0 +1,394 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// 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<PGPSignature>()
private var missingCertificateCallback: MissingPublicKeyCallback? = null
private var sessionKey: SessionKey? = null
private val customDecryptorFactories = mutableMapOf<SubkeyIdentifier, PublicKeyDataDecryptorFactory>()
private val decryptionKeys = mutableMapOf<PGPSecretKeyRing, SecretKeyRingProtector>()
private val decryptionPassphrases = mutableSetOf<Passphrase>()
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<PGPSignature>): 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 <pre>ignoreMDCErrors</pre> 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<PGPPublicKeyRing> = 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<PGPPublicKeyRing> {
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()
}
}

View file

@ -53,7 +53,7 @@ class OpenPgpMessageInputStream(
// Add detached signatures only on the outermost OpenPgpMessageInputStream // Add detached signatures only on the outermost OpenPgpMessageInputStream
if (metadata is Message) { if (metadata is Message) {
signatures.addDetachedSignatures(options.detachedSignatures) signatures.addDetachedSignatures(options.getDetachedSignatures())
} }
when(type) { when(type) {
@ -67,7 +67,7 @@ class OpenPgpMessageInputStream(
} }
Type.cleartext_signed -> { Type.cleartext_signed -> {
val multiPassStrategy = options.multiPassStrategy val multiPassStrategy = options.getMultiPassStrategy()
val detachedSignatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage( val detachedSignatures = ClearsignedMessageUtil.detachSignaturesFromInbandClearsignedMessage(
inputStream, multiPassStrategy.messageOutputStream) inputStream, multiPassStrategy.messageOutputStream)
@ -75,7 +75,7 @@ class OpenPgpMessageInputStream(
signatures.addDetachedSignature(signature) signatures.addDetachedSignature(signature)
} }
options.forceNonOpenPgpData() options.isForceNonOpenPgpData()
nestedInputStream = TeeInputStream(multiPassStrategy.messageInputStream, this.signatures) nestedInputStream = TeeInputStream(multiPassStrategy.messageInputStream, this.signatures)
} }
@ -212,7 +212,7 @@ class OpenPgpMessageInputStream(
val encDataList = packetInputStream!!.readEncryptedDataList() val encDataList = packetInputStream!!.readEncryptedDataList()
if (!encDataList.isIntegrityProtected) { if (!encDataList.isIntegrityProtected) {
LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.") LOGGER.warn("Symmetrically Encrypted Data Packet is not integrity-protected.")
if (!options.isIgnoreMDCErrors) { if (!options.isIgnoreMDCErrors()) {
throw MessageNotIntegrityProtectedException() throw MessageNotIntegrityProtectedException()
} }
} }
@ -223,7 +223,7 @@ class OpenPgpMessageInputStream(
" have an anonymous recipient.") " have an anonymous recipient.")
// try custom decryptor factories // 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.") LOGGER.debug("Attempt decryption with custom decryptor factory with key $key.")
esks.pkesks.filter { esks.pkesks.filter {
// find matching PKESK // find matching PKESK
@ -237,8 +237,8 @@ class OpenPgpMessageInputStream(
} }
// try provided session key // try provided session key
if (options.sessionKey != null) { if (options.getSessionKey() != null) {
val sk = options.sessionKey!! val sk = options.getSessionKey()!!
LOGGER.debug("Attempt decryption with provided session key.") LOGGER.debug("Attempt decryption with provided session key.")
throwIfUnacceptable(sk.algorithm) throwIfUnacceptable(sk.algorithm)
@ -260,7 +260,7 @@ class OpenPgpMessageInputStream(
} }
// try passwords // try passwords
for (passphrase in options.decryptionPassphrases) { for (passphrase in options.getDecryptionPassphrases()) {
for (skesk in esks.skesks) { for (skesk in esks.skesks) {
LOGGER.debug("Attempt decryption with provided passphrase") LOGGER.debug("Attempt decryption with provided passphrase")
val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm) val algorithm = SymmetricKeyAlgorithm.requireFromId(skesk.algorithm)
@ -295,7 +295,7 @@ class OpenPgpMessageInputStream(
} }
LOGGER.debug("Attempt decryption using secret key $decryptionKeyId") LOGGER.debug("Attempt decryption using secret key $decryptionKeyId")
val protector = options.getSecretKeyProtector(decryptionKeys) val protector = options.getSecretKeyProtector(decryptionKeys) ?: continue
if (!protector.hasPassphraseFor(keyId)) { if (!protector.hasPassphraseFor(keyId)) {
LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.")
postponedDueToMissingPassphrase.add(secretKey to pkesk) postponedDueToMissingPassphrase.add(secretKey to pkesk)
@ -317,7 +317,7 @@ class OpenPgpMessageInputStream(
} }
LOGGER.debug("Attempt decryption of anonymous PKESK with key $decryptionKeyId.") 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)) { if (!protector.hasPassphraseFor(secretKey.keyID)) {
LOGGER.debug("Missing passphrase for key $decryptionKeyId. Postponing decryption until all other keys were tried.") 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 // Non-interactive mode: Throw an exception with all locked decryption keys
postponedDueToMissingPassphrase.map { postponedDueToMissingPassphrase.map {
SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID) SubkeyIdentifier(getDecryptionKey(it.first.keyID)!!, it.first.keyID)
@ -340,7 +340,7 @@ class OpenPgpMessageInputStream(
if (it.isNotEmpty()) if (it.isNotEmpty())
throw MissingPassphraseException(it.toSet()) throw MissingPassphraseException(it.toSet())
} }
} else if (options.missingKeyPassphraseStrategy == MissingKeyPassphraseStrategy.INTERACTIVE) { } else if (options.getMissingKeyPassphraseStrategy() == MissingKeyPassphraseStrategy.INTERACTIVE) {
for ((secretKey, pkesk) in postponedDueToMissingPassphrase) { for ((secretKey, pkesk) in postponedDueToMissingPassphrase) {
val keyId = secretKey.keyID val keyId = secretKey.keyID
val decryptionKeys = getDecryptionKey(keyId)!! val decryptionKeys = getDecryptionKey(keyId)!!
@ -532,7 +532,7 @@ class OpenPgpMessageInputStream(
return MessageMetadata((metadata as Message)) 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 { it.any {
k -> k.keyID == keyId k -> k.keyID == keyId
}.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any { }.and(PGPainless.inspectKeyRing(it).decryptionSubkeys.any {
@ -543,7 +543,7 @@ class OpenPgpMessageInputStream(
private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List<Pair<PGPSecretKeyRing, PGPSecretKey>> { private fun findPotentialDecryptionKeys(pkesk: PGPPublicKeyEncryptedData): List<Pair<PGPSecretKeyRing, PGPSecretKey>> {
val algorithm = pkesk.algorithm val algorithm = pkesk.algorithm
val candidates = mutableListOf<Pair<PGPSecretKeyRing, PGPSecretKey>>() val candidates = mutableListOf<Pair<PGPSecretKeyRing, PGPSecretKey>>()
options.decryptionKeys.forEach { options.getDecryptionKeys().forEach {
val info = PGPainless.inspectKeyRing(it) val info = PGPainless.inspectKeyRing(it)
for (key in info.decryptionSubkeys) { for (key in info.decryptionSubkeys) {
if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) { if (key.algorithm == algorithm && info.isSecretKeyAvailable(key.keyID)) {
@ -679,7 +679,7 @@ class OpenPgpMessageInputStream(
SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID)) SubkeyIdentifier(check.verificationKeys, check.onePassSignature.keyID))
try { try {
SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter())
.verify(signature) .verify(signature)
CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy) CertificateValidator.validateCertificateAndVerifyOnePassSignature(check, policy)
LOGGER.debug("Acceptable signature by key ${verification.signingKey}") LOGGER.debug("Acceptable signature by key ${verification.signingKey}")
@ -713,13 +713,13 @@ class OpenPgpMessageInputStream(
} }
fun findCertificate(keyId: Long): PGPPublicKeyRing? { fun findCertificate(keyId: Long): PGPPublicKeyRing? {
val cert = options.certificateSource.getCertificate(keyId) val cert = options.getCertificateSource().getCertificate(keyId)
if (cert != null) { if (cert != null) {
return cert return cert
} }
if (options.missingCertificateCallback != null) { if (options.getMissingCertificateCallback() != null) {
return options.missingCertificateCallback!!.onMissingPublicKeyEncountered(keyId) return options.getMissingCertificateCallback()!!.onMissingPublicKeyEncountered(keyId)
} }
return null // TODO: Missing cert for sig return null // TODO: Missing cert for sig
} }
@ -772,7 +772,7 @@ class OpenPgpMessageInputStream(
for (detached in detachedSignatures) { for (detached in detachedSignatures) {
val verification = SignatureVerification(detached.signature, detached.signingKeyIdentifier) val verification = SignatureVerification(detached.signature, detached.signingKeyIdentifier)
try { try {
SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter())
.verify(detached.signature) .verify(detached.signature)
CertificateValidator.validateCertificateAndVerifyInitializedSignature( CertificateValidator.validateCertificateAndVerifyInitializedSignature(
detached.signature, KeyRingUtils.publicKeys(detached.signingKeyRing), policy) detached.signature, KeyRingUtils.publicKeys(detached.signingKeyRing), policy)
@ -787,7 +787,7 @@ class OpenPgpMessageInputStream(
for (prepended in prependedSignatures) { for (prepended in prependedSignatures) {
val verification = SignatureVerification(prepended.signature, prepended.signingKeyIdentifier) val verification = SignatureVerification(prepended.signature, prepended.signingKeyIdentifier)
try { try {
SignatureValidator.signatureWasCreatedInBounds(options.verifyNotBefore, options.verifyNotAfter) SignatureValidator.signatureWasCreatedInBounds(options.getVerifyNotBefore(), options.getVerifyNotAfter())
.verify(prepended.signature) .verify(prepended.signature)
CertificateValidator.validateCertificateAndVerifyInitializedSignature( CertificateValidator.validateCertificateAndVerifyInitializedSignature(
prepended.signature, KeyRingUtils.publicKeys(prepended.signingKeyRing), policy) prepended.signature, KeyRingUtils.publicKeys(prepended.signingKeyRing), policy)
@ -877,7 +877,7 @@ class OpenPgpMessageInputStream(
val openPgpIn = OpenPgpInputStream(inputStream) val openPgpIn = OpenPgpInputStream(inputStream)
openPgpIn.reset() openPgpIn.reset()
if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData) { if (openPgpIn.isNonOpenPgp || options.isForceNonOpenPgpData()) {
return OpenPgpMessageInputStream(Type.non_openpgp, openPgpIn, options, metadata, policy) return OpenPgpMessageInputStream(Type.non_openpgp, openPgpIn, options, metadata, policy)
} }

View file

@ -14,6 +14,7 @@ import org.pgpainless.key.OpenPgpFingerprint
import org.pgpainless.key.util.RevocationAttributes.Reason import org.pgpainless.key.util.RevocationAttributes.Reason
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil
import org.pgpainless.util.ArmorUtils import org.pgpainless.util.ArmorUtils
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
@ -127,6 +128,7 @@ class SignatureUtils {
} }
@JvmStatic @JvmStatic
@Throws(IOException::class, PGPException::class)
fun readSignatures(inputStream: InputStream): List<PGPSignature> { fun readSignatures(inputStream: InputStream): List<PGPSignature> {
return readSignatures(inputStream, MAX_ITERATIONS) return readSignatures(inputStream, MAX_ITERATIONS)
} }

View file

@ -7,7 +7,6 @@ package org.pgpainless.decryption_verification;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; 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.assertTrue;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ -59,29 +58,6 @@ public class MissingPassphraseForDecryptionTest {
message = out.toByteArray(); 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 @Test
public void interactiveStrategy() throws PGPException, IOException { public void interactiveStrategy() throws PGPException, IOException {
// interactive callback // interactive callback