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}