diff --git a/REUSE.toml b/REUSE.toml index 66b5e867..57d857d9 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -111,6 +111,13 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2022 Paul Schaub " SPDX-License-Identifier = "Apache-2.0" +[[annotations]] +path = "pgpainless-yubikey/src/test/resources/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2025 Paul Schaub " +SPDX-License-Identifier = "Apache-2.0" + + [[annotations]] path = ".github/ISSUE_TEMPLATE/**" precedence = "aggregate" diff --git a/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt index 3094e201..14639d9a 100644 --- a/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt +++ b/pgpainless-core/src/main/kotlin/org/gnupg/GnuPGDummyKeyUtil.kt @@ -57,11 +57,10 @@ class GnuPGDummyKeyUtil private constructor() { */ @JvmStatic fun modify(secretKeys: PGPSecretKeyRing) = Builder(secretKeys) - @JvmStatic fun serialToBytes(sn: Int) = byteArrayOf( - (sn shr 24).toByte(), - (sn shr(16)).toByte(), - (sn shr(8)).toByte(), - sn.toByte()) + @JvmStatic + fun serialToBytes(sn: Int) = + byteArrayOf( + (sn shr 24).toByte(), (sn shr (16)).toByte(), (sn shr (8)).toByte(), sn.toByte()) } class Builder(private val keys: PGPSecretKeyRing) { diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt index 88d5ca59..b78692ab 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/decryption_verification/OpenPgpMessageInputStream.kt @@ -38,7 +38,6 @@ import org.bouncycastle.openpgp.api.OpenPGPKey.OpenPGPSecretKey import org.bouncycastle.openpgp.api.OpenPGPSignature.OpenPGPDocumentSignature import org.bouncycastle.openpgp.api.exception.MalformedOpenPGPSignatureException import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory -import org.bouncycastle.openpgp.operator.PGPDataDecryptorFactory import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory import org.bouncycastle.util.io.TeeInputStream import org.pgpainless.PGPainless @@ -448,16 +447,19 @@ class OpenPgpMessageInputStream( } if (secretKey.hasExternalSecretKey()) { - LOGGER.debug("Decryption key ${secretKey.keyIdentifier} is located on an external device, e.g. a smartcard.") + LOGGER.debug( + "Decryption key ${secretKey.keyIdentifier} is located on an external device, e.g. a smartcard.") for (hardwareTokenBackend in options.hardwareTokenBackends) { - LOGGER.debug("Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.") + LOGGER.debug( + "Attempt decryption with ${hardwareTokenBackend.getBackendName()} backend.") if (decryptWithHardwareKey( - hardwareTokenBackend, - esks, - secretKey, - protector, - SubkeyIdentifier(secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier), - pkesk)) { + hardwareTokenBackend, + esks, + secretKey, + protector, + SubkeyIdentifier( + secretKey.openPGPKey.pgpSecretKeyRing, secretKey.keyIdentifier), + pkesk)) { return true } } @@ -624,16 +626,22 @@ class OpenPgpMessageInputStream( pkesk: PGPPublicKeyEncryptedData ): Boolean { try { - val decrypted = pkesk.getDataStream(decryptorFactory) val sessionKey = SessionKey(pkesk.getSessionKey(decryptorFactory)) throwIfUnacceptable(sessionKey.algorithm) + val pgpSessionKey = PGPSessionKey(sessionKey.algorithm.algorithmId, sessionKey.key) + val sessionKeyEncData = esks.esks.extractSessionKeyEncryptedData() + val decrypted = + sessionKeyEncData.getDataStream( + api.implementation.sessionKeyDataDecryptorFactory(pgpSessionKey)) + val encryptedData = esks.toEncryptedData(sessionKey, layerMetadata.depth) encryptedData.decryptionKey = decryptionKeyId encryptedData.sessionKey = sessionKey encryptedData.addRecipients(esks.pkesks.plus(esks.anonPkesks).map { it.keyIdentifier }) LOGGER.debug("Successfully decrypted data with key $decryptionKeyId") - val integrityProtected = IntegrityProtectedInputStream(decrypted, pkesk, options) + val integrityProtected = + IntegrityProtectedInputStream(decrypted, sessionKeyEncData, options) nestedInputStream = OpenPgpMessageInputStream(integrityProtected, options, encryptedData, api) return true @@ -790,7 +798,7 @@ class OpenPgpMessageInputStream( } } - private class ESKsAndData(private val esks: PGPEncryptedDataList) { + private class ESKsAndData(val esks: PGPEncryptedDataList) { fun toEncryptedData(sk: SessionKey, depth: Int): EncryptedData { return when (EncryptedDataPacketType.of(esks)!!) { EncryptedDataPacketType.SED -> diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt index d551d648..6e810d7d 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/encryption_signing/SigningOptions.kt @@ -313,18 +313,20 @@ class SigningOptions(private val api: PGPainless) { subpacketsCallback) } - fun addInlineSignature(hardwareBackedKey: OpenPGPComponentKey, - hardwareContentSignerBuilderProviderFactory: PGPContentSignerBuilderProviderFactory, - hashAlgorithm: HashAlgorithm, - signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, - subpacketsCallback: Callback? = null - ) = addHardwareSigningMethod( - hardwareBackedKey, - hardwareContentSignerBuilderProviderFactory, - hashAlgorithm, - signatureType, - false, - subpacketsCallback) + fun addInlineSignature( + hardwareBackedKey: OpenPGPComponentKey, + hardwareContentSignerBuilderProviderFactory: PGPContentSignerBuilderProviderFactory, + hashAlgorithm: HashAlgorithm, + signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, + subpacketsCallback: Callback? = null + ) = + addHardwareSigningMethod( + hardwareBackedKey, + hardwareContentSignerBuilderProviderFactory, + hashAlgorithm, + signatureType, + false, + subpacketsCallback) /** * Add detached signatures with all key rings from the provided secret key ring collection. @@ -532,7 +534,14 @@ class SigningOptions(private val api: PGPainless) { hashAlgorithm: HashAlgorithm, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, subpacketsCallback: Callback? = null - ) = addHardwareSigningMethod(hardwareBackedKey, hardwareContentSignerBuilderProviderFactory, hashAlgorithm, signatureType, true, subpacketsCallback) + ) = + addHardwareSigningMethod( + hardwareBackedKey, + hardwareContentSignerBuilderProviderFactory, + hashAlgorithm, + signatureType, + true, + subpacketsCallback) private fun addHardwareSigningMethod( hardwareBackedKey: OpenPGPComponentKey, @@ -540,15 +549,15 @@ class SigningOptions(private val api: PGPainless) { hashAlgorithm: HashAlgorithm, signatureType: DocumentSignatureType = DocumentSignatureType.BINARY_DOCUMENT, detached: Boolean, - subpacketsCallback: Callback? = null) = apply { + subpacketsCallback: Callback? = null + ) = apply { rejectWeakKeys(hardwareBackedKey) val pubkey = hardwareBackedKey.pgpPublicKey - val pgpContentSignerBuilder = hardwareContentSignerBuilderProviderFactory.create(hashAlgorithm) - .get(pubkey) + val pgpContentSignerBuilder = + hardwareContentSignerBuilderProviderFactory.create(hashAlgorithm).get(pubkey) - val generator = PGPSignatureGenerator( - pgpContentSignerBuilder, pubkey) - .apply { + val generator = + PGPSignatureGenerator(pgpContentSignerBuilder, pubkey).apply { init(signatureType.signatureType.code, pubkey) } @@ -582,17 +591,19 @@ class SigningOptions(private val api: PGPainless) { val publicKeyAlgorithm = requireFromId(signingKey.pgpPublicKey.algorithm) val bitStrength = signingKey.pgpPublicKey.bitStrength if (!api.algorithmPolicy.publicKeyAlgorithmPolicy.isAcceptable( - publicKeyAlgorithm, bitStrength)) { + publicKeyAlgorithm, bitStrength)) { throw UnacceptableSigningKeyException( - PublicKeyAlgorithmPolicyException( - signingKey, publicKeyAlgorithm, bitStrength)) + PublicKeyAlgorithmPolicyException(signingKey, publicKeyAlgorithm, bitStrength)) } } - private fun prepareSignatureGenerator(generator: PGPSignatureGenerator, signingKey: PGPPublicKey, subpacketCallback: Callback?) { + private fun prepareSignatureGenerator( + generator: PGPSignatureGenerator, + signingKey: PGPPublicKey, + subpacketCallback: Callback? + ) { // Subpackets - val hashedSubpackets = - SignatureSubpackets.createHashedSubpackets(signingKey) + val hashedSubpackets = SignatureSubpackets.createHashedSubpackets(signingKey) val unhashedSubpackets = SignatureSubpackets.createEmptySubpackets() if (subpacketCallback != null) { subpacketCallback.modifyHashedSubpackets(hashedSubpackets) diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt index 18a67c33..db0151c0 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/exception/KeyException.kt @@ -210,7 +210,6 @@ $algorithm of size $bitSize is not acceptable.""", } } - class GeneralKeyException(message: String, - fingerprint: OpenPgpFingerprint - ) : KeyException(message, fingerprint) + class GeneralKeyException(message: String, fingerprint: OpenPgpFingerprint) : + KeyException(message, fingerprint) } diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt index 07f44826..117b46f1 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/hardware/HardwareTokenBackend.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.hardware import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData diff --git a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/PGPContentSignerBuilderProviderFactory.kt b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/PGPContentSignerBuilderProviderFactory.kt index b9873940..3bf02401 100644 --- a/pgpainless-core/src/main/kotlin/org/pgpainless/signature/PGPContentSignerBuilderProviderFactory.kt +++ b/pgpainless-core/src/main/kotlin/org/pgpainless/signature/PGPContentSignerBuilderProviderFactory.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.signature import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt index 4bb64479..8eeaf2e2 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/Yubikey.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import com.yubico.yubikit.core.YubiKeyDevice @@ -16,8 +20,10 @@ data class Yubikey(val info: DeviceInfo, val device: YubiKeyDevice) { fun storeKeyInSlot(key: OpenPGPPrivateKey, keyRef: KeyRef, adminPin: CharArray) { device.openConnection(SmartCardConnection::class.java).use { // Extract private key - val privateKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) - .getPrivateKey(key.keyPair.privateKey) + val privateKey = + JcaPGPKeyConverter() + .setProvider(BouncyCastleProvider()) + .getPrivateKey(key.keyPair.privateKey) val session = OpenPgpSession(it as SmartCardConnection) diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt index 50d724bf..1a2cd51d 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyDataDecryptorFactory.kt @@ -7,6 +7,7 @@ package org.pgpainless.yubikey import com.yubico.yubikit.core.keys.PublicKeyValues import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.openpgp.OpenPgpSession +import java.util.* import org.bouncycastle.bcpg.ECDHPublicBCPGKey import org.bouncycastle.bcpg.KeyIdentifier import org.bouncycastle.bcpg.PublicKeyAlgorithmTags @@ -27,7 +28,6 @@ import org.pgpainless.decryption_verification.HardwareSecurity import org.pgpainless.key.OpenPgpV4Fingerprint import org.pgpainless.key.SubkeyIdentifier import org.slf4j.LoggerFactory -import java.util.* class YubikeyDataDecryptorFactory( callback: HardwareSecurity.DecryptionCallback, @@ -36,13 +36,11 @@ class YubikeyDataDecryptorFactory( companion object { - @JvmStatic - val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java) + @JvmStatic val LOGGER = LoggerFactory.getLogger(YubikeyDataDecryptorFactory::class.java) val ADMIN_PIN: CharArray = "12345678".toCharArray() val USER_PIN: CharArray = "123456".toCharArray() - @JvmStatic fun createDecryptorFromConnection( smartCardConnection: SmartCardConnection, @@ -51,94 +49,106 @@ class YubikeyDataDecryptorFactory( val openpgpSession = OpenPgpSession(smartCardConnection) val decKeyIdentifier = SubkeyIdentifier(OpenPgpV4Fingerprint(pubkey)) - val isRSAKey = pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL - || pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN - || pubkey.algorithm == PublicKeyAlgorithmTags.RSA_ENCRYPT + val isRSAKey = + pubkey.algorithm == PublicKeyAlgorithmTags.RSA_GENERAL || + pubkey.algorithm == PublicKeyAlgorithmTags.RSA_SIGN || + pubkey.algorithm == PublicKeyAlgorithmTags.RSA_ENCRYPT - val callback = object : HardwareSecurity.DecryptionCallback { - override fun decryptSessionKey( - keyIdentifier: KeyIdentifier, - keyAlgorithm: Int, - sessionKeyData: ByteArray, - pkeskVersion: Int - ): ByteArray { - // TODO: Move user pin verification somewhere else - openpgpSession.verifyAdminPin(ADMIN_PIN) - openpgpSession.verifyUserPin(USER_PIN, true) + val callback = + object : HardwareSecurity.DecryptionCallback { + override fun decryptSessionKey( + keyIdentifier: KeyIdentifier, + keyAlgorithm: Int, + sessionKeyData: ByteArray, + pkeskVersion: Int + ): ByteArray { + // TODO: Move user pin verification somewhere else + openpgpSession.verifyUserPin(USER_PIN, true) - LOGGER.debug("Attempt decryption with key {}", keyIdentifier) + LOGGER.debug("Attempt decryption with key {}", keyIdentifier) - if(isRSAKey) { - // easy - LOGGER.debug("Key is RSA key of length {}", pubkey.bitStrength) - val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData) - return decryptedSessionKey - } else { - // meh... - val curveName = pubkey.getCurveName() - val ecPubKey: ECDHPublicBCPGKey = pubkey.publicKeyPacket.key as ECDHPublicBCPGKey - LOGGER.debug("Key is ECDH key over curve $curveName") - // split session data into peer key and encrypted session key + if (isRSAKey) { + // easy + LOGGER.debug("Key is RSA key of length {}", pubkey.bitStrength) + val decryptedSessionKey = openpgpSession.decrypt(sessionKeyData) + smartCardConnection.close() + return decryptedSessionKey + } else { + // meh... + val curveName = pubkey.getCurveName() + val ecPubKey: ECDHPublicBCPGKey = + pubkey.publicKeyPacket.key as ECDHPublicBCPGKey + LOGGER.debug("Key is ECDH key over curve $curveName") + // split session data into peer key and encrypted session key - // peer key - val pLen = - ((((sessionKeyData[0].toInt() and 0xff) shl 8) + (sessionKeyData[1].toInt() and 0xff)) + 7) / 8 - checkRange(2 + pLen + 1, sessionKeyData) - val pEnc = ByteArray(pLen) - System.arraycopy(sessionKeyData, 2, pEnc, 0, pLen) + // peer key + val pLen = + ((((sessionKeyData[0].toInt() and 0xff) shl 8) + + (sessionKeyData[1].toInt() and 0xff)) + 7) / 8 + checkRange(2 + pLen + 1, sessionKeyData) + val pEnc = ByteArray(pLen) + System.arraycopy(sessionKeyData, 2, pEnc, 0, pLen) - // encrypted session key - val keyLen = sessionKeyData[pLen + 2].toInt() and 0xff - checkRange(2 + pLen + 1 + keyLen, sessionKeyData) - val keyEnc = ByteArray(keyLen) - System.arraycopy(sessionKeyData, 2 + pLen + 1, keyEnc, 0, keyLen) + // encrypted session key + val keyLen = sessionKeyData[pLen + 2].toInt() and 0xff + checkRange(2 + pLen + 1 + keyLen, sessionKeyData) + val keyEnc = ByteArray(keyLen) + System.arraycopy(sessionKeyData, 2 + pLen + 1, keyEnc, 0, keyLen) - // perform ECDH key agreement via the Yubikey - val params = ECNamedCurveTable.getParameterSpec(curveName) - val publicPoint = params.curve.decodePoint(pEnc) - val peerKey = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) - .getPublicKey( - PGPPublicKey( - PublicKeyPacket( - pubkey.version, PublicKeyAlgorithmTags.ECDH, Date(), - ECDHPublicBCPGKey( - ecPubKey.curveOID, - publicPoint, - ecPubKey.hashAlgorithm.toInt(), - ecPubKey.symmetricKeyAlgorithm.toInt(), + // perform ECDH key agreement via the Yubikey + val params = ECNamedCurveTable.getParameterSpec(curveName) + val publicPoint = params.curve.decodePoint(pEnc) + val peerKey = + JcaPGPKeyConverter() + .setProvider(BouncyCastleProvider()) + .getPublicKey( + PGPPublicKey( + PublicKeyPacket( + pubkey.version, + PublicKeyAlgorithmTags.ECDH, + Date(), + ECDHPublicBCPGKey( + ecPubKey.curveOID, + publicPoint, + ecPubKey.hashAlgorithm.toInt(), + ecPubKey.symmetricKeyAlgorithm.toInt(), + ), + ), + BcKeyFingerprintCalculator(), ), - ), + ) + + val secret = + openpgpSession.decrypt(PublicKeyValues.fromPublicKey(peerKey)) + smartCardConnection.close() + + // Use the shared key to decrypt the session key + val hashAlgorithm: Int = ecPubKey.hashAlgorithm.toInt() + val symmetricKeyAlgorithm: Int = ecPubKey.symmetricKeyAlgorithm.toInt() + val userKeyingMaterial = + RFC6637Utils.createUserKeyingMaterial( + pubkey.publicKeyPacket, BcKeyFingerprintCalculator(), + ) + val rfc6637KDFCalculator = + RFC6637KDFCalculator( + BcPGPDigestCalculatorProvider()[hashAlgorithm], + symmetricKeyAlgorithm, + ) + val key = + KeyParameter( + rfc6637KDFCalculator.createKey(secret, userKeyingMaterial)) + + return PGPPad.unpadSessionData( + BcPublicKeyDataDecryptorFactory.unwrapSessionData( + keyEnc, + symmetricKeyAlgorithm, + key, ), ) - - val secret = openpgpSession.decrypt(PublicKeyValues.fromPublicKey(peerKey)) - - // Use the shared key to decrypt the session key - val hashAlgorithm: Int = ecPubKey.hashAlgorithm.toInt() - val symmetricKeyAlgorithm: Int = ecPubKey.symmetricKeyAlgorithm.toInt() - val userKeyingMaterial = RFC6637Utils.createUserKeyingMaterial( - pubkey.publicKeyPacket, - BcKeyFingerprintCalculator(), - ) - val rfc6637KDFCalculator = - RFC6637KDFCalculator( - BcPGPDigestCalculatorProvider()[hashAlgorithm], - symmetricKeyAlgorithm, - ) - val key = - KeyParameter(rfc6637KDFCalculator.createKey(secret, userKeyingMaterial)) - - return PGPPad.unpadSessionData( - BcPublicKeyDataDecryptorFactory.unwrapSessionData( - keyEnc, - symmetricKeyAlgorithm, - key, - ), - ) + } } } - } return YubikeyDataDecryptorFactory(callback, decKeyIdentifier) } diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt index f20e6b1a..7c987c74 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackend.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import com.yubico.yubikit.core.smartcard.SmartCardConnection @@ -21,39 +25,44 @@ class YubikeyHardwareTokenBackend : HardwareTokenBackend { protector: SecretKeyRingProtector, pkesk: PGPPublicKeyEncryptedData ): Iterator { - val devices = YubikeyHelper().listDevices() - return devices.map { yubikey -> - yubikey.device.openConnection(SmartCardConnection::class.java).use { - val decFac = YubikeyDataDecryptorFactory.createDecryptorFromConnection( - it, - secKey.pgpPublicKey - ) - decFac as PublicKeyDataDecryptorFactory + return object : Iterator { + val devices = YubikeyHelper().listDevices().iterator() + + override fun hasNext(): Boolean { + return devices.hasNext() } - }.iterator() + + override fun next(): PublicKeyDataDecryptorFactory { + return devices.next().device.openConnection(SmartCardConnection::class.java).let { + val decFac = + YubikeyDataDecryptorFactory.createDecryptorFromConnection( + it, secKey.pgpPublicKey) + decFac as PublicKeyDataDecryptorFactory + } + } + } } override fun listDeviceSerials(): List { - return YubikeyHelper().listDevices() - .mapNotNull { yk -> yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } } + return YubikeyHelper().listDevices().mapNotNull { yk -> + yk.info.serialNumber?.let { GnuPGDummyKeyUtil.serialToBytes(it) } + } } override fun listKeyFingerprints(): Map> { - return YubikeyHelper().listDevices() - .associate { yk -> - yk.encodedSerialNumber to yk.device.openConnection(SmartCardConnection::class.java).use { + return YubikeyHelper().listDevices().associate { yk -> + yk.encodedSerialNumber to + yk.device.openConnection(SmartCardConnection::class.java).use { val session = OpenPgpSession(it) - //session.getData(KeyRef.DEC.fingerprint) + // session.getData(KeyRef.DEC.fingerprint) session.getData(KeyRef.SIG.fingerprint) - listOfNotNull( session.getData(KeyRef.ATT.fingerprint), session.getData(KeyRef.SIG.fingerprint), session.getData(KeyRef.DEC.fingerprint), - session.getData(KeyRef.AUT.fingerprint) - ) + session.getData(KeyRef.AUT.fingerprint)) } - } + } } } diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt index 1656eacc..978ca668 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyHelper.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import com.yubico.yubikit.core.smartcard.SmartCardConnection @@ -17,11 +21,11 @@ import org.pgpainless.key.OpenPgpFingerprint class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) { - fun listDevices( - manager: YubiKitManager = YubiKitManager() - ): List = manager.listAllDevices() - .filter { it.key is CompositeDevice } - .map { Yubikey(it.value, it.key) } + fun listDevices(manager: YubiKitManager = YubiKitManager()): List = + manager + .listAllDevices() + .filter { it.key is CompositeDevice } + .map { Yubikey(it.value, it.key) } fun factoryReset(yubikey: Yubikey) { yubikey.device.openConnection(SmartCardConnection::class.java).use { @@ -29,10 +33,11 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) { } } - fun moveToYubikey(componentKey: OpenPGPPrivateKey, - yubikey: Yubikey, - adminPin: CharArray, - keyRef: KeyRef = keyRefForKey(componentKey.publicKey) + fun moveToYubikey( + componentKey: OpenPGPPrivateKey, + yubikey: Yubikey, + adminPin: CharArray, + keyRef: KeyRef = keyRefForKey(componentKey.publicKey) ): OpenPGPKey { // Move private key to hardware token yubikey.storeKeyInSlot(componentKey, keyRef, adminPin) @@ -54,8 +59,9 @@ class YubikeyHelper(private val api: PGPainless = PGPainless.getInstance()) { key.isSigningKey -> KeyRef.SIG key.isEncryptionKey -> KeyRef.DEC key.isCertificationKey -> KeyRef.ATT - else -> throw KeyException.GeneralKeyException( - "Cannot determine usage for the key.", OpenPgpFingerprint.of(key)) + else -> + throw KeyException.GeneralKeyException( + "Cannot determine usage for the key.", OpenPgpFingerprint.of(key)) } } } diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt index e3a5aa7b..deed2b67 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyKeyGenerator.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import com.yubico.yubikit.core.keys.PublicKeyValues @@ -6,6 +10,7 @@ import com.yubico.yubikit.management.DeviceInfo import com.yubico.yubikit.openpgp.KeyRef import com.yubico.yubikit.openpgp.OpenPgpCurve import com.yubico.yubikit.openpgp.OpenPgpSession +import java.util.* import openpgp.toSecondsPrecision import org.bouncycastle.bcpg.PublicSubkeyPacket import org.bouncycastle.bcpg.S2K @@ -22,16 +27,17 @@ import org.gnupg.GnuPGDummyKeyUtil import org.pgpainless.PGPainless import org.pgpainless.algorithm.OpenPGPKeyVersion import org.pgpainless.algorithm.PublicKeyAlgorithm -import java.util.* class YubikeyKeyGenerator(private val api: PGPainless) { private val converter = JcaPGPKeyConverter().setProvider(BouncyCastleProvider()) - fun generateModernKey(yubikey: Yubikey, - adminPin: CharArray, - keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, - creationTime: Date = Date()): OpenPGPKey { + fun generateModernKey( + yubikey: Yubikey, + adminPin: CharArray, + keyVersion: OpenPGPKeyVersion = OpenPGPKeyVersion.v4, + creationTime: Date = Date() + ): OpenPGPKey { yubikey.device.openConnection(SmartCardConnection::class.java).use { val session = OpenPgpSession(it) session.verifyAdminPin(adminPin) @@ -42,25 +48,28 @@ class YubikeyKeyGenerator(private val api: PGPainless) { val primarykey = toExternalSecretKey(pubKey, yubikey.info) pkVal = session.generateEcKey(KeyRef.SIG, OpenPgpCurve.SECP521R1) - pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime,PublicKeyAlgorithm.ECDSA) + pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDSA) val signingKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) pkVal = session.generateEcKey(KeyRef.DEC, OpenPgpCurve.SECP521R1) pubKey = toPGPPublicKey(pkVal, keyVersion, creationTime, PublicKeyAlgorithm.ECDH) - val encryptionKey = toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) + val encryptionKey = + toSecretSubKey(toExternalSecretKey(pubKey, yubikey.info), yubikey.info) return OpenPGPKey(PGPSecretKeyRing(listOf(primarykey, signingKey, encryptionKey))) } } - private fun toPGPPublicKey(pkVal: PublicKeyValues, - version: OpenPGPKeyVersion, - creationTime: Date, - algorithm: PublicKeyAlgorithm + private fun toPGPPublicKey( + pkVal: PublicKeyValues, + version: OpenPGPKeyVersion, + creationTime: Date, + algorithm: PublicKeyAlgorithm ): PGPPublicKey { - return converter.getPGPPublicKey(version.numeric, + return converter.getPGPPublicKey( + version.numeric, algorithm.algorithmId, null, pkVal.toPublicKey(), @@ -75,10 +84,8 @@ class YubikeyKeyGenerator(private val api: PGPainless) { 0xfc, null, null, - GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!) - ), - pubkey - ) + GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)), + pubkey) } private fun toGnuStubbedSecretKey(pubKey: PGPPublicKey, deviceInfo: DeviceInfo): PGPSecretKey { @@ -89,23 +96,24 @@ class YubikeyKeyGenerator(private val api: PGPainless) { SecretKeyPacket.USAGE_SHA1, S2K.gnuDummyS2K(S2K.GNUDummyParams.divertToCard()), null, - GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!) - ), + GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)), pubKey) } private fun toSecretSubKey( key: PGPSecretKey, deviceInfo: DeviceInfo, - fingerPrintCalculator: KeyFingerPrintCalculator = api.implementation.keyFingerPrintCalculator() + fingerPrintCalculator: KeyFingerPrintCalculator = + api.implementation.keyFingerPrintCalculator() ): PGPSecretKey { - val pubSubKey = PGPPublicKey( - PublicSubkeyPacket( - key.publicKey.version, - key.publicKey.algorithm, - key.publicKey.creationTime, - key.publicKey.publicKeyPacket.key), - fingerPrintCalculator) + val pubSubKey = + PGPPublicKey( + PublicSubkeyPacket( + key.publicKey.version, + key.publicKey.algorithm, + key.publicKey.creationTime, + key.publicKey.publicKeyPacket.key), + fingerPrintCalculator) return PGPSecretKey( SecretSubkeyPacket( pubSubKey.publicKeyPacket, @@ -114,7 +122,6 @@ class YubikeyKeyGenerator(private val api: PGPainless) { null, null, GnuPGDummyKeyUtil.serialToBytes(deviceInfo.serialNumber!!)), - pubSubKey - ) + pubSubKey) } } diff --git a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyPGPContentSignerBuilderProvider.kt b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyPGPContentSignerBuilderProvider.kt index 213ceb74..7a0e684d 100644 --- a/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyPGPContentSignerBuilderProvider.kt +++ b/pgpainless-yubikey/src/main/kotlin/org/pgpainless/yubikey/YubikeyPGPContentSignerBuilderProvider.kt @@ -1,7 +1,12 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.openpgp.OpenPgpSession +import java.io.OutputStream import org.bouncycastle.openpgp.PGPPrivateKey import org.bouncycastle.openpgp.PGPPublicKey import org.bouncycastle.openpgp.api.OpenPGPImplementation @@ -10,7 +15,6 @@ import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider import org.pgpainless.algorithm.HashAlgorithm import org.pgpainless.yubikey.YubikeyDataDecryptorFactory.Companion.USER_PIN -import java.io.OutputStream class YubikeyPGPContentSignerBuilderProvider( val hashAlgorithm: HashAlgorithm, @@ -18,23 +22,23 @@ class YubikeyPGPContentSignerBuilderProvider( private val implementation: OpenPGPImplementation = OpenPGPImplementation.getInstance() ) : PGPContentSignerBuilderProvider(hashAlgorithm.algorithmId) { - private val softwareSignerBuilderProvider = implementation.pgpContentSignerBuilderProvider(hashAlgorithm.algorithmId) + private val softwareSignerBuilderProvider = + implementation.pgpContentSignerBuilderProvider(hashAlgorithm.algorithmId) override fun get(publicSigningKey: PGPPublicKey): PGPContentSignerBuilder { return object : PGPContentSignerBuilder { - override fun build(signatureType: Int, - privateKey: PGPPrivateKey? - ): PGPContentSigner { + override fun build(signatureType: Int, privateKey: PGPPrivateKey?): PGPContentSigner { // Delegate software-based signing keys to the implementations default // content signer builder provider - return softwareSignerBuilderProvider.get(publicSigningKey) + return softwareSignerBuilderProvider + .get(publicSigningKey) .build(signatureType, privateKey) } - override fun build(signatureType: Int - ): PGPContentSigner { - val digestCalculator = implementation.pgpDigestCalculatorProvider().get(hashAlgorithmId) + override fun build(signatureType: Int): PGPContentSigner { + val digestCalculator = + implementation.pgpDigestCalculatorProvider().get(hashAlgorithmId) val openPgpSession = OpenPgpSession(smartcardConnection) // TODO: Move pin authorization somewhere else diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt index 369f8e95..e8c488c9 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyDecryptionTest.kt @@ -1,6 +1,9 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey -import com.yubico.yubikit.core.smartcard.SmartCardConnection import com.yubico.yubikit.openpgp.KeyRef import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Test @@ -11,95 +14,98 @@ import org.pgpainless.util.Passphrase class YubikeyDecryptionTest : YubikeyTest() { // Complete software key - private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + - "Comment: Alice \n" + - "\n" + - "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\n" + - "1POPHzF3cMIReYhZfiJUEBV19suL\n" + - "=dA6G\n" + - "-----END PGP PRIVATE KEY BLOCK-----" + private val KEY = + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + + "Comment: Alice \n" + + "\n" + + "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\n" + + "1POPHzF3cMIReYhZfiJUEBV19suL\n" + + "=dA6G\n" + + "-----END PGP PRIVATE KEY BLOCK-----" // Software certificate - private val CERT = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + - "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + - "Comment: Alice \n" + - "\n" + - "mJMEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" + - "daRyv+mz2jfFZlQsCkhw2GsrPRJuKz++1JkspKU+4Vot9dIBD94Y+MoZUgHM4m0t\n" + - "ItqAdaRcxZWXDpSB0eZH3/lC+VkMUjiqK1Po4qOdZttgpLz+uHcox3gxanjyndAQ\n" + - "gVQf36u0HEFsaWNlIDxhbGljZUBwZ3BhaW5sZXNzLm9yZz7CwCEEExMKAFEFgmjU\n" + - "JoUJEOsuVw2e4iiRFqEEuyrD4eWVzQXPpc/m6y5XDZ7iKJECmwEFFQoJCAsFFgID\n" + - "AQAECwkIBwknCQEJAgkDCAECngkFiQlmAYACmQEAAEjjAgQOfqnx03DV4MUGGytd\n" + - "G02k5KbyeHbuAooyAcn8LItbaAlwzMn9Pu2uwLFEeqi0yhfF2QILwnh4QPHkhC6U\n" + - "wn5nbwIED8I1+3lbVifmpZm+1xwyU8JldGHvGDd0nkJ+wZsB22rr8dTIOgju2Z2a\n" + - "UOOBRlaCnRfPwYjlMaaacn+T2RZBQhK4lwRo1CaFEgUrgQQAIwQjBAE/KkBQQfj0\n" + - "4fzk0LWTlrbbdh95ZK4SyTLoXVY8lueyRPlTu399uPBZUxVBkJNaStvv+TgzEXXO\n" + - "1cHXOccEI9b04ACpUWbTq1ArP162V7OUuSGIAoxMw3B/jMWlNgPy/ktvv1Tidj66\n" + - "Q3QKtx6Iir340AWuMszisM/9sJRazsJxVFZIvgMBCgnCugQYEwoAKgWCaNQmhQkQ\n" + - "3dKuQkQmRdEWoQQCHpcKMNUN0N4jKkrd0q5CRCZF0QKbDAAAYPsCCOFKIUwIzOrc\n" + - "Hguz8+xc90UQRnfKpmZ61Ex8fkVN2sWRTnA10+dlb8zIDEXeUApSU8NB3W3ktTQM\n" + - "ATfKt/0xluzRAgjN1uXdxss0e0WVUuetJDkeVxBJCSuauqNqJsGQ70/5G8kQY9Wt\n" + - "Cn+5WmQDsjjThmlAlpF0LjLmbBeL7LrUz0ibariTBGjUJoUTBSuBBAAjBCMEAXJu\n" + - "E0+qkMbYx9+TmpzsD5L/8XYCyurEGj1YqTjFwoHLXQKCulsHP/UipGu1ulw1pjS9\n" + - "yXlhzMEb/k+0sxM4xdB0AHmdObt8e2xzeO8qkNm4NdvHANQb+tiQch0gIZ2nR1x+\n" + - "ZjhcRy+Dr/TCZvUk2l98YFcK0jS2qKvnd2HKxp4NCWU9wsCdBBgTCgDMBYJo1CaF\n" + - "CRBLTd5YEMyfuxahBDYBgrodBZ7KXs6saUtN3lgQzJ+7ApsCoSAEGRMKAAYFgmjU\n" + - "JoUACgkQS03eWBDMn7uLJgIInpyhe/J0Fy+likj7uzAt1XrCY44hh0Agdr8wVfpD\n" + - "haGScktFOEf8x9UOjT7eVyRVG1MmPsQqViiIKp8t+Jz3B34CBAkVYweWbPmVQyuv\n" + - "WGHaoTZY4xmbboF/VV6zIT6C1Rg8hPvSzaN70ZPMVLNqtp1AEgGxtWqL542Z49B+\n" + - "OJRoCqnxAACt9QIJAfwhrKW+CB/YI34UIVXC5cqTwvqZkSEwd5F9r0k4wob3AXD0\n" + - "SQ2bWAHY7SBZZlGK1J5UA8SVDp7xvxmQjKpweUHsAgdinLvX6C9DtLelq18l0MJI\n" + - "pjM8eSnn8ruFW7I9JvMleWkuHmzxJscbVmjru9Tzjx8xd3DCEXmIWX4iVBAVdfbL\n" + - "iw==\n" + - "=Oq+Y\n" + - "-----END PGP PUBLIC KEY BLOCK-----" + private val CERT = + "-----BEGIN PGP PUBLIC KEY BLOCK-----\n" + + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + + "Comment: Alice \n" + + "\n" + + "mJMEaNQmhRMFK4EEACMEIwQBgF429XlvPyJdpfXDxVjVEOJc04wcpfkIoX1CzIjm\n" + + "daRyv+mz2jfFZlQsCkhw2GsrPRJuKz++1JkspKU+4Vot9dIBD94Y+MoZUgHM4m0t\n" + + "ItqAdaRcxZWXDpSB0eZH3/lC+VkMUjiqK1Po4qOdZttgpLz+uHcox3gxanjyndAQ\n" + + "gVQf36u0HEFsaWNlIDxhbGljZUBwZ3BhaW5sZXNzLm9yZz7CwCEEExMKAFEFgmjU\n" + + "JoUJEOsuVw2e4iiRFqEEuyrD4eWVzQXPpc/m6y5XDZ7iKJECmwEFFQoJCAsFFgID\n" + + "AQAECwkIBwknCQEJAgkDCAECngkFiQlmAYACmQEAAEjjAgQOfqnx03DV4MUGGytd\n" + + "G02k5KbyeHbuAooyAcn8LItbaAlwzMn9Pu2uwLFEeqi0yhfF2QILwnh4QPHkhC6U\n" + + "wn5nbwIED8I1+3lbVifmpZm+1xwyU8JldGHvGDd0nkJ+wZsB22rr8dTIOgju2Z2a\n" + + "UOOBRlaCnRfPwYjlMaaacn+T2RZBQhK4lwRo1CaFEgUrgQQAIwQjBAE/KkBQQfj0\n" + + "4fzk0LWTlrbbdh95ZK4SyTLoXVY8lueyRPlTu399uPBZUxVBkJNaStvv+TgzEXXO\n" + + "1cHXOccEI9b04ACpUWbTq1ArP162V7OUuSGIAoxMw3B/jMWlNgPy/ktvv1Tidj66\n" + + "Q3QKtx6Iir340AWuMszisM/9sJRazsJxVFZIvgMBCgnCugQYEwoAKgWCaNQmhQkQ\n" + + "3dKuQkQmRdEWoQQCHpcKMNUN0N4jKkrd0q5CRCZF0QKbDAAAYPsCCOFKIUwIzOrc\n" + + "Hguz8+xc90UQRnfKpmZ61Ex8fkVN2sWRTnA10+dlb8zIDEXeUApSU8NB3W3ktTQM\n" + + "ATfKt/0xluzRAgjN1uXdxss0e0WVUuetJDkeVxBJCSuauqNqJsGQ70/5G8kQY9Wt\n" + + "Cn+5WmQDsjjThmlAlpF0LjLmbBeL7LrUz0ibariTBGjUJoUTBSuBBAAjBCMEAXJu\n" + + "E0+qkMbYx9+TmpzsD5L/8XYCyurEGj1YqTjFwoHLXQKCulsHP/UipGu1ulw1pjS9\n" + + "yXlhzMEb/k+0sxM4xdB0AHmdObt8e2xzeO8qkNm4NdvHANQb+tiQch0gIZ2nR1x+\n" + + "ZjhcRy+Dr/TCZvUk2l98YFcK0jS2qKvnd2HKxp4NCWU9wsCdBBgTCgDMBYJo1CaF\n" + + "CRBLTd5YEMyfuxahBDYBgrodBZ7KXs6saUtN3lgQzJ+7ApsCoSAEGRMKAAYFgmjU\n" + + "JoUACgkQS03eWBDMn7uLJgIInpyhe/J0Fy+likj7uzAt1XrCY44hh0Agdr8wVfpD\n" + + "haGScktFOEf8x9UOjT7eVyRVG1MmPsQqViiIKp8t+Jz3B34CBAkVYweWbPmVQyuv\n" + + "WGHaoTZY4xmbboF/VV6zIT6C1Rg8hPvSzaN70ZPMVLNqtp1AEgGxtWqL542Z49B+\n" + + "OJRoCqnxAACt9QIJAfwhrKW+CB/YI34UIVXC5cqTwvqZkSEwd5F9r0k4wob3AXD0\n" + + "SQ2bWAHY7SBZZlGK1J5UA8SVDp7xvxmQjKpweUHsAgdinLvX6C9DtLelq18l0MJI\n" + + "pjM8eSnn8ruFW7I9JvMleWkuHmzxJscbVmjru9Tzjx8xd3DCEXmIWX4iVBAVdfbL\n" + + "iw==\n" + + "=Oq+Y\n" + + "-----END PGP PUBLIC KEY BLOCK-----" - private val MSG = "-----BEGIN PGP MESSAGE-----\n" + - "Version: PGPainless\n" + - "\n" + - "wcAQBhUEAh6XCjDVDdDeIypK3dKuQkQmRdESBCMEAV9Lm/I5jEe9t8Mdd7Pmk7S0\n" + - "3q308GnSq640CbhgORysK4+dnRYMzZFphil7dDsKWe2X7RMz7TDiPQhaoro6z0JP\n" + - "AZMx5eFiL0irdC9qV+0LvSnGJ8CyW3K15mKUomX82unAhquEhLtuPBufAN4bf2ia\n" + - "EiM85oz2U8CZ2Un48QLldDoHMKOdeAqX1xqFeBrD+ObgNsNfCLoYg4SM/EOUc06x\n" + - "U78DC23EfOI428Nfvzq1GiqVhtLAYgIJAQMNZthd/Qa2vPy8EaMLXn/NV35v4PzO\n" + - "39OYkdHRTO6g6OTI4Qf6fpXWoC8GdHIMOHGPMh2hKCXIXPEV0bncfnrUIXk9+miX\n" + - "7pFaM7kn/YGO48QUtY5ZxJdJAcjZA+vHBws8eDKC5Ajl5VYZrX187MQ+x/JID642\n" + - "QNsxUocyYwvRZenRQCuUV0vee08iLia/olzVjYQvsPYg6F/wa0KZRat2WMi/ofy9\n" + - "8C0tMUo31K4v2/z9T58DAR0P8AmLH/+196ijRbJ61U8HdiqYYPz7pevKdRB3N/0b\n" + - "dKkwF/chL+a/fSaxfAtJF2Zua4iW1OyrsbgIyXADUoS12K056A24yYE6dbMGVdhS\n" + - "+kQi5FkaOBCe1HVuETfNZ9XYV1312Dlj\n" + - "=XVu4\n" + - "-----END PGP MESSAGE-----" + private val MSG = + "-----BEGIN PGP MESSAGE-----\n" + + "Version: PGPainless\n" + + "\n" + + "wcAQBhUEAh6XCjDVDdDeIypK3dKuQkQmRdESBCMEAV9Lm/I5jEe9t8Mdd7Pmk7S0\n" + + "3q308GnSq640CbhgORysK4+dnRYMzZFphil7dDsKWe2X7RMz7TDiPQhaoro6z0JP\n" + + "AZMx5eFiL0irdC9qV+0LvSnGJ8CyW3K15mKUomX82unAhquEhLtuPBufAN4bf2ia\n" + + "EiM85oz2U8CZ2Un48QLldDoHMKOdeAqX1xqFeBrD+ObgNsNfCLoYg4SM/EOUc06x\n" + + "U78DC23EfOI428Nfvzq1GiqVhtLAYgIJAQMNZthd/Qa2vPy8EaMLXn/NV35v4PzO\n" + + "39OYkdHRTO6g6OTI4Qf6fpXWoC8GdHIMOHGPMh2hKCXIXPEV0bncfnrUIXk9+miX\n" + + "7pFaM7kn/YGO48QUtY5ZxJdJAcjZA+vHBws8eDKC5Ajl5VYZrX187MQ+x/JID642\n" + + "QNsxUocyYwvRZenRQCuUV0vee08iLia/olzVjYQvsPYg6F/wa0KZRat2WMi/ofy9\n" + + "8C0tMUo31K4v2/z9T58DAR0P8AmLH/+196ijRbJ61U8HdiqYYPz7pevKdRB3N/0b\n" + + "dKkwF/chL+a/fSaxfAtJF2Zua4iW1OyrsbgIyXADUoS12K056A24yYE6dbMGVdhS\n" + + "+kQi5FkaOBCe1HVuETfNZ9XYV1312Dlj\n" + + "=XVu4\n" + + "-----END PGP MESSAGE-----" @Test fun decryptMessageWithYubikey() { @@ -115,12 +121,16 @@ class YubikeyDecryptionTest : YubikeyTest() { // TODO: Make hardware decryption transparent as shown below! - val decIn = api.processMessage() - .onInputStream(msgIn) - .withOptions(ConsumerOptions.get(api) - .addHardwareTokenBackend(YubikeyHardwareTokenBackend()) - .addDecryptionKey(hardwareBasedKey, - SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(String(userPin))))) + val decIn = + api.processMessage() + .onInputStream(msgIn) + .withOptions( + ConsumerOptions.get(api) + .addHardwareTokenBackend(YubikeyHardwareTokenBackend()) + .addDecryptionKey( + hardwareBasedKey, + SecretKeyRingProtector.unlockAnyKeyWith( + Passphrase.fromPassword(String(userPin))))) val msg = decIn.readAllBytes() decIn.close() assertEquals("Hello, World!\n", String(msg)) diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt index 96d2e924..efd3151b 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyHardwareTokenBackendTest.kt @@ -1,11 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import org.gnupg.GnuPGDummyKeyUtil -import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assumptions.assumeTrue import org.junit.jupiter.api.Test -import java.util.Arrays class YubikeyHardwareTokenBackendTest : YubikeyTest() { @@ -14,13 +16,8 @@ class YubikeyHardwareTokenBackendTest : YubikeyTest() { @Test fun testListDeviceSerials() { val serials = backend.listDeviceSerials() - assertTrue(serials.any { - it.contentEquals( - GnuPGDummyKeyUtil.serialToBytes( - allowedSerialNumber - ) - ) - }) + assertTrue( + serials.any { it.contentEquals(GnuPGDummyKeyUtil.serialToBytes(allowedSerialNumber)) }) } @Test diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt index a125e1d5..c8dc779a 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyKeyGeneratorTest.kt @@ -1,18 +1,21 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey +import java.util.* import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.pgpainless.PGPainless import org.pgpainless.algorithm.OpenPGPKeyVersion -import java.util.* class YubikeyKeyGeneratorTest : YubikeyTest() { @Test fun generateKey() { val keyGen = YubikeyKeyGenerator(PGPainless.getInstance()) - val key = keyGen.generateModernKey( - yubikey, adminPin, OpenPGPKeyVersion.v4, Date()) + val key = keyGen.generateModernKey(yubikey, adminPin, OpenPGPKeyVersion.v4, Date()) println(key.toAsciiArmoredString()) for (subkey in key.secretKeys) { diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt index 4b8d41e1..60560fd9 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeySigningTest.kt @@ -1,6 +1,12 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey import com.yubico.yubikit.core.smartcard.SmartCardConnection +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream import org.bouncycastle.openpgp.operator.PGPContentSignerBuilderProvider import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test @@ -9,48 +15,47 @@ import org.pgpainless.decryption_verification.ConsumerOptions import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.encryption_signing.SigningOptions import org.pgpainless.signature.PGPContentSignerBuilderProviderFactory -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream class YubikeySigningTest : YubikeyTest() { - private val KEY = "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + - "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + - "Comment: Alice \n" + - "\n" + - "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\n" + - "1POPHzF3cMIReYhZfiJUEBV19suL\n" + - "=dA6G\n" + - "-----END PGP PRIVATE KEY BLOCK-----" + private val KEY = + "-----BEGIN PGP PRIVATE KEY BLOCK-----\n" + + "Comment: BB2A C3E1 E595 CD05 CFA5 CFE6 EB2E 570D 9EE2 2891\n" + + "Comment: Alice \n" + + "\n" + + "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\n" + + "1POPHzF3cMIReYhZfiJUEBV19suL\n" + + "=dA6G\n" + + "-----END PGP PRIVATE KEY BLOCK-----" @Test fun signMessageWithYubikey() { @@ -65,17 +70,25 @@ class YubikeySigningTest : YubikeyTest() { val msgOut = ByteArrayOutputStream() device.openConnection(SmartCardConnection::class.java).use { val connection = it - val factory = object : PGPContentSignerBuilderProviderFactory { - override fun create(hashAlgorithm: HashAlgorithm): PGPContentSignerBuilderProvider { - return YubikeyPGPContentSignerBuilderProvider(hashAlgorithm, connection) + val factory = + object : PGPContentSignerBuilderProviderFactory { + override fun create( + hashAlgorithm: HashAlgorithm + ): PGPContentSignerBuilderProvider { + return YubikeyPGPContentSignerBuilderProvider(hashAlgorithm, connection) + } } - } - val sigOut = api - .generateMessage() - .onOutputStream(msgOut) - .withOptions(ProducerOptions.sign(SigningOptions.get() - .addInlineSignature(hardwareBasedSigningKey.signingKeys[0], factory, HashAlgorithm.SHA512))) + val sigOut = + api.generateMessage() + .onOutputStream(msgOut) + .withOptions( + ProducerOptions.sign( + SigningOptions.get() + .addInlineSignature( + hardwareBasedSigningKey.signingKeys[0], + factory, + HashAlgorithm.SHA512))) sigOut.write("Hello, World!".toByteArray()) sigOut.close() @@ -84,9 +97,9 @@ class YubikeySigningTest : YubikeyTest() { api.processMessage() .onInputStream(ByteArrayInputStream(msgOut.toByteArray())) - .withOptions(ConsumerOptions.get() - .addVerificationCert(hardwareBasedSigningKey.toCertificate()) - ).use { + .withOptions( + ConsumerOptions.get().addVerificationCert(hardwareBasedSigningKey.toCertificate())) + .use { it.readAllBytes() it.close() assertTrue(it.metadata.isVerifiedSigned()) diff --git a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt index 0853ca8b..12457fce 100644 --- a/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt +++ b/pgpainless-yubikey/src/test/kotlin/org/pgpainless/yubikey/YubikeyTest.kt @@ -1,9 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package org.pgpainless.yubikey +import java.util.Properties import org.bouncycastle.openpgp.api.bc.BcOpenPGPImplementation import org.opentest4j.TestAbortedException import org.pgpainless.PGPainless -import java.util.Properties abstract class YubikeyTest() { @@ -21,17 +25,20 @@ abstract class YubikeyTest() { } } - open val api: PGPainless = PGPainless(BcOpenPGPImplementation()).apply { - hardwareTokenBackends.add(YubikeyHardwareTokenBackend()) - } + open val api: PGPainless = + PGPainless(BcOpenPGPImplementation()).apply { + hardwareTokenBackends.add(YubikeyHardwareTokenBackend()) + } open val helper: YubikeyHelper = YubikeyHelper(api) - val yubikey: Yubikey = YubikeyHelper().listDevices().find { it.serialNumber == allowedSerialNumber } - ?: throw TestAbortedException("No allowed device found.") + val yubikey: Yubikey = + YubikeyHelper().listDevices().find { it.serialNumber == allowedSerialNumber } + ?: throw TestAbortedException("No allowed device found.") private fun getProperty(properties: Properties, key: String): String { return properties.getProperty(key) - ?: throw TestAbortedException("Could not find property $key in pgpainless-yubikey/src/test/resources/yubikey.properties") + ?: throw TestAbortedException( + "Could not find property $key in pgpainless-yubikey/src/test/resources/yubikey.properties") } }