From 75040136ae5ca6108335975430b411f8a560d0ba Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 9 Oct 2022 16:10:42 -0700 Subject: Add decryption callback to CryptoHandler --- app/build.gradle.kts | 1 + app/src/main/java/app/passwordstore/Application.kt | 11 +++-- .../passwordstore/data/crypto/CryptoRepository.kt | 10 ++++- .../injection/crypto/CryptoHandlerModule.kt | 20 ++++++++- .../app/passwordstore/crypto/CryptoHandler.kt | 3 +- .../kotlin/app/passwordstore/crypto/KeyUtils.kt | 26 +++++++++++ .../crypto/PGPainlessCryptoHandler.kt | 38 +++++++++++++--- .../CachingPublicKeyDataDecryptorFactory.kt | 50 ++++++++++++++++++++++ 8 files changed, 146 insertions(+), 13 deletions(-) create mode 100644 crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 32291d33..0c486f65 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -55,6 +55,7 @@ dependencies { coreLibraryDesugaring(libs.android.desugarJdkLibs) implementation(projects.autofillParser) implementation(projects.coroutineUtils) + implementation(projects.cryptoHwsecurity) implementation(projects.cryptoPgpainless) implementation(projects.formatCommon) implementation(projects.passgen.diceware) diff --git a/app/src/main/java/app/passwordstore/Application.kt b/app/src/main/java/app/passwordstore/Application.kt index fb4c0f63..9ec32f15 100644 --- a/app/src/main/java/app/passwordstore/Application.kt +++ b/app/src/main/java/app/passwordstore/Application.kt @@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES +import app.passwordstore.crypto.HWSecurityManager import app.passwordstore.injection.context.FilesDirPath import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.util.extensions.getString @@ -43,14 +44,15 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere @Inject lateinit var proxyUtils: ProxyUtils @Inject lateinit var gitSettings: GitSettings @Inject lateinit var features: Features + @Inject lateinit var deviceManager: HWSecurityManager override fun onCreate() { super.onCreate() instance = this - if ( - BuildConfig.ENABLE_DEBUG_FEATURES || - prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) - ) { + + val enableLogging = BuildConfig.ENABLE_DEBUG_FEATURES || + prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) + if (enableLogging) { LogcatLogger.install(AndroidLogcatLogger(DEBUG)) setVmPolicy() } @@ -60,6 +62,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere runMigrations(filesDirPath, prefs, gitSettings) proxyUtils.setDefaultProxy() DynamicColors.applyToActivitiesIfAvailable(this) + deviceManager.init(enableLogging) Sentry.configureScope { scope -> val user = User() user.data = diff --git a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt index b673e94c..9f8dfbd5 100644 --- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt +++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt @@ -6,16 +6,19 @@ package app.passwordstore.data.crypto import app.passwordstore.crypto.GpgIdentifier +import app.passwordstore.crypto.HWSecurityDeviceHandler import app.passwordstore.crypto.PGPKeyManager import app.passwordstore.crypto.PGPainlessCryptoHandler import app.passwordstore.crypto.errors.CryptoHandlerException import com.github.michaelbull.result.Result import com.github.michaelbull.result.getAll +import com.github.michaelbull.result.getOrThrow import com.github.michaelbull.result.unwrap import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import javax.inject.Inject import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext class CryptoRepository @@ -23,6 +26,7 @@ class CryptoRepository constructor( private val pgpKeyManager: PGPKeyManager, private val pgpCryptoHandler: PGPainlessCryptoHandler, + private val deviceHandler: HWSecurityDeviceHandler ) { suspend fun decrypt( @@ -43,7 +47,11 @@ constructor( out: ByteArrayOutputStream, ): Result { val keys = pgpKeyManager.getAllKeys().unwrap() - return pgpCryptoHandler.decrypt(keys, password, message, out) + return pgpCryptoHandler.decrypt(keys, password, message, out) { encryptedSessionKey -> + runBlocking { + deviceHandler.decryptSessionKey(encryptedSessionKey).getOrThrow() + } + } } private suspend fun encryptPgp( diff --git a/app/src/main/java/app/passwordstore/injection/crypto/CryptoHandlerModule.kt b/app/src/main/java/app/passwordstore/injection/crypto/CryptoHandlerModule.kt index 5a863d8d..6eca052f 100644 --- a/app/src/main/java/app/passwordstore/injection/crypto/CryptoHandlerModule.kt +++ b/app/src/main/java/app/passwordstore/injection/crypto/CryptoHandlerModule.kt @@ -5,14 +5,30 @@ package app.passwordstore.injection.crypto +import android.app.Activity +import androidx.fragment.app.FragmentActivity +import app.passwordstore.crypto.HWSecurityDeviceHandler +import app.passwordstore.crypto.HWSecurityManager import app.passwordstore.crypto.PGPainlessCryptoHandler import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.scopes.ActivityScoped @Module -@InstallIn(SingletonComponent::class) +@InstallIn(ActivityComponent::class) object CryptoHandlerModule { + + @Provides + @ActivityScoped + fun provideDeviceHandler( + activity: Activity, + deviceManager: HWSecurityManager + ): HWSecurityDeviceHandler = HWSecurityDeviceHandler( + deviceManager = deviceManager, + fragmentManager = (activity as FragmentActivity).supportFragmentManager + ) + @Provides fun providePgpCryptoHandler() = PGPainlessCryptoHandler() } diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt index ea42af6d..7f5ca625 100644 --- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt +++ b/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt @@ -11,7 +11,7 @@ import java.io.InputStream import java.io.OutputStream /** Generic interface to implement cryptographic operations on top of. */ -public interface CryptoHandler { +public interface CryptoHandler { /** * Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and @@ -24,6 +24,7 @@ public interface CryptoHandler { passphrase: String, ciphertextStream: InputStream, outputStream: OutputStream, + onDecryptSessionKey: (EncryptedSessionKey) -> DecryptedSessionKey, ): Result /** 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 { +public class PGPainlessCryptoHandler : CryptoHandler { public override fun decrypt( keys: List, passphrase: String, ciphertextStream: InputStream, outputStream: OutputStream, + onDecryptSessionKey: (PGPEncryptedSessionKey) -> PGPSessionKey ): Result = runCatching { if (keys.isEmpty()) throw NoKeysProvided("No keys provided for encryption") @@ -42,18 +47,41 @@ public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler 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 +// +// 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 = mutableMapOf() + + @Throws(PGPException::class) + override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray { + return cachedSessionKeys.getOrPut(cacheKey(secKeyData)) { + factory.recoverSessionData(keyAlgorithm, secKeyData) + }.copy() + } + + public fun clear() { + cachedSessionKeys.clear() + } + + private companion object { + fun cacheKey(secKeyData: Array): 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 + } + } +} -- cgit v1.2.3