1
0
Fork 0
mirror of https://github.com/pgpainless/pgpainless.git synced 2024-12-26 21:07:58 +01:00

Improve CachingSecretKeyRingProtector

This commit is contained in:
Paul Schaub 2021-05-14 18:55:26 +02:00
parent 95121e2a55
commit 9358e58fb3
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
6 changed files with 177 additions and 76 deletions

View file

@ -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<Long, Passphrase> passphraseMap = Collections.singletonMap(keyId, oldPassphrase);
SecretKeyRingProtector protector = new PassphraseMapKeyRingProtector(
SecretKeyRingProtector protector = new CachingSecretKeyRingProtector(
passphraseMap, oldProtectionSettings, null);
return new WithKeyRingEncryptionSettingsImpl(keyId, protector);

View file

@ -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<Long, Passphrase> cache = new HashMap<>();
private final SecretKeyRingProtector protector;
private final SecretKeyPassphraseProvider provider;
public CachingSecretKeyRingProtector(@Nonnull Map<Long, Passphrase> 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<PGPPublicKey> 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<PGPPublicKey> 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);
}
}

View file

@ -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<Long, Passphrase> cache = new HashMap<>();
private final SecretKeyRingProtector protector;
private final SecretKeyPassphraseProvider provider;
public PassphraseMapKeyRingProtector(@Nonnull Map<Long, Passphrase> 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<Long, Passphrase> passphrases, @Nonnull KeyRingProtectionSettings protectionSettings, @Nullable SecretKeyPassphraseProvider missingPassphraseCallback) {
super(passphrases, protectionSettings, missingPassphraseCallback);
}
}

View file

@ -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<Long, Passphrase> passphraseMap) {
return new PassphraseMapKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null);
return new CachingSecretKeyRingProtector(passphraseMap, KeyRingProtectionSettings.secureDefaultSettings(), null);
}
}

View file

@ -98,7 +98,7 @@ public class SecretKeyRingProtectorTest {
public void testFromPassphraseMap() {
Map<Long, Passphrase> 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<Long, Passphrase> passphraseMap = new ConcurrentHashMap<>();
passphraseMap.put(1L, Passphrase.emptyPassphrase());
PassphraseMapKeyRingProtector protector = new PassphraseMapKeyRingProtector(passphraseMap,
CachingSecretKeyRingProtector protector = new CachingSecretKeyRingProtector(passphraseMap,
KeyRingProtectionSettings.secureDefaultSettings(), new SecretKeyPassphraseProvider() {
@Nullable
@Override

View file

@ -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();