mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-12-25 12:27:58 +01:00
Kotlin conversion: KeyRingBuider / KeySpecBuilderInterface
This commit is contained in:
parent
aec93b7fe7
commit
513a5cacfc
6 changed files with 301 additions and 393 deletions
|
@ -1,322 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.key.generation;
|
|
||||||
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.KeyPair;
|
|
||||||
import java.security.KeyPairGenerator;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.bouncycastle.bcpg.sig.KeyFlags;
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPKeyPair;
|
|
||||||
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
|
|
||||||
import org.bouncycastle.openpgp.PGPPrivateKey;
|
|
||||||
import org.bouncycastle.openpgp.PGPPublicKey;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKey;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignature;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignatureGenerator;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator;
|
|
||||||
import org.bouncycastle.openpgp.PGPSignatureSubpacketVector;
|
|
||||||
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
|
|
||||||
import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor;
|
|
||||||
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
|
|
||||||
import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
|
|
||||||
import org.bouncycastle.util.Strings;
|
|
||||||
import org.pgpainless.PGPainless;
|
|
||||||
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.generation.type.KeyType;
|
|
||||||
import org.pgpainless.key.protection.UnlockSecretKey;
|
|
||||||
import org.pgpainless.policy.Policy;
|
|
||||||
import org.pgpainless.provider.ProviderFactory;
|
|
||||||
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets;
|
|
||||||
import org.pgpainless.signature.subpackets.SignatureSubpackets;
|
|
||||||
import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
|
|
||||||
public class KeyRingBuilder implements KeyRingBuilderInterface<KeyRingBuilder> {
|
|
||||||
|
|
||||||
private static final long YEAR_IN_SECONDS = 1000L * 60 * 60 * 24 * 365;
|
|
||||||
|
|
||||||
private KeySpec primaryKeySpec;
|
|
||||||
private final List<KeySpec> subkeySpecs = new ArrayList<>();
|
|
||||||
private final Map<String, SelfSignatureSubpackets.Callback> userIds = new LinkedHashMap<>();
|
|
||||||
private Passphrase passphrase = Passphrase.emptyPassphrase();
|
|
||||||
private Date expirationDate = new Date(System.currentTimeMillis() + YEAR_IN_SECONDS * 5); // Expiration in 5 years
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyRingBuilder setPrimaryKey(@Nonnull KeySpec keySpec) {
|
|
||||||
verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy());
|
|
||||||
verifyMasterKeyCanCertify(keySpec);
|
|
||||||
this.primaryKeySpec = keySpec;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyRingBuilder addSubkey(@Nonnull KeySpec keySpec) {
|
|
||||||
verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy());
|
|
||||||
this.subkeySpecs.add(keySpec);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyRingBuilder addUserId(@Nonnull String userId) {
|
|
||||||
this.userIds.put(userId.trim(), null);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public KeyRingBuilder addUserId(
|
|
||||||
@Nonnull String userId,
|
|
||||||
@Nullable SelfSignatureSubpackets.Callback subpacketsCallback) {
|
|
||||||
this.userIds.put(userId.trim(), subpacketsCallback);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyRingBuilder addUserId(@Nonnull byte[] userId) {
|
|
||||||
return addUserId(Strings.fromUTF8ByteArray(userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyRingBuilder setExpirationDate(@Nullable Date expirationDate) {
|
|
||||||
if (expirationDate == null) {
|
|
||||||
// No expiration
|
|
||||||
this.expirationDate = null;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
Date now = new Date();
|
|
||||||
if (now.after(expirationDate)) {
|
|
||||||
throw new IllegalArgumentException("Expiration date must be in the future.");
|
|
||||||
}
|
|
||||||
this.expirationDate = expirationDate;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public KeyRingBuilder setPassphrase(@Nonnull Passphrase passphrase) {
|
|
||||||
this.passphrase = passphrase;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyKeySpecCompliesToPolicy(KeySpec keySpec, Policy policy) {
|
|
||||||
PublicKeyAlgorithm publicKeyAlgorithm = keySpec.getKeyType().getAlgorithm();
|
|
||||||
int bitStrength = keySpec.getKeyType().getBitStrength();
|
|
||||||
|
|
||||||
if (!policy.getPublicKeyAlgorithmPolicy().isAcceptable(publicKeyAlgorithm, bitStrength)) {
|
|
||||||
throw new IllegalArgumentException("Public key algorithm policy violation: " +
|
|
||||||
publicKeyAlgorithm + " with bit strength " + bitStrength + " is not acceptable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyMasterKeyCanCertify(KeySpec spec) {
|
|
||||||
if (!hasCertifyOthersFlag(spec)) {
|
|
||||||
throw new IllegalArgumentException("Certification Key MUST have KeyFlag CERTIFY_OTHER");
|
|
||||||
}
|
|
||||||
if (!keyIsCertificationCapable(spec)) {
|
|
||||||
throw new IllegalArgumentException("Key algorithm " + spec.getKeyType().getName() + " is not capable of creating certifications.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasCertifyOthersFlag(KeySpec keySpec) {
|
|
||||||
KeyFlags keyFlags = keySpec.getSubpacketGenerator().getKeyFlagsSubpacket();
|
|
||||||
return keyFlags != null && KeyFlag.hasKeyFlag(keyFlags.getFlags(), KeyFlag.CERTIFY_OTHER);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean keyIsCertificationCapable(KeySpec keySpec) {
|
|
||||||
return keySpec.getKeyType().canCertify();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException,
|
|
||||||
InvalidAlgorithmParameterException {
|
|
||||||
PGPDigestCalculator keyFingerprintCalculator = ImplementationFactory.getInstance().getV4FingerprintCalculator();
|
|
||||||
PBESecretKeyEncryptor secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator);
|
|
||||||
PBESecretKeyDecryptor secretKeyDecryptor = buildSecretKeyDecryptor();
|
|
||||||
|
|
||||||
passphrase.clear();
|
|
||||||
|
|
||||||
// Generate Primary Key
|
|
||||||
PGPKeyPair certKey = generateKeyPair(primaryKeySpec);
|
|
||||||
PGPContentSignerBuilder signer = buildContentSigner(certKey);
|
|
||||||
PGPSignatureGenerator signatureGenerator = new PGPSignatureGenerator(signer);
|
|
||||||
|
|
||||||
SignatureSubpackets hashedSubPacketGenerator = primaryKeySpec.getSubpacketGenerator();
|
|
||||||
hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.getPublicKey());
|
|
||||||
if (expirationDate != null) {
|
|
||||||
hashedSubPacketGenerator.setKeyExpirationTime(certKey.getPublicKey(), expirationDate);
|
|
||||||
}
|
|
||||||
if (!userIds.isEmpty()) {
|
|
||||||
hashedSubPacketGenerator.setPrimaryUserId();
|
|
||||||
}
|
|
||||||
|
|
||||||
PGPSignatureSubpacketGenerator generator = new PGPSignatureSubpacketGenerator();
|
|
||||||
SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator);
|
|
||||||
PGPSignatureSubpacketVector hashedSubPackets = generator.generate();
|
|
||||||
PGPKeyRingGenerator ringGenerator;
|
|
||||||
if (userIds.isEmpty()) {
|
|
||||||
ringGenerator = new PGPKeyRingGenerator(
|
|
||||||
certKey,
|
|
||||||
keyFingerprintCalculator,
|
|
||||||
hashedSubPackets,
|
|
||||||
null,
|
|
||||||
signer,
|
|
||||||
secretKeyEncryptor);
|
|
||||||
} else {
|
|
||||||
String primaryUserId = userIds.entrySet().iterator().next().getKey();
|
|
||||||
ringGenerator = new PGPKeyRingGenerator(
|
|
||||||
SignatureType.POSITIVE_CERTIFICATION.getCode(), certKey,
|
|
||||||
primaryUserId, keyFingerprintCalculator,
|
|
||||||
hashedSubPackets, null, signer, secretKeyEncryptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
addSubKeys(certKey, ringGenerator);
|
|
||||||
|
|
||||||
// Generate secret key ring with only primary user id
|
|
||||||
PGPSecretKeyRing secretKeyRing = ringGenerator.generateSecretKeyRing();
|
|
||||||
|
|
||||||
Iterator<PGPSecretKey> secretKeys = secretKeyRing.getSecretKeys();
|
|
||||||
|
|
||||||
// Attempt to add additional user-ids to the primary public key
|
|
||||||
PGPPublicKey primaryPubKey = secretKeys.next().getPublicKey();
|
|
||||||
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.getSecretKey(), secretKeyDecryptor);
|
|
||||||
Iterator<Map.Entry<String, SelfSignatureSubpackets.Callback>> userIdIterator =
|
|
||||||
this.userIds.entrySet().iterator();
|
|
||||||
if (userIdIterator.hasNext()) {
|
|
||||||
userIdIterator.next(); // Skip primary user id
|
|
||||||
}
|
|
||||||
while (userIdIterator.hasNext()) {
|
|
||||||
Map.Entry<String, SelfSignatureSubpackets.Callback> additionalUserId = userIdIterator.next();
|
|
||||||
String userIdString = additionalUserId.getKey();
|
|
||||||
SelfSignatureSubpackets.Callback callback = additionalUserId.getValue();
|
|
||||||
SelfSignatureSubpackets subpackets = null;
|
|
||||||
if (callback == null) {
|
|
||||||
subpackets = hashedSubPacketGenerator;
|
|
||||||
subpackets.setPrimaryUserId(null);
|
|
||||||
// additional user-ids are not primary
|
|
||||||
} else {
|
|
||||||
subpackets = SignatureSubpackets.createHashedSubpackets(primaryPubKey);
|
|
||||||
callback.modifyHashedSubpackets(subpackets);
|
|
||||||
}
|
|
||||||
signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.getCode(), privateKey);
|
|
||||||
signatureGenerator.setHashedSubpackets(
|
|
||||||
SignatureSubpacketsHelper.toVector((SignatureSubpackets) subpackets));
|
|
||||||
PGPSignature additionalUserIdSignature =
|
|
||||||
signatureGenerator.generateCertification(userIdString, primaryPubKey);
|
|
||||||
primaryPubKey = PGPPublicKey.addCertification(primaryPubKey,
|
|
||||||
userIdString, additionalUserIdSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
// "reassemble" secret key ring with modified primary key
|
|
||||||
PGPSecretKey primarySecKey = new PGPSecretKey(
|
|
||||||
privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor);
|
|
||||||
List<PGPSecretKey> secretKeyList = new ArrayList<>();
|
|
||||||
secretKeyList.add(primarySecKey);
|
|
||||||
while (secretKeys.hasNext()) {
|
|
||||||
secretKeyList.add(secretKeys.next());
|
|
||||||
}
|
|
||||||
secretKeyRing = new PGPSecretKeyRing(secretKeyList);
|
|
||||||
return secretKeyRing;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addSubKeys(PGPKeyPair primaryKey, PGPKeyRingGenerator ringGenerator)
|
|
||||||
throws NoSuchAlgorithmException, PGPException, InvalidAlgorithmParameterException {
|
|
||||||
for (KeySpec subKeySpec : subkeySpecs) {
|
|
||||||
PGPKeyPair subKey = generateKeyPair(subKeySpec);
|
|
||||||
if (subKeySpec.isInheritedSubPackets()) {
|
|
||||||
ringGenerator.addSubKey(subKey);
|
|
||||||
} else {
|
|
||||||
PGPSignatureSubpacketVector hashedSubpackets = subKeySpec.getSubpackets();
|
|
||||||
try {
|
|
||||||
hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary(
|
|
||||||
primaryKey, subKey, hashedSubpackets);
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new PGPException("Exception while adding primary key binding signature to signing subkey", e);
|
|
||||||
}
|
|
||||||
ringGenerator.addSubKey(subKey, hashedSubpackets, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private PGPSignatureSubpacketVector addPrimaryKeyBindingSignatureIfNecessary(
|
|
||||||
PGPKeyPair primaryKey, PGPKeyPair subKey, PGPSignatureSubpacketVector hashedSubpackets)
|
|
||||||
throws PGPException, IOException {
|
|
||||||
int keyFlagMask = hashedSubpackets.getKeyFlags();
|
|
||||||
if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) &&
|
|
||||||
!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) {
|
|
||||||
return hashedSubpackets;
|
|
||||||
}
|
|
||||||
|
|
||||||
PGPSignatureGenerator bindingSignatureGenerator = new PGPSignatureGenerator(buildContentSigner(subKey));
|
|
||||||
bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.getCode(), subKey.getPrivateKey());
|
|
||||||
PGPSignature primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.getPublicKey(), subKey.getPublicKey());
|
|
||||||
PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(hashedSubpackets);
|
|
||||||
subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig);
|
|
||||||
return subpacketGenerator.generate();
|
|
||||||
}
|
|
||||||
|
|
||||||
private PGPContentSignerBuilder buildContentSigner(PGPKeyPair certKey) {
|
|
||||||
HashAlgorithm hashAlgorithm = PGPainless.getPolicy()
|
|
||||||
.getSignatureHashAlgorithmPolicy().defaultHashAlgorithm();
|
|
||||||
return ImplementationFactory.getInstance().getPGPContentSignerBuilder(
|
|
||||||
certKey.getPublicKey().getAlgorithm(),
|
|
||||||
hashAlgorithm.getAlgorithmId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private PBESecretKeyEncryptor buildSecretKeyEncryptor(PGPDigestCalculator keyFingerprintCalculator) {
|
|
||||||
SymmetricKeyAlgorithm keyEncryptionAlgorithm = PGPainless.getPolicy()
|
|
||||||
.getSymmetricKeyEncryptionAlgorithmPolicy()
|
|
||||||
.getDefaultSymmetricKeyAlgorithm();
|
|
||||||
if (!passphrase.isValid()) {
|
|
||||||
throw new IllegalStateException("Passphrase was cleared.");
|
|
||||||
}
|
|
||||||
return passphrase.isEmpty() ? null : // unencrypted key pair, otherwise AES-256 encrypted
|
|
||||||
ImplementationFactory.getInstance().getPBESecretKeyEncryptor(
|
|
||||||
keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PBESecretKeyDecryptor buildSecretKeyDecryptor() throws PGPException {
|
|
||||||
if (!passphrase.isValid()) {
|
|
||||||
throw new IllegalStateException("Passphrase was cleared.");
|
|
||||||
}
|
|
||||||
return passphrase.isEmpty() ? null :
|
|
||||||
ImplementationFactory.getInstance().getPBESecretKeyDecryptor(passphrase);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PGPKeyPair generateKeyPair(KeySpec spec)
|
|
||||||
throws NoSuchAlgorithmException, PGPException,
|
|
||||||
InvalidAlgorithmParameterException {
|
|
||||||
KeyType type = spec.getKeyType();
|
|
||||||
KeyPairGenerator certKeyGenerator = KeyPairGenerator.getInstance(type.getName(),
|
|
||||||
ProviderFactory.getProvider());
|
|
||||||
certKeyGenerator.initialize(type.getAlgorithmSpec());
|
|
||||||
|
|
||||||
// Create raw Key Pair
|
|
||||||
KeyPair keyPair = certKeyGenerator.generateKeyPair();
|
|
||||||
|
|
||||||
Date keyCreationDate = spec.getKeyCreationDate() != null ? spec.getKeyCreationDate() : new Date();
|
|
||||||
|
|
||||||
// Form PGP key pair
|
|
||||||
PGPKeyPair pgpKeyPair = ImplementationFactory.getInstance()
|
|
||||||
.getPGPKeyPair(type.getAlgorithm(), keyPair, keyCreationDate);
|
|
||||||
return pgpKeyPair;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.key.generation;
|
|
||||||
|
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.util.Date;
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
import org.bouncycastle.openpgp.PGPException;
|
|
||||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
||||||
import org.pgpainless.key.util.UserId;
|
|
||||||
import org.pgpainless.util.Passphrase;
|
|
||||||
|
|
||||||
public interface KeyRingBuilderInterface<B extends KeyRingBuilderInterface<B>> {
|
|
||||||
|
|
||||||
B setPrimaryKey(@Nonnull KeySpec keySpec);
|
|
||||||
|
|
||||||
default B setPrimaryKey(@Nonnull KeySpecBuilder builder) {
|
|
||||||
return setPrimaryKey(builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
B addSubkey(@Nonnull KeySpec keySpec);
|
|
||||||
|
|
||||||
default B addSubkey(@Nonnull KeySpecBuilder builder) {
|
|
||||||
return addSubkey(builder.build());
|
|
||||||
}
|
|
||||||
|
|
||||||
default B addUserId(UserId userId) {
|
|
||||||
return addUserId(userId.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
B addUserId(@Nonnull String userId);
|
|
||||||
|
|
||||||
B addUserId(@Nonnull byte[] userId);
|
|
||||||
|
|
||||||
B setExpirationDate(@Nonnull Date expirationDate);
|
|
||||||
|
|
||||||
B setPassphrase(@Nonnull Passphrase passphrase);
|
|
||||||
|
|
||||||
PGPSecretKeyRing build() throws NoSuchAlgorithmException, PGPException,
|
|
||||||
InvalidAlgorithmParameterException;
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2018 Paul Schaub <vanitasvitae@fsfe.org>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
|
||||||
|
|
||||||
package org.pgpainless.key.generation;
|
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
|
||||||
|
|
||||||
import org.pgpainless.algorithm.CompressionAlgorithm;
|
|
||||||
import org.pgpainless.algorithm.HashAlgorithm;
|
|
||||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
public interface KeySpecBuilderInterface {
|
|
||||||
|
|
||||||
KeySpecBuilder overridePreferredCompressionAlgorithms(@Nonnull CompressionAlgorithm... compressionAlgorithms);
|
|
||||||
|
|
||||||
KeySpecBuilder overridePreferredHashAlgorithms(@Nonnull HashAlgorithm... preferredHashAlgorithms);
|
|
||||||
|
|
||||||
KeySpecBuilder overridePreferredSymmetricKeyAlgorithms(@Nonnull SymmetricKeyAlgorithm... preferredSymmetricKeyAlgorithms);
|
|
||||||
|
|
||||||
KeySpecBuilder setKeyCreationDate(@Nonnull Date creationDate);
|
|
||||||
|
|
||||||
KeySpec build();
|
|
||||||
}
|
|
|
@ -0,0 +1,244 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.key.generation
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.*
|
||||||
|
import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor
|
||||||
|
import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor
|
||||||
|
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder
|
||||||
|
import org.bouncycastle.openpgp.operator.PGPDigestCalculator
|
||||||
|
import org.bouncycastle.util.Strings
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
|
import org.pgpainless.algorithm.KeyFlag
|
||||||
|
import org.pgpainless.algorithm.SignatureType
|
||||||
|
import org.pgpainless.implementation.ImplementationFactory
|
||||||
|
import org.pgpainless.key.protection.UnlockSecretKey
|
||||||
|
import org.pgpainless.policy.Policy
|
||||||
|
import org.pgpainless.provider.ProviderFactory
|
||||||
|
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets
|
||||||
|
import org.pgpainless.signature.subpackets.SignatureSubpackets
|
||||||
|
import org.pgpainless.signature.subpackets.SignatureSubpacketsHelper
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.KeyPairGenerator
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
const val MILLIS_IN_YEAR = 1000L * 60 * 60 * 24 * 365
|
||||||
|
|
||||||
|
class KeyRingBuilder : KeyRingBuilderInterface<KeyRingBuilder> {
|
||||||
|
|
||||||
|
private var primaryKeySpec: KeySpec? = null
|
||||||
|
private val subKeySpecs = mutableListOf<KeySpec>()
|
||||||
|
private val userIds = mutableMapOf<String, SelfSignatureSubpackets.Callback?>()
|
||||||
|
private var passphrase = Passphrase.emptyPassphrase()
|
||||||
|
private var expirationDate: Date? = Date(System.currentTimeMillis() + (5 * MILLIS_IN_YEAR))
|
||||||
|
|
||||||
|
override fun setPrimaryKey(keySpec: KeySpec): KeyRingBuilder = apply {
|
||||||
|
verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy())
|
||||||
|
verifyPrimaryKeyCanCertify(keySpec)
|
||||||
|
this.primaryKeySpec = keySpec
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addSubkey(keySpec: KeySpec): KeyRingBuilder = apply {
|
||||||
|
verifyKeySpecCompliesToPolicy(keySpec, PGPainless.getPolicy())
|
||||||
|
subKeySpecs.add(keySpec)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addUserId(userId: CharSequence): KeyRingBuilder = apply {
|
||||||
|
userIds[userId.toString().trim()] = null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun addUserId(userId: ByteArray): KeyRingBuilder = addUserId(Strings.fromUTF8ByteArray(userId))
|
||||||
|
|
||||||
|
override fun setExpirationDate(expirationDate: Date?): KeyRingBuilder = apply {
|
||||||
|
if (expirationDate == null) {
|
||||||
|
this.expirationDate = null
|
||||||
|
return@apply
|
||||||
|
}
|
||||||
|
this.expirationDate = expirationDate.let {
|
||||||
|
require(Date() < expirationDate) {
|
||||||
|
"Expiration date must be in the future."
|
||||||
|
}
|
||||||
|
expirationDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setPassphrase(passphrase: Passphrase): KeyRingBuilder = apply {
|
||||||
|
this.passphrase = passphrase
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyKeySpecCompliesToPolicy(keySpec: KeySpec, policy: Policy) {
|
||||||
|
val algorithm = keySpec.keyType.algorithm
|
||||||
|
val bitStrength = keySpec.keyType.bitStrength
|
||||||
|
require(policy.publicKeyAlgorithmPolicy.isAcceptable(algorithm, bitStrength)) {
|
||||||
|
"Public key algorithm policy violation: $algorithm with bit strength $bitStrength is not acceptable."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyPrimaryKeyCanCertify(spec: KeySpec) {
|
||||||
|
require(hasCertifyOthersFlag(spec)) {
|
||||||
|
"Certification Key MUST have the KeyFlag CERTIFY_OTHER."
|
||||||
|
}
|
||||||
|
require(keyIsCertificationCapable(spec)) {
|
||||||
|
"Key algorithm ${spec.keyType.name} is not capable of creation certifications."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hasCertifyOthersFlag(keySpec: KeySpec): Boolean {
|
||||||
|
val flags = keySpec.subpacketGenerator.keyFlagsSubpacket ?: return false
|
||||||
|
return KeyFlag.hasKeyFlag(flags.flags, KeyFlag.CERTIFY_OTHER)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun keyIsCertificationCapable(keySpec: KeySpec) = keySpec.keyType.canCertify()
|
||||||
|
|
||||||
|
override fun build(): PGPSecretKeyRing {
|
||||||
|
val keyFingerprintCalculator = ImplementationFactory.getInstance()
|
||||||
|
.v4FingerprintCalculator
|
||||||
|
val secretKeyEncryptor = buildSecretKeyEncryptor(keyFingerprintCalculator)
|
||||||
|
val secretKeyDecryptor = buildSecretKeyDecryptor()
|
||||||
|
|
||||||
|
passphrase.clear() // Passphrase was used above, so we can get rid of it
|
||||||
|
|
||||||
|
// generate primary key
|
||||||
|
requireNotNull(primaryKeySpec) {
|
||||||
|
"Primary Key spec required."
|
||||||
|
}
|
||||||
|
val certKey = generateKeyPair(primaryKeySpec!!)
|
||||||
|
val signer = buildContentSigner(certKey)
|
||||||
|
val signatureGenerator = PGPSignatureGenerator(signer)
|
||||||
|
|
||||||
|
val hashedSubPacketGenerator = primaryKeySpec!!.subpacketGenerator
|
||||||
|
hashedSubPacketGenerator.setIssuerFingerprintAndKeyId(certKey.publicKey)
|
||||||
|
expirationDate?.let { hashedSubPacketGenerator.setKeyExpirationTime(certKey.publicKey, it) }
|
||||||
|
if (userIds.isNotEmpty()) {
|
||||||
|
hashedSubPacketGenerator.setPrimaryUserId()
|
||||||
|
}
|
||||||
|
|
||||||
|
val generator = PGPSignatureSubpacketGenerator()
|
||||||
|
SignatureSubpacketsHelper.applyTo(hashedSubPacketGenerator, generator)
|
||||||
|
val hashedSubPackets = generator.generate()
|
||||||
|
val ringGenerator = if (userIds.isEmpty()) {
|
||||||
|
PGPKeyRingGenerator(certKey, keyFingerprintCalculator, hashedSubPackets, null, signer,
|
||||||
|
secretKeyEncryptor)
|
||||||
|
} else {
|
||||||
|
userIds.keys.first().let { primaryUserId ->
|
||||||
|
PGPKeyRingGenerator(SignatureType.POSITIVE_CERTIFICATION.code, certKey, primaryUserId,
|
||||||
|
keyFingerprintCalculator, hashedSubPackets, null, signer,
|
||||||
|
secretKeyEncryptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addSubKeys(certKey, ringGenerator)
|
||||||
|
|
||||||
|
// Generate secret key ring with only primary userId
|
||||||
|
val secretKeyRing = ringGenerator.generateSecretKeyRing()
|
||||||
|
val secretKeys = secretKeyRing.secretKeys
|
||||||
|
|
||||||
|
// Attempt to add additional user-ids to the primary public key
|
||||||
|
var primaryPubKey = secretKeys.next().publicKey
|
||||||
|
val privateKey = UnlockSecretKey.unlockSecretKey(secretKeyRing.secretKey, secretKeyDecryptor)
|
||||||
|
val userIdIterator = userIds.entries.iterator()
|
||||||
|
if (userIdIterator.hasNext()) {
|
||||||
|
userIdIterator.next() // Skip primary userId
|
||||||
|
}
|
||||||
|
while (userIdIterator.hasNext()) {
|
||||||
|
val additionalUserId = userIdIterator.next()
|
||||||
|
val userIdString = additionalUserId.key
|
||||||
|
val callback = additionalUserId.value
|
||||||
|
val subpackets = if (callback == null) {
|
||||||
|
hashedSubPacketGenerator.also { it.setPrimaryUserId(null) }
|
||||||
|
} else {
|
||||||
|
SignatureSubpackets.createHashedSubpackets(primaryPubKey).also { callback.modifyHashedSubpackets(it) }
|
||||||
|
}
|
||||||
|
signatureGenerator.init(SignatureType.POSITIVE_CERTIFICATION.code, privateKey)
|
||||||
|
signatureGenerator.setHashedSubpackets(
|
||||||
|
SignatureSubpacketsHelper.toVector(subpackets))
|
||||||
|
val additionalUserIdSignature = signatureGenerator.generateCertification(userIdString, primaryPubKey)
|
||||||
|
primaryPubKey = PGPPublicKey.addCertification(primaryPubKey, userIdString, additionalUserIdSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reassemble secret key ring with modified primary key
|
||||||
|
val primarySecretKey = PGPSecretKey(
|
||||||
|
privateKey, primaryPubKey, keyFingerprintCalculator, true, secretKeyEncryptor)
|
||||||
|
val secretKeyList = mutableListOf(primarySecretKey)
|
||||||
|
while (secretKeys.hasNext()) {
|
||||||
|
secretKeyList.add(secretKeys.next())
|
||||||
|
}
|
||||||
|
return PGPSecretKeyRing(secretKeyList)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addSubKeys(primaryKey: PGPKeyPair, ringGenerator: PGPKeyRingGenerator) {
|
||||||
|
for (subKeySpec in subKeySpecs) {
|
||||||
|
val subKey = generateKeyPair(subKeySpec)
|
||||||
|
if (subKeySpec.isInheritedSubPackets) {
|
||||||
|
ringGenerator.addSubKey(subKey)
|
||||||
|
} else {
|
||||||
|
var hashedSubpackets = subKeySpec.subpackets
|
||||||
|
try {
|
||||||
|
hashedSubpackets = addPrimaryKeyBindingSignatureIfNecessary(primaryKey, subKey, hashedSubpackets)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw PGPException("Exception while adding primary key binding signature to signing subkey.", e)
|
||||||
|
}
|
||||||
|
ringGenerator.addSubKey(subKey, hashedSubpackets, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addPrimaryKeyBindingSignatureIfNecessary(primaryKey: PGPKeyPair, subKey: PGPKeyPair, hashedSubpackets: PGPSignatureSubpacketVector): PGPSignatureSubpacketVector {
|
||||||
|
val keyFlagMask = hashedSubpackets.keyFlags
|
||||||
|
if (!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.SIGN_DATA) &&
|
||||||
|
!KeyFlag.hasKeyFlag(keyFlagMask, KeyFlag.CERTIFY_OTHER)) {
|
||||||
|
return hashedSubpackets
|
||||||
|
}
|
||||||
|
|
||||||
|
val bindingSignatureGenerator = PGPSignatureGenerator(buildContentSigner(subKey))
|
||||||
|
bindingSignatureGenerator.init(SignatureType.PRIMARYKEY_BINDING.code, subKey.privateKey)
|
||||||
|
val primaryKeyBindingSig = bindingSignatureGenerator.generateCertification(primaryKey.publicKey, subKey.publicKey)
|
||||||
|
val subpacketGenerator = PGPSignatureSubpacketGenerator(hashedSubpackets)
|
||||||
|
subpacketGenerator.addEmbeddedSignature(false, primaryKeyBindingSig)
|
||||||
|
return subpacketGenerator.generate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildContentSigner(certKey: PGPKeyPair): PGPContentSignerBuilder {
|
||||||
|
val hashAlgorithm = PGPainless.getPolicy().signatureHashAlgorithmPolicy.defaultHashAlgorithm
|
||||||
|
return ImplementationFactory.getInstance().getPGPContentSignerBuilder(
|
||||||
|
certKey.publicKey.algorithm, hashAlgorithm.algorithmId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSecretKeyEncryptor(keyFingerprintCalculator: PGPDigestCalculator): PBESecretKeyEncryptor? {
|
||||||
|
val keyEncryptionAlgorithm = PGPainless.getPolicy().symmetricKeyEncryptionAlgorithmPolicy.defaultSymmetricKeyAlgorithm
|
||||||
|
check(passphrase.isValid) {
|
||||||
|
"Passphrase was cleared."
|
||||||
|
}
|
||||||
|
return if (passphrase.isEmpty) null else ImplementationFactory.getInstance()
|
||||||
|
.getPBESecretKeyEncryptor(keyEncryptionAlgorithm, keyFingerprintCalculator, passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSecretKeyDecryptor(): PBESecretKeyDecryptor? {
|
||||||
|
check(passphrase.isValid) {
|
||||||
|
"Passphrase was cleared."
|
||||||
|
}
|
||||||
|
return if (passphrase.isEmpty) null else ImplementationFactory.getInstance()
|
||||||
|
.getPBESecretKeyDecryptor(passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun generateKeyPair(spec: KeySpec): PGPKeyPair {
|
||||||
|
spec.keyType.let { type ->
|
||||||
|
// Create raw Key Pair
|
||||||
|
val keyPair = KeyPairGenerator.getInstance(type.name, ProviderFactory.getProvider())
|
||||||
|
.also { it.initialize(type.algorithmSpec) }
|
||||||
|
.generateKeyPair()
|
||||||
|
|
||||||
|
val keyCreationDate = spec.keyCreationDate ?: Date()
|
||||||
|
// Form PGP Key Pair
|
||||||
|
return ImplementationFactory.getInstance()
|
||||||
|
.getPGPKeyPair(type.algorithm, keyPair, keyCreationDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.key.generation
|
||||||
|
|
||||||
|
import org.bouncycastle.openpgp.PGPException
|
||||||
|
import org.bouncycastle.openpgp.PGPSecretKeyRing
|
||||||
|
import org.pgpainless.util.Passphrase
|
||||||
|
import java.security.InvalidAlgorithmParameterException
|
||||||
|
import java.security.NoSuchAlgorithmException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface KeyRingBuilderInterface<B : KeyRingBuilderInterface<B>> {
|
||||||
|
|
||||||
|
fun setPrimaryKey(keySpec: KeySpec): B
|
||||||
|
|
||||||
|
fun setPrimaryKey(builder: KeySpecBuilder): B = setPrimaryKey(builder.build())
|
||||||
|
|
||||||
|
fun addSubkey(keySpec: KeySpec): B
|
||||||
|
|
||||||
|
fun addSubkey(builder: KeySpecBuilder): B = addSubkey(builder.build())
|
||||||
|
|
||||||
|
fun addUserId(userId: CharSequence): B
|
||||||
|
|
||||||
|
fun addUserId(userId: ByteArray): B
|
||||||
|
|
||||||
|
fun setExpirationDate(expirationDate: Date?): B
|
||||||
|
|
||||||
|
fun setPassphrase(passphrase: Passphrase): B
|
||||||
|
|
||||||
|
@Throws(NoSuchAlgorithmException::class, PGPException::class, InvalidAlgorithmParameterException::class)
|
||||||
|
fun build(): PGPSecretKeyRing
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
package org.pgpainless.key.generation
|
||||||
|
|
||||||
|
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||||
|
import org.pgpainless.algorithm.HashAlgorithm
|
||||||
|
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
interface KeySpecBuilderInterface {
|
||||||
|
|
||||||
|
fun overridePreferredCompressionAlgorithms(vararg algorithms: CompressionAlgorithm): KeySpecBuilder
|
||||||
|
|
||||||
|
fun overridePreferredHashAlgorithms(vararg algorithms: HashAlgorithm): KeySpecBuilder
|
||||||
|
|
||||||
|
fun overridePreferredSymmetricKeyAlgorithms(vararg algorithms: SymmetricKeyAlgorithm): KeySpecBuilder
|
||||||
|
|
||||||
|
fun setKeyCreationDate(creationDate: Date): KeySpecBuilder
|
||||||
|
|
||||||
|
fun build(): KeySpec
|
||||||
|
}
|
Loading…
Reference in a new issue