001// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.decryption_verification;
006
007import java.io.IOException;
008import java.io.InputStream;
009import java.util.Collections;
010import java.util.Date;
011import java.util.HashMap;
012import java.util.HashSet;
013import java.util.List;
014import java.util.Map;
015import java.util.Set;
016import javax.annotation.Nonnull;
017import javax.annotation.Nullable;
018
019import org.bouncycastle.openpgp.PGPException;
020import org.bouncycastle.openpgp.PGPPublicKeyRing;
021import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
022import org.bouncycastle.openpgp.PGPSecretKeyRing;
023import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
024import org.bouncycastle.openpgp.PGPSignature;
025import org.pgpainless.decryption_verification.cleartext_signatures.InMemoryMultiPassStrategy;
026import org.pgpainless.decryption_verification.cleartext_signatures.MultiPassStrategy;
027import org.pgpainless.key.protection.SecretKeyRingProtector;
028import org.pgpainless.signature.SignatureUtils;
029import org.pgpainless.util.Passphrase;
030import org.pgpainless.util.SessionKey;
031
032/**
033 * Options for decryption and signature verification.
034 */
035public class ConsumerOptions {
036
037
038    private boolean ignoreMDCErrors = false;
039
040    private Date verifyNotBefore = null;
041    private Date verifyNotAfter = new Date();
042
043    // Set of verification keys
044    private final Set<PGPPublicKeyRing> certificates = new HashSet<>();
045    private final Set<PGPSignature> detachedSignatures = new HashSet<>();
046    private MissingPublicKeyCallback missingCertificateCallback = null;
047
048    // Session key for decryption without passphrase/key
049    private SessionKey sessionKey = null;
050
051    private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
052    private final Set<Passphrase> decryptionPassphrases = new HashSet<>();
053    private MissingKeyPassphraseStrategy missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE;
054
055    private MultiPassStrategy multiPassStrategy = new InMemoryMultiPassStrategy();
056    private boolean cleartextSigned;
057
058    /**
059     * Consider signatures on the message made before the given timestamp invalid.
060     * Null means no limitation.
061     *
062     * @param timestamp timestamp
063     * @return options
064     */
065    public ConsumerOptions verifyNotBefore(Date timestamp) {
066        this.verifyNotBefore = timestamp;
067        return this;
068    }
069
070    /**
071     * Return the earliest creation date on which signatures on the message are considered valid.
072     * Signatures made earlier than this date are considered invalid.
073     *
074     * @return earliest allowed signature creation date or null
075     */
076    public @Nullable Date getVerifyNotBefore() {
077        return verifyNotBefore;
078    }
079
080    /**
081     * Consider signatures on the message made after the given timestamp invalid.
082     * Null means no limitation.
083     *
084     * @param timestamp timestamp
085     * @return options
086     */
087    public ConsumerOptions verifyNotAfter(Date timestamp) {
088        this.verifyNotAfter = timestamp;
089        return this;
090    }
091
092    /**
093     * Return the latest possible creation date on which signatures made on the message are considered valid.
094     * Signatures made later than this date are considered invalid.
095     *
096     * @return Latest possible creation date or null.
097     */
098    public Date getVerifyNotAfter() {
099        return verifyNotAfter;
100    }
101
102    /**
103     * Add a certificate (public key ring) for signature verification.
104     *
105     * @param verificationCert certificate for signature verification
106     * @return options
107     */
108    public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) {
109        this.certificates.add(verificationCert);
110        return this;
111    }
112
113    /**
114     * Add a set of certificates (public key rings) for signature verification.
115     *
116     * @param verificationCerts certificates for signature verification
117     * @return options
118     */
119    public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) {
120        for (PGPPublicKeyRing certificate : verificationCerts) {
121            addVerificationCert(certificate);
122        }
123        return this;
124    }
125
126    public ConsumerOptions addVerificationOfDetachedSignatures(InputStream signatureInputStream) throws IOException, PGPException {
127        List<PGPSignature> signatures = SignatureUtils.readSignatures(signatureInputStream);
128        return addVerificationOfDetachedSignatures(signatures);
129    }
130
131    public ConsumerOptions addVerificationOfDetachedSignatures(List<PGPSignature> detachedSignatures) {
132        for (PGPSignature signature : detachedSignatures) {
133            addVerificationOfDetachedSignature(signature);
134        }
135        return this;
136    }
137
138    /**
139     * Add a detached signature for the signature verification process.
140     *
141     * @param detachedSignature detached signature
142     * @return options
143     */
144    public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) {
145        detachedSignatures.add(detachedSignature);
146        return this;
147    }
148
149    /**
150     * Set a callback that's used when a certificate (public key) is missing for signature verification.
151     *
152     * @param callback callback
153     * @return options
154     */
155    public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) {
156        this.missingCertificateCallback = callback;
157        return this;
158    }
159
160
161    /**
162     * Attempt decryption using a session key.
163     *
164     * Note: PGPainless does not yet support decryption with session keys.
165     *
166     * @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-2.1">RFC4880 on Session Keys</a>
167     *
168     * @param sessionKey session key
169     * @return options
170     */
171    public ConsumerOptions setSessionKey(@Nonnull SessionKey sessionKey) {
172        this.sessionKey = sessionKey;
173        return this;
174    }
175
176    /**
177     * Return the session key.
178     *
179     * @return session key or null
180     */
181    public @Nullable SessionKey getSessionKey() {
182        return sessionKey;
183    }
184
185    /**
186     * Add a key for message decryption.
187     * The key is expected to be unencrypted.
188     *
189     * @param key unencrypted key
190     * @return options
191     */
192    public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) {
193        return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys());
194    }
195
196    /**
197     * Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it
198     * when needed.
199     *
200     * @param key key
201     * @param keyRingProtector protector for the secret key
202     * @return options
203     */
204    public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) {
205        decryptionKeys.put(key, keyRingProtector);
206        return this;
207    }
208
209    /**
210     * Add the keys in the provided key collection for message decryption.
211     *
212     * @param keys key collection
213     * @param keyRingProtector protector for encrypted secret keys
214     * @return options
215     */
216    public ConsumerOptions addDecryptionKeys(@Nonnull PGPSecretKeyRingCollection keys, @Nonnull SecretKeyRingProtector keyRingProtector) {
217        for (PGPSecretKeyRing key : keys) {
218            addDecryptionKey(key, keyRingProtector);
219        }
220        return this;
221    }
222
223    /**
224     * Add a passphrase for message decryption.
225     * This passphrase will be used to try to decrypt messages which were symmetrically encrypted for a passphrase.
226     *
227     * @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.7">Symmetrically Encrypted Data Packet</a>
228     *
229     * @param passphrase passphrase
230     * @return options
231     */
232    public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) {
233        decryptionPassphrases.add(passphrase);
234        return this;
235    }
236
237    public @Nonnull Set<PGPSecretKeyRing> getDecryptionKeys() {
238        return Collections.unmodifiableSet(decryptionKeys.keySet());
239    }
240
241    public @Nonnull Set<Passphrase> getDecryptionPassphrases() {
242        return Collections.unmodifiableSet(decryptionPassphrases);
243    }
244
245    public @Nonnull Set<PGPPublicKeyRing> getCertificates() {
246        return Collections.unmodifiableSet(certificates);
247    }
248
249    public @Nullable MissingPublicKeyCallback getMissingCertificateCallback() {
250        return missingCertificateCallback;
251    }
252
253    public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
254        return decryptionKeys.get(decryptionKeyRing);
255    }
256
257    public @Nonnull Set<PGPSignature> getDetachedSignatures() {
258        return Collections.unmodifiableSet(detachedSignatures);
259    }
260
261    /**
262     * By default, PGPainless will require encrypted messages to make use of SEIP data packets.
263     * Those are Symmetrically Encrypted Integrity Protected Data packets.
264     * Symmetrically Encrypted Data Packets without integrity protection are rejected by default.
265     * Furthermore, PGPainless will throw an exception if verification of the MDC error detection code of the SEIP packet
266     * fails.
267     *
268     * Failure of MDC verification indicates a tampered ciphertext, which might be the cause of an attack or data corruption.
269     *
270     * This method can be used to ignore MDC errors and allow PGPainless to consume encrypted data without integrity protection.
271     * If the flag <pre>ignoreMDCErrors</pre> is set to true, PGPainless will
272     * <ul>
273     *     <li>not throw exceptions for SEIP packets with tampered ciphertext</li>
274     *     <li>not throw exceptions for SEIP packets with tampered MDC</li>
275     *     <li>not throw exceptions for MDCs with bad CTB</li>
276     *     <li>not throw exceptions for MDCs with bad length</li>
277     * </ul>
278     *
279     * It will however still throw an exception if it encounters a SEIP packet with missing or truncated MDC
280     *
281     * @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-5.13">Sym. Encrypted Integrity Protected Data Packet</a>
282     * @param ignoreMDCErrors true if MDC errors or missing MDCs shall be ignored, false otherwise.
283     * @return options
284     */
285    @Deprecated
286    public ConsumerOptions setIgnoreMDCErrors(boolean ignoreMDCErrors) {
287        this.ignoreMDCErrors = ignoreMDCErrors;
288        return this;
289    }
290
291    /**
292     * Return true, if PGPainless is ignoring MDC errors.
293     *
294     * @return ignore mdc errors
295     */
296    boolean isIgnoreMDCErrors() {
297        return ignoreMDCErrors;
298    }
299
300    /**
301     * Specify the {@link MissingKeyPassphraseStrategy}.
302     * This strategy defines, how missing passphrases for unlocking secret keys are handled.
303     * In interactive mode ({@link MissingKeyPassphraseStrategy#INTERACTIVE}) PGPainless will try to obtain missing
304     * passphrases for secret keys via the {@link SecretKeyRingProtector SecretKeyRingProtectors}
305     * {@link org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider} callback.
306     *
307     * In non-interactice mode ({@link MissingKeyPassphraseStrategy#THROW_EXCEPTION}, PGPainless will instead
308     * throw a {@link org.pgpainless.exception.MissingPassphraseException} containing the ids of all keys for which
309     * there are missing passphrases.
310     *
311     * @param strategy strategy
312     * @return options
313     */
314    public ConsumerOptions setMissingKeyPassphraseStrategy(MissingKeyPassphraseStrategy strategy) {
315        this.missingKeyPassphraseStrategy = strategy;
316        return this;
317    }
318
319    /**
320     * Return the currently configured {@link MissingKeyPassphraseStrategy}.
321     *
322     * @return missing key passphrase strategy
323     */
324    MissingKeyPassphraseStrategy getMissingKeyPassphraseStrategy() {
325        return missingKeyPassphraseStrategy;
326    }
327
328    /**
329     * Set a custom multi-pass strategy for processing cleartext-signed messages.
330     * Uses {@link InMemoryMultiPassStrategy} by default.
331     *
332     * @param multiPassStrategy multi-pass caching strategy
333     * @return builder
334     */
335    public ConsumerOptions setMultiPassStrategy(@Nonnull MultiPassStrategy multiPassStrategy) {
336        this.multiPassStrategy = multiPassStrategy;
337        return this;
338    }
339
340    /**
341     * Return the currently configured {@link MultiPassStrategy}.
342     * Defaults to {@link InMemoryMultiPassStrategy}.
343     *
344     * @return multi-pass strategy
345     */
346    public MultiPassStrategy getMultiPassStrategy() {
347        return multiPassStrategy;
348    }
349
350    /**
351     * INTERNAL method to mark cleartext signed messages.
352     * Do not call this manually.
353     */
354    public void setIsCleartextSigned() {
355        this.cleartextSigned = true;
356    }
357
358    /**
359     * Return true if the message is cleartext signed.
360     * @return cleartext signed
361     */
362    public boolean isCleartextSigned() {
363        return this.cleartextSigned;
364    }
365}