From 39ac10a32c3c2f3671c5a5b32d8e19e357bc160f Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sat, 8 Oct 2022 17:17:55 -0700 Subject: [PATCH 1/8] Add hwsecurity dependency --- gradle/libs.versions.toml | 3 +++ settings.gradle.kts | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 13b6233e5a..9c5b32cd89 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ composeCompiler = "1.4.0-alpha02" coroutines = "1.6.4" flowbinding = "1.2.0" hilt = "2.44.2" +hwsecurity = "4.4.0" kotlin = "1.7.21" leakcanary = "2.10" lifecycle = "2.6.0-alpha03" @@ -34,6 +35,8 @@ androidx-recyclerview = "androidx.recyclerview:recyclerview:1.3.0-rc01" androidx-recyclerviewSelection = "androidx.recyclerview:recyclerview-selection:1.2.0-alpha01" androidx-security = "androidx.security:security-crypto:1.1.0-alpha03" androidx-swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" +aps-hwsecurity-openpgp = { module = "com.github.android-password-store.hwsecurity:hwsecurity-openpgp", version.ref = "hwsecurity" } +aps-hwsecurity-ui = { module = "com.github.android-password-store.hwsecurity:hwsecurity-ui", version.ref = "hwsecurity" } aps-sublimeFuzzy = "com.github.android-password-store:sublime-fuzzy:2.2.3" aps-zxingAndroidEmbedded = "com.github.android-password-store:zxing-android-embedded:4.2.1" build-agp = { module = "com.android.tools.build:gradle", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index e051f05315..4877d63d99 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -4,6 +4,8 @@ */ @file:Suppress("UnstableApiUsage") +import me.champeau.gradle.igp.gitRepositories + rootProject.name = "APS" // Plugin repositories @@ -41,6 +43,8 @@ pluginManagement { includeModule("com.github.ben-manes", "gradle-versions-plugin") includeModule("com.gradle", "gradle-enterprise-gradle-plugin") includeModule("com.gradle.enterprise", "com.gradle.enterprise.gradle.plugin") + includeModule("me.champeau.includegit", "me.champeau.includegit.gradle.plugin") + includeModule("me.champeau.gradle.includegit", "plugin") includeModule("me.tylerbwong.gradle.metalava", "plugin") } } @@ -52,7 +56,10 @@ pluginManagement { } } -plugins { id("com.gradle.enterprise") version "3.12" } +plugins { + id("com.gradle.enterprise") version "3.12" + id("me.champeau.includegit") version "0.1.5" +} gradleEnterprise { buildScan { @@ -159,6 +166,34 @@ dependencyResolutionManagement { } } +gitRepositories { + checkoutsDirectory.set(rootProject.projectDir.resolve("build/checkouts")) + include("hwsecurity") { + uri.set("https://github.com/tadfisher/hwsecurity.git") + branch.set("pendingintent-mutability") + includeBuild { + dependencySubstitution { + for (module in listOf( + "core", + "intent-usb", + "intent-nfc", + "provider", + "fido", + "fido2", + "openpgp", + "piv", + "sshj", + "ssh", + "ui", + )) { + substitute(module("com.github.android-password-store.hwsecurity:hwsecurity-$module")) + .using(project(":hwsecurity:$module")) + } + } + } + } +} + // Experimental features enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From e4f9f01b0485a9e289d5a08d7252c0d862b217db Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sat, 8 Oct 2022 17:18:58 -0700 Subject: [PATCH 2/8] Use pgpainless hardwareDecryption branch for testing --- gradle.properties | 3 +++ settings.gradle.kts | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/gradle.properties b/gradle.properties index c592e29bf0..55f81c5189 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,9 @@ org.gradle.vfs.watch=true # Enable experimental configuration caching org.gradle.unsafe.configuration-cache=true +# FIXME Turn cache errors into warnings; can be removed when no +# longer building dependencies from source +org.gradle.unsafe.configuration-cache-problems=warn # Enable Kotlin incremental compilation kotlin.incremental=true diff --git a/settings.gradle.kts b/settings.gradle.kts index 4877d63d99..b4482f9d75 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -168,6 +168,16 @@ dependencyResolutionManagement { gitRepositories { checkoutsDirectory.set(rootProject.projectDir.resolve("build/checkouts")) + include("pgpainless") { + uri.set("https://github.com/pgpainless/pgpainless.git") + branch.set("hardwareDecryption") + includeBuild { + dependencySubstitution { + substitute(module("org.pgpainless:pgpainless-core")) + .using(project(":pgpainless-core")) + } + } + } include("hwsecurity") { uri.set("https://github.com/tadfisher/hwsecurity.git") branch.set("pendingintent-mutability") From fadc3a47d01462cf70aa913ce8cd4b9e2c370f0f Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 9 Oct 2022 15:10:10 -0700 Subject: [PATCH 3/8] Add crypto-hwsecurity library --- .../app/passwordstore/crypto/DeviceHandler.kt | 12 ++ .../crypto/errors/CryptoException.kt | 39 +++- crypto-hwsecurity/api/crypto-hwsecurity.api | 64 ++++++ crypto-hwsecurity/build.gradle.kts | 27 +++ .../src/main/AndroidManifest.xml | 1 + .../passwordstore/crypto/DeviceIdentifier.kt | 52 +++++ .../app/passwordstore/crypto/DeviceKeyInfo.kt | 26 +++ .../passwordstore/crypto/HWSecurityDevice.kt | 46 +++++ .../crypto/HWSecurityDeviceHandler.kt | 52 +++++ .../passwordstore/crypto/HWSecurityManager.kt | 182 ++++++++++++++++++ crypto-pgpainless/build.gradle.kts | 2 +- .../app/passwordstore/crypto/PGPSessionKey.kt | 16 ++ settings.gradle.kts | 2 + 13 files changed, 517 insertions(+), 4 deletions(-) create mode 100644 crypto-common/src/main/kotlin/app/passwordstore/crypto/DeviceHandler.kt create mode 100644 crypto-hwsecurity/api/crypto-hwsecurity.api create mode 100644 crypto-hwsecurity/build.gradle.kts create mode 100644 crypto-hwsecurity/src/main/AndroidManifest.xml create mode 100644 crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt create mode 100644 crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt create mode 100644 crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt create mode 100644 crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt create mode 100644 crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt create mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt 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 0000000000..74eb0cfa50 --- /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 { + public suspend fun pairWithPublicKey(publicKey: Key): Result + + public suspend fun decryptSessionKey( + encryptedSessionKey: EncryptedSessionKey + ): Result +} 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 81bdf95f84..328a7a3227 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 +) : 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 0000000000..cba8a0ea0a --- /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 (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 ([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 (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 (Ljava/lang/String;)V + public fun getMessage ()Ljava/lang/String; +} + +public final class app/passwordstore/crypto/HWSecurityManager { + public fun (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 0000000000..6f9e4ebc36 --- /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 0000000000..cc947c5679 --- /dev/null +++ b/crypto-hwsecurity/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + 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 0000000000..be27f1398d --- /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 0000000000..79f8cb06b5 --- /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 0000000000..b053cd9220 --- /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 0000000000..f52c5265d8 --- /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 { + + override suspend fun pairWithPublicKey( + publicKey: PGPKey + ): Result = 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 = 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 0000000000..a97a2e20a0 --- /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 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() + + fragment.setSecurityKeyDialogCallback(object : SecurityKeyDialogCallback { + private var result: Result = 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 56c0c76219..8df98c7b4f 100644 --- a/crypto-pgpainless/build.gradle.kts +++ b/crypto-pgpainless/build.gradle.kts @@ -16,7 +16,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 0000000000..42055cb466 --- /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 b4482f9d75..14ac895869 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -220,6 +220,8 @@ include("coroutine-utils-testing") include("crypto-common") +include("crypto-hwsecurity") + include("crypto-pgpainless") include("format-common") From 4dda3c299add3ac446156780f9b3c019c8ae0126 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 9 Oct 2022 16:10:42 -0700 Subject: [PATCH 4/8] Add decryption callback to CryptoHandler --- app/build.gradle.kts | 1 + .../java/app/passwordstore/Application.kt | 14 +- .../data/crypto/CryptoRepository.kt | 9 +- .../injection/crypto/CryptoHandlerModule.kt | 21 +- .../app/passwordstore/crypto/CryptoHandler.kt | 9 +- .../crypto/errors/CryptoException.kt | 15 +- .../passwordstore/crypto/DeviceIdentifier.kt | 90 +++++---- .../app/passwordstore/crypto/DeviceKeyInfo.kt | 11 +- .../passwordstore/crypto/HWSecurityDevice.kt | 47 +++-- .../crypto/HWSecurityDeviceHandler.kt | 62 +++--- .../passwordstore/crypto/HWSecurityManager.kt | 191 +++++++++--------- .../app/passwordstore/crypto/KeyUtils.kt | 26 +++ .../app/passwordstore/crypto/PGPSessionKey.kt | 6 +- .../crypto/PGPainlessCryptoHandler.kt | 50 ++++- .../CachingPublicKeyDataDecryptorFactory.kt | 51 +++++ settings.gradle.kts | 30 +-- 16 files changed, 396 insertions(+), 237 deletions(-) create mode 100644 crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 62e1f8575b..d3c0fcbe4c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { coreLibraryDesugaring(libs.android.desugarJdkLibs) implementation(projects.autofillParser) implementation(projects.coroutineUtils) + implementation(projects.cryptoHwsecurity) implementation(projects.cryptoPgpainless) implementation(projects.formatCommonImpl) 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 35e1ae2252..8360687ce2 100644 --- a/app/src/main/java/app/passwordstore/Application.kt +++ b/app/src/main/java/app/passwordstore/Application.kt @@ -7,7 +7,12 @@ package app.passwordstore import android.content.SharedPreferences import android.os.Build import android.os.StrictMode -import androidx.appcompat.app.AppCompatDelegate.* +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 androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode +import app.passwordstore.crypto.HWSecurityManager import app.passwordstore.injection.context.FilesDirPath import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.util.extensions.getString @@ -42,16 +47,18 @@ 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 LeakCanary.config = LeakCanary.config.copy(eventListeners = LeakCanary.config.eventListeners + SentryLeakUploader) - if ( + + val enableLogging = BuildConfig.ENABLE_DEBUG_FEATURES || prefs.getBoolean(PreferenceKeys.ENABLE_DEBUG_LOGGING, false) - ) { + if (enableLogging) { LogcatLogger.install(AndroidLogcatLogger(DEBUG)) AppWatcher.manualInstall(this) setVmPolicy() @@ -62,6 +69,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 f3a82f39d6..5227bcb779 100644 --- a/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt +++ b/app/src/main/java/app/passwordstore/data/crypto/CryptoRepository.kt @@ -7,6 +7,7 @@ package app.passwordstore.data.crypto import android.content.SharedPreferences import app.passwordstore.crypto.GpgIdentifier +import app.passwordstore.crypto.HWSecurityDeviceHandler import app.passwordstore.crypto.PGPDecryptOptions import app.passwordstore.crypto.PGPEncryptOptions import app.passwordstore.crypto.PGPKeyManager @@ -16,11 +17,13 @@ import app.passwordstore.injection.prefs.SettingsPreferences import app.passwordstore.util.settings.PreferenceKeys 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 @@ -29,6 +32,7 @@ constructor( private val pgpKeyManager: PGPKeyManager, private val pgpCryptoHandler: PGPainlessCryptoHandler, @SettingsPreferences private val settings: SharedPreferences, + private val deviceHandler: HWSecurityDeviceHandler, ) { suspend fun decrypt( @@ -50,7 +54,10 @@ constructor( ): Result { val decryptionOptions = PGPDecryptOptions.Builder().build() val keys = pgpKeyManager.getAllKeys().unwrap() - return pgpCryptoHandler.decrypt(keys, password, message, out, decryptionOptions) + return pgpCryptoHandler.decrypt(keys, password, message, out, decryptionOptions) { + 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 5a863d8d88..aa06be881b 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,31 @@ 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 898cf0585b..afb8cbd0a6 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,13 @@ import java.io.InputStream import java.io.OutputStream /** Generic interface to implement cryptographic operations on top of. */ -public interface CryptoHandler { +public interface CryptoHandler< + Key, + EncOpts : CryptoOptions, + DecryptOpts : CryptoOptions, + EncryptedSessionKey, + DecryptedSessionKey, +> { /** * Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and @@ -25,6 +31,7 @@ public interface CryptoHandler DecryptedSessionKey, ): Result /** 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 328a7a3227..56d6b77031 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,8 @@ public sealed class CryptoException(message: String? = null, cause: Throwable? = Exception(message, cause) /** Sealed exception types for [KeyManager]. */ -public sealed class KeyManagerException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause) +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") @@ -46,13 +47,12 @@ 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) +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 -) : CryptoHandlerException(message) { +public class MultipleKeySpecific(message: String?, public val errors: List) : + CryptoHandlerException(message) { init { for (error in errors) { addSuppressed(error) @@ -68,7 +68,8 @@ public sealed class DeviceHandlerException(message: String? = null, cause: Throw public class DeviceOperationCanceled(message: String) : DeviceHandlerException(message, null) /** The device crypto operation failed. */ -public class DeviceOperationFailed(message: String?, cause: Throwable? = null) : DeviceHandlerException(message, cause) +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( diff --git a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt index be27f1398d..c3300b10b3 100644 --- a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt +++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt @@ -1,52 +1,56 @@ @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 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 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 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) - } + 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" -} +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 index 79f8cb06b5..c08b43f3ec 100644 --- a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt +++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt @@ -4,14 +4,15 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.key.OpenPgpFingerprint public data class DeviceKeyInfo( - public val algorithm: PublicKeyAlgorithm, - public val fingerprint: OpenPgpFingerprint + public val algorithm: PublicKeyAlgorithm, + public val fingerprint: OpenPgpFingerprint ) { - override fun toString(): String = "${algorithm.displayName()} ${fingerprint.prettyPrint()}" + override fun toString(): String = "${algorithm.displayName()} ${fingerprint.prettyPrint()}" } @Suppress("DEPRECATION") -private fun PublicKeyAlgorithm.displayName(): String = when (this) { +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)" @@ -23,4 +24,4 @@ private fun PublicKeyAlgorithm.displayName(): String = when (this) { 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 index b053cd9220..f647fcdc63 100644 --- a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt +++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt @@ -8,39 +8,38 @@ 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?, + 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) - ) - } + 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 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) { +internal fun KeyFormat.toKeyAlgorithm(): PublicKeyAlgorithm = + when (this) { is RsaKeyFormat -> PublicKeyAlgorithm.RSA_GENERAL - is EcKeyFormat -> when (val id = algorithmId()) { + 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 index f52c5265d8..9c4c06d88e 100644 --- a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt +++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt @@ -16,37 +16,39 @@ public class HWSecurityDeviceHandler( override suspend fun pairWithPublicKey( publicKey: PGPKey - ): Result = 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) - } - } + ): Result = + 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 = runCatching { - deviceManager.decryptSessionKey(fragmentManager, encryptedSessionKey) - }.mapError { error -> - DeviceOperationFailed("Failed to decrypt session key", error) - } + ): Result = + 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 index a97a2e20a0..f41ddb1d3b 100644 --- a/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt +++ b/crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt @@ -32,22 +32,18 @@ import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.decryption_verification.HardwareSecurity.HardwareSecurityException @Singleton -public class HWSecurityManager @Inject constructor( +public class HWSecurityManager +@Inject +constructor( private val application: Application, ) { - private val securityKeyManager: SecurityKeyManager by lazy { - SecurityKeyManager.getInstance() - } + private val securityKeyManager: SecurityKeyManager by lazy { SecurityKeyManager.getInstance() } - public fun init( - enableLogging: Boolean = false - ) { + public fun init(enableLogging: Boolean = false) { securityKeyManager.init( application, - SecurityKeyManagerConfig.Builder() - .setEnableDebugLogging(enableLogging) - .build() + SecurityKeyManagerConfig.Builder().setEnableDebugLogging(enableLogging).build() ) } @@ -59,88 +55,92 @@ public class HWSecurityManager @Inject constructor( 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() - - fragment.setSecurityKeyDialogCallback(object : SecurityKeyDialogCallback { - private var result: Result = 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() - } + ): T = + withContext(Dispatchers.Main) { + val fragment = + OpenPgpSecurityKeyDialogFragment.newInstance( + SecurityKeyDialogOptions.builder() + .setPinMode(pinMode) + .setFormFactor(SecurityKeyDialogOptions.FormFactor.SECURITY_KEY) + .setPreventScreenshots(false) // TODO + .build() + ) + + val deferred = CompletableDeferred() + + fragment.setSecurityKeyDialogCallback( + object : SecurityKeyDialogCallback { + private var result: Result = 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 onSecurityKeyDialogCancel() { + deferred.cancel() + } - override fun onSecurityKeyDialogDismiss() { - deferred.completeWith(result) - } - }) + override fun onSecurityKeyDialogDismiss() { + deferred.completeWith(result) + } + } + ) - fragment.show(fragmentManager) + fragment.show(fragmentManager) - val value = deferred.await() - // HWSecurity doesn't clean up fast enough for LeakCanary's liking. - securityKeyManager.clearConnectedSecurityKeys() - value - } + 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 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) + ): 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}") + } + } - else -> throw HWSecurityException("Unsupported encryption algorithm: ${a.name}") - } + PGPSessionKey(encryptedSessionKey.algorithm.algorithmId, contents) } - - PGPSessionKey(encryptedSessionKey.algorithm.algorithmId, contents) - } } public class HWSecurityException(override val message: String) : HardwareSecurityException() @@ -150,8 +150,7 @@ private fun decryptSessionKeyRsa( securityKey: OpenPgpSecurityKey, pin: ByteSecret, ): ByteArray { - return PsoDecryptOp - .create(securityKey.openPgpAppletConnection) + return PsoDecryptOp.create(securityKey.openPgpAppletConnection) .verifyAndDecryptSessionKey(pin, encryptedSessionKey.contents, 0, null) } @@ -161,22 +160,18 @@ private fun decryptSessionKeyEcdh( 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() - ) + 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/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt index 47c06c4ffb..d14879036c 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/PGPSessionKey.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt index 42055cb466..fd1ba4027b 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPSessionKey.kt @@ -10,7 +10,5 @@ public class PGPEncryptedSessionKey( public val contents: ByteArray ) -public fun PGPSessionKey( - algorithm: PublicKeyAlgorithm, - sessionKey: ByteArray -): PGPSessionKey = PGPSessionKey(algorithm.algorithmId, sessionKey) +public fun PGPSessionKey(algorithm: PublicKeyAlgorithm, sessionKey: ByteArray): PGPSessionKey = + PGPSessionKey(algorithm.algorithmId, sessionKey) 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 e1084decdc..e0b14367b6 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt @@ -15,20 +15,32 @@ 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.gnupg.GnuPGDummyKeyUtil import org.pgpainless.PGPainless +import org.pgpainless.algorithm.PublicKeyAlgorithm import org.pgpainless.decryption_verification.ConsumerOptions +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.SubkeyIdentifier import org.pgpainless.key.protection.SecretKeyRingProtector import org.pgpainless.util.Passphrase public class PGPainlessCryptoHandler @Inject constructor() : - CryptoHandler { + CryptoHandler< + PGPKey, + PGPEncryptOptions, + PGPDecryptOptions, + PGPEncryptedSessionKey, + PGPSessionKey, + > { public override fun decrypt( keys: List, @@ -36,6 +48,7 @@ public class PGPainlessCryptoHandler @Inject constructor() : ciphertextStream: InputStream, outputStream: OutputStream, options: PGPDecryptOptions, + onDecryptSessionKey: (PGPEncryptedSessionKey) -> PGPSessionKey, ): Result = runCatching { if (keys.isEmpty()) throw NoKeysProvided("No keys provided for encryption") @@ -44,18 +57,47 @@ public class PGPainlessCryptoHandler @Inject constructor() : .map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) } .run(::PGPSecretKeyRingCollection) val protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(passphrase)) + val hardwareBackedKeys = + keyringCollection.mapNotNull { keyring -> + keyring?.takeIf { + it.publicKey.keyID in + GnuPGDummyKeyUtil.getIdsOfKeysWithGnuPGS2KDivertedToCard(keyring) + .map(SubkeyIdentifier::getKeyId) + } + } PGPainless.decryptAndOrVerify() .onInputStream(ciphertextStream) .withOptions( - ConsumerOptions() - .addDecryptionKeys(keyringCollection, protector) - .addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) + ConsumerOptions().apply { + for (key in hardwareBackedKeys) { + addCustomDecryptorFactory( + CachingPublicKeyDataDecryptorFactory( + HardwareDataDecryptorFactory(SubkeyIdentifier(key)) { + _, + 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 0000000000..090020e773 --- /dev/null +++ b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/CachingPublicKeyDataDecryptorFactory.kt @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// 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 +import org.pgpainless.decryption_verification.CustomPublicKeyDataDecryptorFactory + +/** + * 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: CustomPublicKeyDataDecryptorFactory +) : CustomPublicKeyDataDecryptorFactory by factory { + + private val cachedSessionKeys: MutableMap = mutableMapOf() + + @Throws(PGPException::class) + override fun recoverSessionData(keyAlgorithm: Int, secKeyData: Array): ByteArray { + return cachedSessionKeys + .getOrPut(cacheKey(secKeyData)) { factory.recoverSessionData(keyAlgorithm, secKeyData) } + .copy() + } + + public fun clear() { + cachedSessionKeys.clear() + } + + private companion object { + fun cacheKey(secKeyData: Array): 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 + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 14ac895869..c6d8dfb31f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -173,8 +173,7 @@ gitRepositories { branch.set("hardwareDecryption") includeBuild { dependencySubstitution { - substitute(module("org.pgpainless:pgpainless-core")) - .using(project(":pgpainless-core")) + substitute(module("org.pgpainless:pgpainless-core")).using(project(":pgpainless-core")) } } } @@ -183,19 +182,20 @@ gitRepositories { branch.set("pendingintent-mutability") includeBuild { dependencySubstitution { - for (module in listOf( - "core", - "intent-usb", - "intent-nfc", - "provider", - "fido", - "fido2", - "openpgp", - "piv", - "sshj", - "ssh", - "ui", - )) { + for (module in + listOf( + "core", + "intent-usb", + "intent-nfc", + "provider", + "fido", + "fido2", + "openpgp", + "piv", + "sshj", + "ssh", + "ui", + )) { substitute(module("com.github.android-password-store.hwsecurity:hwsecurity-$module")) .using(project(":hwsecurity:$module")) } From 751272bfceba5462a6ca1804eeb4521f45cd5b32 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 9 Oct 2022 16:11:28 -0700 Subject: [PATCH 5/8] Quick and dirty hardware key import --- .../ui/pgp/PGPKeyImportActivity.kt | 67 +++++++++----- app/src/main/res/values/strings.xml | 3 + .../app/passwordstore/crypto/KeyUtils.kt | 90 +++++++++++++++++++ .../app/passwordstore/crypto/PGPKeyManager.kt | 12 ++- .../org/bouncycastle/bcpg/GnuExtendedS2K.kt | 16 ++++ 5 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt diff --git a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt index 0b3fdd4e6a..b8f7100f04 100644 --- a/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt +++ b/app/src/main/java/app/passwordstore/ui/pgp/PGPKeyImportActivity.kt @@ -7,20 +7,26 @@ package app.passwordstore.ui.pgp import android.os.Bundle +import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts.OpenDocument import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import app.passwordstore.R +import app.passwordstore.crypto.HWSecurityDeviceHandler import app.passwordstore.crypto.KeyUtils.tryGetId import app.passwordstore.crypto.PGPKey import app.passwordstore.crypto.PGPKeyManager import app.passwordstore.crypto.errors.KeyAlreadyExistsException +import app.passwordstore.crypto.errors.NoSecretKeyException import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result +import com.github.michaelbull.result.getOrThrow import com.github.michaelbull.result.runCatching import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @AndroidEntryPoint @@ -32,9 +38,10 @@ class PGPKeyImportActivity : AppCompatActivity() { */ private var lastBytes: ByteArray? = null @Inject lateinit var keyManager: PGPKeyManager + @Inject lateinit var deviceHandler: HWSecurityDeviceHandler private val pgpKeyImportAction = - registerForActivityResult(OpenDocument()) { uri -> + (this as ComponentActivity).registerForActivityResult(OpenDocument()) { uri -> runCatching { if (uri == null) { return@runCatching null @@ -50,6 +57,7 @@ class PGPKeyImportActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + pgpKeyImportAction.launch(arrayOf("*/*")) } @@ -68,6 +76,17 @@ class PGPKeyImportActivity : AppCompatActivity() { return key } + private fun pairDevice(bytes: ByteArray) { + lifecycleScope.launch { + val result = + keyManager.addKey( + deviceHandler.pairWithPublicKey(PGPKey(bytes)).getOrThrow(), + replace = true + ) + handleImportResult(result) + } + } + private fun handleImportResult(result: Result) { when (result) { is Ok -> { @@ -89,26 +108,34 @@ class PGPKeyImportActivity : AppCompatActivity() { .setCancelable(false) .show() } - is Err -> { - if (result.error is KeyAlreadyExistsException && lastBytes != null) { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.pgp_key_import_failed)) - .setMessage(getString(R.string.pgp_key_import_failed_replace_message)) - .setPositiveButton(R.string.dialog_yes) { _, _ -> - handleImportResult(runCatching { importKey(lastBytes!!, replace = true) }) - } - .setNegativeButton(R.string.dialog_no) { _, _ -> finish() } - .setCancelable(false) - .show() - } else { - MaterialAlertDialogBuilder(this) - .setTitle(getString(R.string.pgp_key_import_failed)) - .setMessage(result.error.message) - .setPositiveButton(android.R.string.ok) { _, _ -> finish() } - .setCancelable(false) - .show() + is Err -> + when { + result.error is KeyAlreadyExistsException && lastBytes != null -> + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.pgp_key_import_failed)) + .setMessage(getString(R.string.pgp_key_import_failed_replace_message)) + .setPositiveButton(R.string.dialog_yes) { _, _ -> + handleImportResult(runCatching { importKey(lastBytes!!, replace = true) }) + } + .setNegativeButton(R.string.dialog_no) { _, _ -> finish() } + .setCancelable(false) + .show() + result.error is NoSecretKeyException && lastBytes != null -> + MaterialAlertDialogBuilder(this) + .setTitle(R.string.pgp_key_import_failed_no_secret) + .setMessage(R.string.pgp_key_import_failed_no_secret_message) + .setPositiveButton(R.string.dialog_yes) { _, _ -> pairDevice(lastBytes!!) } + .setNegativeButton(R.string.dialog_no) { _, _ -> finish() } + .setCancelable(false) + .show() + else -> + MaterialAlertDialogBuilder(this) + .setTitle(getString(R.string.pgp_key_import_failed)) + .setMessage(result.error.message + "\n" + result.error.stackTraceToString()) + .setPositiveButton(android.R.string.ok) { _, _ -> finish() } + .setCancelable(false) + .show() } - } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3840c0bc64..8ac284d10e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -334,6 +334,7 @@ Select\nGPG Key Select a GPG key to initialize your store with Select key + Pair hardware key Potentially incorrect URL @@ -360,6 +361,8 @@ Create new password or folder Failed to import PGP key An existing key with this ID was found, do you want to replace it? + No secret PGP key + This is a public key. Would you like to pair a hardware security device? Successfully imported PGP key The key ID of the imported key is given below, please review it for correctness:\n%1$s PGP settings 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 d14879036c..640c5e418f 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt @@ -9,7 +9,15 @@ import app.passwordstore.crypto.GpgIdentifier.KeyId import app.passwordstore.crypto.GpgIdentifier.UserId import com.github.michaelbull.result.get import com.github.michaelbull.result.runCatching +import java.io.ByteArrayOutputStream +import org.bouncycastle.bcpg.GnuExtendedS2K +import org.bouncycastle.bcpg.S2K +import org.bouncycastle.bcpg.SecretKeyPacket +import org.bouncycastle.bcpg.SecretSubkeyPacket +import org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags import org.bouncycastle.openpgp.PGPKeyRing +import org.bouncycastle.openpgp.PGPPublicKey +import org.bouncycastle.openpgp.PGPPublicKeyRing import org.bouncycastle.openpgp.PGPSecretKey import org.bouncycastle.openpgp.PGPSecretKeyRing import org.pgpainless.algorithm.EncryptionPurpose @@ -37,6 +45,28 @@ public object KeyUtils { val keyRing = tryParseKeyring(key) ?: return null return UserId(keyRing.publicKey.userIDs.next()) } + + public fun tryCreateStubKey( + publicKey: PGPKey, + serial: ByteArray, + stubFingerprints: List + ): PGPKey? { + val keyRing = tryParseKeyring(publicKey) as? PGPPublicKeyRing ?: return null + val secretKeyRing = + keyRing.fold(PGPSecretKeyRing(emptyList())) { ring, key -> + PGPSecretKeyRing.insertSecretKey( + ring, + if (stubFingerprints.any { it == OpenPgpFingerprint.parseFromBinary(key.fingerprint) }) { + toCardSecretKey(key, serial) + } else { + toDummySecretKey(key) + } + ) + } + + return PGPKey(secretKeyRing.encoded) + } + public fun tryGetEncryptionKeyFingerprint(key: PGPKey): OpenPgpFingerprint? { val keyRing = tryParseKeyring(key) ?: return null val encryptionSubkey = @@ -59,3 +89,63 @@ public object KeyUtils { return info.getSecretKey(encryptionKey.keyID) } } + +private fun toDummySecretKey(publicKey: PGPPublicKey): PGPSecretKey { + + return PGPSecretKey( + if (publicKey.isMasterKey) { + SecretKeyPacket( + publicKey.publicKeyPacket, + SymmetricKeyAlgorithmTags.NULL, + SecretKeyPacket.USAGE_CHECKSUM, + GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + byteArrayOf(), + byteArrayOf() + ) + } else { + SecretSubkeyPacket( + publicKey.publicKeyPacket, + SymmetricKeyAlgorithmTags.NULL, + SecretKeyPacket.USAGE_CHECKSUM, + GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_NO_PRIVATE_KEY), + byteArrayOf(), + byteArrayOf() + ) + }, + publicKey + ) +} + +@Suppress("MagicNumber") +private fun toCardSecretKey(publicKey: PGPPublicKey, serial: ByteArray): PGPSecretKey { + return PGPSecretKey( + if (publicKey.isMasterKey) { + SecretKeyPacket( + publicKey.publicKeyPacket, + SymmetricKeyAlgorithmTags.NULL, + SecretKeyPacket.USAGE_CHECKSUM, + GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ByteArray(8), + encodeSerial(serial), + ) + } else { + SecretSubkeyPacket( + publicKey.publicKeyPacket, + SymmetricKeyAlgorithmTags.NULL, + SecretKeyPacket.USAGE_CHECKSUM, + GnuExtendedS2K(S2K.GNU_PROTECTION_MODE_DIVERT_TO_CARD), + ByteArray(8), + encodeSerial(serial), + ) + }, + publicKey + ) +} + +@Suppress("MagicNumber") +private fun encodeSerial(serial: ByteArray): ByteArray { + val out = ByteArrayOutputStream() + out.write(serial.size) + out.write(serial, 0, minOf(16, serial.size)) + return out.toByteArray() +} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt index be2ec47417..49a3d3194a 100644 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt +++ b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt @@ -9,12 +9,12 @@ package app.passwordstore.crypto import androidx.annotation.VisibleForTesting import app.passwordstore.crypto.KeyUtils.tryGetId import app.passwordstore.crypto.KeyUtils.tryParseKeyring -import app.passwordstore.crypto.errors.InvalidKeyException import app.passwordstore.crypto.errors.KeyAlreadyExistsException import app.passwordstore.crypto.errors.KeyDeletionFailedException import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException import app.passwordstore.crypto.errors.KeyNotFoundException import app.passwordstore.crypto.errors.NoKeysAvailableException +import app.passwordstore.crypto.errors.NoSecretKeyException import app.passwordstore.util.coroutines.runSuspendCatching import com.github.michaelbull.result.Result import com.github.michaelbull.result.unwrap @@ -40,12 +40,16 @@ constructor( withContext(dispatcher) { runSuspendCatching { if (!keyDirExists()) throw KeyDirectoryUnavailableException - val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException + val incomingKeyRing = tryParseKeyring(key) + + if (incomingKeyRing is PGPPublicKeyRing) { + throw NoSecretKeyException(tryGetId(key)?.toString() ?: "Failed to retrieve key ID") + } + val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") if (keyFile.exists()) { val existingKeyBytes = keyFile.readBytes() - val existingKeyRing = - tryParseKeyring(PGPKey(existingKeyBytes)) ?: throw InvalidKeyException + val existingKeyRing = tryParseKeyring(PGPKey(existingKeyBytes)) when { existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPSecretKeyRing -> { keyFile.writeBytes(key.contents) diff --git a/crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt new file mode 100644 index 0000000000..234ca5c9f8 --- /dev/null +++ b/crypto-pgpainless/src/main/kotlin/org/bouncycastle/bcpg/GnuExtendedS2K.kt @@ -0,0 +1,16 @@ +package org.bouncycastle.bcpg + +/** + * Add a constructor for GNU-extended S2K + * + * This extension is documented on GnuPG documentation DETAILS file, section "GNU extensions to the + * S2K algorithm". Its support is already present in S2K class but lack for a constructor. + * + * @author Léonard Dallot + */ +public class GnuExtendedS2K(mode: Int) : S2K(SIMPLE) { + init { + this.type = GNU_DUMMY_S2K + this.protectionMode = mode + } +} From a3edd32aaf9f04cbf17468ff4f163bc129b80936 Mon Sep 17 00:00:00 2001 From: Tad Fisher Date: Sun, 9 Oct 2022 16:12:12 -0700 Subject: [PATCH 6/8] Fix crashes when inserting USB key while dialog is showing --- app/src/main/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e187240d8c..0a5192e287 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -50,6 +50,7 @@ Date: Mon, 19 Dec 2022 10:34:18 +0530 Subject: [PATCH 7/8] feat: un-fork hwsecurity and pgpainless --- settings.gradle.kts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index c6d8dfb31f..00976ee4d4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -168,18 +168,9 @@ dependencyResolutionManagement { gitRepositories { checkoutsDirectory.set(rootProject.projectDir.resolve("build/checkouts")) - include("pgpainless") { - uri.set("https://github.com/pgpainless/pgpainless.git") - branch.set("hardwareDecryption") - includeBuild { - dependencySubstitution { - substitute(module("org.pgpainless:pgpainless-core")).using(project(":pgpainless-core")) - } - } - } include("hwsecurity") { - uri.set("https://github.com/tadfisher/hwsecurity.git") - branch.set("pendingintent-mutability") + uri.set("https://github.com/android-password-store/hwsecurity.git") + branch.set("main") includeBuild { dependencySubstitution { for (module in From 247670e58f482d1aa901afe591b82ff4187c556c Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Mon, 19 Dec 2022 11:35:17 +0530 Subject: [PATCH 8/8] chore: refresh Detekt baseline --- detekt-baselines/app.xml | 1 - detekt-baselines/crypto-common.xml | 7 +++++++ detekt-baselines/crypto-pgpainless.xml | 6 ++++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 detekt-baselines/crypto-common.xml diff --git a/detekt-baselines/app.xml b/detekt-baselines/app.xml index 2b6993f817..c4269062cd 100644 --- a/detekt-baselines/app.xml +++ b/detekt-baselines/app.xml @@ -96,6 +96,5 @@ UseCheckOrError:SshKey.kt$SshKey$throw IllegalStateException("SSH key does not exist in Keystore") UseCheckOrError:SshKeyGenActivity.kt$SshKeyGenActivity$throw IllegalStateException("Impossible key type selection") UtilityClassWithPublicConstructor:AutofillMatcher.kt$AutofillMatcher - WildcardImport:Application.kt$import androidx.appcompat.app.AppCompatDelegate.* diff --git a/detekt-baselines/crypto-common.xml b/detekt-baselines/crypto-common.xml new file mode 100644 index 0000000000..d0f2e97fff --- /dev/null +++ b/detekt-baselines/crypto-common.xml @@ -0,0 +1,7 @@ + + + + + LongParameterList:CryptoHandler.kt$CryptoHandler$( keys: List<Key>, passphrase: String, ciphertextStream: InputStream, outputStream: OutputStream, options: DecryptOpts, onDecryptSessionKey: (EncryptedSessionKey) -> DecryptedSessionKey, ) + + diff --git a/detekt-baselines/crypto-pgpainless.xml b/detekt-baselines/crypto-pgpainless.xml index c3e6912dc8..f9db469512 100644 --- a/detekt-baselines/crypto-pgpainless.xml +++ b/detekt-baselines/crypto-pgpainless.xml @@ -1,7 +1,9 @@ - + - + + ForbiddenComment:CachingPublicKeyDataDecryptorFactory.kt$CachingPublicKeyDataDecryptorFactory$* 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? ForbiddenComment:PGPKeyManager.kt$PGPKeyManager$// TODO: This is a temp hack for now and in future it should check that the GPGKeyManager can + MatchingDeclarationName:PGPSessionKey.kt$PGPEncryptedSessionKey