diff options
author | Tad Fisher <tadfisher@gmail.com> | 2022-10-09 16:10:42 -0700 |
---|---|---|
committer | Tad Fisher <tadfisher@gmail.com> | 2022-10-09 16:17:20 -0700 |
commit | 75040136ae5ca6108335975430b411f8a560d0ba (patch) | |
tree | 05fe8daf46eee5ec300d6f15f681867f81b4b09d /crypto-pgpainless | |
parent | 4b7457c7f712b92f21604d8612ec8ff19df75c81 (diff) |
Add decryption callback to CryptoHandler
Diffstat (limited to 'crypto-pgpainless')
3 files changed, 109 insertions, 5 deletions
diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt index 47c06c4f..d1487903 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt @@ -10,6 +10,11 @@ import app.passwordstore.crypto.GpgIdentifier.UserId import com.github.michaelbull.result.get import com.github.michaelbull.result.runCatching import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPSecretKey +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.algorithm.EncryptionPurpose +import org.pgpainless.key.OpenPgpFingerprint +import org.pgpainless.key.info.KeyRingInfo import org.pgpainless.key.parsing.KeyRingReader /** Utility methods to deal with [PGPKey]s. */ @@ -32,4 +37,25 @@ public object KeyUtils { val keyRing = tryParseKeyring(key) ?: return null return UserId(keyRing.publicKey.userIDs.next()) } + public fun tryGetEncryptionKeyFingerprint(key: PGPKey): OpenPgpFingerprint? { + val keyRing = tryParseKeyring(key) ?: return null + val encryptionSubkey = + KeyRingInfo(keyRing).getEncryptionSubkeys(EncryptionPurpose.ANY).lastOrNull() + return encryptionSubkey?.let(OpenPgpFingerprint::of) + } + + public fun tryGetEncryptionKey(key: PGPKey): PGPSecretKey? { + val keyRing = tryParseKeyring(key) as? PGPSecretKeyRing ?: return null + return tryGetEncryptionKey(keyRing) + } + + public fun tryGetEncryptionKey(keyRing: PGPSecretKeyRing): PGPSecretKey? { + val info = KeyRingInfo(keyRing) + return tryGetEncryptionKey(info) + } + + private fun tryGetEncryptionKey(info: KeyRingInfo): PGPSecretKey? { + val encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).lastOrNull() ?: return null + return info.getSecretKey(encryptionKey.keyID) + } } diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt index faa94dff..128ee57b 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt @@ -14,26 +14,31 @@ import com.github.michaelbull.result.mapError import com.github.michaelbull.result.runCatching import java.io.InputStream import java.io.OutputStream -import javax.inject.Inject +import org.bouncycastle.CachingPublicKeyDataDecryptorFactory import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPPublicKeyRingCollection import org.bouncycastle.openpgp.PGPSecretKeyRing import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.bouncycastle.openpgp.PGPSessionKey import org.pgpainless.PGPainless +import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.decryption_verification.HardwareSecurity +import org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory import org.pgpainless.encryption_signing.EncryptionOptions import org.pgpainless.encryption_signing.ProducerOptions import org.pgpainless.exception.WrongPassphraseException import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.util.Passphrase -public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKey> { +public class PGPainlessCryptoHandler : CryptoHandler<PGPKey, PGPEncryptedSessionKey, PGPSessionKey> { public override fun decrypt( keys: List<PGPKey>, passphrase: String, ciphertextStream: InputStream, outputStream: OutputStream, + onDecryptSessionKey: (PGPEncryptedSessionKey) -> PGPSessionKey ): Result<Unit, CryptoHandlerException> = runCatching { if (keys.isEmpty()) throw NoKeysProvided("No keys provided for encryption") @@ -42,18 +47,41 @@ public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKe .map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) } .run(::PGPSecretKeyRingCollection) val protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(passphrase)) + val hardwareBackedKeys = + keyringCollection.mapNotNull { keyring -> + KeyUtils.tryGetEncryptionKey(keyring) + ?.takeIf { it.keyID in HardwareSecurity.getIdsOfHardwareBackedKeys(keyring) } + } PGPainless.decryptAndOrVerify() .onInputStream(ciphertextStream) .withOptions( - ConsumerOptions() - .addDecryptionKeys(keyringCollection, protector) - .addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) + ConsumerOptions().apply { + for (key in hardwareBackedKeys) { + addCustomDecryptorFactory( + setOf(key.keyID), + CachingPublicKeyDataDecryptorFactory( + HardwareDataDecryptorFactory { keyAlgorithm, secKeyData -> + onDecryptSessionKey( + PGPEncryptedSessionKey( + key.publicKey, + PublicKeyAlgorithm.requireFromId(keyAlgorithm), + secKeyData + ) + ).key + } + ) + ) + } + addDecryptionKeys(keyringCollection, protector) + addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) + } ) .use { decryptionStream -> decryptionStream.copyTo(outputStream) } return@runCatching } .mapError { error -> when (error) { + is CryptoHandlerException -> error is WrongPassphraseException -> IncorrectPassphraseException(error) else -> UnknownError(error) } diff --git a/crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt new file mode 100644 index 00000000..25ede0ec --- /dev/null +++ b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub <vanitasvitae@fsfe.org> +// +// SPDX-License-Identifier: Apache-2.0 + +package org.bouncycastle + +import org.bouncycastle.openpgp.PGPException +import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory +import org.bouncycastle.util.encoders.Base64 + +/** + * Implementation of the [PublicKeyDataDecryptorFactory] which caches decrypted session keys. + * That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted. + * + * This implementation changes the behavior or [.recoverSessionData] to first return any + * cache hits. + * If no hit is found, the method call is delegated to the underlying [PublicKeyDataDecryptorFactory]. + * The result of that is then placed in the cache and returned. + * + * TODO: Do we also cache invalid session keys? + */ +public class CachingPublicKeyDataDecryptorFactory( + private val factory: PublicKeyDataDecryptorFactory +) : PublicKeyDataDecryptorFactory by factory { + + private val cachedSessionKeys: MutableMap<String, ByteArray> = mutableMapOf() + + @Throws(PGPException::class) + override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array<ByteArray>): ByteArray { + return cachedSessionKeys.getOrPut(cacheKey(secKeyData)) { + factory.recoverSessionData(keyAlgorithm, secKeyData) + }.copy() + } + + public fun clear() { + cachedSessionKeys.clear() + } + + private companion object { + fun cacheKey(secKeyData: Array<ByteArray>): String { + return Base64.toBase64String(secKeyData[0]) + } + + private fun ByteArray.copy(): ByteArray { + val copy = ByteArray(size) + System.arraycopy(this, 0, copy, 0, copy.size) + return copy + } + } +} |