fix(keystore): grant plane serves patch-mode keys

Domain.GRANT readback only recognized synthetic keys (generatedKeys), so
patch-mode keys (real TEE key whose attestation we patch on read, cached
in teeResponses) fell through to the real keystore2 unpatched. Android 16
made KeyStoreManager.grantKeyAccess a public API, so the owner read
returned our patched chain while the grant read returned the raw real
chain -> duck SELF_/ISOLATED_CHAIN_SPLIT.

Gate grant()/ungrant()/resolveGrant on ownsKeyResponse() (synthetic OR
patch-mode) so every access plane serves the same cached KeyEntryResponse.
Pre-36 still answers PERMISSION_DENIED; no behavior change on Android 15.
This commit is contained in:
Enginex0
2026-05-30 13:42:31 +01:00
parent a58c4798c3
commit ca857c30ac
2 changed files with 17 additions and 5 deletions
@@ -339,11 +339,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
?: return TransactionResult.ContinueAndSkipPost ?: return TransactionResult.ContinueAndSkipPost
val granteeUid = data.readInt() val granteeUid = data.readInt()
val accessVector = data.readInt() val accessVector = data.readInt()
// Only synthetic keys are ours; real keys fall through to the real keystore2, which // Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant
// applies the same SELinux gate the platform would. // coherently so the Domain.GRANT readback returns the same chain the owner read returns.
// Real hardware keys fall through to the real keystore2, which applies the same SELinux
// gate the platform would.
val ownerKeyId = val ownerKeyId =
resolveOwnerKeyId(key, callingUid) resolveOwnerKeyId(key, callingUid)
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) } ?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
?: return TransactionResult.ContinueAndSkipPost ?: return TransactionResult.ContinueAndSkipPost
// Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and // Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and
// SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16 // SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16
@@ -372,7 +374,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
val granteeUid = data.readInt() val granteeUid = data.readInt()
val ownerKeyId = val ownerKeyId =
resolveOwnerKeyId(key, callingUid) resolveOwnerKeyId(key, callingUid)
?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) } ?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) }
?: return TransactionResult.ContinueAndSkipPost ?: return TransactionResult.ContinueAndSkipPost
// Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+. // Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+.
if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) { if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) {
@@ -1220,9 +1220,19 @@ class KeyMintSecurityLevelInterceptor(
/** Caller-bound resolve: only the designated grantee, only while the key exists. */ /** Caller-bound resolve: only the designated grantee, only while the key exists. */
fun resolveGrant(grantId: Long, callerUid: Int): SoftwareGrant? = fun resolveGrant(grantId: Long, callerUid: Int): SoftwareGrant? =
softwareGrants[grantId]?.takeIf { softwareGrants[grantId]?.takeIf {
it.granteeUid == callerUid && generatedKeys.containsKey(it.ownerKeyId) it.granteeUid == callerUid && ownsKeyResponse(it.ownerKeyId)
} }
/**
* True when this interceptor holds a coherent [KeyEntryResponse] for [keyId] — synthetic
* (`generatedKeys`) OR patch-mode (`teeResponses`, a real TEE key whose attestation we
* patched). The grant plane must virtualize both: gating on `generatedKeys` alone left
* patch-mode keys' `Domain.GRANT` readback falling through to the real keystore2 unpatched,
* splitting the grant chain against the owner's patched read (duck SELF_/ISOLATED_CHAIN_SPLIT,
* surfaced once Android 16 made KeyStoreManager.grantKeyAccess a public API).
*/
fun ownsKeyResponse(keyId: KeyIdentifier): Boolean = getGeneratedKeyResponse(keyId) != null
fun revokeGrant(ownerKeyId: KeyIdentifier, granteeUid: Int) { fun revokeGrant(ownerKeyId: KeyIdentifier, granteeUid: Int) {
softwareGrants.entries softwareGrants.entries
.filter { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid } .filter { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid }