Add decryption callback to CryptoHandler

This commit is contained in:
Tad Fisher 2022-10-09 16:10:42 -07:00
parent 4b7457c7f7
commit 75040136ae
No known key found for this signature in database
GPG key ID: 3A7425F7E7B22251
8 changed files with 146 additions and 13 deletions

View file

@ -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)

View file

@ -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 =

View file

@ -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<Unit, CryptoHandlerException> {
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(

View file

@ -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()
}

View file

@ -11,7 +11,7 @@ import java.io.InputStream
import java.io.OutputStream
/** Generic interface to implement cryptographic operations on top of. */
public interface CryptoHandler<Key> {
public interface CryptoHandler<Key, EncryptedSessionKey, DecryptedSessionKey> {
/**
* Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and
@ -24,6 +24,7 @@ public interface CryptoHandler<Key> {
passphrase: String,
ciphertextStream: InputStream,
outputStream: OutputStream,
onDecryptSessionKey: (EncryptedSessionKey) -> DecryptedSessionKey,
): Result<Unit, CryptoHandlerException>
/**

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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
}
}
}