aboutsummaryrefslogtreecommitdiff
path: root/crypto-pgpainless
diff options
context:
space:
mode:
authorTad Fisher <tadfisher@gmail.com>2022-10-09 16:10:42 -0700
committerTad Fisher <tadfisher@gmail.com>2022-10-09 16:17:20 -0700
commit75040136ae5ca6108335975430b411f8a560d0ba (patch)
tree05fe8daf46eee5ec300d6f15f681867f81b4b09d /crypto-pgpainless
parent4b7457c7f712b92f21604d8612ec8ff19df75c81 (diff)
Add decryption callback to CryptoHandler
Diffstat (limited to 'crypto-pgpainless')
-rw-r--r--crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt26
-rw-r--r--crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt38
-rw-r--r--crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt50
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
+ }
+ }
+}