mirror of
https://github.com/android-password-store/Android-Password-Store.git
synced 2026-02-22 18:31:50 +01:00
Add crypto-hwsecurity library
This commit is contained in:
parent
a244a0f3b8
commit
4b7457c7f7
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
64
crypto-hwsecurity/api/crypto-hwsecurity.api
Normal file
64
crypto-hwsecurity/api/crypto-hwsecurity.api
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
27
crypto-hwsecurity/build.gradle.kts
Normal file
27
crypto-hwsecurity/build.gradle.kts
Normal file
|
|
@ -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)
|
||||
}
|
||||
1
crypto-hwsecurity/src/main/AndroidManifest.xml
Normal file
1
crypto-hwsecurity/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<manifest />
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -217,6 +217,8 @@ include("coroutine-utils-testing")
|
|||
|
||||
include("crypto-common")
|
||||
|
||||
include("crypto-hwsecurity")
|
||||
|
||||
include("crypto-pgpainless")
|
||||
|
||||
include("format-common")
|
||||
|
|
|
|||
Loading…
Reference in a new issue