From be5c2a01a123144189ab055cb7dc608b2b548d76 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 8 May 2025 16:54:39 +0200 Subject: [PATCH] Port GnuPGDummyExtension implementation --- .../java/org/gnupg/GnuPGDummyExtension.java | 31 --- .../java/org/gnupg/GnuPGDummyKeyUtil.java | 228 ------------------ .../src/main/java/org/gnupg/package-info.java | 8 - .../kotlin/org/gnupg/GnuPGDummyExtension.kt | 16 ++ .../kotlin/org/gnupg/GnuPGDummyKeyUtil.kt | 204 ++++++++++++++++ 5 files changed, 220 insertions(+), 267 deletions(-) delete mode 100644 pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java delete mode 100644 pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java delete mode 100644 pgpainless-core/src/main/java/org/gnupg/package-info.java create mode 100644 pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt create mode 100644 pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java deleted file mode 100644 index d744e222..00000000 --- a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyExtension.java +++ /dev/null @@ -1,31 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.gnupg; - -import org.bouncycastle.bcpg.S2K; - -public enum GnuPGDummyExtension { - - /** - * Do not store the secret part at all. - */ - NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), - - /** - * A stub to access smartcards. - */ - DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), - ; - - private final int id; - - GnuPGDummyExtension(int id) { - this.id = id; - } - - public int getId() { - return id; - } -} diff --git a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java b/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java deleted file mode 100644 index d2f1dedb..00000000 --- a/pgpainless-core/src/main/java/org/gnupg/GnuPGDummyKeyUtil.java +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package org.gnupg; - -import org.bouncycastle.bcpg.KeyIdentifier; -import org.bouncycastle.bcpg.PublicKeyPacket; -import org.bouncycastle.bcpg.S2K; -import org.bouncycastle.bcpg.SecretKeyPacket; -import org.bouncycastle.bcpg.SecretSubkeyPacket; -import org.bouncycastle.openpgp.PGPSecretKey; -import org.bouncycastle.openpgp.PGPSecretKeyRing; -import org.bouncycastle.openpgp.api.OpenPGPKey; -import org.pgpainless.key.SubkeyIdentifier; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * This class can be used to remove private keys from secret software-keys by replacing them with - * stub secret keys in the style of GnuPGs proprietary extensions. - * - * @see - * GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm - */ -public final class GnuPGDummyKeyUtil { - - private GnuPGDummyKeyUtil() { - - } - - /** - * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard by GnuPG. - * Note, that this functionality is based on GnuPGs proprietary S2K extensions, which are not strictly required - * for dealing with hardware-backed keys. - * - * @param secretKeys secret keys - * @return set of keys with S2K type GNU_DUMMY_S2K and protection mode DIVERT_TO_CARD - */ - public static Set getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) { - Set hardwareBackedKeys = new HashSet<>(); - for (PGPSecretKey secretKey : secretKeys) { - S2K s2K = secretKey.getS2K(); - if (s2K == null) { - continue; - } - - int type = s2K.getType(); - int mode = s2K.getProtectionMode(); - // TODO: Is GNU_DUMMY_S2K appropriate? - if (type == S2K.GNU_DUMMY_S2K && mode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) { - SubkeyIdentifier hardwareBackedKey = new SubkeyIdentifier(secretKeys, secretKey.getKeyIdentifier()); - hardwareBackedKeys.add(hardwareBackedKey); - } - } - return hardwareBackedKeys; - } - - public static Builder modify(@Nonnull OpenPGPKey key) { - return modify(key.getPGPSecretKeyRing()); - } - - /** - * Modify the given {@link PGPSecretKeyRing}. - * - * @param secretKeys secret keys - * @return builder - */ - public static Builder modify(@Nonnull PGPSecretKeyRing secretKeys) { - return new Builder(secretKeys); - } - - public static final class Builder { - - private final PGPSecretKeyRing keys; - - private Builder(@Nonnull PGPSecretKeyRing keys) { - this.keys = keys; - } - - /** - * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#NO_PRIVATE_KEY}. - * - * @param filter filter to select keys for removal - * @return modified key ring - */ - public PGPSecretKeyRing removePrivateKeys(@Nonnull KeyFilter filter) { - return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter); - } - - /** - * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. - * This method will set the serial number of the card to 0x00000000000000000000000000000000. - * NOTE: This method does not actually move any keys to a card. - * - * @param filter filter to select keys for removal - * @return modified key ring - */ - public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter) { - return divertPrivateKeysToCard(filter, new byte[16]); - } - - /** - * Remove all private keys that match the given {@link KeyFilter} from the key ring and replace them with - * GNU_DUMMY keys with S2K protection mode {@link GnuPGDummyExtension#DIVERT_TO_CARD}. - * This method will include the card serial number into the encoded dummy key. - * NOTE: This method does not actually move any keys to a card. - * - * @param filter filter to select keys for removal - * @param cardSerialNumber serial number of the card (at most 16 bytes long) - * @return modified key ring - */ - public PGPSecretKeyRing divertPrivateKeysToCard(@Nonnull KeyFilter filter, @Nullable byte[] cardSerialNumber) { - if (cardSerialNumber != null && cardSerialNumber.length > 16) { - throw new IllegalArgumentException("Card serial number length cannot exceed 16 bytes."); - } - return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter); - } - - private PGPSecretKeyRing replacePrivateKeys(@Nonnull GnuPGDummyExtension extension, - @Nullable byte[] serial, - @Nonnull KeyFilter filter) { - byte[] encodedSerial = serial != null ? encodeSerial(serial) : null; - S2K s2k = extensionToS2K(extension); - - List secretKeyList = new ArrayList<>(); - for (PGPSecretKey secretKey : keys) { - if (!filter.filter(secretKey.getKeyIdentifier())) { - // No conversion, do not modify subkey - secretKeyList.add(secretKey); - continue; - } - - PublicKeyPacket publicKeyPacket = secretKey.getPublicKey().getPublicKeyPacket(); - if (secretKey.isMasterKey()) { - SecretKeyPacket keyPacket = new SecretKeyPacket(publicKeyPacket, - 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); - PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); - secretKeyList.add(onCard); - } else { - SecretSubkeyPacket keyPacket = new SecretSubkeyPacket(publicKeyPacket, - 0, SecretKeyPacket.USAGE_SHA1, s2k, null, encodedSerial); - PGPSecretKey onCard = new PGPSecretKey(keyPacket, secretKey.getPublicKey()); - secretKeyList.add(onCard); - } - } - - return new PGPSecretKeyRing(secretKeyList); - } - - private byte[] encodeSerial(@Nonnull byte[] serial) { - byte[] encoded = new byte[serial.length + 1]; - encoded[0] = (byte) (serial.length & 0xff); - System.arraycopy(serial, 0, encoded, 1, serial.length); - return encoded; - } - - private S2K extensionToS2K(@Nonnull GnuPGDummyExtension extension) { - return S2K.gnuDummyS2K(extension == GnuPGDummyExtension.DIVERT_TO_CARD ? - S2K.GNUDummyParams.divertToCard() : S2K.GNUDummyParams.noPrivateKey()); - } - } - - /** - * Filter for selecting keys. - */ - @FunctionalInterface - public interface KeyFilter { - - /** - * Return true, if the given key should be selected, false otherwise. - * - * @param keyIdentifier id of the key - * @return select - */ - boolean filter(KeyIdentifier keyIdentifier); - - /** - * Select any key. - * - * @return filter - */ - static KeyFilter any() { - return keyId -> true; - } - - /** - * Select only the given keyId. - * - * @param onlyKeyId only acceptable key id - * @return filter - * @deprecated use {@link #only(KeyIdentifier)} instead. - */ - @Deprecated - static KeyFilter only(long onlyKeyId) { - return only(new KeyIdentifier(onlyKeyId)); - } - - /** - * Select only the given keyIdentifier. - * - * @param onlyKeyIdentifier only acceptable key identifier - * @return filter - */ - static KeyFilter only(KeyIdentifier onlyKeyIdentifier) { - return keyIdentifier -> keyIdentifier.matches(onlyKeyIdentifier); - } - - /** - * Select all keyIds which are contained in the given set of ids. - * - * @param ids set of acceptable keyIds - * @return filter - */ - static KeyFilter selected(Collection ids) { - // noinspection Convert2MethodRef - return keyId -> ids.contains(keyId); - } - } -} diff --git a/pgpainless-core/src/main/java/org/gnupg/package-info.java b/pgpainless-core/src/main/java/org/gnupg/package-info.java deleted file mode 100644 index 03268619..00000000 --- a/pgpainless-core/src/main/java/org/gnupg/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes related to creating keys with GNU DUMMY S2K values. - */ -package org.gnupg; diff --git a/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt new file mode 100644 index 00000000..4831878a --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyExtension.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg + +import org.bouncycastle.bcpg.S2K + +enum class GnuPGDummyExtension(val id: Int) { + + /** Do not store the secret part at all. */ + NO_PRIVATE_KEY(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + + /** A stub to access smartcards. */ + DIVERT_TO_CARD(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD) +} diff --git a/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt new file mode 100644 index 00000000..467a87bc --- /dev/null +++ b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package org.gnupg + +import kotlin.experimental.and +import org.bouncycastle.bcpg.KeyIdentifier +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.bcpg.SecretSubkeyPacket +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.api.OpenPGPKey +import org.gnupg.GnuPGDummyKeyUtil.KeyFilter +import org.pgpainless.key.SubkeyIdentifier + +/** + * This class can be used to remove private keys from secret software-keys by replacing them with + * stub secret keys in the style of GnuPGs proprietary extensions. + * + * @see + * [GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm](https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489) + */ +class GnuPGDummyKeyUtil private constructor() { + + companion object { + + /** + * Return the key-ids of all keys which appear to be stored on a hardware token / smartcard + * by GnuPG. Note, that this functionality is based on GnuPGs proprietary S2K extensions, + * which are not strictly required for dealing with hardware-backed keys. + * + * @param secretKeys secret keys + * @return set of keys with S2K type [S2K.GNU_DUMMY_S2K] and protection mode + * [GnuPGDummyExtension.DIVERT_TO_CARD] + */ + @JvmStatic + fun getIdsOfKeysWithGnuPGS2KDivertedToCard( + secretKeys: PGPSecretKeyRing + ): Set = + secretKeys + .filter { + it.s2K?.type == S2K.GNU_DUMMY_S2K && + it.s2K?.protectionMode == S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD + } + .map { SubkeyIdentifier(secretKeys, it.keyIdentifier) } + .toSet() + + @JvmStatic fun modify(key: OpenPGPKey): Builder = modify(key.pgpSecretKeyRing) + + /** + * Modify the given [PGPSecretKeyRing]. + * + * @param secretKeys secret keys + * @return builder + */ + @JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys) + } + + class Builder(private val keys: PGPSecretKeyRing) { + + /** + * Remove all private keys that match the given [KeyFilter] from the key ring and replace + * them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.NO_PRIVATE_KEY]. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + fun removePrivateKeys(filter: KeyFilter): PGPSecretKeyRing { + return replacePrivateKeys(GnuPGDummyExtension.NO_PRIVATE_KEY, null, filter) + } + + /** + * Remove all private keys that match the given [KeyFilter] from the key ring and replace + * them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.DIVERT_TO_CARD]. + * This method will set the serial number of the card to 0x00000000000000000000000000000000. + * NOTE: This method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @return modified key ring + */ + fun divertPrivateKeysToCard(filter: KeyFilter): PGPSecretKeyRing { + return divertPrivateKeysToCard(filter, ByteArray(16)) + } + + /** + * Remove all private keys that match the given [KeyFilter] from the key ring and replace + * them with GNU_DUMMY keys with S2K protection mode [GnuPGDummyExtension.DIVERT_TO_CARD]. + * This method will include the card serial number into the encoded dummy key. NOTE: This + * method does not actually move any keys to a card. + * + * @param filter filter to select keys for removal + * @param cardSerialNumber serial number of the card (at most 16 bytes long) + * @return modified key ring + */ + fun divertPrivateKeysToCard( + filter: KeyFilter, + cardSerialNumber: ByteArray? + ): PGPSecretKeyRing { + require(cardSerialNumber == null || cardSerialNumber.size <= 16) { + "Card serial number length cannot exceed 16 bytes." + } + return replacePrivateKeys(GnuPGDummyExtension.DIVERT_TO_CARD, cardSerialNumber, filter) + } + + private fun replacePrivateKeys( + extension: GnuPGDummyExtension, + serial: ByteArray?, + filter: KeyFilter + ): PGPSecretKeyRing { + val encodedSerial: ByteArray? = serial?.let { encodeSerial(it) } + val s2k: S2K = extensionToS2K(extension) + + return PGPSecretKeyRing( + keys + .map { + if (!filter.filter(it.keyIdentifier)) { + // Leave non-filtered key intact + it + } else { + val publicKeyPacket = it.publicKey.publicKeyPacket + // Convert key packet + val keyPacket: SecretKeyPacket = + if (it.isMasterKey) { + SecretKeyPacket( + publicKeyPacket, + 0, + SecretKeyPacket.USAGE_SHA1, + s2k, + null, + encodedSerial) + } else { + SecretSubkeyPacket( + publicKeyPacket, + 0, + SecretKeyPacket.USAGE_SHA1, + s2k, + null, + encodedSerial) + } + PGPSecretKey(keyPacket, it.publicKey) + } + } + .toList()) + } + + private fun encodeSerial(serial: ByteArray): ByteArray { + val encoded = ByteArray(serial.size + 1) + encoded[0] = serial.size.toByte().and(0xff.toByte()) + System.arraycopy(serial, 0, encoded, 1, serial.size) + return encoded + } + + private fun extensionToS2K(extension: GnuPGDummyExtension): S2K { + return S2K.gnuDummyS2K( + if (extension == GnuPGDummyExtension.DIVERT_TO_CARD) + S2K.GNUDummyParams.divertToCard() + else S2K.GNUDummyParams.noPrivateKey()) + } + } + + /** Filter for selecting keys. */ + fun interface KeyFilter { + fun filter(keyIdentifier: KeyIdentifier): Boolean + + companion object { + + /** + * Select any key. + * + * @return filter + */ + @JvmStatic fun any(): KeyFilter = KeyFilter { true } + + /** + * Select only the given keyId. + * + * @param onlyKeyId only acceptable key id + * @return filter + */ + @JvmStatic + @Deprecated("Use only(KeyIdentifier) instead.") + fun only(onlyKeyId: Long) = only(KeyIdentifier(onlyKeyId)) + + /** + * Select only the given keyIdentifier. + * + * @param onlyKeyIdentifier only acceptable key identifier + * @return filter + */ + @JvmStatic + fun only(onlyKeyIdentifier: KeyIdentifier) = KeyFilter { it.matches(onlyKeyIdentifier) } + + /** + * Select all keyIds which are contained in the given set of ids. + * + * @param ids set of acceptable keyIds + * @return filter + */ + @JvmStatic fun selected(ids: Collection) = KeyFilter { ids.contains(it) } + } + } +}