11package id.walt.crypto
22
3+ import android.os.Build
4+ import android.security.keystore.KeyPermanentlyInvalidatedException
5+ import android.security.keystore.KeyProperties
6+ import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
7+ import androidx.fragment.app.FragmentActivity
38import at.asitplus.signum.indispensable.josef.io.joseCompliantSerializer
49import at.asitplus.signum.indispensable.josef.toJsonWebKey
510import at.asitplus.signum.supreme.SignatureResult
11+ import at.asitplus.signum.supreme.os.AndroidKeystoreSigner
612import at.asitplus.signum.supreme.os.AndroidKeyStoreProvider
13+ import at.asitplus.signum.supreme.os.needsAuthentication
14+ import at.asitplus.signum.supreme.os.needsAuthenticationForEveryUse
715import dev.whyoleg.cryptography.CryptographyProvider
816import dev.whyoleg.cryptography.providers.jdk.JDK
917import id.walt.crypto.keys.JwkKeyMeta
1018import id.walt.crypto.keys.Key
19+ import id.walt.crypto.keys.KeyHardwareBacking
1120import id.walt.crypto.keys.KeyMeta
1221import id.walt.crypto.keys.KeyType
22+ import id.walt.crypto.keys.KeyUseAuthorizationAware
23+ import id.walt.crypto.keys.KeyUseAuthorizationException
24+ import id.walt.crypto.keys.KeyUseAuthorizationFailure
25+ import id.walt.crypto.keys.KeyUseAuthorizationPolicy
26+ import id.walt.crypto.keys.KeyUseAuthorizationPrompt
1327import kotlinx.serialization.json.Json
1428import kotlinx.serialization.json.JsonElement
1529import kotlinx.serialization.json.JsonObject
1630import org.bouncycastle.jce.provider.BouncyCastleProvider
1731import kotlin.io.encoding.Base64
32+ import kotlin.time.Duration
1833import kotlin.uuid.Uuid
1934
2035sealed class AndroidKey : Key () {
2136
2237 class Options (
2338 val kid : String = Uuid .random().toString(),
2439 val keyType : KeyType ,
25- )
40+ val keyUseAuthorizationPolicy : KeyUseAuthorizationPolicy = KeyUseAuthorizationPolicy .None ,
41+ val authorizationPrompt : KeyUseAuthorizationPrompt = KeyUseAuthorizationPrompt (),
42+ interactionContext : Any? = null ,
43+ ) {
44+ internal val interactionContext: FragmentActivity ? = interactionContext as ? FragmentActivity
45+ }
2646
2747 class Platform internal constructor(
2848 private val options : Options ,
2949 override val hasPrivateKey : Boolean = true ,
30- ) : AndroidKey() {
50+ ) : AndroidKey(), KeyUseAuthorizationAware {
3151
3252 companion object {
3353 suspend fun create (options : Options ): Platform {
54+ options.requireSupportedProtectedCombination()
3455 when (val curve = options.keyType.toPlatformKeyStoreCurve()) {
3556 null -> AndroidKeyStoreProvider .createSigningKey(options.kid) {
3657 rsa { }
58+ if (options.keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .BiometricCurrentSet ) {
59+ configureBiometricCurrentSet(options)
60+ }
3761 }.getOrThrow()
3862 else -> AndroidKeyStoreProvider .createSigningKey(options.kid) {
3963 ec { this .curve = curve }
64+ if (options.keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .BiometricCurrentSet ) {
65+ configureBiometricCurrentSet(options)
66+ }
4067 }.getOrThrow()
4168 }
4269 return Platform (options)
4370 }
4471
4572 suspend fun load (options : Options ): Platform {
46- AndroidKeyStoreProvider .getSignerForKey(options.kid).getOrThrow()
73+ options.requireSupportedProtectedCombination()
74+ runCatching { options.loadSigner() }
75+ .getOrElse { throw options.mapPlatformFailure(it) }
4776 return Platform (options)
4877 }
4978
@@ -53,8 +82,23 @@ sealed class AndroidKey : Key() {
5382 }
5483
5584 override val keyType get() = options.keyType
56-
57- private suspend fun signer () = AndroidKeyStoreProvider .getSignerForKey(options.kid).getOrThrow()
85+ override val keyUseAuthorizationPolicy get() = options.keyUseAuthorizationPolicy
86+ override val isPlatformBacked: Boolean = true
87+
88+ private suspend fun signer (): AndroidKeystoreSigner = runCatching { options.loadSigner() }
89+ .getOrElse { throw options.mapPlatformFailure(it) }
90+
91+ private fun requireSigningInteractionContext () {
92+ if (
93+ options.keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .BiometricCurrentSet &&
94+ options.interactionContext == null
95+ ) {
96+ throw KeyUseAuthorizationException (
97+ failure = KeyUseAuthorizationFailure .InteractionContextUnavailable ,
98+ message = " A FragmentActivity interaction context is required for biometric key use" ,
99+ )
100+ }
101+ }
58102
59103 override suspend fun getKeyId (): String = options.kid
60104
@@ -76,15 +120,24 @@ sealed class AndroidKey : Key() {
76120
77121 override suspend fun signRaw (plaintext : ByteArray , customSignatureAlgorithm : String? ): ByteArray {
78122 check(hasPrivateKey) { " Only private key can do signing." }
79- val result = signer().sign(plaintext)
80- check(result is SignatureResult .Success ) { " Signing failed: $result " }
81- return result.signature.rawByteArray
123+ requireSigningInteractionContext()
124+ return signer().sign(plaintext).mapPlatformFailure(options).signatureBytesOrThrow(
125+ protectedKeyUse = options.keyUseAuthorizationPolicy != KeyUseAuthorizationPolicy .None ,
126+ )
82127 }
83128
84129 override suspend fun signJws (plaintext : ByteArray , headers : Map <String , JsonElement >): String {
85130 check(hasPrivateKey) { " Only private key can do signing." }
131+ requireSigningInteractionContext()
86132 val signer = signer()
87- return signJwsWithPlatformSigner(options.keyType, plaintext, headers) { data -> signer.sign(data) }
133+ return signJwsWithPlatformSigner(
134+ options.keyType,
135+ plaintext,
136+ headers,
137+ protectedKeyUse = options.keyUseAuthorizationPolicy != KeyUseAuthorizationPolicy .None ,
138+ ) { data ->
139+ signer.sign(data).mapPlatformFailure(options)
140+ }
88141 }
89142
90143 override suspend fun verifyRaw (
@@ -102,6 +155,23 @@ sealed class AndroidKey : Key() {
102155 override suspend fun getPublicKey (): Key = Platform (options, hasPrivateKey = false )
103156 override suspend fun getPublicKeyRepresentation (): ByteArray = signer().publicKey.encodeToTlv().derEncoded
104157 override suspend fun getMeta (): KeyMeta = JwkKeyMeta (getKeyId())
158+ @Suppress(" DEPRECATION" )
159+ override suspend fun effectiveHardwareBacking (): KeyHardwareBacking {
160+ val keyInfo = signer().keyInfo
161+ return if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .S ) {
162+ when (keyInfo.securityLevel) {
163+ KeyProperties .SECURITY_LEVEL_SOFTWARE -> KeyHardwareBacking .Software
164+ KeyProperties .SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> KeyHardwareBacking .TrustedEnvironment
165+ KeyProperties .SECURITY_LEVEL_STRONGBOX -> KeyHardwareBacking .StrongBox
166+ KeyProperties .SECURITY_LEVEL_UNKNOWN_SECURE -> KeyHardwareBacking .SecureHardware
167+ else -> KeyHardwareBacking .Unknown
168+ }
169+ } else if (keyInfo.isInsideSecureHardware) {
170+ KeyHardwareBacking .SecureHardware
171+ } else {
172+ KeyHardwareBacking .Software
173+ }
174+ }
105175 override suspend fun deleteKey (): Boolean = runCatching {
106176 AndroidKeyStoreProvider .deleteSigningKey(options.kid).getOrThrow()
107177 }.isSuccess
@@ -159,3 +229,93 @@ sealed class AndroidKey : Key() {
159229 override suspend fun deleteKey (): Boolean = delegate.deleteKey()
160230 }
161231}
232+
233+ private fun at.asitplus.signum.supreme.os.AndroidSigningKeyConfiguration.configureBiometricCurrentSet (
234+ options : AndroidKey .Options ,
235+ ) {
236+ hardware {
237+ protection {
238+ factors {
239+ biometry = true
240+ biometryWithNewFactors = false
241+ deviceLock = false
242+ }
243+ timeout = Duration .ZERO
244+ }
245+ }
246+ signer {
247+ unlockPrompt {
248+ message = options.authorizationPrompt.message
249+ cancelText = options.authorizationPrompt.cancelText
250+ allowedAuthenticators = BIOMETRIC_STRONG
251+ options.interactionContext?.let { activity = it }
252+ }
253+ }
254+ }
255+
256+ private suspend fun AndroidKey.Options.loadSigner (): AndroidKeystoreSigner {
257+ val signer = AndroidKeyStoreProvider .getSignerForKey(kid) {
258+ unlockPrompt {
259+ message = authorizationPrompt.message
260+ cancelText = authorizationPrompt.cancelText
261+ if (keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .BiometricCurrentSet ) {
262+ allowedAuthenticators = BIOMETRIC_STRONG
263+ }
264+ interactionContext?.let { activity = it }
265+ }
266+ }.getOrThrow()
267+ if (keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .BiometricCurrentSet ) {
268+ val biometricOnly = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .R ) {
269+ signer.keyInfo.userAuthenticationType == KeyProperties .AUTH_BIOMETRIC_STRONG
270+ } else {
271+ true
272+ }
273+ if (
274+ ! signer.needsAuthentication ||
275+ ! signer.needsAuthenticationForEveryUse ||
276+ ! signer.keyInfo.isInvalidatedByBiometricEnrollment ||
277+ ! biometricOnly
278+ ) {
279+ throw KeyUseAuthorizationException (
280+ failure = KeyUseAuthorizationFailure .ProtectedKeyInvalidated ,
281+ message = " The stored key does not enforce biometric current-set authorization for every use" ,
282+ )
283+ }
284+ }
285+ return signer
286+ }
287+
288+ private fun AndroidKey.Options.requireSupportedProtectedCombination () {
289+ if (
290+ keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .BiometricCurrentSet &&
291+ keyType != KeyType .secp256r1
292+ ) {
293+ throw KeyUseAuthorizationException (
294+ failure = KeyUseAuthorizationFailure .UnsupportedCombination ,
295+ message = " Android biometric current-set authorization is supported only for secp256r1 keys" ,
296+ )
297+ }
298+ }
299+
300+ private fun AndroidKey.Options.mapPlatformFailure (throwable : Throwable ): Throwable {
301+ if (keyUseAuthorizationPolicy == KeyUseAuthorizationPolicy .None ) return throwable
302+ val causes = generateSequence(throwable as Throwable ? ) { it.cause }.toList()
303+ return when {
304+ causes.any { it is KeyPermanentlyInvalidatedException } -> KeyUseAuthorizationException (
305+ failure = KeyUseAuthorizationFailure .ProtectedKeyInvalidated ,
306+ message = " The protected key was invalidated" ,
307+ cause = throwable,
308+ )
309+ causes.any { it is NoSuchElementException } -> KeyUseAuthorizationException (
310+ failure = KeyUseAuthorizationFailure .ProtectedKeyMissing ,
311+ message = " The protected key is missing" ,
312+ cause = throwable,
313+ )
314+ else -> throwable
315+ }
316+ }
317+
318+ private fun SignatureResult <* >.mapPlatformFailure (options : AndroidKey .Options ): SignatureResult <* > = when (this ) {
319+ is SignatureResult .Error -> SignatureResult .Error (options.mapPlatformFailure(exception))
320+ else -> this
321+ }
0 commit comments