This commit is contained in:
Paul Schaub 2021-11-08 22:45:08 +01:00
parent 04ada88188
commit 3f09fa0cc7
9 changed files with 163 additions and 103 deletions

View File

@ -59,6 +59,10 @@ public abstract class ImplementationFactory {
public abstract PBESecretKeyDecryptor getPBESecretKeyDecryptor(Passphrase passphrase) throws PGPException;
public PGPDigestCalculator getV4FingerprintCalculator() throws PGPException {
return getPGPDigestCalculator(HashAlgorithm.SHA1);
}
public PGPDigestCalculator getPGPDigestCalculator(HashAlgorithm algorithm) throws PGPException {
return getPGPDigestCalculator(algorithm.getAlgorithmId());
}

View File

@ -41,6 +41,7 @@ import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.key.protection.UnlockSecretKey;
import org.pgpainless.provider.ProviderFactory;
import org.pgpainless.signature.subpackets.SignatureSubpacketGeneratorUtil;
@ -52,13 +53,13 @@ public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
private final Charset UTF8 = Charset.forName("UTF-8");
private PGPSignatureGenerator signatureGenerator;
private PGPDigestCalculator digestCalculator;
private PGPDigestCalculator keyFingerprintCalculator;
private PBESecretKeyEncryptor secretKeyEncryptor;
private KeySpec primaryKeySpec;
private final List<KeySpec> subkeySpecs = new ArrayList<>();
private final Set<String> userIds = new LinkedHashSet<>();
private Passphrase passphrase = null;
private Passphrase passphrase = Passphrase.emptyPassphrase();
private Date expirationDate = null;
@Override
@ -126,13 +127,11 @@ public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
if (userIds.isEmpty()) {
throw new IllegalStateException("At least one user-id is required.");
}
digestCalculator = buildDigestCalculator();
keyFingerprintCalculator = ImplementationFactory.getInstance().getV4FingerprintCalculator();
secretKeyEncryptor = buildSecretKeyEncryptor();
PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor();
if (passphrase != null) {
passphrase.clear();
}
passphrase.clear();
// Generate Primary Key
PGPKeyPair certKey = generateKeyPair(primaryKeySpec);
@ -171,8 +170,7 @@ public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
// "reassemble" secret key ring with modified primary key
PGPSecretKey primarySecKey = new PGPSecretKey(
privateKey,
primaryPubKey, digestCalculator, true, secretKeyEncryptor);
privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor);
List<PGPSecretKey> secretKeyList = new ArrayList<>();
secretKeyList.add(primarySecKey);
while (secretKeys.hasNext()) {
@ -190,7 +188,7 @@ public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
String primaryUserId = userIds.iterator().next();
return new PGPKeyRingGenerator(
SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey,
primaryUserId, digestCalculator,
primaryUserId, keyFingerprintCalculator,
hashedSubPackets, null, signer, secretKeyEncryptor);
}
@ -236,22 +234,20 @@ public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
private PBESecretKeyEncryptor buildSecretKeyEncryptor() {
SymmetricKeyAlgorithm keyEncryptionAlgorithm = PGPainless.getPolicy().getSymmetricKeyEncryptionAlgorithmPolicy()
.getDefaultSymmetricKeyAlgorithm();
PBESecretKeyEncryptor encryptor = passphrase == null || passphrase.isEmpty() ?
null : // unencrypted key pair, otherwise AES-256 encrypted
if (!passphrase.isValid()) {
throw new IllegalStateException("Passphrase was cleared.");
}
return passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted
ImplementationFactory.getInstance().getPBESecretKeyEncryptor(
keyEncryptionAlgorithm, digestCalculator, passphrase);
return encryptor;
keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase);
}
private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException {
PBESecretKeyDecryptor decryptor = passphrase == null || passphrase.isEmpty() ?
null :
if (!passphrase.isValid()) {
throw new IllegalStateException("Passphrase was cleared.");
}
return passphrase.isEmpty() ? null :
ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase);
return decryptor;
}
private PGPDigestCalculator buildDigestCalculator() throws PGPException {
return ImplementationFactory.getInstance().getPGPDigestCalculator(HashAlgorithm.SHA1);
}
public static PGPKeyPair generateKeyPair(KeySpec spec)
@ -269,4 +265,30 @@ public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance().getPGPKeyPair(type.getAlgorithm(), keyPair, new Date());
return pgpKeyPair;
}
public static PGPSecretKey generatePGPSecretKey(KeySpec keySpec, @Nonnull Passphrase passphrase, boolean isPrimary)
throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPDigestCalculator keyFingerprintCalculator = ImplementationFactory.getInstance()
.getV4FingerprintCalculator();
PGPKeyPair keyPair = generateKeyPair(keySpec);
SecretKeyRingProtector protector;
synchronized (passphrase.lock) {
if (!passphrase.isValid()) {
throw new IllegalStateException("Passphrase has been cleared.");
}
if (!passphrase.isEmpty()) {
protector = SecretKeyRingProtector.unlockSingleKeyWith(passphrase, keyPair.getKeyID());
} else {
protector = SecretKeyRingProtector.unprotectedKeys();
}
return new PGPSecretKey(
keyPair.getPrivateKey(),
keyPair.getPublicKey(),
keyFingerprintCalculator,
isPrimary,
protector.getEncryptor(keyPair.getKeyID()));
}
}
}

View File

@ -41,7 +41,6 @@ import org.pgpainless.algorithm.HashAlgorithm;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.algorithm.PublicKeyAlgorithm;
import org.pgpainless.algorithm.SignatureType;
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
import org.pgpainless.implementation.ImplementationFactory;
import org.pgpainless.key.OpenPgpFingerprint;
import org.pgpainless.key.generation.KeyRingBuilder;
@ -182,28 +181,27 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
KeyFlag keyFlag,
KeyFlag... additionalKeyFlags) throws PGPException, IOException {
KeyFlag[] flags = concat(keyFlag, additionalKeyFlags);
SignatureSubpacketsUtil.assureKeyCanCarryFlags(PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm()));
boolean isSigningKey = CollectionUtils.contains(flags, KeyFlag.SIGN_DATA) ||
CollectionUtils.contains(flags, KeyFlag.CERTIFY_OTHER);
if (!isSigningKey) {
return addSubKey(subkey.getPublicKey(),
bindingSignatureCallback,
primaryKeyProtector,
keyFlag,
additionalKeyFlags);
}
PublicKeyAlgorithm subkeyAlgorithm = PublicKeyAlgorithm.fromId(subkey.getPublicKey().getAlgorithm());
SignatureSubpacketsUtil.assureKeyCanCarryFlags(subkeyAlgorithm);
PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
SubkeyBindingSignatureBuilder bindingSigBuilder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector);
SubkeyBindingSignatureBuilder bindingSigBuilder =
new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector);
bindingSigBuilder.applyCallback(bindingSignatureCallback);
bindingSigBuilder.getHashedSubpackets().setKeyFlags(flags);
PrimaryKeyBindingSignatureBuilder backSigBuilder = new PrimaryKeyBindingSignatureBuilder(subkey, subkeyProtector);
backSigBuilder.applyCallback(backSignatureCallback);
PGPSignature backSig = backSigBuilder.build(primaryKey.getPublicKey());
boolean isSigningKey = CollectionUtils.contains(flags, KeyFlag.SIGN_DATA) ||
CollectionUtils.contains(flags, KeyFlag.CERTIFY_OTHER);
if (isSigningKey) {
// Add embedded back-signature made by subkey over primary key
PrimaryKeyBindingSignatureBuilder backSigBuilder =
new PrimaryKeyBindingSignatureBuilder(subkey, subkeyProtector);
backSigBuilder.applyCallback(backSignatureCallback);
PGPSignature backSig = backSigBuilder.build(primaryKey.getPublicKey());
bindingSigBuilder.getHashedSubpackets().addEmbeddedSignature(backSig);
}
bindingSigBuilder.getHashedSubpackets().addEmbeddedSignature(backSig);
PGPSignature bindingSig = bindingSigBuilder.build(subkey.getPublicKey());
subkey = KeyRingUtils.secretKeyPlusSignature(subkey, bindingSig);
secretKeyRing = KeyRingUtils.secretKeysPlusSecretKey(secretKeyRing, subkey);
@ -211,54 +209,10 @@ public class SecretKeyRingEditor implements SecretKeyRingEditorInterface {
return this;
}
@Override
public SecretKeyRingEditorInterface addSubKey(PGPPublicKey subkey,
SelfSignatureSubpackets.Callback bindingSignatureCallback,
SecretKeyRingProtector primaryKeyProtector,
KeyFlag keyFlag,
KeyFlag... additionalKeyFlags) throws PGPException {
KeyFlag[] flags = concat(keyFlag, additionalKeyFlags);
boolean isSigningKey = CollectionUtils.contains(flags, KeyFlag.SIGN_DATA) ||
CollectionUtils.contains(flags, KeyFlag.CERTIFY_OTHER);
if (isSigningKey) {
throw new IllegalArgumentException("Cannot bind a signing capable subkey without access to the secret subkey.\n" +
"Please use addSubKey(PGPSecretKey secretSubKey, [...]) instead.");
}
PGPSignature bindingSignature = createSubkeyBindingSignature(subkey, bindingSignatureCallback, primaryKeyProtector, flags);
subkey = PGPPublicKey.addCertification(subkey, bindingSignature);
secretKeyRing = KeyRingUtils.secretKeysPlusPublicKey(secretKeyRing, subkey);
return this;
}
private PGPSignature createSubkeyBindingSignature(PGPPublicKey subkey,
SelfSignatureSubpackets.Callback bindingSignatureCallback,
SecretKeyRingProtector primaryKeyProtector,
KeyFlag... keyFlags) throws PGPException {
PGPSecretKey primaryKey = secretKeyRing.getSecretKey();
SubkeyBindingSignatureBuilder builder = new SubkeyBindingSignatureBuilder(primaryKey, primaryKeyProtector);
builder.applyCallback(bindingSignatureCallback);
builder.getHashedSubpackets().setKeyFlags(keyFlags);
PGPSignature signature = builder.build(subkey);
return signature;
}
private PGPSecretKey generateSubKey(@Nonnull KeySpec keySpec,
@Nonnull Passphrase subKeyPassphrase)
throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException {
PGPDigestCalculator checksumCalculator = ImplementationFactory.getInstance()
.getPGPDigestCalculator(defaultDigestHashAlgorithm);
PBESecretKeyEncryptor subKeyEncryptor = subKeyPassphrase.isEmpty() ? null :
ImplementationFactory.getInstance().getPBESecretKeyEncryptor(SymmetricKeyAlgorithm.AES_256, subKeyPassphrase);
PGPKeyPair keyPair = KeyRingBuilder.generateKeyPair(keySpec);
PGPSecretKey secretKey = new PGPSecretKey(keyPair.getPrivateKey(), keyPair.getPublicKey(),
checksumCalculator, false, subKeyEncryptor);
return secretKey;
return KeyRingBuilder.generatePGPSecretKey(keySpec, subKeyPassphrase, false);
}
@Override

View File

@ -12,7 +12,6 @@ import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
@ -59,7 +58,7 @@ public interface SecretKeyRingEditorInterface {
* @return the builder
*/
SecretKeyRingEditorInterface addSubKey(@Nonnull KeySpec keySpec,
@Nonnull Passphrase subKeyPassphrase,
@Nullable Passphrase subKeyPassphrase,
SecretKeyRingProtector secretKeyRingProtector)
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException;
@ -78,12 +77,6 @@ public interface SecretKeyRingEditorInterface {
KeyFlag keyFlag,
KeyFlag... additionalKeyFlags) throws PGPException, IOException;
SecretKeyRingEditorInterface addSubKey(PGPPublicKey subkey,
SelfSignatureSubpackets.Callback bindingSignatureCallback,
SecretKeyRingProtector primaryKeyProtector,
KeyFlag keyFlag,
KeyFlag... additionalKeyFlags) throws PGPException;
/**
* Revoke the key ring.
* The revocation will be a hard revocation, rendering the whole key invalid for any past or future signatures.

View File

@ -56,7 +56,7 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se
* @param keyId id of the key
* @param passphrase passphrase
*/
public void addPassphrase(@Nonnull Long keyId, @Nullable Passphrase passphrase) {
public void addPassphrase(@Nonnull Long keyId, @Nonnull Passphrase passphrase) {
this.cache.put(keyId, passphrase);
}
@ -66,7 +66,7 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se
* @param keyRing key ring
* @param passphrase passphrase
*/
public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nullable Passphrase passphrase) {
public void addPassphrase(@Nonnull PGPKeyRing keyRing, @Nonnull Passphrase passphrase) {
Iterator<PGPPublicKey> keys = keyRing.getPublicKeys();
while (keys.hasNext()) {
PGPPublicKey publicKey = keys.next();
@ -80,11 +80,11 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se
* @param key key
* @param passphrase passphrase
*/
public void addPassphrase(@Nonnull PGPPublicKey key, @Nullable Passphrase passphrase) {
public void addPassphrase(@Nonnull PGPPublicKey key, @Nonnull Passphrase passphrase) {
addPassphrase(key.getKeyID(), passphrase);
}
public void addPassphrase(@Nonnull OpenPgpFingerprint fingerprint, @Nullable Passphrase passphrase) {
public void addPassphrase(@Nonnull OpenPgpFingerprint fingerprint, @Nonnull Passphrase passphrase) {
addPassphrase(fingerprint.getKeyId(), passphrase);
}
@ -95,9 +95,10 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se
* @param keyId id of the key
*/
public void forgetPassphrase(@Nonnull Long keyId) {
Passphrase passphrase = cache.get(keyId);
passphrase.clear();
cache.remove(keyId);
Passphrase passphrase = cache.remove(keyId);
if (passphrase != null) {
passphrase.clear();
}
}
/**
@ -140,12 +141,13 @@ public class CachingSecretKeyRingProtector implements SecretKeyRingProtector, Se
@Override
public boolean hasPassphrase(Long keyId) {
return cache.containsKey(keyId);
Passphrase passphrase = cache.get(keyId);
return passphrase != null && passphrase.isValid();
}
@Override
public boolean hasPassphraseFor(Long keyId) {
return cache.containsKey(keyId);
return hasPassphrase(keyId);
}
@Override

View File

@ -64,12 +64,16 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect
}
public static PasswordBasedSecretKeyRingProtector forKey(PGPSecretKey key, Passphrase passphrase) {
return forKeyId(key.getPublicKey().getKeyID(), passphrase);
}
public static PasswordBasedSecretKeyRingProtector forKeyId(long singleKeyId, Passphrase passphrase) {
KeyRingProtectionSettings protectionSettings = KeyRingProtectionSettings.secureDefaultSettings();
SecretKeyPassphraseProvider passphraseProvider = new SecretKeyPassphraseProvider() {
@Override
@Nullable
@Override
public Passphrase getPassphraseFor(Long keyId) {
if (key.getKeyID() == keyId) {
if (keyId == singleKeyId) {
return passphrase;
}
return null;
@ -77,7 +81,7 @@ public class PasswordBasedSecretKeyRingProtector implements SecretKeyRingProtect
@Override
public boolean hasPassphrase(Long keyId) {
return keyId == key.getKeyID();
return keyId == singleKeyId;
}
};
return new PasswordBasedSecretKeyRingProtector(protectionSettings, passphraseProvider);

View File

@ -98,6 +98,10 @@ public interface SecretKeyRingProtector {
return PasswordBasedSecretKeyRingProtector.forKey(key, passphrase);
}
static SecretKeyRingProtector unlockSingleKeyWith(Passphrase passphrase, long keyId) {
return PasswordBasedSecretKeyRingProtector.forKeyId(keyId, passphrase);
}
/**
* Protector for unprotected keys.
* This protector returns null for all {@link #getEncryptor(Long)}/{@link #getDecryptor(Long)} calls,

View File

@ -10,7 +10,7 @@ import java.util.Arrays;
public class Passphrase {
private final Object lock = new Object();
public final Object lock = new Object();
private final char[] chars;
private boolean valid = true;

View File

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2021 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package org.pgpainless.key.modification;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.List;
import org.bouncycastle.bcpg.sig.NotationData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.junit.JUtils;
import org.junit.jupiter.api.Test;
import org.pgpainless.PGPainless;
import org.pgpainless.algorithm.EncryptionPurpose;
import org.pgpainless.algorithm.KeyFlag;
import org.pgpainless.key.OpenPgpV4Fingerprint;
import org.pgpainless.key.generation.KeyRingBuilder;
import org.pgpainless.key.generation.KeySpec;
import org.pgpainless.key.generation.type.KeyType;
import org.pgpainless.key.generation.type.xdh.XDHSpec;
import org.pgpainless.key.info.KeyRingInfo;
import org.pgpainless.key.protection.SecretKeyRingProtector;
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets;
import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil;
import org.pgpainless.util.Passphrase;
public class AddSubkeyWithModifiedBindingSignatureSubpackets {
@Test
public void bindEncryptionSubkeyAndModifyBindingSignatureHashedSubpackets() throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException {
SecretKeyRingProtector protector = SecretKeyRingProtector.unprotectedKeys();
PGPSecretKeyRing secretKeys = PGPainless.generateKeyRing()
.modernKeyRing("Alice <alice@pgpainless.org>", null);
KeyRingInfo before = PGPainless.inspectKeyRing(secretKeys);
PGPSecretKey secretSubkey = KeyRingBuilder.generatePGPSecretKey(
KeySpec.getBuilder(KeyType.XDH(XDHSpec._X25519), KeyFlag.ENCRYPT_COMMS).build(),
Passphrase.emptyPassphrase(), false);
secretKeys = PGPainless.modifyKeyRing(secretKeys)
.addSubKey(secretSubkey, new SelfSignatureSubpackets.Callback() {
@Override
public void modifyHashedSubpackets(SelfSignatureSubpackets hashedSubpackets) {
hashedSubpackets.setKeyExpirationTime(true, 1000);
hashedSubpackets.addNotationData(false, "test@test.test", "test");
}
}, null, SecretKeyRingProtector.unprotectedKeys(), protector, KeyFlag.ENCRYPT_COMMS)
.done();
KeyRingInfo after = PGPainless.inspectKeyRing(secretKeys);
List<PGPPublicKey> encryptionKeys = after.getEncryptionSubkeys(EncryptionPurpose.COMMUNICATIONS);
encryptionKeys.removeAll(before.getEncryptionSubkeys(EncryptionPurpose.COMMUNICATIONS));
assertFalse(encryptionKeys.isEmpty());
assertEquals(1, encryptionKeys.size());
PGPPublicKey newKey = encryptionKeys.get(0);
JUtils.assertEquals(new Date().getTime() + 1000 * 1000, after.getSubkeyExpirationDate(new OpenPgpV4Fingerprint(newKey)).getTime(), 2000);
assertTrue(newKey.getSignatures().hasNext());
PGPSignature binding = newKey.getSignatures().next();
List<NotationData> notations = SignatureSubpacketsUtil.getHashedNotationData(binding);
assertEquals(1, notations.size());
assertEquals("test@test.test", notations.get(0).getNotationName());
}
}