mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-23 12:52:07 +01:00
Postpone decryption of PKESK if secret key passphrase is missing and try next PKESK first before passphrase retrieval using callback
Fixes #186
This commit is contained in:
parent
81379a5176
commit
ce645fc429
14 changed files with 466 additions and 69 deletions
|
@ -257,7 +257,7 @@ public class ConsumerOptions {
|
||||||
return missingCertificateCallback;
|
return missingCertificateCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public @Nullable SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
|
public @Nonnull SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
|
||||||
return decryptionKeys.get(decryptionKeyRing);
|
return decryptionKeys.get(decryptionKeyRing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,7 @@ import org.pgpainless.exception.WrongConsumingMethodException;
|
||||||
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.UnlockSecretKey;
|
import org.pgpainless.key.protection.UnlockSecretKey;
|
||||||
import org.pgpainless.signature.DetachedSignature;
|
import org.pgpainless.signature.DetachedSignature;
|
||||||
import org.pgpainless.signature.OnePassSignatureCheck;
|
import org.pgpainless.signature.OnePassSignatureCheck;
|
||||||
|
@ -68,6 +69,7 @@ import org.pgpainless.signature.SignatureUtils;
|
||||||
import org.pgpainless.util.CRCingArmoredInputStreamWrapper;
|
import org.pgpainless.util.CRCingArmoredInputStreamWrapper;
|
||||||
import org.pgpainless.util.IntegrityProtectedInputStream;
|
import org.pgpainless.util.IntegrityProtectedInputStream;
|
||||||
import org.pgpainless.util.Passphrase;
|
import org.pgpainless.util.Passphrase;
|
||||||
|
import org.pgpainless.util.Tuple;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -276,93 +278,173 @@ public final class DecryptionStreamFactory {
|
||||||
|
|
||||||
PGPPrivateKey decryptionKey = null;
|
PGPPrivateKey decryptionKey = null;
|
||||||
PGPPublicKeyEncryptedData encryptedSessionKey = null;
|
PGPPublicKeyEncryptedData encryptedSessionKey = null;
|
||||||
|
|
||||||
|
List<PGPPBEEncryptedData> passphraseProtected = new ArrayList<>();
|
||||||
|
List<PGPPublicKeyEncryptedData> publicKeyProtected = new ArrayList<>();
|
||||||
|
List<Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData>> postponedDueToMissingPassphrase = new ArrayList<>();
|
||||||
|
|
||||||
|
// Sort PKESK and SKESK packets
|
||||||
while (encryptedDataIterator.hasNext()) {
|
while (encryptedDataIterator.hasNext()) {
|
||||||
PGPEncryptedData encryptedData = encryptedDataIterator.next();
|
PGPEncryptedData encryptedData = encryptedDataIterator.next();
|
||||||
|
// TODO: Maybe just skip non-integrity-protected packages?
|
||||||
// TODO: Can we just skip non-integrity-protected packages?
|
|
||||||
if (!encryptedData.isIntegrityProtected()) {
|
if (!encryptedData.isIntegrityProtected()) {
|
||||||
throw new MessageNotIntegrityProtectedException();
|
throw new MessageNotIntegrityProtectedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data is passphrase encrypted
|
// SKESK
|
||||||
if (encryptedData instanceof PGPPBEEncryptedData) {
|
if (encryptedData instanceof PGPPBEEncryptedData) {
|
||||||
PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData;
|
passphraseProtected.add((PGPPBEEncryptedData) encryptedData);
|
||||||
for (Passphrase passphrase : options.getDecryptionPassphrases()) {
|
|
||||||
PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance()
|
|
||||||
.getPBEDataDecryptorFactory(passphrase);
|
|
||||||
try {
|
|
||||||
InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor);
|
|
||||||
|
|
||||||
SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId(
|
|
||||||
pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor));
|
|
||||||
throwIfAlgorithmIsRejected(symmetricKeyAlgorithm);
|
|
||||||
resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm);
|
|
||||||
|
|
||||||
integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData);
|
|
||||||
|
|
||||||
return integrityProtectedEncryptedInputStream;
|
|
||||||
} catch (PGPException e) {
|
|
||||||
LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// PKESK
|
||||||
// data is public key encrypted
|
|
||||||
else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
|
else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
|
||||||
PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData;
|
publicKeyProtected.add((PGPPublicKeyEncryptedData) encryptedData);
|
||||||
long keyId = publicKeyEncryptedData.getKeyID();
|
}
|
||||||
if (!options.getDecryptionKeys().isEmpty()) {
|
}
|
||||||
// Known key id
|
|
||||||
if (keyId != 0) {
|
|
||||||
LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId));
|
|
||||||
resultBuilder.addRecipientKeyId(keyId);
|
|
||||||
PGPSecretKeyRing decryptionKeyRing = findDecryptionKeyRing(keyId);
|
|
||||||
if (decryptionKeyRing != null) {
|
|
||||||
PGPSecretKey secretKey = decryptionKeyRing.getSecretKey(keyId);
|
|
||||||
LOGGER.debug("Found respective secret key {}", Long.toHexString(keyId));
|
|
||||||
// Watch out! This assignment is possibly done multiple times.
|
|
||||||
encryptedSessionKey = publicKeyEncryptedData;
|
|
||||||
decryptionKey = UnlockSecretKey.unlockSecretKey(secretKey, options.getSecretKeyProtector(decryptionKeyRing));
|
|
||||||
resultBuilder.setDecryptionKey(new SubkeyIdentifier(decryptionKeyRing, decryptionKey.getKeyID()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hidden recipient
|
// Try decryption with passphrases first
|
||||||
else {
|
for (PGPPBEEncryptedData pbeEncryptedData : passphraseProtected) {
|
||||||
LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys.");
|
for (Passphrase passphrase : options.getDecryptionPassphrases()) {
|
||||||
outerloop: for (PGPSecretKeyRing ring : options.getDecryptionKeys()) {
|
PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance()
|
||||||
KeyRingInfo info = new KeyRingInfo(ring);
|
.getPBEDataDecryptorFactory(passphrase);
|
||||||
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS);
|
try {
|
||||||
for (PGPPublicKey pubkey : encryptionSubkeys) {
|
InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor);
|
||||||
PGPSecretKey key = ring.getSecretKey(pubkey.getKeyID());
|
|
||||||
if (key == null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(key, options.getSecretKeyProtector(ring).getDecryptor(key.getKeyID()));
|
SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId(
|
||||||
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey);
|
pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor));
|
||||||
try {
|
throwIfAlgorithmIsRejected(symmetricKeyAlgorithm);
|
||||||
publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key
|
resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm);
|
||||||
LOGGER.debug("Found correct key {} for hidden recipient decryption.", Long.toHexString(key.getKeyID()));
|
|
||||||
decryptionKey = privateKey;
|
integrityProtectedEncryptedInputStream = new IntegrityProtectedInputStream(decryptedDataStream, pbeEncryptedData);
|
||||||
resultBuilder.setDecryptionKey(new SubkeyIdentifier(ring, decryptionKey.getKeyID()));
|
|
||||||
encryptedSessionKey = publicKeyEncryptedData;
|
return integrityProtectedEncryptedInputStream;
|
||||||
break outerloop;
|
} catch (PGPException e) {
|
||||||
} catch (PGPException | ClassCastException e) {
|
LOGGER.debug("Probable passphrase mismatch, skip PBE encrypted data block", e);
|
||||||
LOGGER.debug("Skipping wrong key {} for hidden recipient decryption.", Long.toHexString(key.getKeyID()), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then try decryption with public key encryption
|
||||||
|
for (PGPPublicKeyEncryptedData publicKeyEncryptedData : publicKeyProtected) {
|
||||||
|
PGPPrivateKey privateKey = null;
|
||||||
|
if (options.getDecryptionKeys().isEmpty()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
long keyId = publicKeyEncryptedData.getKeyID();
|
||||||
|
// Wildcard KeyID
|
||||||
|
if (keyId == 0L) {
|
||||||
|
LOGGER.debug("Hidden recipient detected. Try to decrypt with all available secret keys.");
|
||||||
|
for (PGPSecretKeyRing secretKeys : options.getDecryptionKeys()) {
|
||||||
|
if (privateKey != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyRingInfo info = new KeyRingInfo(secretKeys);
|
||||||
|
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS);
|
||||||
|
for (PGPPublicKey pubkey : encryptionSubkeys) {
|
||||||
|
PGPSecretKey secretKey = secretKeys.getSecretKey(pubkey.getKeyID());
|
||||||
|
// Skip missing secret key
|
||||||
|
if (secretKey == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-wildcard key-id
|
||||||
|
else {
|
||||||
|
LOGGER.debug("PGPEncryptedData is encrypted for key {}", Long.toHexString(keyId));
|
||||||
|
resultBuilder.addRecipientKeyId(keyId);
|
||||||
|
|
||||||
|
PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId);
|
||||||
|
if (secretKeys == null) {
|
||||||
|
LOGGER.debug("Missing certificate of {}. Skip.", Long.toHexString(keyId));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
PGPSecretKey secretKey = secretKeys.getSecretKey(keyId);
|
||||||
|
privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, true);
|
||||||
|
}
|
||||||
|
if (privateKey == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
decryptionKey = privateKey;
|
||||||
|
encryptedSessionKey = publicKeyEncryptedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try postponed keys with missing passphrases (will cause missing passphrase callbacks to fire)
|
||||||
|
if (encryptedSessionKey == null) {
|
||||||
|
for (Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData> missingPassphrases : postponedDueToMissingPassphrase) {
|
||||||
|
SubkeyIdentifier keyId = missingPassphrases.getA();
|
||||||
|
PGPPublicKeyEncryptedData publicKeyEncryptedData = missingPassphrases.getB();
|
||||||
|
PGPSecretKeyRing secretKeys = findDecryptionKeyRing(keyId.getKeyId());
|
||||||
|
PGPSecretKey secretKey = secretKeys.getSecretKey(keyId.getSubkeyId());
|
||||||
|
|
||||||
|
PGPPrivateKey privateKey = tryPublicKeyDecryption(secretKeys, secretKey, publicKeyEncryptedData, postponedDueToMissingPassphrase, false);
|
||||||
|
if (privateKey == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptionKey = privateKey;
|
||||||
|
encryptedSessionKey = publicKeyEncryptedData;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return decryptWith(encryptedSessionKey, decryptionKey);
|
return decryptWith(encryptedSessionKey, decryptionKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try decryption of the provided public-key-encrypted-data using the given secret key.
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* 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 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(
|
||||||
|
PGPSecretKeyRing secretKeys,
|
||||||
|
PGPSecretKey secretKey,
|
||||||
|
PGPPublicKeyEncryptedData publicKeyEncryptedData,
|
||||||
|
List<Tuple<SubkeyIdentifier, PGPPublicKeyEncryptedData>> postponed,
|
||||||
|
boolean postponeIfMissingPassphrase) throws PGPException {
|
||||||
|
SecretKeyRingProtector protector = options.getSecretKeyProtector(secretKeys);
|
||||||
|
|
||||||
|
if (postponeIfMissingPassphrase && !protector.hasPassphraseFor(secretKey.getKeyID())) {
|
||||||
|
// Postpone decryption with key with missing passphrase
|
||||||
|
SubkeyIdentifier identifier = new SubkeyIdentifier(secretKeys, secretKey.getKeyID());
|
||||||
|
postponed.add(new Tuple<>(identifier, publicKeyEncryptedData));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(
|
||||||
|
secretKey, protector.getDecryptor(secretKey.getKeyID()));
|
||||||
|
|
||||||
|
// test if we have the right private key
|
||||||
|
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance()
|
||||||
|
.getPublicKeyDataDecryptorFactory(privateKey);
|
||||||
|
try {
|
||||||
|
publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key
|
||||||
|
LOGGER.debug("Found correct decryption key {}.", Long.toHexString(secretKey.getKeyID()));
|
||||||
|
resultBuilder.setDecryptionKey(new SubkeyIdentifier(secretKeys, privateKey.getKeyID()));
|
||||||
|
return privateKey;
|
||||||
|
} catch (PGPException | ClassCastException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey)
|
private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey)
|
||||||
throws PGPException {
|
throws PGPException {
|
||||||
if (decryptionKey == null) {
|
if (decryptionKey == null || encryptedSessionKey == null) {
|
||||||
throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found");
|
throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -149,6 +149,16 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se
|
||||||
return passphrase;
|
return passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return cache.containsKey(keyId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphraseFor(Long keyId) {
|
||||||
|
return cache.containsKey(keyId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException {
|
public PBESecretKeyDecryptor getDecryptor(@Nonnull Long keyId) throws PGPException {
|
||||||
|
|
|
@ -65,6 +65,11 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return keyRing.getPublicKey(keyId) != null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider);
|
return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider);
|
||||||
}
|
}
|
||||||
|
@ -80,10 +85,20 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return keyId == key.getKeyID();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider);
|
return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphraseFor(Long keyId) {
|
||||||
|
return passphraseProvider.hasPassphrase(keyId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException {
|
public PBESecretKeyDecryptor getDecryptor(Long keyId) throws PGPException {
|
||||||
|
|
|
@ -38,6 +38,8 @@ import org.pgpainless.util.Passphrase;
|
||||||
*/
|
*/
|
||||||
public interface SecretKeyRingProtector {
|
public interface SecretKeyRingProtector {
|
||||||
|
|
||||||
|
boolean hasPassphraseFor(Long keyId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a decryptor for the key of id {@code keyId}.
|
* Return a decryptor for the key of id {@code keyId}.
|
||||||
* This method returns null if the key is unprotected.
|
* This method returns null if the key is unprotected.
|
||||||
|
|
|
@ -25,6 +25,11 @@ import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
|
||||||
*/
|
*/
|
||||||
public class UnprotectedKeysProtector implements SecretKeyRingProtector {
|
public class UnprotectedKeysProtector implements SecretKeyRingProtector {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphraseFor(Long keyId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public PBESecretKeyDecryptor getDecryptor(Long keyId) {
|
public PBESecretKeyDecryptor getDecryptor(Long keyId) {
|
||||||
|
|
|
@ -45,4 +45,9 @@ public class MapBasedPassphraseProvider implements SecretKeyPassphraseProvider {
|
||||||
public Passphrase getPassphraseFor(Long keyId) {
|
public Passphrase getPassphraseFor(Long keyId) {
|
||||||
return map.get(keyId);
|
return map.get(keyId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return map.containsKey(keyId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,4 +46,6 @@ public interface SecretKeyPassphraseProvider {
|
||||||
* @return passphrase or null, if no passphrase record has been found.
|
* @return passphrase or null, if no passphrase record has been found.
|
||||||
*/
|
*/
|
||||||
@Nullable Passphrase getPassphraseFor(Long keyId);
|
@Nullable Passphrase getPassphraseFor(Long keyId);
|
||||||
|
|
||||||
|
boolean hasPassphrase(Long keyId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,4 +36,9 @@ public class SolitaryPassphraseProvider implements SecretKeyPassphraseProvider {
|
||||||
// always return the same passphrase.
|
// always return the same passphrase.
|
||||||
return passphrase;
|
return passphrase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
35
pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java
Normal file
35
pgpainless-core/src/main/java/org/pgpainless/util/Tuple.java
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
public class Tuple<A, B> {
|
||||||
|
|
||||||
|
private final A a;
|
||||||
|
private final B b;
|
||||||
|
|
||||||
|
public Tuple(A a, B b) {
|
||||||
|
this.a = a;
|
||||||
|
this.b = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
public A getA() {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public B getB() {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,221 @@
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
* 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.decryption_verification;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
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 javax.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPException;
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
|
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.key.protection.CachingSecretKeyRingProtector;
|
||||||
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||||
|
import org.pgpainless.key.protection.passphrase_provider.SecretKeyPassphraseProvider;
|
||||||
|
import org.pgpainless.util.Passphrase;
|
||||||
|
|
||||||
|
public class PostponeDecryptionUsingKeyWithMissingPassphraseTest {
|
||||||
|
|
||||||
|
private static PGPSecretKeyRing k1;
|
||||||
|
private static PGPSecretKeyRing k2;
|
||||||
|
private static final Passphrase p1 = Passphrase.fromPassword("P1");
|
||||||
|
private static final Passphrase p2 = Passphrase.fromPassword("P2");
|
||||||
|
|
||||||
|
private static final String PLAINTEXT = "Hello, World!\n";
|
||||||
|
|
||||||
|
// message is encrypted for both k1 and k2.
|
||||||
|
// The first PKESK is for k1, the second for k2
|
||||||
|
private static final String ENCRYPTED_FOR_K1_K2 = "-----BEGIN PGP MESSAGE-----\n" +
|
||||||
|
"Version: PGPainless\n" +
|
||||||
|
"\n" +
|
||||||
|
"hF4Dp8eMx2kPzEYSAQdAk0P2LL3pqZdq46eAGFkkESamDoPTn0EOLuPP+iA8lx8w\n" +
|
||||||
|
"RAMb6mUxPDVGqoXt05h2ps4BOTpy+Utsli0+BUzXTvtGM6RDTkaCuZvHQwPsggnN\n" +
|
||||||
|
"hF4DENqQkAsc7GgSAQdAJHwMR6+P5+HxwF8RqBEfrMCr0ZXWaLbekXf+FGTf/HYw\n" +
|
||||||
|
"+Et5NgaJazx0BdCf+D11Q4Vvem4Z9UEFL7x89B4mnv1dkJWRNwH6CkCNYVyIVrHi\n" +
|
||||||
|
"0kABB8V6DKCC1PNYlwCbSARz6X+xS9NsTFjGyROXajVEQ3x3ecLyKnyKxpcCJ2cb\n" +
|
||||||
|
"lfnLZ5ezQoyoukRkcdul1CWf\n" +
|
||||||
|
"=pvaA\n" +
|
||||||
|
"-----END PGP MESSAGE-----";
|
||||||
|
|
||||||
|
private static final String ENCRYPTED_FOR_K2_PASS_K1 = "-----BEGIN PGP MESSAGE-----\n" +
|
||||||
|
"Version: PGPainless\n" +
|
||||||
|
"\n" +
|
||||||
|
"hF4DENqQkAsc7GgSAQdAqtuQIjsRLypFfT8UykXqOv0dnrZWcrZBEiek4DNufmsw\n" +
|
||||||
|
"zRgbpnyKme7LaM+Lu0yJk9wUsvdpypB5GrKY9cD1Hg5nx4bGyujC6olowa/8o6Xe\n" +
|
||||||
|
"jC4ECQMCrjrXCpFawJtgj0y9PpciV6TpHJtI+lGbMed1+c5u3+U/HpRjLl3wBv9C\n" +
|
||||||
|
"hF4Dp8eMx2kPzEYSAQdA+Qrv5R4hOnOuVHDJpCCW72ONcdnzEhw45MxT/7mp3nQw\n" +
|
||||||
|
"8xs3dyVjMwmvqhbce9LIRdEM5YBWj3nBQM5ZQURAaQHPTTFuqCd8AgbeUz5FOFrA\n" +
|
||||||
|
"0kABJpvij5utFmhTVDqm3TrWOAmZ/eba0GMg0g/vFh7HoEGr1gRHLpc+vaIMs+fF\n" +
|
||||||
|
"uXVb1J9NX60PiBqxnM2iIBtD\n" +
|
||||||
|
"=p8Ye\n" +
|
||||||
|
"-----END PGP MESSAGE-----";
|
||||||
|
private static final Passphrase PASSPHRASE = Passphrase.fromPassword("Wow!");
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
public static void prepareKeys() throws IOException {
|
||||||
|
k1 = PGPainless.readKeyRing().secretKeyRing("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
||||||
|
"Version: PGPainless\n" +
|
||||||
|
"Comment: 9D0D 40DD 5B0A B5C7 9E73 E847 71BD 67EB 18A5 1034\n" +
|
||||||
|
"Comment: K1\n" +
|
||||||
|
"\n" +
|
||||||
|
"lIYEYUH25xYJKwYBBAHaRw8BAQdAfnFMBZxpsZiJ0yheGIzWEQixVWexv3oxBpUS\n" +
|
||||||
|
"kboJPIP+CQMCBjB6dP815OFgNjItEDhZVpGgZfHd9eqdNGj7RRiz7QN6Egk4kpGF\n" +
|
||||||
|
"Mqd0wZ8Ey4vmiYaeSP7QT+Wf9EccHOR4D8XD+y//Pu5aJx1X7gmVvLQCSzGIeAQT\n" +
|
||||||
|
"FgoAIAUCYUH25wIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEHG9Z+sYpRA0\n" +
|
||||||
|
"OqkA/il0Tw+95YKXK5oPgqoHTzR5zaRyjmZ3r8Pjp5S+gCZYAP0RMmleSMVxkf+o\n" +
|
||||||
|
"4FBKwE+Vv41GPYKBUQ2op/mCOyc3CpyLBGFB9ucSCisGAQQBl1UBBQEBB0C2DoDx\n" +
|
||||||
|
"UjTXA/vFikJr64fB8qcCMyBx5ODBwBG9woSvLAMBCAf+CQMCBjB6dP815OFg8JuS\n" +
|
||||||
|
"6Z6j+M+7X8QNhZtHohmGbbWntREzAAVlN+UEmLljpcKdZKqlPgGoacw2ta/928FR\n" +
|
||||||
|
"6GD7tjyAPzSRTqPo6+pwBjU4/4h1BBgWCgAdBQJhQfbnAhsMBRYCAwEABAsJCAcF\n" +
|
||||||
|
"FQoJCAsCHgEACgkQcb1n6xilEDReNgD+L+B9YfbIPGd4NnOvt+9qrrzmRPXbhTu7\n" +
|
||||||
|
"9Vw0VmW7YfcBANH+0tH7HYbL5NOzGI888E28V0VhHqhhvtlctI574qAInIYEYUH2\n" +
|
||||||
|
"5xYJKwYBBAHaRw8BAQdARWEfFJZIKcsMrb/A3/AwFgwTLqMmoK6XTuTUfuqxZCb+\n" +
|
||||||
|
"CQMCBjB6dP815OFgHXyV5OYmG3BDr8xnw8boGZEdZRQARrbmYLCBEblH8X7X/jJy\n" +
|
||||||
|
"/SdBnsKed/dVItHAENVBjkbFXx7V8z7jqmZAEDSFR7R6o4jVBBgWCgB9BQJhQfbn\n" +
|
||||||
|
"AhsCBRYCAwEABAsJCAcFFQoJCAsCHgFfIAQZFgoABgUCYUH25wAKCRD9CYoy8Jjb\n" +
|
||||||
|
"qYXCAP487XkaTSeqHiygM9x5jKJQzBTNoa8LP5kmhk/qiRMKWAEAnEOvrTRMS9OL\n" +
|
||||||
|
"qLPQDJ5Zl4fwjXDC4MDEstxkwEkUXQEACgkQcb1n6xilEDTK+QD6AwFVz+NguD9k\n" +
|
||||||
|
"MElK0o9VDLUWheP9tXE/sHcCVXKrm4kA/2TK8puF9FKpBb3pJhsvLfFuklVlXEBv\n" +
|
||||||
|
"/lv8PRbqIHsN\n" +
|
||||||
|
"=PETI\n" +
|
||||||
|
"-----END PGP PRIVATE KEY BLOCK-----");
|
||||||
|
k2 = PGPainless.readKeyRing().secretKeyRing("-----BEGIN PGP PRIVATE KEY BLOCK-----\n" +
|
||||||
|
"Version: PGPainless\n" +
|
||||||
|
"Comment: 1458 6C84 3082 7226 5F4A 1EAC 15C9 772F 51A3 F48A\n" +
|
||||||
|
"Comment: K2\n" +
|
||||||
|
"\n" +
|
||||||
|
"lIYEYUH25xYJKwYBBAHaRw8BAQdAQHpL7nSEOpOdEVcmNxTsjJmqPYI7ObVGZqCi\n" +
|
||||||
|
"snlK8XP+CQMCySs/5txmbAtgB6fPvXfs7I0bYIEcGNZqSPMqVU04EjLyvmeP2EZL\n" +
|
||||||
|
"L5ezq3U4Z835xEILFN5ngBxajMEu1A0pksiabHTR28RspoBDph+4/bQCSzKIeAQT\n" +
|
||||||
|
"FgoAIAUCYUH25wIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEBXJdy9Ro/SK\n" +
|
||||||
|
"vyQBALXaK7xt/JbIE4jqhWbliIHm8bskX3WG+jME5XjfDjBGAQC8hcKiWbOAF1tK\n" +
|
||||||
|
"8KH2mzeHsh0yhybUvlq6wq7GZ3aZCZyLBGFB9ucSCisGAQQBl1UBBQEBB0CDCUwj\n" +
|
||||||
|
"XBrIL5xf7TDKNOCyXgepXp+Ca3q2q0qmWm1nYwMBCAf+CQMCySs/5txmbAtg0/tL\n" +
|
||||||
|
"Rw8WbfHVGS3u+aEuookij7swVMTspPY/s1W3Mt1TP85lM1Bkn5fDr4UP9prEQNc7\n" +
|
||||||
|
"/fWsqvc1b9ZRBBqmwPOsKDfd3oh1BBgWCgAdBQJhQfbnAhsMBRYCAwEABAsJCAcF\n" +
|
||||||
|
"FQoJCAsCHgEACgkQFcl3L1Gj9IoDBAD/YNTgbTvgM6UsqJ1DFiaihR1kV3nv2fuc\n" +
|
||||||
|
"EAJfu7guvbsA/0gnPBxywJd4cK7spoAZjyjdgN8RPcZcUo6vXbMnT4YHnIYEYUH2\n" +
|
||||||
|
"5xYJKwYBBAHaRw8BAQdApegYPw86Q19XMX1M5YykP51E27ZvwBIMc1bORa7xAFv+\n" +
|
||||||
|
"CQMCySs/5txmbAtgFrYkIkpujELJEpD1hJlFSZzIxiA193PXfdo9CbFHBkjwIBh7\n" +
|
||||||
|
"idT7l1gA+eHhiC0QyEPt3un3P4gj4UMeBPtCwqTUxo887IjVBBgWCgB9BQJhQfbn\n" +
|
||||||
|
"AhsCBRYCAwEABAsJCAcFFQoJCAsCHgFfIAQZFgoABgUCYUH25wAKCRBQkr/WCypZ\n" +
|
||||||
|
"Xp2zAP43zOQPtlbM1cabvP8kaWEsYG/x9ka4GtT/vkFh2cg3NAD/aSi13QhFHIVq\n" +
|
||||||
|
"FI+3tH0vnxFAWmU9u7JnvM2+3ULHDA0ACgkQFcl3L1Gj9IqeqQEAx/2y5PMGl7t4\n" +
|
||||||
|
"oHOJ4zhtqzTo33qjbu05eneS+zp4ElYA/RP/IGjIVz9wzraOKzBptB1BOaiqu3JG\n" +
|
||||||
|
"xnMR9GND5bgA\n" +
|
||||||
|
"=Sknt\n" +
|
||||||
|
"-----END PGP PRIVATE KEY BLOCK-----\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingPassphraseFirst() throws PGPException, IOException {
|
||||||
|
SecretKeyRingProtector protector1 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Passphrase getPassphraseFor(Long keyId) {
|
||||||
|
fail("Although the first PKESK is for k1, we should have skipped it and tried k2 first, which has passphrase available.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
SecretKeyRingProtector protector2 = SecretKeyRingProtector.unlockAllKeysWith(p2, k2);
|
||||||
|
|
||||||
|
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
|
||||||
|
.onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8)))
|
||||||
|
.withOptions(new ConsumerOptions()
|
||||||
|
.addDecryptionKey(k1, protector1)
|
||||||
|
.addDecryptionKey(k2, protector2));
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
Streams.pipeAll(decryptionStream, out);
|
||||||
|
decryptionStream.close();
|
||||||
|
|
||||||
|
assertEquals(PLAINTEXT, out.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingPassphraseSecond() throws PGPException, IOException {
|
||||||
|
SecretKeyRingProtector protector1 = SecretKeyRingProtector.unlockAllKeysWith(p1, k1);
|
||||||
|
SecretKeyRingProtector protector2 = new CachingSecretKeyRingProtector(new SecretKeyPassphraseProvider() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Passphrase getPassphraseFor(Long keyId) {
|
||||||
|
fail("This callback should not get called, since the first PKESK is for k1, which has a passphrase available.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
|
||||||
|
.onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K1_K2.getBytes(StandardCharsets.UTF_8)))
|
||||||
|
.withOptions(new ConsumerOptions()
|
||||||
|
.addDecryptionKey(k1, protector1)
|
||||||
|
.addDecryptionKey(k2, protector2));
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
Streams.pipeAll(decryptionStream, out);
|
||||||
|
decryptionStream.close();
|
||||||
|
|
||||||
|
assertEquals(PLAINTEXT, out.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void messagePassphraseFirst() throws PGPException, IOException {
|
||||||
|
SecretKeyPassphraseProvider provider = new SecretKeyPassphraseProvider() {
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public Passphrase getPassphraseFor(Long keyId) {
|
||||||
|
fail("Since we provide a decryption passphrase, we should not try to decrypt any key.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
SecretKeyRingProtector protector = new CachingSecretKeyRingProtector(provider);
|
||||||
|
|
||||||
|
DecryptionStream decryptionStream = PGPainless.decryptAndOrVerify()
|
||||||
|
.onInputStream(new ByteArrayInputStream(ENCRYPTED_FOR_K2_PASS_K1.getBytes(StandardCharsets.UTF_8)))
|
||||||
|
.withOptions(new ConsumerOptions()
|
||||||
|
.addDecryptionPassphrase(PASSPHRASE)
|
||||||
|
.addDecryptionKey(k1, protector)
|
||||||
|
.addDecryptionKey(k2, protector));
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
Streams.pipeAll(decryptionStream, out);
|
||||||
|
decryptionStream.close();
|
||||||
|
|
||||||
|
assertEquals(PLAINTEXT, out.toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -46,6 +46,11 @@ public class CachingSecretKeyRingProtectorTest {
|
||||||
long doubled = keyId * 2;
|
long doubled = keyId * 2;
|
||||||
return Passphrase.fromPassword(Long.toString(doubled));
|
return Passphrase.fromPassword(Long.toString(doubled));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private CachingSecretKeyRingProtector protector;
|
private CachingSecretKeyRingProtector protector;
|
||||||
|
|
|
@ -49,6 +49,11 @@ public class PassphraseProtectedKeyTest {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return keyId.equals(TestKeys.CRYPTIE_KEY_ID);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -121,6 +121,11 @@ public class SecretKeyRingProtectorTest {
|
||||||
public Passphrase getPassphraseFor(Long keyId) {
|
public Passphrase getPassphraseFor(Long keyId) {
|
||||||
return Passphrase.fromPassword("missingP455w0rd");
|
return Passphrase.fromPassword("missingP455w0rd");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPassphrase(Long keyId) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
assertEquals(Passphrase.emptyPassphrase(), protector.getPassphraseFor(1L));
|
assertEquals(Passphrase.emptyPassphrase(), protector.getPassphraseFor(1L));
|
||||||
|
|
Loading…
Reference in a new issue