diff options
Diffstat (limited to 'crypto-hwsecurity')
8 files changed, 450 insertions, 0 deletions
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() + ) +} |