Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit 7504013

Browse files
committed
Add decryption callback to CryptoHandler
1 parent 4b7457c commit 7504013

File tree

8 files changed

+146
-13
lines changed

8 files changed

+146
-13
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ dependencies {
5555
coreLibraryDesugaring(libs.android.desugarJdkLibs)
5656
implementation(projects.autofillParser)
5757
implementation(projects.coroutineUtils)
58+
implementation(projects.cryptoHwsecurity)
5859
implementation(projects.cryptoPgpainless)
5960
implementation(projects.formatCommon)
6061
implementation(projects.passgen.diceware)

app/src/main/java/app/passwordstore/Application.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
1212
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
1313
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
1414
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
15+
import app.passwordstore.crypto.HWSecurityManager
1516
import app.passwordstore.injection.context.FilesDirPath
1617
import app.passwordstore.injection.prefs.SettingsPreferences
1718
import app.passwordstore.util.extensions.getString
@@ -43,14 +44,15 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
4344
@Inject lateinit var proxyUtils: ProxyUtils
4445
@Inject lateinit var gitSettings: GitSettings
4546
@Inject lateinit var features: Features
47+
@Inject lateinit var deviceManager: HWSecurityManager
4648

4749
override fun onCreate() {
4850
super.onCreate()
4951
instance = this
50-
if (
51-
BuildConfig.ENABLE_DEBUG_FEATURES ||
52-
prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)
53-
) {
52+
53+
val enableLogging = BuildConfig.ENABLE_DEBUG_FEATURES ||
54+
prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false)
55+
if (enableLogging) {
5456
LogcatLogger.install(AndroidLogcatLogger(DEBUG))
5557
setVmPolicy()
5658
}
@@ -60,6 +62,7 @@ class Application : android.app.Application(), SharedPreferences.OnSharedPrefere
6062
runMigrations(filesDirPath, prefs, gitSettings)
6163
proxyUtils.setDefaultProxy()
6264
DynamicColors.applyToActivitiesIfAvailable(this)
65+
deviceManager.init(enableLogging)
6366
Sentry.configureScope { scope ->
6467
val user = User()
6568
user.data =

app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,27 @@
66
package app.passwordstore.data.crypto
77

88
import app.passwordstore.crypto.GpgIdentifier
9+
import app.passwordstore.crypto.HWSecurityDeviceHandler
910
import app.passwordstore.crypto.PGPKeyManager
1011
import app.passwordstore.crypto.PGPainlessCryptoHandler
1112
import app.passwordstore.crypto.errors.CryptoHandlerException
1213
import com.github.michaelbull.result.Result
1314
import com.github.michaelbull.result.getAll
15+
import com.github.michaelbull.result.getOrThrow
1416
import com.github.michaelbull.result.unwrap
1517
import java.io.ByteArrayInputStream
1618
import java.io.ByteArrayOutputStream
1719
import javax.inject.Inject
1820
import kotlinx.coroutines.Dispatchers
21+
import kotlinx.coroutines.runBlocking
1922
import kotlinx.coroutines.withContext
2023

2124
class CryptoRepository
2225
@Inject
2326
constructor(
2427
private val pgpKeyManager: PGPKeyManager,
2528
private val pgpCryptoHandler: PGPainlessCryptoHandler,
29+
private val deviceHandler: HWSecurityDeviceHandler
2630
) {
2731

2832
suspend fun decrypt(
@@ -43,7 +47,11 @@ constructor(
4347
out: ByteArrayOutputStream,
4448
): Result<Unit, CryptoHandlerException> {
4549
val keys = pgpKeyManager.getAllKeys().unwrap()
46-
return pgpCryptoHandler.decrypt(keys, password, message, out)
50+
return pgpCryptoHandler.decrypt(keys, password, message, out) { encryptedSessionKey ->
51+
runBlocking {
52+
deviceHandler.decryptSessionKey(encryptedSessionKey).getOrThrow()
53+
}
54+
}
4755
}
4856

4957
private suspend fun encryptPgp(

app/src/main/java/app/passwordstore/injection/crypto/CryptoHandlerModule.kt

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,30 @@
55

66
package app.passwordstore.injection.crypto
77

8+
import android.app.Activity
9+
import androidx.fragment.app.FragmentActivity
10+
import app.passwordstore.crypto.HWSecurityDeviceHandler
11+
import app.passwordstore.crypto.HWSecurityManager
812
import app.passwordstore.crypto.PGPainlessCryptoHandler
913
import dagger.Module
1014
import dagger.Provides
1115
import dagger.hilt.InstallIn
12-
import dagger.hilt.components.SingletonComponent
16+
import dagger.hilt.android.components.ActivityComponent
17+
import dagger.hilt.android.scopes.ActivityScoped
1318

1419
@Module
15-
@InstallIn(SingletonComponent::class)
20+
@InstallIn(ActivityComponent::class)
1621
object CryptoHandlerModule {
22+
23+
@Provides
24+
@ActivityScoped
25+
fun provideDeviceHandler(
26+
activity: Activity,
27+
deviceManager: HWSecurityManager
28+
): HWSecurityDeviceHandler = HWSecurityDeviceHandler(
29+
deviceManager = deviceManager,
30+
fragmentManager = (activity as FragmentActivity).supportFragmentManager
31+
)
32+
1733
@Provides fun providePgpCryptoHandler() = PGPainlessCryptoHandler()
1834
}

crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import java.io.InputStream
1111
import java.io.OutputStream
1212

1313
/** Generic interface to implement cryptographic operations on top of. */
14-
public interface CryptoHandler<Key> {
14+
public interface CryptoHandler<Key, EncryptedSessionKey, DecryptedSessionKey> {
1515

1616
/**
1717
* Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and
@@ -24,6 +24,7 @@ public interface CryptoHandler<Key> {
2424
passphrase: String,
2525
ciphertextStream: InputStream,
2626
outputStream: OutputStream,
27+
onDecryptSessionKey: (EncryptedSessionKey) -> DecryptedSessionKey,
2728
): Result<Unit, CryptoHandlerException>
2829

2930
/**

crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import app.passwordstore.crypto.GpgIdentifier.UserId
1010
import com.github.michaelbull.result.get
1111
import com.github.michaelbull.result.runCatching
1212
import org.bouncycastle.openpgp.PGPKeyRing
13+
import org.bouncycastle.openpgp.PGPSecretKey
14+
import org.bouncycastle.openpgp.PGPSecretKeyRing
15+
import org.pgpainless.algorithm.EncryptionPurpose
16+
import org.pgpainless.key.OpenPgpFingerprint
17+
import org.pgpainless.key.info.KeyRingInfo
1318
import org.pgpainless.key.parsing.KeyRingReader
1419

1520
/** Utility methods to deal with [PGPKey]s. */
@@ -32,4 +37,25 @@ public object KeyUtils {
3237
val keyRing = tryParseKeyring(key) ?: return null
3338
return UserId(keyRing.publicKey.userIDs.next())
3439
}
40+
public fun tryGetEncryptionKeyFingerprint(key: PGPKey): OpenPgpFingerprint? {
41+
val keyRing = tryParseKeyring(key) ?: return null
42+
val encryptionSubkey =
43+
KeyRingInfo(keyRing).getEncryptionSubkeys(EncryptionPurpose.ANY).lastOrNull()
44+
return encryptionSubkey?.let(OpenPgpFingerprint::of)
45+
}
46+
47+
public fun tryGetEncryptionKey(key: PGPKey): PGPSecretKey? {
48+
val keyRing = tryParseKeyring(key) as? PGPSecretKeyRing ?: return null
49+
return tryGetEncryptionKey(keyRing)
50+
}
51+
52+
public fun tryGetEncryptionKey(keyRing: PGPSecretKeyRing): PGPSecretKey? {
53+
val info = KeyRingInfo(keyRing)
54+
return tryGetEncryptionKey(info)
55+
}
56+
57+
private fun tryGetEncryptionKey(info: KeyRingInfo): PGPSecretKey? {
58+
val encryptionKey = info.getEncryptionSubkeys(EncryptionPurpose.ANY).lastOrNull() ?: return null
59+
return info.getSecretKey(encryptionKey.keyID)
60+
}
3561
}

crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,31 @@ import com.github.michaelbull.result.mapError
1414
import com.github.michaelbull.result.runCatching
1515
import java.io.InputStream
1616
import java.io.OutputStream
17-
import javax.inject.Inject
17+
import org.bouncycastle.CachingPublicKeyDataDecryptorFactory
1818
import org.bouncycastle.openpgp.PGPPublicKeyRing
1919
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection
2020
import org.bouncycastle.openpgp.PGPSecretKeyRing
2121
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection
22+
import org.bouncycastle.openpgp.PGPSessionKey
2223
import org.pgpainless.PGPainless
24+
import org.pgpainless.algorithm.PublicKeyAlgorithm
2325
import org.pgpainless.decryption_verification.ConsumerOptions
26+
import org.pgpainless.decryption_verification.HardwareSecurity
27+
import org.pgpainless.decryption_verification.HardwareSecurity.HardwareDataDecryptorFactory
2428
import org.pgpainless.encryption_signing.EncryptionOptions
2529
import org.pgpainless.encryption_signing.ProducerOptions
2630
import org.pgpainless.exception.WrongPassphraseException
2731
import org.pgpainless.key.protection.SecretKeyRingProtector
2832
import org.pgpainless.util.Passphrase
2933

30-
public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKey> {
34+
public class PGPainlessCryptoHandler : CryptoHandler<PGPKey, PGPEncryptedSessionKey, PGPSessionKey> {
3135

3236
public override fun decrypt(
3337
keys: List<PGPKey>,
3438
passphrase: String,
3539
ciphertextStream: InputStream,
3640
outputStream: OutputStream,
41+
onDecryptSessionKey: (PGPEncryptedSessionKey) -> PGPSessionKey
3742
): Result<Unit, CryptoHandlerException> =
3843
runCatching {
3944
if (keys.isEmpty()) throw NoKeysProvided("No keys provided for encryption")
@@ -42,18 +47,41 @@ public class PGPainlessCryptoHandler @Inject constructor() : CryptoHandler<PGPKe
4247
.map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) }
4348
.run(::PGPSecretKeyRingCollection)
4449
val protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(passphrase))
50+
val hardwareBackedKeys =
51+
keyringCollection.mapNotNull { keyring ->
52+
KeyUtils.tryGetEncryptionKey(keyring)
53+
?.takeIf { it.keyID in HardwareSecurity.getIdsOfHardwareBackedKeys(keyring) }
54+
}
4555
PGPainless.decryptAndOrVerify()
4656
.onInputStream(ciphertextStream)
4757
.withOptions(
48-
ConsumerOptions()
49-
.addDecryptionKeys(keyringCollection, protector)
50-
.addDecryptionPassphrase(Passphrase.fromPassword(passphrase))
58+
ConsumerOptions().apply {
59+
for (key in hardwareBackedKeys) {
60+
addCustomDecryptorFactory(
61+
setOf(key.keyID),
62+
CachingPublicKeyDataDecryptorFactory(
63+
HardwareDataDecryptorFactory { keyAlgorithm, secKeyData ->
64+
onDecryptSessionKey(
65+
PGPEncryptedSessionKey(
66+
key.publicKey,
67+
PublicKeyAlgorithm.requireFromId(keyAlgorithm),
68+
secKeyData
69+
)
70+
).key
71+
}
72+
)
73+
)
74+
}
75+
addDecryptionKeys(keyringCollection, protector)
76+
addDecryptionPassphrase(Passphrase.fromPassword(passphrase))
77+
}
5178
)
5279
.use { decryptionStream -> decryptionStream.copyTo(outputStream) }
5380
return@runCatching
5481
}
5582
.mapError { error ->
5683
when (error) {
84+
is CryptoHandlerException -> error
5785
is WrongPassphraseException -> IncorrectPassphraseException(error)
5886
else -> UnknownError(error)
5987
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// SPDX-FileCopyrightText: 2022 Paul Schaub <[email protected]>
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
5+
package org.bouncycastle
6+
7+
import org.bouncycastle.openpgp.PGPException
8+
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory
9+
import org.bouncycastle.util.encoders.Base64
10+
11+
/**
12+
* Implementation of the [PublicKeyDataDecryptorFactory] which caches decrypted session keys.
13+
* That way, if a message needs to be decrypted multiple times, expensive private key operations can be omitted.
14+
*
15+
* This implementation changes the behavior or [.recoverSessionData] to first return any
16+
* cache hits.
17+
* If no hit is found, the method call is delegated to the underlying [PublicKeyDataDecryptorFactory].
18+
* The result of that is then placed in the cache and returned.
19+
*
20+
* TODO: Do we also cache invalid session keys?
21+
*/
22+
public class CachingPublicKeyDataDecryptorFactory(
23+
private val factory: PublicKeyDataDecryptorFactory
24+
) : PublicKeyDataDecryptorFactory by factory {
25+
26+
private val cachedSessionKeys: MutableMap<String, ByteArray> = mutableMapOf()
27+
28+
@Throws(PGPException::class)
29+
override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array<ByteArray>): ByteArray {
30+
return cachedSessionKeys.getOrPut(cacheKey(secKeyData)) {
31+
factory.recoverSessionData(keyAlgorithm, secKeyData)
32+
}.copy()
33+
}
34+
35+
public fun clear() {
36+
cachedSessionKeys.clear()
37+
}
38+
39+
private companion object {
40+
fun cacheKey(secKeyData: Array<ByteArray>): String {
41+
return Base64.toBase64String(secKeyData[0])
42+
}
43+
44+
private fun ByteArray.copy(): ByteArray {
45+
val copy = ByteArray(size)
46+
System.arraycopy(this, 0, copy, 0, copy.size)
47+
return copy
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)