From dc064d17278f82b2bb94dd3b28bacb3bbeb6da5a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 4 Sep 2023 16:32:01 +0200 Subject: [PATCH] Kotlin conversion: KeyRingUtils --- .../org/pgpainless/key/util/KeyRingUtils.java | 589 ------------------ .../extensions/PGPSecretKeyRingExtensions.kt | 11 + .../org/pgpainless/key/util/KeyRingUtils.kt | 484 ++++++++++++++ 3 files changed, 495 insertions(+), 589 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java create mode 100644 pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt create mode 100644 pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt diff --git a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java b/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java deleted file mode 100644 index 91de3be4..00000000 --- a/pgpainless-core/src/main/java/org/pgpainless/key/util/KeyRingUtils.java +++ /dev/null @@ -1,589 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.pgpainless.key.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.openpgp.PGPException; -import org.bouncycastle.openpgp.PGPKeyRing; -import org.bouncycastle.openpgp.PGPPrivateKey; -import org.bouncycastle.openpgp.PGPPublicKey; -import org.bouncycastle.openpgp.PGPPublicKeyRing; -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; -import org.bouncycastle.openpgp.PGPSignature; -import org.bouncycastle.openpgp.PGPUserAttributeSubpacketVector; -import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; -import org.bouncycastle.openpgp.operator.PBESecretKeyEncryptor; -import org.bouncycastle.util.Strings; -import org.pgpainless.PGPainless; -import org.pgpainless.implementation.ImplementationFactory; -import org.pgpainless.key.protection.SecretKeyRingProtector; -import org.pgpainless.key.protection.UnlockSecretKey; -import org.pgpainless.key.protection.fixes.S2KUsageFix; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public final class KeyRingUtils { - - private KeyRingUtils() { - - } - - private static final Logger LOGGER = LoggerFactory.getLogger(KeyRingUtils.class); - - /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. - * If it has no primary secret key, throw a {@link NoSuchElementException}. - * - * @param secretKeys secret keys - * @return primary secret key - */ - @Nonnull - public static PGPSecretKey requirePrimarySecretKeyFrom(@Nonnull PGPSecretKeyRing secretKeys) { - PGPSecretKey primarySecretKey = getPrimarySecretKeyFrom(secretKeys); - if (primarySecretKey == null) { - throw new NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key."); - } - return primarySecretKey; - } - - /** - * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing} or null if it has none. - * - * @param secretKeys secret key ring - * @return primary secret key - */ - @Nullable - public static PGPSecretKey getPrimarySecretKeyFrom(@Nonnull PGPSecretKeyRing secretKeys) { - PGPSecretKey secretKey = secretKeys.getSecretKey(); - if (secretKey.isMasterKey()) { - return secretKey; - } - return null; - } - - /** - * Return the primary {@link PGPPublicKey} from the provided key ring. - * Throws a {@link NoSuchElementException} if the key ring has no primary public key. - * - * @param keyRing key ring - * @return primary public key - */ - @Nonnull - public static PGPPublicKey requirePrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = getPrimaryPublicKeyFrom(keyRing); - if (primaryPublicKey == null) { - throw new NoSuchElementException("Provided PGPKeyRing has no primary public key."); - } - return primaryPublicKey; - } - - /** - * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has none. - * - * @param keyRing key ring - * @return primary public key - */ - @Nullable - public static PGPPublicKey getPrimaryPublicKeyFrom(@Nonnull PGPKeyRing keyRing) { - PGPPublicKey primaryPublicKey = keyRing.getPublicKey(); - if (primaryPublicKey.isMasterKey()) { - return primaryPublicKey; - } - return null; - } - - /** - * Return the public key with the given subKeyId from the keyRing. - * If no such subkey exists, return null. - * @param keyRing key ring - * @param subKeyId subkey id - * @return subkey or null - */ - @Nullable - public static PGPPublicKey getPublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { - return keyRing.getPublicKey(subKeyId); - } - - /** - * Require the public key with the given subKeyId from the keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. - * - * @param keyRing key ring - * @param subKeyId subkey id - * @return subkey - */ - @Nonnull - public static PGPPublicKey requirePublicKeyFrom(@Nonnull PGPKeyRing keyRing, long subKeyId) { - PGPPublicKey publicKey = getPublicKeyFrom(keyRing, subKeyId); - if (publicKey == null) { - throw new NoSuchElementException("KeyRing does not contain public key with keyID " + Long.toHexString(subKeyId)); - } - return publicKey; - } - - /** - * Require the secret key with the given secret subKeyId from the secret keyRing. - * If no such subkey exists, throw an {@link NoSuchElementException}. - * - * @param keyRing secret key ring - * @param subKeyId subkey id - * @return secret subkey - */ - @Nonnull - public static PGPSecretKey requireSecretKeyFrom(@Nonnull PGPSecretKeyRing keyRing, long subKeyId) { - PGPSecretKey secretKey = keyRing.getSecretKey(subKeyId); - if (secretKey == null) { - throw new NoSuchElementException("KeyRing does not contain secret key with keyID " + Long.toHexString(subKeyId)); - } - return secretKey; - } - - @Nonnull - public static PGPPublicKeyRing publicKeys(@Nonnull PGPKeyRing keys) { - if (keys instanceof PGPPublicKeyRing) { - return (PGPPublicKeyRing) keys; - } else if (keys instanceof PGPSecretKeyRing) { - return publicKeyRingFrom((PGPSecretKeyRing) keys); - } else { - throw new IllegalArgumentException("Unknown keys class: " + keys.getClass().getName()); - } - } - - /** - * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @return public key ring - */ - @Nonnull - public static PGPPublicKeyRing publicKeyRingFrom(@Nonnull PGPSecretKeyRing secretKeys) { - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = secretKeys.getPublicKeys(); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - PGPPublicKeyRing publicKeyRing = new PGPPublicKeyRing(publicKeyList); - return publicKeyRing; - } - - /** - * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in - * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. - * - * @param secretKeyRings secret key ring collection - * @return public key ring collection - */ - @Nonnull - public static PGPPublicKeyRingCollection publicKeyRingCollectionFrom(@Nonnull PGPSecretKeyRingCollection secretKeyRings) { - List certificates = new ArrayList<>(); - for (PGPSecretKeyRing secretKey : secretKeyRings) { - certificates.add(PGPainless.extractCertificate(secretKey)); - } - return new PGPPublicKeyRingCollection(certificates); - } - - /** - * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. - * - * @param secretKey secret key - * @param protector protector to unlock the secret key - * @return private key - * - * @throws PGPException if something goes wrong (e.g. wrong passphrase) - */ - @Nonnull - public static PGPPrivateKey unlockSecretKey(@Nonnull PGPSecretKey secretKey, @Nonnull SecretKeyRingProtector protector) - throws PGPException { - return UnlockSecretKey.unlockSecretKey(secretKey, protector); - } - - /** - * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. - * - * @param rings array of public key rings - * @return key ring collection - */ - @Nonnull - public static PGPPublicKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPPublicKeyRing... rings) { - return new PGPPublicKeyRingCollection(Arrays.asList(rings)); - } - - /** - * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. - * - * @param rings array of secret key rings - * @return secret key ring collection - */ - @Nonnull - public static PGPSecretKeyRingCollection keyRingsToKeyRingCollection(@Nonnull PGPSecretKeyRing... rings) { - return new PGPSecretKeyRingCollection(Arrays.asList(rings)); - } - - /** - * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. - * - * @param ring public key ring - * @param keyId id of the key in question - * @return true if ring contains said key, false otherwise - */ - public static boolean keyRingContainsKeyWithId(@Nonnull PGPPublicKeyRing ring, - long keyId) { - return ring.getPublicKey(keyId) != null; - } - - /** - * Inject a key certification for the primary key into the given key ring. - * - * @param keyRing key ring - * @param certification key signature - * @return key ring with injected signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPSignature certification) { - return injectCertification(keyRing, keyRing.getPublicKey(), certification); - } - - /** - * Inject a key certification for the given key into the given key ring. - * - * @param keyRing key ring - * @param certifiedKey signed public key - * @param certification key signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected signature - * - * @throws NoSuchElementException in case that the signed key is not part of the key ring - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPPublicKey certifiedKey, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - certifiedKey = PGPPublicKey.addCertification(certifiedKey, certification); - List publicKeyList = new ArrayList<>(); - Iterator publicKeyIterator = publicKeys.iterator(); - boolean added = false; - while (publicKeyIterator.hasNext()) { - PGPPublicKey key = publicKeyIterator.next(); - if (key.getKeyID() == certifiedKey.getKeyID()) { - added = true; - publicKeyList.add(certifiedKey); - } else { - publicKeyList.add(key); - } - } - if (!added) { - throw new NoSuchElementException("Cannot find public key with id " + Long.toHexString(certifiedKey.getKeyID()) + " in the provided key ring."); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a user-id certification into the given key ring. - * - * @param keyRing key ring - * @param userId signed user-id - * @param certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected certification - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull String userId, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - Iterator publicKeyIterator = publicKeys.iterator(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - primaryKey = PGPPublicKey.addCertification(primaryKey, userId, certification); - - List publicKeyList = new ArrayList<>(); - publicKeyList.add(primaryKey); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a user-attribute vector certification into the given key ring. - * - * @param keyRing key ring - * @param userAttributes certified user attributes - * @param certification certification signature - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected user-attribute certification - */ - @Nonnull - public static T injectCertification(@Nonnull T keyRing, - @Nonnull PGPUserAttributeSubpacketVector userAttributes, - @Nonnull PGPSignature certification) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - Iterator publicKeyIterator = publicKeys.iterator(); - PGPPublicKey primaryKey = publicKeyIterator.next(); - primaryKey = PGPPublicKey.addCertification(primaryKey, userAttributes, certification); - - List publicKeyList = new ArrayList<>(); - publicKeyList.add(primaryKey); - while (publicKeyIterator.hasNext()) { - publicKeyList.add(publicKeyIterator.next()); - } - - publicKeys = new PGPPublicKeyRing(publicKeyList); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.replacePublicKeys(secretKeys, publicKeys); - return (T) secretKeys; - } - } - - /** - * Inject a {@link PGPPublicKey} into the given key ring. - * - * @param keyRing key ring - * @param publicKey public key - * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} - * @return key ring with injected public key - */ - @Nonnull - public static T keysPlusPublicKey(@Nonnull T keyRing, - @Nonnull PGPPublicKey publicKey) { - PGPSecretKeyRing secretKeys = null; - PGPPublicKeyRing publicKeys; - if (keyRing instanceof PGPSecretKeyRing) { - secretKeys = (PGPSecretKeyRing) keyRing; - publicKeys = PGPainless.extractCertificate(secretKeys); - } else { - publicKeys = (PGPPublicKeyRing) keyRing; - } - - publicKeys = PGPPublicKeyRing.insertPublicKey(publicKeys, publicKey); - if (secretKeys == null) { - return (T) publicKeys; - } else { - secretKeys = PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey); - return (T) secretKeys; - } - } - - /** - * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. - * - * @param secretKeys secret key ring - * @param secretKey secret key - * @return secret key ring with injected secret key - */ - @Nonnull - public static PGPSecretKeyRing keysPlusSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - @Nonnull PGPSecretKey secretKey) { - return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey); - } - - /** - * Inject the given signature into the public part of the given secret key. - * @param secretKey secret key - * @param signature signature - * @return secret key with the signature injected in its public key - */ - @Nonnull - public static PGPSecretKey secretKeyPlusSignature(@Nonnull PGPSecretKey secretKey, - @Nonnull PGPSignature signature) { - PGPPublicKey publicKey = secretKey.getPublicKey(); - publicKey = PGPPublicKey.addCertification(publicKey, signature); - PGPSecretKey newSecretKey = PGPSecretKey.replacePublicKey(secretKey, publicKey); - return newSecretKey; - } - - /** - * Remove the secret key of the subkey identified by the given secret key id from the key ring. - * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. - * - * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. - * - * @param secretKeys secret key ring - * @param secretKeyId id of the secret key to remove - * @return secret key ring with removed secret key - * - * @throws IOException in case of an error during serialization / deserialization of the key - * @throws PGPException in case of a broken key - */ - @Nonnull - public static PGPSecretKeyRing stripSecretKey(@Nonnull PGPSecretKeyRing secretKeys, - long secretKeyId) - throws IOException, PGPException { - - if (secretKeys.getPublicKey().getKeyID() == secretKeyId) { - throw new IllegalArgumentException("Bouncy Castle currently cannot deal with stripped secret primary keys."); - } - - if (secretKeys.getSecretKey(secretKeyId) == null) { - throw new NoSuchElementException("PGPSecretKeyRing does not contain secret key " + Long.toHexString(secretKeyId)); - } - - // Since BCs constructors for secret key rings are mostly private, we need to encode the key ring how we want it - // and then parse it again. - ByteArrayOutputStream encoded = new ByteArrayOutputStream(); - for (PGPSecretKey secretKey : secretKeys) { - if (secretKey.getKeyID() == secretKeyId) { - // only encode the public part of the target key - secretKey.getPublicKey().encode(encoded); - } else { - // otherwise, encode secret + public key - secretKey.encode(encoded); - } - } - for (Iterator it = secretKeys.getExtraPublicKeys(); it.hasNext(); ) { - PGPPublicKey extra = it.next(); - extra.encode(encoded); - } - // Parse the key back into an object - return new PGPSecretKeyRing(encoded.toByteArray(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - /** - * Strip all user-ids, user-attributes and signatures from the given public key. - * - * @param bloatedKey public key - * @return stripped public key - * @throws PGPException if the packet is faulty or the required calculations fail - */ - public static PGPPublicKey getStrippedDownPublicKey(PGPPublicKey bloatedKey) throws PGPException { - return new PGPPublicKey(bloatedKey.getPublicKeyPacket(), ImplementationFactory.getInstance().getKeyFingerprintCalculator()); - } - - public static List getUserIdsIgnoringInvalidUTF8(PGPPublicKey key) { - List userIds = new ArrayList<>(); - Iterator it = key.getRawUserIDs(); - while (it.hasNext()) { - byte[] rawUserId = it.next(); - try { - userIds.add(Strings.fromUTF8ByteArray(rawUserId)); - } catch (IllegalArgumentException e) { - LOGGER.warn("Invalid UTF-8 user-ID encountered: " + new String(rawUserId)); - } - } - return userIds; - } - - public static PGPSecretKeyRing changePassphrase(Long keyId, - PGPSecretKeyRing secretKeys, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) - throws PGPException { - List secretKeyList = new ArrayList<>(); - if (keyId == null) { - // change passphrase of whole key ring - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - secretKey = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); - secretKeyList.add(secretKey); - } - } else { - // change passphrase of selected subkey only - Iterator secretKeyIterator = secretKeys.getSecretKeys(); - while (secretKeyIterator.hasNext()) { - PGPSecretKey secretKey = secretKeyIterator.next(); - if (secretKey.getPublicKey().getKeyID() == keyId) { - // Re-encrypt only the selected subkey - secretKey = KeyRingUtils.reencryptPrivateKey(secretKey, oldProtector, newProtector); - } - secretKeyList.add(secretKey); - } - } - - PGPSecretKeyRing newRing = new PGPSecretKeyRing(secretKeyList); - newRing = s2kUsageFixIfNecessary(newRing, newProtector); - return newRing; - } - - - public static PGPSecretKey reencryptPrivateKey( - PGPSecretKey secretKey, - SecretKeyRingProtector oldProtector, - SecretKeyRingProtector newProtector) - throws PGPException { - S2K s2k = secretKey.getS2K(); - // If the key uses GNU_DUMMY_S2K, we leave it as is and skip this block - if (s2k == null || s2k.getType() != S2K.GNU_DUMMY_S2K) { - long secretKeyId = secretKey.getKeyID(); - PBESecretKeyDecryptor decryptor = oldProtector.getDecryptor(secretKeyId); - PBESecretKeyEncryptor encryptor = newProtector.getEncryptor(secretKeyId); - secretKey = PGPSecretKey.copyWithNewPassword(secretKey, decryptor, encryptor); - } - return secretKey; - } - - - public static PGPSecretKeyRing s2kUsageFixIfNecessary(PGPSecretKeyRing secretKeys, SecretKeyRingProtector protector) - throws PGPException { - boolean hasS2KUsageChecksum = false; - for (PGPSecretKey secKey : secretKeys) { - if (secKey.getS2KUsage() == SecretKeyPacket.USAGE_CHECKSUM) { - hasS2KUsageChecksum = true; - break; - } - } - if (hasS2KUsageChecksum) { - secretKeys = S2KUsageFix.replaceUsageChecksumWithUsageSha1( - secretKeys, protector, true); - } - return secretKeys; - } -} diff --git a/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt new file mode 100644 index 00000000..3a9c2918 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/bouncycastle/extensions/PGPSecretKeyRingExtensions.kt @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle.extensions + +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing + +val PGPSecretKeyRing.certificate: PGPPublicKeyRing + get() = PGPPublicKeyRing(this.publicKeys.asSequence().toList()) \ No newline at end of file diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt new file mode 100644 index 00000000..bd0edf28 --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/key/util/KeyRingUtils.kt @@ -0,0 +1,484 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.pgpainless.key.util + +import _kotlin.hexKeyId +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.extensions.certificate +import org.bouncycastle.openpgp.* +import org.bouncycastle.util.Strings +import org.pgpainless.exception.MissingPassphraseException +import org.pgpainless.implementation.ImplementationFactory +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.key.protection.UnlockSecretKey +import org.pgpainless.key.protection.fixes.S2KUsageFix +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import kotlin.jvm.Throws + +class KeyRingUtils { + + companion object { + + @JvmStatic + private val LOGGER: Logger = LoggerFactory.getLogger(KeyRingUtils::class.java) + + /** + * Return the primary {@link PGPSecretKey} from the provided {@link PGPSecretKeyRing}. + * If it has no primary secret key, throw a {@link NoSuchElementException}. + * + * @param secretKeys secret keys + * @return primary secret key + */ + @JvmStatic + fun requirePrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey { + return getPrimarySecretKeyFrom(secretKeys) + ?: throw NoSuchElementException("Provided PGPSecretKeyRing has no primary secret key.") + } + + /** + * Return the primary secret key from the given secret key ring. + * If the key ring only contains subkeys (e.g. if the primary secret key is stripped), + * this method may return null. + * + * @param secretKeys secret key ring + * @return primary secret key + */ + @JvmStatic + fun getPrimarySecretKeyFrom(secretKeys: PGPSecretKeyRing): PGPSecretKey? { + return secretKeys.secretKey.let { + if (it.isMasterKey) { + it + } else { + null + } + } + } + + /** + * Return the primary {@link PGPPublicKey} from the provided key ring. + * Throws a {@link NoSuchElementException} if the key ring has no primary public key. + * + * @param keyRing key ring + * @return primary public key + */ + @JvmStatic + fun requirePrimaryPublicKeyFrom(keyRing: PGPKeyRing): PGPPublicKey { + return getPrimaryPublicKey(keyRing) + ?: throw NoSuchElementException("Provided PGPKeyRing has no primary public key.") + } + + /** + * Return the primary {@link PGPPublicKey} from the provided key ring or null if it has none. + * + * @param keyRing key ring + * @return primary public key + */ + @JvmStatic + fun getPrimaryPublicKey(keyRing: PGPKeyRing): PGPPublicKey? { + return keyRing.publicKey.let { + if (it.isMasterKey) { + it + } else { + null + } + } + } + + /** + * Require the public key with the given subKeyId from the keyRing. + * If no such subkey exists, throw an {@link NoSuchElementException}. + * + * @param keyRing key ring + * @param subKeyId subkey id + * @return subkey + */ + @JvmStatic + fun requirePublicKeyFrom(keyRing: PGPKeyRing, subKeyId: Long): PGPPublicKey { + return keyRing.getPublicKey(subKeyId) + ?: throw NoSuchElementException("KeyRing does not contain public key with keyId ${subKeyId.hexKeyId()}.") + } + + /** + * Require the secret key with the given secret subKeyId from the secret keyRing. + * If no such subkey exists, throw an {@link NoSuchElementException}. + * + * @param keyRing secret key ring + * @param subKeyId subkey id + * @return secret subkey + */ + @JvmStatic + fun requireSecretKeyFrom(keyRing: PGPSecretKeyRing, subKeyId: Long): PGPSecretKey { + return keyRing.getSecretKey(subKeyId) + ?: throw NoSuchElementException("KeyRing does not contain secret key with keyID ${subKeyId.hexKeyId()}.") + } + + @JvmStatic + fun publicKeys(keys: PGPKeyRing): PGPPublicKeyRing { + return when (keys) { + is PGPPublicKeyRing -> keys + is PGPSecretKeyRing -> keys.certificate + else -> throw IllegalArgumentException("Unknown keys class: ${keys.javaClass.name}") + } + } + + /** + * Extract a {@link PGPPublicKeyRing} containing all public keys from the provided {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @return public key ring + */ + @JvmStatic + @Deprecated("Deprecated in favor of PGPSecretKeyRing extension method.", + ReplaceWith("secretKeys.certificate", "org.bouncycastle.extensions.certificate")) + fun publicKeyRingFrom(secretKeys: PGPSecretKeyRing): PGPPublicKeyRing { + return secretKeys.certificate + } + + /** + * Extract {@link PGPPublicKeyRing PGPPublicKeyRings} from all {@link PGPSecretKeyRing PGPSecretKeyRings} in + * the given {@link PGPSecretKeyRingCollection} and return them as a {@link PGPPublicKeyRingCollection}. + * + * @param secretKeyRings secret key ring collection + * @return public key ring collection + */ + @JvmStatic + fun publicKeyRingCollectionFrom(secretKeyRings: PGPSecretKeyRingCollection): PGPPublicKeyRingCollection { + return PGPPublicKeyRingCollection( + secretKeyRings.keyRings.asSequence() + .map { it.certificate } + .toList()) + } + + /** + * Unlock a {@link PGPSecretKey} and return the resulting {@link PGPPrivateKey}. + * + * @param secretKey secret key + * @param protector protector to unlock the secret key + * @return private key + * + * @throws PGPException if something goes wrong (e.g. wrong passphrase) + */ + @JvmStatic + fun unlockSecretKey(secretKey: PGPSecretKey, protector: SecretKeyRingProtector): PGPPrivateKey { + return UnlockSecretKey.unlockSecretKey(secretKey, protector) + } + + /** + * Create a new {@link PGPPublicKeyRingCollection} from an array of {@link PGPPublicKeyRing PGPPublicKeyRings}. + * + * @param certificates array of public key rings + * @return key ring collection + */ + @JvmStatic + fun keyRingsToKeyRingCollection(vararg certificates: PGPPublicKeyRing): PGPPublicKeyRingCollection { + return PGPPublicKeyRingCollection(certificates.toList()) + } + + /** + * Create a new {@link PGPSecretKeyRingCollection} from an array of {@link PGPSecretKeyRing PGPSecretKeyRings}. + * + * @param secretKeys array of secret key rings + * @return secret key ring collection + */ + @JvmStatic + fun keyRingsToKeyRingCollection(vararg secretKeys: PGPSecretKeyRing): PGPSecretKeyRingCollection { + return PGPSecretKeyRingCollection(secretKeys.toList()) + } + + /** + * Return true, if the given {@link PGPPublicKeyRing} contains a {@link PGPPublicKey} for the given key id. + * + * @param certificate public key ring + * @param keyId id of the key in question + * @return true if ring contains said key, false otherwise + */ + @JvmStatic + fun keyRingContainsKeyWithId(certificate: PGPPublicKeyRing, keyId: Long): Boolean { + return certificate.getPublicKey(keyId) != null + } + + /** + * Inject a key certification for the primary key into the given key ring. + * + * @param keyRing key ring + * @param certification key signature + * @return key ring with injected signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + */ + @JvmStatic + fun injectCertification(keyRing: T, certification: PGPSignature): T { + return injectCertification(keyRing, keyRing.publicKey, certification) + } + + /** + * Inject a key certification for the given key into the given key ring. + * + * @param keyRing key ring + * @param certifiedKey signed public key + * @param certification key signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected signature + * + * @throws NoSuchElementException in case that the signed key is not part of the key ring + */ + @JvmStatic + fun injectCertification(keyRing: T, certifiedKey: PGPPublicKey, certification: PGPSignature): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + if (!keyRingContainsKeyWithId(certificate, certifiedKey.keyID)) { + throw NoSuchElementException("Cannot find public key with id ${certifiedKey.keyID.hexKeyId()} in the provided key ring.") + } + + certificate = PGPPublicKeyRing( + certificate.publicKeys.asSequence().map { + if (it.keyID == certifiedKey.keyID) { + PGPPublicKey.addCertification(it, certification) + } else { + it + } + }.toList()) + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a user-id certification into the given key ring. + * + * @param keyRing key ring + * @param userId signed user-id + * @param certification signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected certification + */ + @JvmStatic + fun injectCertification(keyRing: T, userId: CharSequence, certification: PGPSignature): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + certificate = PGPPublicKeyRing( + listOf( + PGPPublicKey.addCertification(requirePrimaryPublicKeyFrom(certificate), + userId.toString(), certification) + ).plus(certificate.publicKeys.asSequence().drop(1))) + + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a user-attribute vector certification into the given key ring. + * + * @param keyRing key ring + * @param userAttributes certified user attributes + * @param certification certification signature + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected user-attribute certification + */ + @JvmStatic + fun injectCertification(keyRing: T, userAttributes: PGPUserAttributeSubpacketVector, certification: PGPSignature): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + certificate = PGPPublicKeyRing( + listOf( + PGPPublicKey.addCertification(requirePrimaryPublicKeyFrom(certificate), + userAttributes, certification) + ).plus(certificate.publicKeys.asSequence().drop(1))) + + return if (secretKeys == null) { + certificate as T + } else { + PGPSecretKeyRing.replacePublicKeys(secretKeys, certificate) as T + } + } + + /** + * Inject a {@link PGPPublicKey} into the given key ring. + * + * @param keyRing key ring + * @param publicKey public key + * @param either {@link PGPPublicKeyRing} or {@link PGPSecretKeyRing} + * @return key ring with injected public key + */ + @JvmStatic + fun keysPlusPublicKey(keyRing: T, publicKey: PGPPublicKey): T { + val secretAndPublicKeys = secretAndPublicKeys(keyRing) + val secretKeys: PGPSecretKeyRing? = secretAndPublicKeys.first + var certificate: PGPPublicKeyRing = secretAndPublicKeys.second + + return if (secretKeys == null) { + PGPPublicKeyRing.insertPublicKey(certificate, publicKey) as T + } else { + PGPSecretKeyRing.insertOrReplacePublicKey(secretKeys, publicKey) as T + } + } + + @JvmStatic + private fun secretAndPublicKeys(keyRing: PGPKeyRing): Pair { + var secretKeys: PGPSecretKeyRing? = null + val certificate: PGPPublicKeyRing + when (keyRing) { + is PGPSecretKeyRing -> { + secretKeys = keyRing + certificate = secretKeys.certificate + } + is PGPPublicKeyRing -> { + certificate = keyRing + } + else -> throw IllegalArgumentException("keyRing is an unknown PGPKeyRing subclass: ${keyRing.javaClass.name}") + } + return secretKeys to certificate + } + + /** + * Inject a {@link PGPSecretKey} into a {@link PGPSecretKeyRing}. + * + * @param secretKeys secret key ring + * @param secretKey secret key + * @return secret key ring with injected secret key + */ + @JvmStatic + fun keysPlusSecretKey(secretKeys: PGPSecretKeyRing, secretKey: PGPSecretKey): PGPSecretKeyRing { + return PGPSecretKeyRing.insertSecretKey(secretKeys, secretKey) + } + + /** + * Inject the given signature into the public part of the given secret key. + * @param secretKey secret key + * @param signature signature + * @return secret key with the signature injected in its public key + */ + @JvmStatic + fun secretKeyPlusSignature(secretKey: PGPSecretKey, signature: PGPSignature): PGPSecretKey { + PGPPublicKey.addCertification(secretKey.publicKey, signature).let { + return PGPSecretKey.replacePublicKey(secretKey, it) + } + } + + /** + * Remove the secret key of the subkey identified by the given secret key id from the key ring. + * The public part stays attached to the key ring, so that it can still be used for encryption / verification of signatures. + * + * This method is intended to be used to remove secret primary keys from live keys when those are kept in offline storage. + * + * @param secretKeys secret key ring + * @param keyId id of the secret key to remove + * @return secret key ring with removed secret key + * + * @throws IOException in case of an error during serialization / deserialization of the key + * @throws PGPException in case of a broken key + */ + @JvmStatic + fun stripSecretKey(secretKeys: PGPSecretKeyRing, keyId: Long): PGPSecretKeyRing { + require(keyId != secretKeys.publicKey.keyID) { + "Bouncy Castle currently cannot deal with stripped primary secret keys." + } + if (secretKeys.getSecretKey(keyId) == null) { + throw NoSuchElementException("PGPSecretKeyRing does not contain secret key ${keyId.hexKeyId()}.") + } + + val out = ByteArrayOutputStream() + secretKeys.forEach { + if (it.keyID == keyId) { + // only encode the public key + it.publicKey.encode(out) + } else { + // else encode the whole secret + public key + it.encode(out) + } + } + secretKeys.extraPublicKeys.forEach { + it.encode(out) + } + return PGPSecretKeyRing(out.toByteArray(), ImplementationFactory.getInstance().keyFingerprintCalculator) + } + + /** + * Strip all user-ids, user-attributes and signatures from the given public key. + * + * @param bloatedKey public key + * @return stripped public key + * @throws PGPException if the packet is faulty or the required calculations fail + */ + @JvmStatic + fun getStrippedDownPublicKey(bloatedKey: PGPPublicKey): PGPPublicKey { + return PGPPublicKey(bloatedKey.publicKeyPacket, ImplementationFactory.getInstance().keyFingerprintCalculator) + } + + @JvmStatic + fun getUserIdsIgnoringInvalidUTF8(key: PGPPublicKey): List { + return buildList { + key.rawUserIDs.forEach { + try { + add(Strings.fromUTF8ByteArray(it)) + } catch (e : IllegalArgumentException) { + LOGGER.warn("Invalid UTF-8 user-ID encountered: ${String(it)}") + } + } + } + } + + @JvmStatic + @Throws(MissingPassphraseException::class, PGPException::class) + fun changePassphrase(keyId: Long?, + secretKeys: PGPSecretKeyRing, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector): PGPSecretKeyRing { + return if (keyId == null) { + PGPSecretKeyRing( + secretKeys.secretKeys.asSequence().map { + reencryptPrivateKey(it, oldProtector, newProtector) + }.toList() + ) + } else { + PGPSecretKeyRing( + secretKeys.secretKeys.asSequence().map { + if (it.keyID == keyId) { + reencryptPrivateKey(it, oldProtector, newProtector) + } else { + it + } + }.toList() + ) + }.let { s2kUsageFixIfNecessary(it, newProtector) } + } + + @JvmStatic + fun reencryptPrivateKey(secretKey: PGPSecretKey, + oldProtector: SecretKeyRingProtector, + newProtector: SecretKeyRingProtector): PGPSecretKey { + if (secretKey.s2K != null && secretKey.s2K.type == S2K.GNU_DUMMY_S2K) { + // If the key uses GNU_DUMMY_S2K we leave it as is + return secretKey + } + + return PGPSecretKey.copyWithNewPassword(secretKey, + oldProtector.getDecryptor(secretKey.keyID), + newProtector.getEncryptor(secretKey.keyID)) + } + + @JvmStatic + fun s2kUsageFixIfNecessary(secretKeys: PGPSecretKeyRing, protector: SecretKeyRingProtector): PGPSecretKeyRing { + if (secretKeys.secretKeys.asSequence().any { it.s2KUsage == SecretKeyPacket.USAGE_CHECKSUM }) { + return S2KUsageFix.replaceUsageChecksumWithUsageSha1(secretKeys, protector, true) + } + return secretKeys + } + + } +} \ No newline at end of file