aboutsummaryrefslogtreecommitdiff
path: root/crypto-hwsecurity/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'crypto-hwsecurity/src/main')
-rw-r--r--crypto-hwsecurity/src/main/AndroidManifest.xml1
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceIdentifier.kt52
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/DeviceKeyInfo.kt26
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDevice.kt46
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityDeviceHandler.kt52
-rw-r--r--crypto-hwsecurity/src/main/kotlin/app/passwordstore/crypto/HWSecurityManager.kt182
6 files changed, 359 insertions, 0 deletions
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()
+ )
+}