001// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
002//
003// SPDX-License-Identifier: Apache-2.0
004
005package org.pgpainless.key.protection;
006
007import java.util.HashMap;
008import java.util.Iterator;
009import java.util.Map;
010import javax.annotation.Nonnull;
011import javax.annotation.Nullable;
012
013import org.bouncycastle.openpgp.PGPException;
014import org.bouncycastle.openpgp.PGPKeyRing;
015import org.bouncycastle.openpgp.PGPPublicKey;
016import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
017import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
018import org.pgpainless.key.OpenPgpFingerprint;
019import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider;
020import org.pgpainless.util.Passphrase;
021
022/**
023 * Implementation of the {@link SecretKeyRingProtector} which holds a map of key ids and their passwords.
024 * In case the needed passphrase is not contained in the map, the {@code missingPassphraseCallback} will be consulted,
025 * and the passphrase is added to the map.
026 *
027 * If you need to unlock multiple {@link PGPKeyRing PGPKeyRings}, it is advised to use a separate
028 * {@link CachingSecretKeyRingProtector} instance for each ring.
029 */
030public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, SecretKeyPassphraseProvider {
031
032    private final Map<Long, Passphrase> cache = new HashMap<>();
033    private final SecretKeyRingProtector protector;
034    private final SecretKeyPassphraseProvider provider;
035
036    public CachingSecretKeyRingProtector() {
037        this(null);
038    }
039
040    public CachingSecretKeyRingProtector(@Nullable SecretKeyPassphraseProvider missingPassphraseCallback) {
041        this(
042                new HashMap<>(),
043                KeyRingProtectionSettings.secureDefaultSettings(),
044                missingPassphraseCallback
045        );
046    }
047
048    public CachingSecretKeyRingProtector(@Nonnull Map<Long, Passphrase> passphrases,
049                                         @Nonnull KeyRingProtectionSettings protectionSettings,
050                                         @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) {
051        this.cache.putAll(passphrases);
052        this.protector = new PasswordBasedSecretKeyRingProtector(protectionSettings, this);
053        this.provider = missingPassphraseCallback;
054    }
055
056    /**
057     * Add a passphrase to the cache.
058     * If the cache already contains a passphrase for the given key-id, a {@link IllegalArgumentException} is thrown.
059     * The reason for this is to prevent accidental override of passphrases when dealing with multiple key rings
060     * containing a key with the same key-id but different passphrases.
061     *
062     * If you can ensure that there will be no key-id clash, and you want to replace the passphrase, you can use
063     * {@link #replacePassphrase(Long, Passphrase)} to replace the passphrase.
064     *
065     * @param keyId id of the key
066     * @param passphrase passphrase
067     */
068    public void addPassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) {
069        if (this.cache.containsKey(keyId)) {
070            throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" +
071                    "If you want to replace the passphrase, use replacePassphrase(Long, Passphrase) instead.");
072        }
073        this.cache.put(keyId, passphrase);
074    }
075
076    /**
077     * Replace the passphrase for the given key-id in the cache.
078     *
079     * @param keyId keyId
080     * @param passphrase passphrase
081     */
082    public void replacePassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) {
083        this.cache.put(keyId, passphrase);
084    }
085
086    /**
087     * Remember the given passphrase for all keys in the given key ring.
088     * If for the key-id of any key on the key ring the cache already contains a passphrase, a
089     * {@link IllegalArgumentException} is thrown before any changes are committed to the cache.
090     * This is to prevent accidental passphrase override when dealing with multiple key rings containing
091     * keys with conflicting key-ids.
092     *
093     * If you can ensure that there will be no key-id clashes, and you want to replace the passphrases for the key ring,
094     * use {@link #replacePassphrase(PGPKeyRing, Passphrase)} instead.
095     *
096     * If you need to unlock multiple {@link PGPKeyRing PGPKeyRings}, it is advised to use a separate
097     * {@link CachingSecretKeyRingProtector} instance for each ring.
098     *
099     * @param keyRing key ring
100     * @param passphrase passphrase
101     */
102    public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) {
103        Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
104        // check for existing passphrases before doing anything
105        while (keys.hasNext()) {
106            long keyId = keys.next().getKeyID();
107            if (cache.containsKey(keyId)) {
108                throw new IllegalArgumentException("The cache already holds a passphrase for ID " + Long.toHexString(keyId) + ".\n" +
109                        "If you want to replace the passphrase, use replacePassphrase(PGPKeyRing, Passphrase) instead.");
110            }
111        }
112
113        // only then insert
114        keys = keyRing.getPublicKeys();
115        while (keys.hasNext()) {
116            PGPPublicKey publicKey = keys.next();
117            addPassphrase(publicKey, passphrase);
118        }
119    }
120
121    /**
122     * Replace the cached passphrases for all keys in the key ring with the provided passphrase.
123     *
124     * @param keyRing key ring
125     * @param passphrase passphrase
126     */
127    public void replacePassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) {
128        Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
129        while (keys.hasNext()) {
130            PGPPublicKey publicKey = keys.next();
131            replacePassphrase(publicKey.getKeyID(), passphrase);
132        }
133    }
134
135    /**
136     * Remember the given passphrase for the given (sub-)key.
137     *
138     * @param key key
139     * @param passphrase passphrase
140     */
141    public void addPassphrase(@Nonnull PGPPublicKey key, @Nonnull Passphrase passphrase) {
142        addPassphrase(key.getKeyID(), passphrase);
143    }
144
145    public void addPassphrase(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull Passphrase passphrase) {
146        addPassphrase(fingerprint.getKeyId(), passphrase);
147    }
148
149    /**
150     * Remove a passphrase from the cache.
151     * The passphrase will be cleared and then removed.
152     *
153     * @param keyId id of the key
154     */
155    public void forgetPassphrase(@Nonnull Long keyId) {
156        Passphrase passphrase = cache.remove(keyId);
157        if (passphrase != null) {
158            passphrase.clear();
159        }
160    }
161
162    /**
163     * Forget the passphrase to all keys in the provided key ring.
164     *
165     * @param keyRing key ring
166     */
167    public void forgetPassphrase(@Nonnull PGPKeyRing keyRing) {
168        Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
169        while (keys.hasNext()) {
170            PGPPublicKey publicKey = keys.next();
171            forgetPassphrase(publicKey);
172        }
173    }
174
175    /**
176     * Forget the passphrase of the given public key.
177     *
178     * @param key key
179     */
180    public void forgetPassphrase(@Nonnull PGPPublicKey key) {
181        forgetPassphrase(key.getKeyID());
182    }
183
184    @Override
185    @Nullable
186    public Passphrase getPassphraseFor(Long keyId) {
187        Passphrase passphrase = cache.get(keyId);
188        if (passphrase == null || !passphrase.isValid()) {
189            if (provider == null) {
190                return null;
191            }
192            passphrase = provider.getPassphraseFor(keyId);
193            if (passphrase != null) {
194                cache.put(keyId, passphrase);
195            }
196        }
197        return passphrase;
198    }
199
200    @Override
201    public boolean hasPassphrase(Long keyId) {
202        Passphrase passphrase = cache.get(keyId);
203        return passphrase != null && passphrase.isValid();
204    }
205
206    @Override
207    public boolean hasPassphraseFor(Long keyId) {
208        return hasPassphrase(keyId);
209    }
210
211    @Override
212    @Nullable
213    public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException {
214        return protector.getDecryptor(keyId);
215    }
216
217    @Override
218    @Nullable
219    public PBESecretKeyEncryptor getEncryptor(@Nonnull Long keyId) throws PGPException {
220        return protector.getEncryptor(keyId);
221    }
222}