aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crypto-common/src/main/kotlin/app/passwordstore/crypto/DeviceHandler.kt12
-rw-r--r--crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt39
-rw-r--r--crypto-hwsecurity/api/crypto-hwsecurity.api64
-rw-r--r--crypto-hwsecurity/build.gradle.kts27
-rw-r--r--crypto-hwsecurity/src/main/AndroidManifest.xml1
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt52
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt26
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt46
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt52
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt182
-rw-r--r--crypto-pgpainless/build.gradle.kts2
-rw-r--r--crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt16
-rw-r--r--settings.gradle.kts2
13 files changed, 517 insertions, 4 deletions
diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/DeviceHandler.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/DeviceHandler.kt
new file mode 100644
index 00000000..74eb0cfa
--- /dev/null
+++ b/crypto-common/src/main/kotlin/app/passwordstore/crypto/DeviceHandler.kt
@@ -0,0 +1,12 @@
+package app.passwordstore.crypto
+
+import app.passwordstore.crypto.errors.DeviceHandlerException
+import com.github.michaelbull.result.Result
+
+public interface DeviceHandler<Key, EncryptedSessionKey, DecryptedSessionKey> {
+ public suspend fun pairWithPublicKey(publicKey: Key): Result<Key, DeviceHandlerException>
+
+ public suspend fun decryptSessionKey(
+ encryptedSessionKey: EncryptedSessionKey
+ ): Result<DecryptedSessionKey, DeviceHandlerException>
+}
diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt
index 81bdf95f..328a7a32 100644
--- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt
+++ b/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt
@@ -6,7 +6,7 @@ public sealed class CryptoException(message: String? = null, cause: Throwable? =
Exception(message, cause)
/** Sealed exception types for [KeyManager]. */
-public sealed class KeyManagerException(message: String? = null) : CryptoException(message)
+public sealed class KeyManagerException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause)
/** Store contains no keys. */
public object NoKeysAvailableException : KeyManagerException("No keys were found")
@@ -19,8 +19,8 @@ public object KeyDirectoryUnavailableException :
public object KeyDeletionFailedException : KeyManagerException("Couldn't delete the key file")
/** Failed to parse the key as a known type. */
-public object InvalidKeyException :
- KeyManagerException("Given key cannot be parsed as a known key type")
+public class InvalidKeyException(cause: Throwable? = null) :
+ KeyManagerException("Given key cannot be parsed as a known key type", cause)
/** No key matching `keyId` could be found. */
public class KeyNotFoundException(keyId: String) :
@@ -30,6 +30,9 @@ public class KeyNotFoundException(keyId: String) :
public class KeyAlreadyExistsException(keyId: String) :
KeyManagerException("Pre-existing key was found for $keyId")
+public class NoSecretKeyException(keyId: String) :
+ KeyManagerException("No secret keys found for $keyId")
+
/** Sealed exception types for [app.passwordstore.crypto.CryptoHandler]. */
public sealed class CryptoHandlerException(message: String? = null, cause: Throwable? = null) :
CryptoException(message, cause)
@@ -42,3 +45,33 @@ public class NoKeysProvided(message: String?) : CryptoHandlerException(message,
/** An unexpected error that cannot be mapped to a known type. */
public class UnknownError(cause: Throwable) : CryptoHandlerException(null, cause)
+
+public class KeySpecific(public val key: Any, cause: Throwable?) : CryptoHandlerException(key.toString(), cause)
+
+/** Wrapper containing possibly multiple child exceptions via [suppressedExceptions]. */
+public class MultipleKeySpecific(
+ message: String?,
+ public val errors: List<KeySpecific>
+) : CryptoHandlerException(message) {
+ init {
+ for (error in errors) {
+ addSuppressed(error)
+ }
+ }
+}
+
+/** Sealed exception types for [app.passwordstore.crypto.DeviceHandler]. */
+public sealed class DeviceHandlerException(message: String? = null, cause: Throwable? = null) :
+ CryptoHandlerException(message, cause)
+
+/** The device crypto operation was canceled by the user. */
+public class DeviceOperationCanceled(message: String) : DeviceHandlerException(message, null)
+
+/** The device crypto operation failed. */
+public class DeviceOperationFailed(message: String?, cause: Throwable? = null) : DeviceHandlerException(message, cause)
+
+/** The device's key fingerprint doesn't match the fingerprint we are trying to pair it to. */
+public class DeviceFingerprintMismatch(
+ public val publicFingerprint: String,
+ public val deviceFingerprint: String,
+) : DeviceHandlerException()
diff --git a/crypto-hwsecurity/api/crypto-hwsecurity.api b/crypto-hwsecurity/api/crypto-hwsecurity.api
new file mode 100644
index 00000000..cba8a0ea
--- /dev/null
+++ b/crypto-hwsecurity/api/crypto-hwsecurity.api
@@ -0,0 +1,64 @@
+public final class app/passwordstore/crypto/DeviceIdentifier {
+ public static final synthetic fun box-impl ([B)Lapp/passwordstore/crypto/DeviceIdentifier;
+ public static fun constructor-impl ([B)[B
+ public fun equals (Ljava/lang/Object;)Z
+ public static fun equals-impl ([BLjava/lang/Object;)Z
+ public static final fun equals-impl0 ([B[B)Z
+ public static final fun getManufacturer-impl ([B)I
+ public static final fun getOpenPgpVersion-impl ([B)Ljava/lang/String;
+ public static final fun getSerialNumber-impl ([B)[B
+ public fun hashCode ()I
+ public static fun hashCode-impl ([B)I
+ public fun toString ()Ljava/lang/String;
+ public static fun toString-impl ([B)Ljava/lang/String;
+ public final synthetic fun unbox-impl ()[B
+}
+
+public final class app/passwordstore/crypto/DeviceIdentifierKt {
+ public static final fun getManufacturerName-0zlKB64 ([B)Ljava/lang/String;
+}
+
+public final class app/passwordstore/crypto/DeviceKeyInfo {
+ public fun <init> (Lorg/pgpainless/algorithm/PublicKeyAlgorithm;Lorg/pgpainless/key/OpenPgpFingerprint;)V
+ public final fun component1 ()Lorg/pgpainless/algorithm/PublicKeyAlgorithm;
+ public final fun component2 ()Lorg/pgpainless/key/OpenPgpFingerprint;
+ public final fun copy (Lorg/pgpainless/algorithm/PublicKeyAlgorithm;Lorg/pgpainless/key/OpenPgpFingerprint;)Lapp/passwordstore/crypto/DeviceKeyInfo;
+ public static synthetic fun copy$default (Lapp/passwordstore/crypto/DeviceKeyInfo;Lorg/pgpainless/algorithm/PublicKeyAlgorithm;Lorg/pgpainless/key/OpenPgpFingerprint;ILjava/lang/Object;)Lapp/passwordstore/crypto/DeviceKeyInfo;
+ public fun equals (Ljava/lang/Object;)Z
+ public final fun getAlgorithm ()Lorg/pgpainless/algorithm/PublicKeyAlgorithm;
+ public final fun getFingerprint ()Lorg/pgpainless/key/OpenPgpFingerprint;
+ public fun hashCode ()I
+ public fun toString ()Ljava/lang/String;
+}
+
+public final class app/passwordstore/crypto/HWSecurityDevice {
+ public synthetic fun <init> ([BLjava/lang/String;Lapp/passwordstore/crypto/DeviceKeyInfo;Lapp/passwordstore/crypto/DeviceKeyInfo;Lapp/passwordstore/crypto/DeviceKeyInfo;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+ public final fun getAuthKeyInfo ()Lapp/passwordstore/crypto/DeviceKeyInfo;
+ public final fun getEncryptKeyInfo ()Lapp/passwordstore/crypto/DeviceKeyInfo;
+ public final fun getId-z5xZLwU ()[B
+ public final fun getName ()Ljava/lang/String;
+ public final fun getSignKeyInfo ()Lapp/passwordstore/crypto/DeviceKeyInfo;
+}
+
+public final class app/passwordstore/crypto/HWSecurityDeviceHandler : app/passwordstore/crypto/DeviceHandler {
+ public fun <init> (Lapp/passwordstore/crypto/HWSecurityManager;Landroidx/fragment/app/FragmentManager;)V
+ public fun decryptSessionKey (Lapp/passwordstore/crypto/PGPEncryptedSessionKey;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public synthetic fun decryptSessionKey (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public synthetic fun pairWithPublicKey (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public fun pairWithPublicKey-P2gA-3I ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public final class app/passwordstore/crypto/HWSecurityException : org/pgpainless/decryption_verification/HardwareSecurity$HardwareSecurityException {
+ public fun <init> (Ljava/lang/String;)V
+ public fun getMessage ()Ljava/lang/String;
+}
+
+public final class app/passwordstore/crypto/HWSecurityManager {
+ public fun <init> (Landroid/app/Application;)V
+ public final fun decryptSessionKey (Landroidx/fragment/app/FragmentManager;Lapp/passwordstore/crypto/PGPEncryptedSessionKey;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public final fun init (Z)V
+ public static synthetic fun init$default (Lapp/passwordstore/crypto/HWSecurityManager;ZILjava/lang/Object;)V
+ public final fun isHardwareAvailable ()Z
+ public final fun readDevice (Landroidx/fragment/app/FragmentManager;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
diff --git a/crypto-hwsecurity/build.gradle.kts b/crypto-hwsecurity/build.gradle.kts
new file mode 100644
index 00000000..6f9e4ebc
--- /dev/null
+++ b/crypto-hwsecurity/build.gradle.kts
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+plugins {
+ id("com.github.android-password-store.android-library")
+ id("com.github.android-password-store.kotlin-android")
+ id("com.github.android-password-store.kotlin-library")
+}
+
+android {
+ namespace = "app.passwordstore.crypto.hwsecurity"
+}
+
+dependencies {
+ implementation(projects.cryptoPgpainless)
+ implementation(libs.androidx.activity.ktx)
+ implementation(libs.androidx.annotation)
+ implementation(libs.androidx.appcompat)
+ implementation(libs.androidx.fragment.ktx)
+ implementation(libs.androidx.material)
+ implementation(libs.aps.hwsecurity.openpgp)
+ implementation(libs.aps.hwsecurity.ui)
+ implementation(libs.dagger.hilt.android)
+ implementation(libs.kotlin.coroutines.android)
+ implementation(libs.thirdparty.kotlinResult)
+}
diff --git a/crypto-hwsecurity/src/main/AndroidManifest.xml b/crypto-hwsecurity/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..cc947c56
--- /dev/null
+++ b/crypto-hwsecurity/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest />
diff --git a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt
new file mode 100644
index 00000000..be27f139
--- /dev/null
+++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt
@@ -0,0 +1,52 @@
+@file:Suppress("MagicNumber")
+package app.passwordstore.crypto
+
+@JvmInline
+public value class DeviceIdentifier(
+ private val aid: ByteArray
+) {
+ init {
+ require(aid.size == 16) { "Invalid device application identifier" }
+ }
+
+ public val openPgpVersion: String get() = "${aid[6]}.${aid[7]}"
+
+ public val manufacturer: Int
+ get() = ((aid[8].toInt() and 0xff) shl 8) or (aid[9].toInt() and 0xff)
+
+ public val serialNumber: ByteArray get() = aid.sliceArray(10..13)
+ }
+
+// https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=scd/app-openpgp.c;hb=HEAD#l292
+public val DeviceIdentifier.manufacturerName: String get() = when (manufacturer) {
+ 0x0001 -> "PPC Card Systems"
+ 0x0002 -> "Prism"
+ 0x0003 -> "OpenFortress"
+ 0x0004 -> "Wewid"
+ 0x0005 -> "ZeitControl"
+ 0x0006 -> "Yubico"
+ 0x0007 -> "OpenKMS"
+ 0x0008 -> "LogoEmail"
+ 0x0009 -> "Fidesmo"
+ 0x000A -> "VivoKey"
+ 0x000B -> "Feitian Technologies"
+ 0x000D -> "Dangerous Things"
+ 0x000E -> "Excelsecu"
+ 0x000F -> "Nitrokey"
+ 0x002A -> "Magrathea"
+ 0x0042 -> "GnuPG e.V."
+ 0x1337 -> "Warsaw Hackerspace"
+ 0x2342 -> "warpzone"
+ 0x4354 -> "Confidential Technologies"
+ 0x5343 -> "SSE Carte à puce"
+ 0x5443 -> "TIF-IT e.V."
+ 0x63AF -> "Trustica"
+ 0xBA53 -> "c-base e.V."
+ 0xBD0E -> "Paranoidlabs"
+ 0xCA05 -> "Atos CardOS"
+ 0xF1D0 -> "CanoKeys"
+ 0xF517 -> "FSIJ"
+ 0xF5EC -> "F-Secure"
+ 0x0000, 0xFFFF -> "test card"
+ else -> "unknown"
+}
diff --git a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt
new file mode 100644
index 00000000..79f8cb06
--- /dev/null
+++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt
@@ -0,0 +1,26 @@
+package app.passwordstore.crypto
+
+import org.pgpainless.algorithm.PublicKeyAlgorithm
+import org.pgpainless.key.OpenPgpFingerprint
+
+public data class DeviceKeyInfo(
+ public val algorithm: PublicKeyAlgorithm,
+ public val fingerprint: OpenPgpFingerprint
+) {
+ override fun toString(): String = "${algorithm.displayName()} ${fingerprint.prettyPrint()}"
+}
+
+@Suppress("DEPRECATION")
+private fun PublicKeyAlgorithm.displayName(): String = when (this) {
+ PublicKeyAlgorithm.RSA_GENERAL -> "RSA"
+ PublicKeyAlgorithm.RSA_ENCRYPT -> "RSA (encrypt-only, deprecated)"
+ PublicKeyAlgorithm.RSA_SIGN -> "RSA (sign-only, deprecated)"
+ PublicKeyAlgorithm.ELGAMAL_ENCRYPT -> "ElGamal"
+ PublicKeyAlgorithm.DSA -> "DSA"
+ PublicKeyAlgorithm.EC -> "EC (deprecated)"
+ PublicKeyAlgorithm.ECDH -> "ECDH"
+ PublicKeyAlgorithm.ECDSA -> "ECDSA"
+ PublicKeyAlgorithm.ELGAMAL_GENERAL -> "ElGamal (general, deprecated)"
+ PublicKeyAlgorithm.DIFFIE_HELLMAN -> "Diffie-Hellman"
+ PublicKeyAlgorithm.EDDSA -> "EDDSA"
+}
diff --git a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt
new file mode 100644
index 00000000..b053cd92
--- /dev/null
+++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt
@@ -0,0 +1,46 @@
+package app.passwordstore.crypto
+
+import de.cotech.hw.openpgp.OpenPgpSecurityKey
+import de.cotech.hw.openpgp.internal.openpgp.EcKeyFormat
+import de.cotech.hw.openpgp.internal.openpgp.KeyFormat
+import de.cotech.hw.openpgp.internal.openpgp.RsaKeyFormat
+import org.pgpainless.algorithm.PublicKeyAlgorithm
+import org.pgpainless.key.OpenPgpFingerprint
+
+public class HWSecurityDevice(
+ public val id: DeviceIdentifier,
+ public val name: String,
+ public val encryptKeyInfo: DeviceKeyInfo?,
+ public val signKeyInfo: DeviceKeyInfo?,
+ public val authKeyInfo: DeviceKeyInfo?,
+)
+
+internal fun OpenPgpSecurityKey.toDevice(): HWSecurityDevice =
+ with (openPgpAppletConnection.openPgpCapabilities) {
+ HWSecurityDevice(
+ id = DeviceIdentifier(aid),
+ name = securityKeyName,
+ encryptKeyInfo = keyInfo(encryptKeyFormat, fingerprintEncrypt),
+ signKeyInfo = keyInfo(signKeyFormat, fingerprintSign),
+ authKeyInfo = keyInfo(authKeyFormat, fingerprintAuth)
+ )
+ }
+
+internal fun keyInfo(
+ format: KeyFormat?,
+ fingerprint: ByteArray?
+): DeviceKeyInfo? {
+ if (format == null || fingerprint == null) return null
+ return DeviceKeyInfo(format.toKeyAlgorithm(), OpenPgpFingerprint.parseFromBinary(fingerprint))
+}
+
+internal fun KeyFormat.toKeyAlgorithm(): PublicKeyAlgorithm = when (this) {
+ is RsaKeyFormat -> PublicKeyAlgorithm.RSA_GENERAL
+ is EcKeyFormat -> when (val id = algorithmId()) {
+ PublicKeyAlgorithm.ECDH.algorithmId -> PublicKeyAlgorithm.ECDH
+ PublicKeyAlgorithm.ECDSA.algorithmId -> PublicKeyAlgorithm.ECDSA
+ PublicKeyAlgorithm.EDDSA.algorithmId -> PublicKeyAlgorithm.EDDSA
+ else -> throw IllegalArgumentException("Unknown EC algorithm ID: $id")
+ }
+ else -> throw IllegalArgumentException("Unknown key format")
+}
diff --git a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt
new file mode 100644
index 00000000..f52c5265
--- /dev/null
+++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt
@@ -0,0 +1,52 @@
+package app.passwordstore.crypto
+
+import androidx.fragment.app.FragmentManager
+import app.passwordstore.crypto.errors.DeviceFingerprintMismatch
+import app.passwordstore.crypto.errors.DeviceHandlerException
+import app.passwordstore.crypto.errors.DeviceOperationFailed
+import com.github.michaelbull.result.Result
+import com.github.michaelbull.result.mapError
+import com.github.michaelbull.result.runCatching
+import org.bouncycastle.openpgp.PGPSessionKey
+
+public class HWSecurityDeviceHandler(
+ private val deviceManager: HWSecurityManager,
+ private val fragmentManager: FragmentManager,
+) : DeviceHandler<PGPKey, PGPEncryptedSessionKey, PGPSessionKey> {
+
+ override suspend fun pairWithPublicKey(
+ publicKey: PGPKey
+ ): Result<PGPKey, DeviceHandlerException> = runCatching {
+ val publicFingerprint = KeyUtils.tryGetEncryptionKeyFingerprint(publicKey)
+ ?: throw DeviceOperationFailed("Failed to get encryption key fingerprint")
+ val device = deviceManager.readDevice(fragmentManager)
+ if (publicFingerprint != device.encryptKeyInfo?.fingerprint) {
+ throw DeviceFingerprintMismatch(
+ publicFingerprint.toString(),
+ device.encryptKeyInfo?.fingerprint?.toString() ?: "Missing encryption key"
+ )
+ }
+ KeyUtils.tryCreateStubKey(
+ publicKey,
+ device.id.serialNumber,
+ listOfNotNull(
+ device.encryptKeyInfo.fingerprint,
+ device.signKeyInfo?.fingerprint,
+ device.authKeyInfo?.fingerprint
+ )
+ ) ?: throw DeviceOperationFailed("Failed to create stub secret key")
+ }.mapError { error ->
+ when (error) {
+ is DeviceHandlerException -> error
+ else -> DeviceOperationFailed("Failed to pair device", error)
+ }
+ }
+
+ override suspend fun decryptSessionKey(
+ encryptedSessionKey: PGPEncryptedSessionKey
+ ): Result<PGPSessionKey, DeviceHandlerException> = runCatching {
+ deviceManager.decryptSessionKey(fragmentManager, encryptedSessionKey)
+ }.mapError { error ->
+ DeviceOperationFailed("Failed to decrypt session key", error)
+ }
+}
diff --git a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt
new file mode 100644
index 00000000..a97a2e20
--- /dev/null
+++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt
@@ -0,0 +1,182 @@
+package app.passwordstore.crypto
+
+import android.app.Application
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import de.cotech.hw.SecurityKeyManager
+import de.cotech.hw.SecurityKeyManagerConfig
+import de.cotech.hw.openpgp.OpenPgpSecurityKey
+import de.cotech.hw.openpgp.OpenPgpSecurityKeyDialogFragment
+import de.cotech.hw.openpgp.internal.operations.PsoDecryptOp
+import de.cotech.hw.secrets.ByteSecret
+import de.cotech.hw.secrets.PinProvider
+import de.cotech.hw.ui.SecurityKeyDialogInterface
+import de.cotech.hw.ui.SecurityKeyDialogInterface.SecurityKeyDialogCallback
+import de.cotech.hw.ui.SecurityKeyDialogOptions
+import de.cotech.hw.ui.SecurityKeyDialogOptions.PinMode
+import java.io.IOException
+import javax.inject.Inject
+import javax.inject.Singleton
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.completeWith
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.bouncycastle.bcpg.ECDHPublicBCPGKey
+import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags
+import org.bouncycastle.openpgp.PGPSessionKey
+import org.pgpainless.algorithm.PublicKeyAlgorithm
+import org.pgpainless.decryption_verification.HardwareSecurity.HardwareSecurityException
+
+@Singleton
+public class HWSecurityManager @Inject constructor(
+ private val application: Application,
+) {
+
+ private val securityKeyManager: SecurityKeyManager by lazy {
+ SecurityKeyManager.getInstance()
+ }
+
+ public fun init(
+ enableLogging: Boolean = false
+ ) {
+ securityKeyManager.init(
+ application,
+ SecurityKeyManagerConfig.Builder()
+ .setEnableDebugLogging(enableLogging)
+ .build()
+ )
+ }
+
+ public fun isHardwareAvailable(): Boolean {
+ return securityKeyManager.isNfcHardwareAvailable || securityKeyManager.isUsbHostModeAvailable
+ }
+
+ private suspend fun <T : Any> withOpenDevice(
+ fragmentManager: FragmentManager,
+ pinMode: PinMode,
+ block: suspend (OpenPgpSecurityKey, PinProvider?) -> T
+ ): T = withContext(Dispatchers.Main) {
+ val fragment = OpenPgpSecurityKeyDialogFragment.newInstance(
+ SecurityKeyDialogOptions.builder()
+ .setPinMode(pinMode)
+ .setFormFactor(SecurityKeyDialogOptions.FormFactor.SECURITY_KEY)
+ .setPreventScreenshots(false) // TODO
+ .build()
+ )
+
+ val deferred = CompletableDeferred<T>()
+
+ fragment.setSecurityKeyDialogCallback(object : SecurityKeyDialogCallback<OpenPgpSecurityKey> {
+ private var result: Result<T> = Result.failure(CancellationException())
+
+ override fun onSecurityKeyDialogDiscovered(
+ dialogInterface: SecurityKeyDialogInterface,
+ securityKey: OpenPgpSecurityKey,
+ pinProvider: PinProvider?
+ ) {
+ fragment.lifecycleScope.launch {
+ fragment.repeatOnLifecycle(Lifecycle.State.CREATED) {
+ runCatching {
+ fragment.postProgressMessage("Decrypting password entry")
+ result = Result.success(block(securityKey, pinProvider))
+ fragment.successAndDismiss()
+ }.onFailure { e ->
+ when (e) {
+ is IOException -> fragment.postError(e)
+ else -> {
+ result = Result.failure(e)
+ fragment.dismiss()
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onSecurityKeyDialogCancel() {
+ deferred.cancel()
+ }
+
+ override fun onSecurityKeyDialogDismiss() {
+ deferred.completeWith(result)
+ }
+ })
+
+ fragment.show(fragmentManager)
+
+ val value = deferred.await()
+ // HWSecurity doesn't clean up fast enough for LeakCanary's liking.
+ securityKeyManager.clearConnectedSecurityKeys()
+ value
+ }
+
+ public suspend fun readDevice(
+ fragmentManager: FragmentManager
+ ): HWSecurityDevice = withOpenDevice(fragmentManager, PinMode.NO_PIN_INPUT) { securityKey, _ ->
+ securityKey.toDevice()
+ }
+
+ public suspend fun decryptSessionKey(
+ fragmentManager: FragmentManager,
+ encryptedSessionKey: PGPEncryptedSessionKey
+ ): PGPSessionKey = withOpenDevice(fragmentManager, PinMode.PIN_INPUT) { securityKey, pinProvider ->
+ val pin = pinProvider?.getPin(securityKey.openPgpInstanceAid)
+ ?: throw HWSecurityException("PIN required for decryption")
+
+ val contents = withContext(Dispatchers.IO) {
+ when (val a = encryptedSessionKey.algorithm) {
+ PublicKeyAlgorithm.RSA_GENERAL ->
+ decryptSessionKeyRsa(encryptedSessionKey, securityKey, pin)
+
+ PublicKeyAlgorithm.ECDH ->
+ decryptSessionKeyEcdh(encryptedSessionKey, securityKey, pin)
+
+ else -> throw HWSecurityException("Unsupported encryption algorithm: ${a.name}")
+ }
+ }
+
+ PGPSessionKey(encryptedSessionKey.algorithm.algorithmId, contents)
+ }
+}
+
+public class HWSecurityException(override val message: String) : HardwareSecurityException()
+
+private fun decryptSessionKeyRsa(
+ encryptedSessionKey: PGPEncryptedSessionKey,
+ securityKey: OpenPgpSecurityKey,
+ pin: ByteSecret,
+): ByteArray {
+ return PsoDecryptOp
+ .create(securityKey.openPgpAppletConnection)
+ .verifyAndDecryptSessionKey(pin, encryptedSessionKey.contents, 0, null)
+}
+
+@Suppress("MagicNumber")
+private fun decryptSessionKeyEcdh(
+ encryptedSessionKey: PGPEncryptedSessionKey,
+ securityKey: OpenPgpSecurityKey,
+ pin: ByteSecret,
+): ByteArray {
+ val key = encryptedSessionKey.publicKey.publicKeyPacket.key.run {
+ this as? ECDHPublicBCPGKey
+ ?: throw HWSecurityException("Expected ECDHPublicBCPGKey but got ${this::class.simpleName}")
+ }
+ val symmetricKeySize = when (val id = key.symmetricKeyAlgorithm.toInt()) {
+ SymmetricKeyAlgorithmTags.AES_128 -> 128
+ SymmetricKeyAlgorithmTags.AES_192 -> 192
+ SymmetricKeyAlgorithmTags.AES_256 -> 256
+ else -> throw HWSecurityException("Unexpected symmetric key algorithm: $id")
+ }
+ return PsoDecryptOp
+ .create(securityKey.openPgpAppletConnection)
+ .verifyAndDecryptSessionKey(
+ pin,
+ encryptedSessionKey.contents,
+ symmetricKeySize,
+ byteArrayOf()
+ )
+}
diff --git a/crypto-pgpainless/build.gradle.kts b/crypto-pgpainless/build.gradle.kts
index 93d56eff..bff2bcf2 100644
--- a/crypto-pgpainless/build.gradle.kts
+++ b/crypto-pgpainless/build.gradle.kts
@@ -15,7 +15,7 @@ dependencies {
implementation(libs.dagger.hilt.core)
implementation(libs.kotlin.coroutines.core)
implementation(libs.thirdparty.kotlinResult)
- implementation(libs.thirdparty.pgpainless)
+ api(libs.thirdparty.pgpainless)
testImplementation(libs.bundles.testDependencies)
testImplementation(libs.kotlin.coroutines.test)
testImplementation(libs.testing.testparameterinjector)
diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt
new file mode 100644
index 00000000..42055cb4
--- /dev/null
+++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt
@@ -0,0 +1,16 @@
+package app.passwordstore.crypto
+
+import org.bouncycastle.openpgp.PGPPublicKey
+import org.bouncycastle.openpgp.PGPSessionKey
+import org.pgpainless.algorithm.PublicKeyAlgorithm
+
+public class PGPEncryptedSessionKey(
+ public val publicKey: PGPPublicKey,
+ public val algorithm: PublicKeyAlgorithm,
+ public val contents: ByteArray
+)
+
+public fun PGPSessionKey(
+ algorithm: PublicKeyAlgorithm,
+ sessionKey: ByteArray
+): PGPSessionKey = PGPSessionKey(algorithm.algorithmId, sessionKey)
diff --git a/settings.gradle.kts b/settings.gradle.kts
index f547cffb..d0e44bc1 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -217,6 +217,8 @@ include("coroutine-utils-testing")
include("crypto-common")
+include("crypto-hwsecurity")
+
include("crypto-pgpainless")
include("format-common")