aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/build.gradle.kts1
-rw-r--r--app/src/main/java/app/passwordstore/Application.kt11
-rw-r--r--app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt10
-rw-r--r--app/src/main/java/app/passwordstore/injection/crypto/CryptoHandlerModule.kt20
-rw-r--r--crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt3
-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
8 files changed, 146 insertions, 13 deletions
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<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(
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<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>
/**
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
+ }
+ }
+}