Skip to content

Commit a39789f

Browse files
authored
Gracefully handle when the LSKF is removed. (#566)
Introduce KeyInvalidatedException for SecureArea and mention that any implementation may throw this expection and it means that the key is now unusable. Also add getKeyInvalidate() method as a way to check directly without having to use a key. For AndroidKeystoreSecureArea, introduce ScreenLockRequiredException and throw this if trying to create an authbound key when no screenlock is set up. On Document, introduce deleteInvalidatedCredentials() which uses this facility to delete all pending and certified keys that are invalidated. Also add hasUsableCredential() as a convenience function. Use hot flows instead of the home-grown observer pattern on DocumentStore, IssuingAuthority, and IssuingAuthorityRepository. Rename CardViewModel to DocumentModel and move instantiation so it's created by WalletApplication instead of MainActivity. This is important because this model is used by more than just the UI. Also rename objects exposed by DocumentModel and the composables rendering these. Move all credential housekeeping into DocumentModel and add support for batch processing of the very chatty DOCUMENT_UPDATED events. This saves a lot of processing. Add SettingsModel.screenLockIsSetup and use this to show a snackbar telling the user that this is the case and some things might not work. Catch exceptions and show errors in the UI if the user is trying to refresh a credential (w/ special-case for ScreenLockRequiredException). In DocumentModel, add an observer for screenLockIsSetup and call deleteInvalidatedCredentials() on all documents and make sure we convey this to the user through the status field with "Credentials expired or missing". Add a worker for daily pings to the issuer (for pulling PII updates, refreshing MSOs, etc). Test: Manually tested Test: All unit tests pass. Signed-off-by: David Zeuthen <zeuthen@google.com>
1 parent c46a5cf commit a39789f

32 files changed

+1362
-853
lines changed

gradle/libs.versions.toml

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
androidx-navigation-ktx = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "navigation" }
6767
androidx-navigation-ui-ktx = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "navigation" }
6868

69+
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref="kotlinx-coroutines" }
6970
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref="kotlinx-coroutines" }
7071
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref="kotlinx-coroutines" }
7172

identity-android/src/main/java/com/android/identity/android/securearea/AndroidKeystoreSecureArea.kt

+60-37
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.android.identity.crypto.CertificateChain
3333
import com.android.identity.crypto.EcCurve
3434
import com.android.identity.crypto.EcPublicKey
3535
import com.android.identity.crypto.javaPublicKey
36+
import com.android.identity.securearea.KeyInvalidatedException
3637
import com.android.identity.securearea.KeyLockedException
3738
import com.android.identity.securearea.KeyPurpose
3839
import com.android.identity.securearea.SecureArea
@@ -197,6 +198,12 @@ class AndroidKeystoreSecureArea(
197198
else -> throw IllegalArgumentException("Curve is not supported")
198199
}
199200
if (aSettings.userAuthenticationRequired) {
201+
val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
202+
if (!keyguardManager.isDeviceSecure) {
203+
throw ScreenLockRequiredException(
204+
"Screen lock must be set up to create keys with user authentication"
205+
)
206+
}
200207
builder.setUserAuthenticationRequired(true)
201208
val timeoutMillis = aSettings.userAuthenticationTimeoutMillis
202209
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -246,7 +253,7 @@ class AndroidKeystoreSecureArea(
246253
try {
247254
kpg.initialize(builder.build())
248255
} catch (e: InvalidAlgorithmParameterException) {
249-
throw IllegalStateException(e.message, e)
256+
throw IllegalStateException(e)
250257
}
251258
kpg.generateKeyPair()
252259
} catch (e: NoSuchAlgorithmException) {
@@ -282,11 +289,13 @@ class AndroidKeystoreSecureArea(
282289
* @param existingAlias the alias of the existing key.
283290
*/
284291
fun createKeyForExistingAlias(existingAlias: String) {
292+
293+
val ks = KeyStore.getInstance("AndroidKeyStore")
294+
ks.load(null)
295+
val entry = ks.getEntry(existingAlias, null)
296+
?: throw IllegalArgumentException("A key with this alias doesn't exist")
297+
285298
var keyInfo: android.security.keystore.KeyInfo = try {
286-
val ks = KeyStore.getInstance("AndroidKeyStore")
287-
ks.load(null)
288-
val entry = ks.getEntry(existingAlias, null)
289-
?: throw IllegalArgumentException("No entry for alias")
290299
val privateKey = (entry as KeyStore.PrivateKeyEntry).privateKey
291300
val factory = KeyFactory.getInstance(privateKey.algorithm, "AndroidKeyStore")
292301
try {
@@ -415,11 +424,10 @@ class AndroidKeystoreSecureArea(
415424
}
416425
}
417426
}
427+
428+
val (entry, _) = loadKey(alias)
429+
418430
return try {
419-
val ks = KeyStore.getInstance("AndroidKeyStore")
420-
ks.load(null)
421-
val entry = ks.getEntry(alias, null)
422-
?: throw IllegalArgumentException("No entry for alias")
423431
val privateKey = (entry as KeyStore.PrivateKeyEntry).privateKey
424432
val s = Signature.getInstance(getSignatureAlgorithmName(signatureAlgorithm))
425433
s.initSign(privateKey)
@@ -447,11 +455,8 @@ class AndroidKeystoreSecureArea(
447455
otherKey: EcPublicKey,
448456
keyUnlockData: com.android.identity.securearea.KeyUnlockData?
449457
): ByteArray {
458+
val (entry, _) = loadKey(alias)
450459
return try {
451-
val ks = KeyStore.getInstance("AndroidKeyStore")
452-
ks.load(null)
453-
val entry = ks.getEntry(alias, null)
454-
?: throw IllegalArgumentException("No entry for alias")
455460
val privateKey = (entry as KeyStore.PrivateKeyEntry).privateKey
456461
val ka = KeyAgreement.getInstance("ECDH", "AndroidKeyStore")
457462
ka.init(privateKey)
@@ -474,19 +479,37 @@ class AndroidKeystoreSecureArea(
474479
}
475480
}
476481

482+
// @throws IllegalArgumentException if the key doesn't exist.
483+
// @throws KeyInvalidatedException if LSKF was removed and the key is no longer available.
484+
private fun loadKey(alias: String): Pair<KeyStore.Entry, ByteArray> {
485+
val data = storageEngine[PREFIX + alias] ?: throw IllegalArgumentException("No key with given alias")
486+
487+
val ks = KeyStore.getInstance("AndroidKeyStore")
488+
ks.load(null)
489+
// If the LSKF is removed, all auth-bound keys are removed and the result is
490+
// that KeyStore.getEntry() returns null.
491+
val entry = ks.getEntry(alias, null)
492+
?: throw KeyInvalidatedException("This key is no longer available")
493+
494+
return Pair(entry, data)
495+
}
496+
497+
override fun getKeyInvalidated(alias: String): Boolean {
498+
try {
499+
loadKey(alias)
500+
} catch (e: KeyInvalidatedException) {
501+
return true
502+
}
503+
return false
504+
}
505+
477506
override fun getKeyInfo(alias: String): AndroidKeystoreKeyInfo {
507+
val (entry, data) = loadKey(alias)
478508
return try {
479-
val ks = KeyStore.getInstance("AndroidKeyStore")
480-
ks.load(null)
481-
val entry = ks.getEntry(alias, null)
482-
?: throw IllegalArgumentException("No entry for alias")
483509
val privateKey = (entry as KeyStore.PrivateKeyEntry).privateKey
484510
val factory = KeyFactory.getInstance(privateKey.algorithm, "AndroidKeyStore")
485511
val keyInfo =
486512
factory.getKeySpec(privateKey, android.security.keystore.KeyInfo::class.java)
487-
488-
val data = storageEngine[PREFIX + alias]
489-
?: throw IllegalArgumentException("No key with given alias")
490513
val map = Cbor.decode(data)
491514
val keyPurposes = map["keyPurposes"].asNumber.keyPurposeSet
492515
val userAuthenticationRequired = map["userAuthenticationRequired"].asBoolean
@@ -572,16 +595,16 @@ class AndroidKeystoreSecureArea(
572595
* @param context the application context.
573596
*/
574597
class Capabilities(context: Context) {
575-
private val mKeyguardManager: KeyguardManager
576-
private val mApiLevel: Int
577-
private val mTeeFeatureLevel: Int
578-
private val mSbFeatureLevel: Int
598+
private val keyguardManager: KeyguardManager
599+
private val apiLevel: Int
600+
private val teeFeatureLevel: Int
601+
private val sbFeatureLevel: Int
579602

580603
init {
581-
mKeyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
582-
mTeeFeatureLevel = getFeatureVersionKeystore(context, false)
583-
mSbFeatureLevel = getFeatureVersionKeystore(context, true)
584-
mApiLevel = Build.VERSION.SDK_INT
604+
keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
605+
teeFeatureLevel = getFeatureVersionKeystore(context, false)
606+
sbFeatureLevel = getFeatureVersionKeystore(context, true)
607+
apiLevel = Build.VERSION.SDK_INT
585608
}
586609

587610
val secureLockScreenSetup: Boolean
@@ -591,7 +614,7 @@ class AndroidKeystoreSecureArea(
591614
* This checks whether the device currently has a secure lock
592615
* screen (either PIN, pattern, or password).
593616
*/
594-
get() = mKeyguardManager.isDeviceSecure
617+
get() = keyguardManager.isDeviceSecure
595618

596619
/**
597620
* Whether it's possible to specify multiple authentication types.
@@ -602,63 +625,63 @@ class AndroidKeystoreSecureArea(
602625
* Biometric only, or both).
603626
*/
604627
val multipleAuthenticationTypesSupported: Boolean
605-
get() = mApiLevel >= Build.VERSION_CODES.R
628+
get() = apiLevel >= Build.VERSION_CODES.R
606629

607630
/**
608631
* Whether Attest Keys are supported.
609632
*
610633
* This is only supported in KeyMint 1.0 (version 100) and higher.
611634
*/
612635
val attestKeySupported: Boolean
613-
get() = mTeeFeatureLevel >= 100
636+
get() = teeFeatureLevel >= 100
614637

615638
/**
616639
* Whether Key Agreement is supported.
617640
*
618641
* This is only supported in KeyMint 1.0 (version 100) and higher.
619642
*/
620643
val keyAgreementSupported: Boolean
621-
get() = mTeeFeatureLevel >= 100
644+
get() = teeFeatureLevel >= 100
622645

623646
/**
624647
* Whether Curve25519 is supported.
625648
*
626649
* This is only supported in KeyMint 2.0 (version 200) and higher.
627650
*/
628651
val curve25519Supported: Boolean
629-
get() = mTeeFeatureLevel >= 200
652+
get() = teeFeatureLevel >= 200
630653

631654
/**
632655
* Whether StrongBox is supported.
633656
*
634657
* StrongBox requires dedicated hardware and is not available on all devices.
635658
*/
636659
val strongBoxSupported: Boolean
637-
get() = mSbFeatureLevel > 0
660+
get() = sbFeatureLevel > 0
638661

639662
/**
640663
* Whether StrongBox Attest Keys are supported.
641664
*
642665
* This is only supported in StrongBox KeyMint 1.0 (version 100) and higher.
643666
*/
644667
val strongBoxAttestKeySupported: Boolean
645-
get() = mSbFeatureLevel >= 100
668+
get() = sbFeatureLevel >= 100
646669

647670
/**
648671
* Whether StrongBox Key Agreement is supported.
649672
*
650673
* This is only supported in StrongBox KeyMint 1.0 (version 100) and higher.
651674
*/
652675
val strongBoxKeyAgreementSupported: Boolean
653-
get() = mSbFeatureLevel >= 100
676+
get() = sbFeatureLevel >= 100
654677

655678
/**
656679
* Whether StrongBox Curve25519 is supported.
657680
*
658681
* This is only supported in StrongBox KeyMint 2.0 (version 200) and higher.
659682
*/
660683
val strongBoxCurve25519Supported: Boolean
661-
get() = mSbFeatureLevel >= 200
684+
get() = sbFeatureLevel >= 200
662685
}
663686

664687
companion object {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.android.identity.android.securearea
2+
3+
/**
4+
* Exception thrown when trying to create a key with user authentication but
5+
* no screen lock has been set up.
6+
*/
7+
class ScreenLockRequiredException : Exception {
8+
/**
9+
* Construct a new exception.
10+
*/
11+
constructor()
12+
13+
/**
14+
* Construct a new exception.
15+
*
16+
*/
17+
constructor(message: String) : super(message)
18+
19+
/**
20+
* Construct a new exception.
21+
*
22+
* @param message the message.
23+
* @param cause the cause.
24+
*/
25+
constructor(
26+
message: String,
27+
cause: Exception
28+
) : super(message, cause)
29+
30+
/**
31+
* Construct a new exception.
32+
*
33+
* @param cause the cause.
34+
*/
35+
constructor(cause: Exception) : super(cause)
36+
}

identity/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ dependencies {
1919
implementation libs.bouncy.castle.bcpkix
2020
implementation libs.kotlinx.io.bytestring
2121
implementation libs.kotlinx.datetime
22+
implementation libs.kotlinx.coroutines.core
2223

2324
testImplementation libs.bundles.unit.testing
2425
testImplementation libs.bouncy.castle.bcprov
26+
testImplementation libs.kotlinx.coroutine.test
2527
}
2628

2729
apply from: '../publish-helper.gradle'

identity/src/main/java/com/android/identity/document/Document.kt

+40
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import com.android.identity.util.ApplicationData
2525
import com.android.identity.util.Logger
2626
import com.android.identity.util.SimpleApplicationData
2727
import com.android.identity.util.Timestamp
28+
import kotlinx.datetime.Clock
29+
import kotlinx.datetime.Instant
2830

2931
/**
3032
* This class represents a document created in [DocumentStore].
@@ -262,6 +264,44 @@ class Document private constructor(
262264
return credential
263265
}
264266

267+
/**
268+
* Goes through all credentials and deletes the ones with keys that are invalidated.
269+
*/
270+
fun deleteInvalidatedCredentials() {
271+
for (pendingCredential in pendingCredentials) {
272+
if (pendingCredential.secureArea.getKeyInvalidated(pendingCredential.alias)) {
273+
Logger.i(TAG, "Deleting invalidated pending credential ${pendingCredential.alias}")
274+
pendingCredential.delete()
275+
}
276+
}
277+
for (credential in certifiedCredentials) {
278+
if (credential.secureArea.getKeyInvalidated(credential.alias)) {
279+
Logger.i(TAG, "Deleting invalidated credential ${credential.alias}")
280+
credential.delete()
281+
}
282+
}
283+
}
284+
285+
/**
286+
* Returns whether an usable credential exists at a given point in time.
287+
*
288+
* @param at the point in time to check for.
289+
* @returns `true` if an usable credential exists for the given time, `false` otherwise
290+
*/
291+
fun hasUsableCredential(at: Instant = Clock.System.now()): Boolean {
292+
val credentials = certifiedCredentials
293+
if (credentials.isEmpty()) {
294+
return false
295+
}
296+
for (credential in credentials) {
297+
val validFrom = Instant.fromEpochMilliseconds(credential.validFrom.toEpochMilli())
298+
val validUntil = Instant.fromEpochMilliseconds(credential.validUntil.toEpochMilli())
299+
if (at >= validFrom && at < validUntil) {
300+
return true
301+
}
302+
}
303+
return false
304+
}
265305

266306
internal fun removeCredential(credential: Credential) {
267307
val listToModify = if (credential.isCertified) _certifiedCredentials

0 commit comments

Comments
 (0)