mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-09-09 10:19:39 +02:00
Port GnuPGDummyExtension implementation
This commit is contained in:
parent
0e6fa4b619
commit
be5c2a01a1
5 changed files with 220 additions and 267 deletions
|
@ -1,31 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -1,228 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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 <a href="https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=doc/DETAILS;hb=HEAD#l1489">
|
||||
* GnuPGs doc/DETAILS - GNU extensions to the S2K algorithm</a>
|
||||
*/
|
||||
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<SubkeyIdentifier> getIdsOfKeysWithGnuPGS2KDivertedToCard(@Nonnull PGPSecretKeyRing secretKeys) {
|
||||
Set<SubkeyIdentifier> 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<PGPSecretKey> 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<KeyIdentifier> ids) {
|
||||
// noinspection Convert2MethodRef
|
||||
return keyId -> ids.contains(keyId);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
/**
|
||||
* Utility classes related to creating keys with GNU DUMMY S2K values.
|
||||
*/
|
||||
package org.gnupg;
|
|
@ -0,0 +1,16 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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)
|
||||
}
|
204
pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt
Normal file
204
pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt
Normal file
|
@ -0,0 +1,204 @@
|
|||
// SPDX-FileCopyrightText: 2025 Paul Schaub <vanitasvitae@fsfe.org>
|
||||
//
|
||||
// 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<SubkeyIdentifier> =
|
||||
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<KeyIdentifier>) = KeyFilter { ids.contains(it) }
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue