Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit fadc3a4

Browse files
tadfishermsfjarvis
authored andcommitted
Add crypto-hwsecurity library
1 parent e4f9f01 commit fadc3a4

File tree

13 files changed

+517
-4
lines changed

13 files changed

+517
-4
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package app.passwordstore.crypto
2+
3+
import app.passwordstore.crypto.errors.DeviceHandlerException
4+
import com.github.michaelbull.result.Result
5+
6+
public interface DeviceHandler<Key, EncryptedSessionKey, DecryptedSessionKey> {
7+
public suspend fun pairWithPublicKey(publicKey: Key): Result<Key, DeviceHandlerException>
8+
9+
public suspend fun decryptSessionKey(
10+
encryptedSessionKey: EncryptedSessionKey
11+
): Result<DecryptedSessionKey, DeviceHandlerException>
12+
}

crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ public sealed class CryptoException(message: String? = null, cause: Throwable? =
66
Exception(message, cause)
77

88
/** Sealed exception types for [KeyManager]. */
9-
public sealed class KeyManagerException(message: String? = null) : CryptoException(message)
9+
public sealed class KeyManagerException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause)
1010

1111
/** Store contains no keys. */
1212
public object NoKeysAvailableException : KeyManagerException("No keys were found")
@@ -19,8 +19,8 @@ public object KeyDirectoryUnavailableException :
1919
public object KeyDeletionFailedException : KeyManagerException("Couldn't delete the key file")
2020

2121
/** Failed to parse the key as a known type. */
22-
public object InvalidKeyException :
23-
KeyManagerException("Given key cannot be parsed as a known key type")
22+
public class InvalidKeyException(cause: Throwable? = null) :
23+
KeyManagerException("Given key cannot be parsed as a known key type", cause)
2424

2525
/** No key matching `keyId` could be found. */
2626
public class KeyNotFoundException(keyId: String) :
@@ -30,6 +30,9 @@ public class KeyNotFoundException(keyId: String) :
3030
public class KeyAlreadyExistsException(keyId: String) :
3131
KeyManagerException("Pre-existing key was found for $keyId")
3232

33+
public class NoSecretKeyException(keyId: String) :
34+
KeyManagerException("No secret keys found for $keyId")
35+
3336
/** Sealed exception types for [app.passwordstore.crypto.CryptoHandler]. */
3437
public sealed class CryptoHandlerException(message: String? = null, cause: Throwable? = null) :
3538
CryptoException(message, cause)
@@ -42,3 +45,33 @@ public class NoKeysProvided(message: String?) : CryptoHandlerException(message,
4245

4346
/** An unexpected error that cannot be mapped to a known type. */
4447
public class UnknownError(cause: Throwable) : CryptoHandlerException(null, cause)
48+
49+
public class KeySpecific(public val key: Any, cause: Throwable?) : CryptoHandlerException(key.toString(), cause)
50+
51+
/** Wrapper containing possibly multiple child exceptions via [suppressedExceptions]. */
52+
public class MultipleKeySpecific(
53+
message: String?,
54+
public val errors: List<KeySpecific>
55+
) : CryptoHandlerException(message) {
56+
init {
57+
for (error in errors) {
58+
addSuppressed(error)
59+
}
60+
}
61+
}
62+
63+
/** Sealed exception types for [app.passwordstore.crypto.DeviceHandler]. */
64+
public sealed class DeviceHandlerException(message: String? = null, cause: Throwable? = null) :
65+
CryptoHandlerException(message, cause)
66+
67+
/** The device crypto operation was canceled by the user. */
68+
public class DeviceOperationCanceled(message: String) : DeviceHandlerException(message, null)
69+
70+
/** The device crypto operation failed. */
71+
public class DeviceOperationFailed(message: String?, cause: Throwable? = null) : DeviceHandlerException(message, cause)
72+
73+
/** The device's key fingerprint doesn't match the fingerprint we are trying to pair it to. */
74+
public class DeviceFingerprintMismatch(
75+
public val publicFingerprint: String,
76+
public val deviceFingerprint: String,
77+
) : DeviceHandlerException()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
public final class app/passwordstore/crypto/DeviceIdentifier {
2+
public static final synthetic fun box-impl ([B)Lapp/passwordstore/crypto/DeviceIdentifier;
3+
public static fun constructor-impl ([B)[B
4+
public fun equals (Ljava/lang/Object;)Z
5+
public static fun equals-impl ([BLjava/lang/Object;)Z
6+
public static final fun equals-impl0 ([B[B)Z
7+
public static final fun getManufacturer-impl ([B)I
8+
public static final fun getOpenPgpVersion-impl ([B)Ljava/lang/String;
9+
public static final fun getSerialNumber-impl ([B)[B
10+
public fun hashCode ()I
11+
public static fun hashCode-impl ([B)I
12+
public fun toString ()Ljava/lang/String;
13+
public static fun toString-impl ([B)Ljava/lang/String;
14+
public final synthetic fun unbox-impl ()[B
15+
}
16+
17+
public final class app/passwordstore/crypto/DeviceIdentifierKt {
18+
public static final fun getManufacturerName-0zlKB64 ([B)Ljava/lang/String;
19+
}
20+
21+
public final class app/passwordstore/crypto/DeviceKeyInfo {
22+
public fun <init> (Lorg/pgpainless/algorithm/PublicKeyAlgorithm;Lorg/pgpainless/key/OpenPgpFingerprint;)V
23+
public final fun component1 ()Lorg/pgpainless/algorithm/PublicKeyAlgorithm;
24+
public final fun component2 ()Lorg/pgpainless/key/OpenPgpFingerprint;
25+
public final fun copy (Lorg/pgpainless/algorithm/PublicKeyAlgorithm;Lorg/pgpainless/key/OpenPgpFingerprint;)Lapp/passwordstore/crypto/DeviceKeyInfo;
26+
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;
27+
public fun equals (Ljava/lang/Object;)Z
28+
public final fun getAlgorithm ()Lorg/pgpainless/algorithm/PublicKeyAlgorithm;
29+
public final fun getFingerprint ()Lorg/pgpainless/key/OpenPgpFingerprint;
30+
public fun hashCode ()I
31+
public fun toString ()Ljava/lang/String;
32+
}
33+
34+
public final class app/passwordstore/crypto/HWSecurityDevice {
35+
public synthetic fun <init> ([BLjava/lang/String;Lapp/passwordstore/crypto/DeviceKeyInfo;Lapp/passwordstore/crypto/DeviceKeyInfo;Lapp/passwordstore/crypto/DeviceKeyInfo;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
36+
public final fun getAuthKeyInfo ()Lapp/passwordstore/crypto/DeviceKeyInfo;
37+
public final fun getEncryptKeyInfo ()Lapp/passwordstore/crypto/DeviceKeyInfo;
38+
public final fun getId-z5xZLwU ()[B
39+
public final fun getName ()Ljava/lang/String;
40+
public final fun getSignKeyInfo ()Lapp/passwordstore/crypto/DeviceKeyInfo;
41+
}
42+
43+
public final class app/passwordstore/crypto/HWSecurityDeviceHandler : app/passwordstore/crypto/DeviceHandler {
44+
public fun <init> (Lapp/passwordstore/crypto/HWSecurityManager;Landroidx/fragment/app/FragmentManager;)V
45+
public fun decryptSessionKey (Lapp/passwordstore/crypto/PGPEncryptedSessionKey;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
46+
public synthetic fun decryptSessionKey (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
47+
public synthetic fun pairWithPublicKey (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
48+
public fun pairWithPublicKey-P2gA-3I ([BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
49+
}
50+
51+
public final class app/passwordstore/crypto/HWSecurityException : org/pgpainless/decryption_verification/HardwareSecurity$HardwareSecurityException {
52+
public fun <init> (Ljava/lang/String;)V
53+
public fun getMessage ()Ljava/lang/String;
54+
}
55+
56+
public final class app/passwordstore/crypto/HWSecurityManager {
57+
public fun <init> (Landroid/app/Application;)V
58+
public final fun decryptSessionKey (Landroidx/fragment/app/FragmentManager;Lapp/passwordstore/crypto/PGPEncryptedSessionKey;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
59+
public final fun init (Z)V
60+
public static synthetic fun init$default (Lapp/passwordstore/crypto/HWSecurityManager;ZILjava/lang/Object;)V
61+
public final fun isHardwareAvailable ()Z
62+
public final fun readDevice (Landroidx/fragment/app/FragmentManager;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
63+
}
64+

crypto-hwsecurity/build.gradle.kts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
plugins {
6+
id("com.github.android-password-store.android-library")
7+
id("com.github.android-password-store.kotlin-android")
8+
id("com.github.android-password-store.kotlin-library")
9+
}
10+
11+
android {
12+
namespace = "app.passwordstore.crypto.hwsecurity"
13+
}
14+
15+
dependencies {
16+
implementation(projects.cryptoPgpainless)
17+
implementation(libs.androidx.activity.ktx)
18+
implementation(libs.androidx.annotation)
19+
implementation(libs.androidx.appcompat)
20+
implementation(libs.androidx.fragment.ktx)
21+
implementation(libs.androidx.material)
22+
implementation(libs.aps.hwsecurity.openpgp)
23+
implementation(libs.aps.hwsecurity.ui)
24+
implementation(libs.dagger.hilt.android)
25+
implementation(libs.kotlin.coroutines.android)
26+
implementation(libs.thirdparty.kotlinResult)
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<manifest />
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@file:Suppress("MagicNumber")
2+
package app.passwordstore.crypto
3+
4+
@JvmInline
5+
public value class DeviceIdentifier(
6+
private val aid: ByteArray
7+
) {
8+
init {
9+
require(aid.size == 16) { "Invalid device application identifier" }
10+
}
11+
12+
public val openPgpVersion: String get() = "${aid[6]}.${aid[7]}"
13+
14+
public val manufacturer: Int
15+
get() = ((aid[8].toInt() and 0xff) shl 8) or (aid[9].toInt() and 0xff)
16+
17+
public val serialNumber: ByteArray get() = aid.sliceArray(10..13)
18+
}
19+
20+
// https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob;f=scd/app-openpgp.c;hb=HEAD#l292
21+
public val DeviceIdentifier.manufacturerName: String get() = when (manufacturer) {
22+
0x0001 -> "PPC Card Systems"
23+
0x0002 -> "Prism"
24+
0x0003 -> "OpenFortress"
25+
0x0004 -> "Wewid"
26+
0x0005 -> "ZeitControl"
27+
0x0006 -> "Yubico"
28+
0x0007 -> "OpenKMS"
29+
0x0008 -> "LogoEmail"
30+
0x0009 -> "Fidesmo"
31+
0x000A -> "VivoKey"
32+
0x000B -> "Feitian Technologies"
33+
0x000D -> "Dangerous Things"
34+
0x000E -> "Excelsecu"
35+
0x000F -> "Nitrokey"
36+
0x002A -> "Magrathea"
37+
0x0042 -> "GnuPG e.V."
38+
0x1337 -> "Warsaw Hackerspace"
39+
0x2342 -> "warpzone"
40+
0x4354 -> "Confidential Technologies"
41+
0x5343 -> "SSE Carte à puce"
42+
0x5443 -> "TIF-IT e.V."
43+
0x63AF -> "Trustica"
44+
0xBA53 -> "c-base e.V."
45+
0xBD0E -> "Paranoidlabs"
46+
0xCA05 -> "Atos CardOS"
47+
0xF1D0 -> "CanoKeys"
48+
0xF517 -> "FSIJ"
49+
0xF5EC -> "F-Secure"
50+
0x0000, 0xFFFF -> "test card"
51+
else -> "unknown"
52+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package app.passwordstore.crypto
2+
3+
import org.pgpainless.algorithm.PublicKeyAlgorithm
4+
import org.pgpainless.key.OpenPgpFingerprint
5+
6+
public data class DeviceKeyInfo(
7+
public val algorithm: PublicKeyAlgorithm,
8+
public val fingerprint: OpenPgpFingerprint
9+
) {
10+
override fun toString(): String = "${algorithm.displayName()} ${fingerprint.prettyPrint()}"
11+
}
12+
13+
@Suppress("DEPRECATION")
14+
private fun PublicKeyAlgorithm.displayName(): String = when (this) {
15+
PublicKeyAlgorithm.RSA_GENERAL -> "RSA"
16+
PublicKeyAlgorithm.RSA_ENCRYPT -> "RSA (encrypt-only, deprecated)"
17+
PublicKeyAlgorithm.RSA_SIGN -> "RSA (sign-only, deprecated)"
18+
PublicKeyAlgorithm.ELGAMAL_ENCRYPT -> "ElGamal"
19+
PublicKeyAlgorithm.DSA -> "DSA"
20+
PublicKeyAlgorithm.EC -> "EC (deprecated)"
21+
PublicKeyAlgorithm.ECDH -> "ECDH"
22+
PublicKeyAlgorithm.ECDSA -> "ECDSA"
23+
PublicKeyAlgorithm.ELGAMAL_GENERAL -> "ElGamal (general, deprecated)"
24+
PublicKeyAlgorithm.DIFFIE_HELLMAN -> "Diffie-Hellman"
25+
PublicKeyAlgorithm.EDDSA -> "EDDSA"
26+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package app.passwordstore.crypto
2+
3+
import de.cotech.hw.openpgp.OpenPgpSecurityKey
4+
import de.cotech.hw.openpgp.internal.openpgp.EcKeyFormat
5+
import de.cotech.hw.openpgp.internal.openpgp.KeyFormat
6+
import de.cotech.hw.openpgp.internal.openpgp.RsaKeyFormat
7+
import org.pgpainless.algorithm.PublicKeyAlgorithm
8+
import org.pgpainless.key.OpenPgpFingerprint
9+
10+
public class HWSecurityDevice(
11+
public val id: DeviceIdentifier,
12+
public val name: String,
13+
public val encryptKeyInfo: DeviceKeyInfo?,
14+
public val signKeyInfo: DeviceKeyInfo?,
15+
public val authKeyInfo: DeviceKeyInfo?,
16+
)
17+
18+
internal fun OpenPgpSecurityKey.toDevice(): HWSecurityDevice =
19+
with (openPgpAppletConnection.openPgpCapabilities) {
20+
HWSecurityDevice(
21+
id = DeviceIdentifier(aid),
22+
name = securityKeyName,
23+
encryptKeyInfo = keyInfo(encryptKeyFormat, fingerprintEncrypt),
24+
signKeyInfo = keyInfo(signKeyFormat, fingerprintSign),
25+
authKeyInfo = keyInfo(authKeyFormat, fingerprintAuth)
26+
)
27+
}
28+
29+
internal fun keyInfo(
30+
format: KeyFormat?,
31+
fingerprint: ByteArray?
32+
): DeviceKeyInfo? {
33+
if (format == null || fingerprint == null) return null
34+
return DeviceKeyInfo(format.toKeyAlgorithm(), OpenPgpFingerprint.parseFromBinary(fingerprint))
35+
}
36+
37+
internal fun KeyFormat.toKeyAlgorithm(): PublicKeyAlgorithm = when (this) {
38+
is RsaKeyFormat -> PublicKeyAlgorithm.RSA_GENERAL
39+
is EcKeyFormat -> when (val id = algorithmId()) {
40+
PublicKeyAlgorithm.ECDH.algorithmId -> PublicKeyAlgorithm.ECDH
41+
PublicKeyAlgorithm.ECDSA.algorithmId -> PublicKeyAlgorithm.ECDSA
42+
PublicKeyAlgorithm.EDDSA.algorithmId -> PublicKeyAlgorithm.EDDSA
43+
else -> throw IllegalArgumentException("Unknown EC algorithm ID: $id")
44+
}
45+
else -> throw IllegalArgumentException("Unknown key format")
46+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package app.passwordstore.crypto
2+
3+
import androidx.fragment.app.FragmentManager
4+
import app.passwordstore.crypto.errors.DeviceFingerprintMismatch
5+
import app.passwordstore.crypto.errors.DeviceHandlerException
6+
import app.passwordstore.crypto.errors.DeviceOperationFailed
7+
import com.github.michaelbull.result.Result
8+
import com.github.michaelbull.result.mapError
9+
import com.github.michaelbull.result.runCatching
10+
import org.bouncycastle.openpgp.PGPSessionKey
11+
12+
public class HWSecurityDeviceHandler(
13+
private val deviceManager: HWSecurityManager,
14+
private val fragmentManager: FragmentManager,
15+
) : DeviceHandler<PGPKey, PGPEncryptedSessionKey, PGPSessionKey> {
16+
17+
override suspend fun pairWithPublicKey(
18+
publicKey: PGPKey
19+
): Result<PGPKey, DeviceHandlerException> = runCatching {
20+
val publicFingerprint = KeyUtils.tryGetEncryptionKeyFingerprint(publicKey)
21+
?: throw DeviceOperationFailed("Failed to get encryption key fingerprint")
22+
val device = deviceManager.readDevice(fragmentManager)
23+
if (publicFingerprint != device.encryptKeyInfo?.fingerprint) {
24+
throw DeviceFingerprintMismatch(
25+
publicFingerprint.toString(),
26+
device.encryptKeyInfo?.fingerprint?.toString() ?: "Missing encryption key"
27+
)
28+
}
29+
KeyUtils.tryCreateStubKey(
30+
publicKey,
31+
device.id.serialNumber,
32+
listOfNotNull(
33+
device.encryptKeyInfo.fingerprint,
34+
device.signKeyInfo?.fingerprint,
35+
device.authKeyInfo?.fingerprint
36+
)
37+
) ?: throw DeviceOperationFailed("Failed to create stub secret key")
38+
}.mapError { error ->
39+
when (error) {
40+
is DeviceHandlerException -> error
41+
else -> DeviceOperationFailed("Failed to pair device", error)
42+
}
43+
}
44+
45+
override suspend fun decryptSessionKey(
46+
encryptedSessionKey: PGPEncryptedSessionKey
47+
): Result<PGPSessionKey, DeviceHandlerException> = runCatching {
48+
deviceManager.decryptSessionKey(fragmentManager, encryptedSessionKey)
49+
}.mapError { error ->
50+
DeviceOperationFailed("Failed to decrypt session key", error)
51+
}
52+
}

0 commit comments

Comments
 (0)