diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java index 20078df6..0adb208a 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/modification/secretkeyring/SecretKeyRingEditor.java @@ -55,7 +55,7 @@ import org.pgpainless.key.generation.KeyRingBuilder; import org.pgpainless.key.generation.KeySpec; import org.pgpainless.key.info.KeyRingInfo; import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; +import org.pgpainless.key.protection.CachingSecretKeyRingProtector; import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector; import org.pgpainless.key.protection.SecretKeyRingProtector; import org.pgpainless.key.protection.UnlockSecretKey; @@ -590,7 +590,7 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface { @Nullable Passphrase oldPassphrase, @Nonnull KeyRingProtectionSettings oldProtectionSettings) { Map passphraseMap = Collections.singletonMap(keyId, oldPassphrase); - SecretKeyRingProtector protector = new PassphraseMapKeyRingProtector( + SecretKeyRingProtector protector = new CachingSecretKeyRingProtector( passphraseMap, oldProtectionSettings, null); return new WithKeyRingEncryptionSettingsImpl(keyId, protector); diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java new file mode 100644 index 00000000..5ad6a5e1 --- /dev/null +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/CachingSecretKeyRingProtector.java @@ -0,0 +1,146 @@ +/* + * Copyright 2018 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.pgpainless.key.protection; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPKeyRing; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; +import org.pgpainless.util.Passphrase; + +/** + * Implementation of the {@link SecretKeyRingProtector} which holds a map of key ids and their passwords. + * In case the needed passphrase is not contained in the map, the {@code missingPassphraseCallback} will be consulted, + * and the passphrase is added to the map. + */ +public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, SecretKeyPassphraseProvider { + + private final Map cache = new HashMap<>(); + private final SecretKeyRingProtector protector; + private final SecretKeyPassphraseProvider provider; + + public CachingSecretKeyRingProtector(@Nonnull Map passphrases, + @Nonnull KeyRingProtectionSettings protectionSettings, + @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { + this.cache.putAll(passphrases); + this.protector = new PasswordBasedSecretKeyRingProtector(protectionSettings, this); + this.provider = missingPassphraseCallback; + } + + /** + * Add a passphrase to the cache. + * + * @param keyId id of the key + * @param passphrase passphrase + */ + public void addPassphrase(@Nonnull Long keyId, @Nullable Passphrase passphrase) { + this.cache.put(keyId, passphrase); + } + + /** + * Remember the given passphrase for all keys in the given key ring. + * + * @param keyRing key ring + * @param passphrase passphrase + */ + public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nullable Passphrase passphrase) { + Iterator keys = keyRing.getPublicKeys(); + while (keys.hasNext()) { + PGPPublicKey publicKey = keys.next(); + addPassphrase(publicKey, passphrase); + } + } + + /** + * Remember the given passphrase for the given (sub-)key. + * + * @param key key + * @param passphrase passphrase + */ + public void addPassphrase(@Nonnull PGPPublicKey key, @Nullable Passphrase passphrase) { + addPassphrase(key.getKeyID(), passphrase); + } + + /** + * Remove a passphrase from the cache. + * The passphrase will be cleared and then removed. + * + * @param keyId id of the key + */ + public void forgetPassphrase(@Nonnull Long keyId) { + Passphrase passphrase = cache.get(keyId); + passphrase.clear(); + cache.remove(keyId); + } + + /** + * Forget the passphrase to all keys in the provided key ring. + * + * @param keyRing key ring + */ + public void forgetPassphrase(@Nonnull PGPKeyRing keyRing) { + Iterator keys = keyRing.getPublicKeys(); + while (keys.hasNext()) { + PGPPublicKey publicKey = keys.next(); + forgetPassphrase(publicKey); + } + } + + /** + * Forget the passphrase of the given public key. + * + * @param key key + */ + public void forgetPassphrase(@Nonnull PGPPublicKey key) { + forgetPassphrase(key.getKeyID()); + } + + @Override + @Nullable + public Passphrase getPassphraseFor(Long keyId) { + Passphrase passphrase = cache.get(keyId); + if (passphrase == null || !passphrase.isValid()) { + if (provider == null) { + return null; + } + passphrase = provider.getPassphraseFor(keyId); + if (passphrase != null) { + cache.put(keyId, passphrase); + } + } + return passphrase; + } + + @Override + @Nullable + public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { + return protector.getDecryptor(keyId); + } + + @Override + @Nullable + public PBESecretKeyEncryptor getEncryptor(@Nonnull Long keyId) throws PGPException { + return protector.getEncryptor(keyId); + } +} diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java index 81606632..fd9b0a4f 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/PassphraseMapKeyRingProtector.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 Paul Schaub. + * Copyright 2021 Paul Schaub. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,83 +15,21 @@ */ package org.pgpainless.key.protection; -import java.util.HashMap; import java.util.Map; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; /** - * Implementation of the {@link SecretKeyRingProtector} which holds a map of key ids and their passwords. - * In case the needed passphrase is not contained in the map, the {@code missingPassphraseCallback} will be consulted, - * and the passphrase is added to the map. + * Stub class for API backwards compatibility. + * @deprecated use {@link CachingSecretKeyRingProtector} instead. */ -public class PassphraseMapKeyRingProtector implements SecretKeyRingProtector, SecretKeyPassphraseProvider { +@Deprecated +public class PassphraseMapKeyRingProtector extends CachingSecretKeyRingProtector { - private final Map cache = new HashMap<>(); - private final SecretKeyRingProtector protector; - private final SecretKeyPassphraseProvider provider; - - public PassphraseMapKeyRingProtector(@Nonnull Map passphrases, - @Nonnull KeyRingProtectionSettings protectionSettings, - @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { - this.cache.putAll(passphrases); - this.protector = new PasswordBasedSecretKeyRingProtector(protectionSettings, this); - this.provider = missingPassphraseCallback; - } - - /** - * Add a passphrase to the cache. - * - * @param keyId id of the key - * @param passphrase passphrase - */ - public void addPassphrase(@Nonnull Long keyId, @Nullable Passphrase passphrase) { - this.cache.put(keyId, passphrase); - } - - /** - * Remove a passphrase from the cache. - * The passphrase will be cleared and then removed. - * - * @param keyId id of the key - */ - public void forgetPassphrase(@Nonnull Long keyId) { - Passphrase passphrase = cache.get(keyId); - passphrase.clear(); - cache.remove(keyId); - } - - @Override - @Nullable - public Passphrase getPassphraseFor(Long keyId) { - Passphrase passphrase = cache.get(keyId); - if (passphrase == null || !passphrase.isValid()) { - if (provider == null) { - return null; - } - passphrase = provider.getPassphraseFor(keyId); - if (passphrase != null) { - cache.put(keyId, passphrase); - } - } - return passphrase; - } - - @Override - @Nullable - public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException { - return protector.getDecryptor(keyId); - } - - @Override - @Nullable - public PBESecretKeyEncryptor getEncryptor(@Nonnull Long keyId) throws PGPException { - return protector.getEncryptor(keyId); + public PassphraseMapKeyRingProtector(@Nonnull Map passphrases, @Nonnull KeyRingProtectionSettings protectionSettings, @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) { + super(passphrases, protectionSettings, missingPassphraseCallback); } } diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java index 92a35400..5ea3d0e6 100644 --- a/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java +++ b/pgpainless-core/src/main/java/org/pgpainless/key/protection/SecretKeyRingProtector.java @@ -15,6 +15,7 @@ */ package org.pgpainless.key.protection; +import java.util.HashMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.annotation.Nullable; @@ -24,6 +25,7 @@ import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; +import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider; import org.pgpainless.util.Passphrase; /** @@ -50,6 +52,21 @@ public interface SecretKeyRingProtector { */ @Nullable PBESecretKeyEncryptor getEncryptor(Long keyId) throws PGPException; + /** + * Return a protector for secret keys. + * The protector maintains an in-memory cache of passphrases and can be extended with new passphrases + * at runtime. + * + * @param missingPassphraseCallback callback that is used to provide missing passphrases. + * @return caching secret key protector + */ + static CachingSecretKeyRingProtector defaultSecretKeyRingProtector(SecretKeyPassphraseProvider missingPassphraseCallback) { + return new CachingSecretKeyRingProtector( + new HashMap<>(), + KeyRingProtectionSettings.secureDefaultSettings(), + missingPassphraseCallback); + } + /** * Use the provided passphrase to lock/unlock all subkeys in the provided key ring. * @@ -92,6 +109,6 @@ public interface SecretKeyRingProtector { * @return protector */ static SecretKeyRingProtector fromPassphraseMap(Map passphraseMap) { - return new PassphraseMapKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null); + return new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null); } } diff --git a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java index 9e04297b..8c85ad69 100644 --- a/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java +++ b/pgpainless-core/src/test/java/org/pgpainless/key/protection/SecretKeyRingProtectorTest.java @@ -98,7 +98,7 @@ public class SecretKeyRingProtectorTest { public void testFromPassphraseMap() { Map passphraseMap = new ConcurrentHashMap<>(); passphraseMap.put(1L, Passphrase.emptyPassphrase()); - PassphraseMapKeyRingProtector protector = (PassphraseMapKeyRingProtector) SecretKeyRingProtector.fromPassphraseMap(passphraseMap); + CachingSecretKeyRingProtector protector = (CachingSecretKeyRingProtector) SecretKeyRingProtector.fromPassphraseMap(passphraseMap); assertNotNull(protector.getPassphraseFor(1L)); assertNull(protector.getPassphraseFor(5L)); @@ -114,7 +114,7 @@ public class SecretKeyRingProtectorTest { public void testMissingPassphraseCallback() { Map passphraseMap = new ConcurrentHashMap<>(); passphraseMap.put(1L, Passphrase.emptyPassphrase()); - PassphraseMapKeyRingProtector protector = new PassphraseMapKeyRingProtector(passphraseMap, + CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), new SecretKeyPassphraseProvider() { @Nullable @Override diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java index afc0dd74..f49d17cb 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/commands/Encrypt.java @@ -39,7 +39,7 @@ import org.pgpainless.encryption_signing.EncryptionBuilderInterface; import org.pgpainless.encryption_signing.EncryptionStream; import org.pgpainless.key.OpenPgpV4Fingerprint; import org.pgpainless.key.protection.KeyRingProtectionSettings; -import org.pgpainless.key.protection.PassphraseMapKeyRingProtector; +import org.pgpainless.key.protection.CachingSecretKeyRingProtector; import org.pgpainless.util.Passphrase; import picocli.CommandLine; @@ -148,7 +148,7 @@ public class Encrypt implements Runnable { .usingSecureAlgorithms(); EncryptionBuilderInterface.Armor builder_armor; if (signWith.length != 0) { - EncryptionBuilderInterface.DocumentType documentType = builder.signWith(new PassphraseMapKeyRingProtector(passphraseMap, + EncryptionBuilderInterface.DocumentType documentType = builder.signWith(new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null), secretKeys); if (type == Type.text || type == Type.mime) { builder_armor = documentType.signCanonicalText();