mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-09-13 04:09: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.bouncycastle.openpgp.PGPSignature;
|
||||||
import org.pgpainless.exception.NotYetImplementedException;
|
import org.pgpainless.exception.NotYetImplementedException;
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||||
|
import org.pgpainless.key.protection.SingleSecretKeyRingProtector;
|
||||||
import org.pgpainless.signature.SignatureUtils;
|
import org.pgpainless.signature.SignatureUtils;
|
||||||
import org.pgpainless.util.Passphrase;
|
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.
|
* when needed.
|
||||||
*
|
*
|
||||||
* @param key key
|
* @param keyRing key ring
|
||||||
* @param keyRingProtector protector for the secret key
|
* @param keyRingProtector protector for the secret key
|
||||||
* @return options
|
* @return options
|
||||||
*/
|
*/
|
||||||
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) {
|
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing keyRing, @Nonnull SecretKeyRingProtector keyRingProtector) {
|
||||||
decryptionKeys.put(key, 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,10 +50,12 @@ import org.pgpainless.exception.MissingLiteralDataException;
|
||||||
import org.pgpainless.exception.SignatureValidationException;
|
import org.pgpainless.exception.SignatureValidationException;
|
||||||
import org.pgpainless.exception.UnacceptableAlgorithmException;
|
import org.pgpainless.exception.UnacceptableAlgorithmException;
|
||||||
import org.pgpainless.exception.WrongConsumingMethodException;
|
import org.pgpainless.exception.WrongConsumingMethodException;
|
||||||
|
import org.pgpainless.exception.WrongPassphraseException;
|
||||||
import org.pgpainless.implementation.ImplementationFactory;
|
import org.pgpainless.implementation.ImplementationFactory;
|
||||||
import org.pgpainless.key.SubkeyIdentifier;
|
import org.pgpainless.key.SubkeyIdentifier;
|
||||||
import org.pgpainless.key.info.KeyRingInfo;
|
import org.pgpainless.key.info.KeyRingInfo;
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||||
|
import org.pgpainless.key.protection.SingleSecretKeyRingProtector;
|
||||||
import org.pgpainless.key.protection.UnlockSecretKey;
|
import org.pgpainless.key.protection.UnlockSecretKey;
|
||||||
import org.pgpainless.signature.DetachedSignatureCheck;
|
import org.pgpainless.signature.DetachedSignatureCheck;
|
||||||
import org.pgpainless.signature.OnePassSignatureCheck;
|
import org.pgpainless.signature.OnePassSignatureCheck;
|
||||||
|
@ -262,7 +264,7 @@ public final class DecryptionStreamFactory {
|
||||||
private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException {
|
private PGPSignatureList parseSignatures(PGPObjectFactory objectFactory) throws IOException {
|
||||||
PGPSignatureList signatureList = null;
|
PGPSignatureList signatureList = null;
|
||||||
Object pgpObject = objectFactory.nextObject();
|
Object pgpObject = objectFactory.nextObject();
|
||||||
while (pgpObject != null && signatureList == null) {
|
while (pgpObject != null && signatureList == null) {
|
||||||
if (pgpObject instanceof PGPSignatureList) {
|
if (pgpObject instanceof PGPSignatureList) {
|
||||||
signatureList = (PGPSignatureList) pgpObject;
|
signatureList = (PGPSignatureList) pgpObject;
|
||||||
} else {
|
} else {
|
||||||
|
@ -397,6 +399,14 @@ public final class DecryptionStreamFactory {
|
||||||
encryptedSessionKey = publicKeyEncryptedData;
|
encryptedSessionKey = publicKeyEncryptedData;
|
||||||
break;
|
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);
|
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
|
* 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
|
* postponeIfMissingPassphrase is true, data decryption is postponed by pushing a tuple of the encrypted data decryption key
|
||||||
* identifier to the postponed list.
|
* 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.
|
* 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 secretKeys secret key ring
|
||||||
* @param secretKey secret key
|
* @param secretKey secret key
|
||||||
* @param publicKeyEncryptedData encrypted data which is tried to decrypt using the 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 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
|
* @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
|
* @return private key if decryption is successful, null if decryption is unsuccessful or postponed
|
||||||
*
|
|
||||||
* @throws PGPException in case of an OpenPGP error
|
* @throws PGPException in case of an OpenPGP error
|
||||||
*/
|
*/
|
||||||
private PGPPrivateKey tryPublicKeyDecryption(
|
private PGPPrivateKey tryPublicKeyDecryption(
|
||||||
|
@ -434,8 +443,16 @@ public final class DecryptionStreamFactory {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(
|
PGPPrivateKey privateKey;
|
||||||
secretKey, protector.getDecryptor(secretKey.getKeyID()));
|
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
|
// test if we have the right private key
|
||||||
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
|
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
|
||||||
|
|
|
@ -4,9 +4,15 @@
|
||||||
|
|
||||||
package org.pgpainless.exception;
|
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;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
|
||||||
public class WrongPassphraseException extends PGPException {
|
public class WrongPassphraseException extends PGPException {
|
||||||
|
private Map<Long, Exception> keyIds = Collections.emptyMap();
|
||||||
|
|
||||||
public WrongPassphraseException(String message) {
|
public WrongPassphraseException(String message) {
|
||||||
super(message);
|
super(message);
|
||||||
|
@ -14,9 +20,21 @@ public class WrongPassphraseException extends PGPException {
|
||||||
|
|
||||||
public WrongPassphraseException(long keyId, PGPException cause) {
|
public WrongPassphraseException(long keyId, PGPException cause) {
|
||||||
this("Wrong passphrase provided for key " + Long.toHexString(keyId), 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) {
|
public WrongPassphraseException(String message, PGPException cause) {
|
||||||
super(message, 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;
|
package org.pgpainless.decryption_verification;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
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 static org.junit.jupiter.api.Assertions.fail;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||||
import org.bouncycastle.util.io.Streams;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.junit.jupiter.api.BeforeAll;
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
|
import org.pgpainless.exception.WrongPassphraseException;
|
||||||
import org.pgpainless.key.protection.CachingSecretKeyRingProtector;
|
import org.pgpainless.key.protection.CachingSecretKeyRingProtector;
|
||||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||||
|
import org.pgpainless.key.protection.SingleSecretKeyRingProtector;
|
||||||
import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider;
|
import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider;
|
||||||
import org.pgpainless.util.Passphrase;
|
import org.pgpainless.util.Passphrase;
|
||||||
|
|
||||||
|
@ -177,6 +182,21 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest {
|
||||||
assertEquals(PLAINTEXT, out.toString());
|
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
|
@Test
|
||||||
public void messagePassphraseFirst() throws PGPException, IOException {
|
public void messagePassphraseFirst() throws PGPException, IOException {
|
||||||
SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() {
|
SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() {
|
||||||
|
@ -207,4 +227,8 @@ public class PostponeDecryptionUsingKeyWithMissingPassphraseTest {
|
||||||
|
|
||||||
assertEquals(PLAINTEXT, out.toString());
|
assertEquals(PLAINTEXT, out.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class SingleCachingSecretKeyRingProtector extends CachingSecretKeyRingProtector
|
||||||
|
implements SingleSecretKeyRingProtector {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue