From 9f0a9ccfa6fc16c770940fa13fa7d0541e24d512 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 6 Sep 2025 19:51:11 +0200 Subject: [PATCH] WIP: Explore implementing a concrete HardwareSecurity implementation using Yubikit --- .../ConsumerOptions.kt | 4 +- .../CustomPublicKeyDataDecryptorFactory.kt | 4 +- .../HardwareSecurity.kt | 6 +- pgpainless-yubikey/build.gradle | 35 ++++++++++ .../yubikey/YubikeyDataDecryptorFactory.kt | 67 +++++++++++++++++++ settings.gradle | 3 +- version.gradle | 1 + 7 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 pgpainless-yubikey/build.gradle create mode 100644 pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt index d8433f25..92656602 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/ConsumerOptions.kt @@ -38,7 +38,7 @@ class ConsumerOptions(private val api: PGPainless) { private var sessionKey: SessionKey? = null private val customDecryptorFactories = - mutableMapOf() + mutableMapOf() private val decryptionKeys = mutableMapOf() private val decryptionPassphrases = mutableSetOf() private var missingKeyPassphraseStrategy = MissingKeyPassphraseStrategy.INTERACTIVE @@ -245,7 +245,7 @@ class ConsumerOptions(private val api: PGPainless) { * @return options */ fun addCustomDecryptorFactory(factory: CustomPublicKeyDataDecryptorFactory) = apply { - customDecryptorFactories[factory.subkeyIdentifier] = factory + customDecryptorFactories[factory.keyIdentifier] = factory } /** diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt index cb6254dc..2281a771 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/CustomPublicKeyDataDecryptorFactory.kt @@ -4,9 +4,9 @@ package org.pgpainless.decryption_verification +import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.openpgp.operator.AbstractPublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory -import org.pgpainless.key.SubkeyIdentifier /** * Custom [PublicKeyDataDecryptorFactory] which can enable customized implementations of message @@ -23,5 +23,5 @@ abstract class CustomPublicKeyDataDecryptorFactory : AbstractPublicKeyDataDecryp * * @return subkey identifier */ - abstract val subkeyIdentifier: SubkeyIdentifier + abstract val keyIdentifier: KeyIdentifier } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt index 27f53bc6..db8df8c9 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/HardwareSecurity.kt @@ -72,8 +72,8 @@ class HardwareSecurity { * session keys to a [DecryptionCallback]. Users can provide such a callback to delegate * decryption of messages to hardware security SDKs. */ - class HardwareDataDecryptorFactory( - override val subkeyIdentifier: SubkeyIdentifier, + open class HardwareDataDecryptorFactory( + override val keyIdentifier: KeyIdentifier, private val callback: DecryptionCallback, ) : CustomPublicKeyDataDecryptorFactory() { @@ -110,7 +110,7 @@ class HardwareSecurity { ): ByteArray { return try { callback.decryptSessionKey( - subkeyIdentifier.keyIdentifier, keyAlgorithm, secKeyData[0], pkeskVersion) + keyIdentifier, keyAlgorithm, secKeyData[0], pkeskVersion) } catch (e: HardwareSecurityException) { throw PGPException("Hardware-backed decryption failed.", e) } diff --git a/pgpainless-yubikey/build.gradle b/pgpainless-yubikey/build.gradle new file mode 100644 index 00000000..051942a2 --- /dev/null +++ b/pgpainless-yubikey/build.gradle @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // Logging + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + implementation(project(":pgpainless-core")) + + api "com.yubico.yubikit:openpgp:$yubikitVersion" + + implementation "com.google.code.findbugs:jsr305:3.0.2" +} + +// https://docs.gradle.org/current/userguide/java_library_plugin.html#sec:java_library_modular_auto +tasks.named('jar') { + manifest { + attributes('Automatic-Module-Name': 'org.pgpainless.yubikey') + } +} diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt new file mode 100644 index 00000000..bf5706a1 --- /dev/null +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt @@ -0,0 +1,67 @@ +package org.pgpainless.yubikey + +import com.yubico.yubikit.core.smartcard.SmartCardConnection +import com.yubico.yubikit.openpgp.KeyRef +import com.yubico.yubikit.openpgp.OpenPgpSession +import org.bouncycastle.bcpg.KeyIdentifier +import org.pgpainless.decryption_verification.HardwareSecurity + +class YubikeyDataDecryptorFactory( + smartCardConnection: SmartCardConnection, + callback: HardwareSecurity.DecryptionCallback, + keyIdentifier: KeyIdentifier, +) : HardwareSecurity.HardwareDataDecryptorFactory(keyIdentifier, callback) { + + companion object { + @JvmStatic + fun createDecryptorFromConnection(smartCardConnection: SmartCardConnection): HardwareSecurity.HardwareDataDecryptorFactory { + val openpgpSession = OpenPgpSession(smartCardConnection) + val fingerprintBytes = openpgpSession.getData(KeyRef.DEC.fingerprint) + val decKeyIdentifier = KeyIdentifier(fingerprintBytes) + val rsa = true + + val callback = object : HardwareSecurity.DecryptionCallback { + override fun decryptSessionKey( + keyIdentifier: KeyIdentifier, + keyAlgorithm: Int, + sessionKeyData: ByteArray, + pkeskVersion: Int + ): ByteArray { + openpgpSession.verifyUserPin("asdasd".toCharArray(), true) + if(rsa) { + val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData) + + return decryptedSessionKey + } else { + /* + val secret = openpgpSession.decrypt(sessionKeyData) + val hashAlgorithm: Int = ecPubKey.getHashAlgorithm().toInt() + val symmetricKeyAlgorithm: Int = ecPubKey.getSymmetricKeyAlgorithm().toInt() + val userKeyingMaterial = RFC6637Utils.createUserKeyingMaterial( + this.pgpPrivKey.getPublicKeyPacket(), + BcKeyFingerprintCalculator(), + ) + val rfc6637KDFCalculator = RFC6637KDFCalculator( + BcPGPDigestCalculatorProvider()[hashAlgorithm], symmetricKeyAlgorithm, + ) + val key = + KeyParameter(rfc6637KDFCalculator.createKey(secret, userKeyingMaterial)) + return PGPPad.unpadSessionData( + BcPublicKeyDataDecryptorFactory.unwrapSessionData( + keyEnc, + symmetricKeyAlgorithm, + key, + ), + ) + */ + throw UnsupportedOperationException("ECDH decryption is not yet implemented.") + } + } + + } + + return YubikeyDataDecryptorFactory(smartCardConnection, callback, decKeyIdentifier) + } + } + +} diff --git a/settings.gradle b/settings.gradle index aea19392..2fefb183 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,6 @@ rootProject.name = 'PGPainless' include 'pgpainless-core', 'pgpainless-sop', - 'pgpainless-cli' + 'pgpainless-cli', + 'pgpainless-yubikey' diff --git a/version.gradle b/version.gradle index 776f30f6..8350c5a8 100644 --- a/version.gradle +++ b/version.gradle @@ -14,5 +14,6 @@ allprojects { mockitoVersion = '4.5.1' slf4jVersion = '1.7.36' sopJavaVersion = '14.0.1-SNAPSHOT' + yubikitVersion = '2.8.2' } }