mirror of
https://github.com/android-password-store/Android-Password-Store.git
synced 2026-04-11 16:57:20 +02:00
Add decryption callback to CryptoHandler
This commit is contained in:
parent
4b7457c7f7
commit
75040136ae
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue