mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-09-12 19:59:38 +02:00
Added SingleSecretKeyRingProtector to resolve a situation when we want to know all failed private keys during the decryption process.
This commit is contained in:
parent
4e16cf13c5
commit
e36d5e849c
5 changed files with 116 additions and 13 deletions
|
@ -24,6 +24,7 @@ import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
|||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.pgpainless.exception.NotYetImplementedException;
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.SingleSecretKeyRingProtector;
|
||||
import org.pgpainless.signature.SignatureUtils;
|
||||
import org.pgpainless.util.Passphrase;
|
||||
|
||||
|
@ -195,15 +196,22 @@ public class ConsumerOptions {
|
|||
}
|
||||
|
||||
/**
|
||||
* Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it
|
||||
* Add a key ring for message decryption. If the key ring is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it
|
||||
* when needed.
|
||||
*
|
||||
* @param key key
|
||||
* @param keyRing key ring
|
||||
* @param keyRingProtector protector for the secret key
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) {
|
||||
decryptionKeys.put(key, keyRingProtector);
|
||||
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing keyRing, @Nonnull SecretKeyRingProtector keyRingProtector) {
|
||||
decryptionKeys.forEach((key, value) -> {
|
||||
if (value instanceof SingleSecretKeyRingProtector && value != keyRingProtector){
|
||||
throw new IllegalStateException("SingleSecretKeyRingProtector is found. " +
|
||||
"Please specify the same SecretKeyRingProtector for all keys.");
|
||||
}
|
||||
});
|
||||
|
||||
decryptionKeys.put(keyRing, keyRingProtector);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,10 +50,12 @@ import org.pgpainless.exception.MissingLiteralDataException;
|
|||
import org.pgpainless.exception.SignatureValidationException;
|
||||
import org.pgpainless.exception.UnacceptableAlgorithmException;
|
||||
import org.pgpainless.exception.WrongConsumingMethodException;
|
||||
import org.pgpainless.exception.WrongPassphraseException;
|
||||
import org.pgpainless.implementation.ImplementationFactory;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
import org.pgpainless.key.info.KeyRingInfo;
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.SingleSecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.UnlockSecretKey;
|
||||
import org.pgpainless.signature.DetachedSignatureCheck;
|
||||
import org.pgpainless.signature.OnePassSignatureCheck;
|
||||
|
@ -262,7 +264,7 @@ public final class DecryptionStreamFactory {
|
|||
private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException {
|
||||
PGPSignatureList signatureList = null;
|
||||
Object pgpObject = objectFactory.nextObject();
|
||||
while (pgpObject != null && signatureList == null) {
|
||||
while (pgpObject != null && signatureList == null) {
|
||||
if (pgpObject instanceof PGPSignatureList) {
|
||||
signatureList = (PGPSignatureList) pgpObject;
|
||||
} else {
|
||||
|
@ -397,6 +399,14 @@ public final class DecryptionStreamFactory {
|
|||
encryptedSessionKey = publicKeyEncryptedData;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!options.getDecryptionKeys().isEmpty()) {
|
||||
SecretKeyRingProtector firstPeeked = options.getSecretKeyProtector(options.getDecryptionKeys().iterator().next());
|
||||
|
||||
if (decryptionKey == null && firstPeeked instanceof SingleSecretKeyRingProtector) {
|
||||
throw new WrongPassphraseException(((SingleSecretKeyRingProtector) firstPeeked).getFailedKeyIds());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decryptWith(encryptedSessionKey, decryptionKey);
|
||||
|
@ -407,16 +417,15 @@ public final class DecryptionStreamFactory {
|
|||
* If the secret key is encrypted and the secret key protector does not have a passphrase available and the boolean
|
||||
* postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key
|
||||
* identifier to the postponed list.
|
||||
*
|
||||
* <p>
|
||||
* This method only returns a non-null private key, if the private key is able to decrypt the message successfully.
|
||||
*
|
||||
* @param secretKeys secret key ring
|
||||
* @param secretKey secret key
|
||||
* @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key
|
||||
* @param postponed list of postponed decryptions due to missing secret key passphrases
|
||||
* @param secretKeys secret key ring
|
||||
* @param secretKey secret key
|
||||
* @param publicKeyEncryptedData encrypted data which is tried to decrypt using the secret key
|
||||
* @param postponed list of postponed decryptions due to missing secret key passphrases
|
||||
* @param postponeIfMissingPassphrase flag to specify whether missing secret key passphrases should result in postponed decryption
|
||||
* @return private key if decryption is successful, null if decryption is unsuccessful or postponed
|
||||
*
|
||||
* @throws PGPException in case of an OpenPGP error
|
||||
*/
|
||||
private PGPPrivateKey tryPublicKeyDecryption(
|
||||
|
@ -434,8 +443,16 @@ public final class DecryptionStreamFactory {
|
|||
return null;
|
||||
}
|
||||
|
||||
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(
|
||||
secretKey, protector.getDecryptor(secretKey.getKeyID()));
|
||||
PGPPrivateKey privateKey;
|
||||
try {
|
||||
privateKey = UnlockSecretKey.unlockSecretKey(
|
||||
secretKey, protector.getDecryptor(secretKey.getKeyID()));
|
||||
} catch (Exception e) {
|
||||
if (protector instanceof SingleSecretKeyRingProtector) {
|
||||
((SingleSecretKeyRingProtector) protector).addFailedKeyId(secretKey.getKeyID(), e);
|
||||
return null;
|
||||
} else throw e;
|
||||
}
|
||||
|
||||
// test if we have the right private key
|
||||
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
|
||||
|
|
|
@ -4,9 +4,15 @@
|
|||
|
||||
package org.pgpainless.exception;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
|
||||
public class WrongPassphraseException extends PGPException {
|
||||
private Map<Long, Exception> keyIds = Collections.emptyMap();
|
||||
|
||||
public WrongPassphraseException(String message) {
|
||||
super(message);
|
||||
|
@ -14,9 +20,21 @@ public class WrongPassphraseException extends PGPException {
|
|||
|
||||
public WrongPassphraseException(long keyId, PGPException cause) {
|
||||
this("Wrong passphrase provided for key " + Long.toHexString(keyId), cause);
|
||||
this.keyIds = new HashMap<>();
|
||||
this.keyIds.put(keyId, cause);
|
||||
}
|
||||
|
||||
public WrongPassphraseException(Map<Long, Exception> keyIds) {
|
||||
this("Wrong passphrase provided for keys: " +
|
||||
keyIds.keySet().stream().map(Long::toHexString).collect(Collectors.joining(", ")));
|
||||
this.keyIds = keyIds;
|
||||
}
|
||||
|
||||
public WrongPassphraseException(String message, PGPException cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public Map<Long, Exception> getKeyIds() {
|
||||
return keyIds;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package org.pgpainless.key.protection;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* This interface assumes that all handled keys will use a single
|
||||
* realization of {@link SecretKeyRingProtector}.
|
||||
*/
|
||||
public interface SingleSecretKeyRingProtector {
|
||||
Map<Long, Exception> failedKeyIds = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Return a map that contains a key id of each key that was not unlocked.
|
||||
*
|
||||
* @return a map of key ids.
|
||||
*/
|
||||
@Nonnull
|
||||
default Map<Long, Exception> getFailedKeyIds() {
|
||||
return failedKeyIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key id of some key that was not unlocked due to {@code e}
|
||||
*
|
||||
* @param keyId the key id
|
||||
* @param e an instance of {@link Exception}
|
||||
*/
|
||||
default void addFailedKeyId(long keyId, Exception e) {
|
||||
failedKeyIds.put(keyId, e);
|
||||
}
|
||||
}
|
|
@ -5,22 +5,27 @@
|
|||
package org.pgpainless.decryption_verification;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||
import org.bouncycastle.util.io.Streams;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.exception.WrongPassphraseException;
|
||||
import org.pgpainless.key.protection.CachingSecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.SingleSecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider;
|
||||
import org.pgpainless.util.Passphrase;
|
||||
|
||||
|
@ -177,6 +182,21 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest {
|
|||
assertEquals(PLAINTEXT, out.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingPassphraseFirstAndSecond() throws PGPException, IOException {
|
||||
PGPSecretKeyRingCollection keyRings = new PGPSecretKeyRingCollection(Arrays.asList(k1, k2));
|
||||
assertThrows(WrongPassphraseException.class, () -> {
|
||||
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
|
||||
.onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8)))
|
||||
.withOptions(new ConsumerOptions()
|
||||
.addDecryptionKeys(keyRings, new SingleCachingSecretKeyRingProtector()));
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
Streams.pipeAll(decryptionStream, out);
|
||||
decryptionStream.close();
|
||||
}, "Wrong passphrase provided for keys: a7c78cc7690fcc46, 10da90900b1cec68");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void messagePassphraseFirst() throws PGPException, IOException {
|
||||
SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() {
|
||||
|
@ -207,4 +227,8 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest {
|
|||
|
||||
assertEquals(PLAINTEXT, out.toString());
|
||||
}
|
||||
|
||||
private static class SingleCachingSecretKeyRingProtector extends CachingSecretKeyRingProtector
|
||||
implements SingleSecretKeyRingProtector {
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue