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