1
0
Fork 0
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:
DenBond7 2021-10-14 13:05:28 +03:00
parent 4e16cf13c5
commit e36d5e849c
No known key found for this signature in database
GPG key ID: F74FC4E6441BA8C3
5 changed files with 116 additions and 13 deletions

View file

@ -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;
}

View file

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

View file

@ -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;
}
}

View file

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

View file

@ -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 {
}
}