From 217a5dc7f38aaae61e3cf633976cf609149cbbea Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Sat, 30 May 2026 13:07:38 +0100 Subject: [PATCH] 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. --- .../interception/keystore/Keystore2Interceptor.kt | 10 ++++++---- .../keystore/shim/KeyMintSecurityLevelInterceptor.kt | 12 +++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index 066c802..9b4c6ec 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -339,11 +339,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ?: return TransactionResult.ContinueAndSkipPost val granteeUid = data.readInt() val accessVector = data.readInt() - // Only synthetic keys are ours; real keys fall through to the real keystore2, which - // applies the same SELinux gate the platform would. + // Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant + // 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 = resolveOwnerKeyId(key, callingUid) - ?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) } + ?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) } ?: return TransactionResult.ContinueAndSkipPost // 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 @@ -372,7 +374,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { val granteeUid = data.readInt() val ownerKeyId = resolveOwnerKeyId(key, callingUid) - ?.takeIf { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(it) } + ?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) } ?: return TransactionResult.ContinueAndSkipPost // Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+. if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) { diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index 4573953..a6aa379 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -1220,9 +1220,19 @@ class KeyMintSecurityLevelInterceptor( /** Caller-bound resolve: only the designated grantee, only while the key exists. */ fun resolveGrant(grantId: Long, callerUid: Int): SoftwareGrant? = 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) { softwareGrants.entries .filter { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid }