From c3f8f087a64c69a935d8ec91691e70ced570ef95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=BD=BC?= <110387028+XiaoTong6666@users.noreply.github.com> Date: Sat, 31 Jan 2026 18:37:25 +0800 Subject: [PATCH] Support key enumeration via listEntries interception (#84) Previously, generated keys were functional but invisible to enumeration APIs like `KeyStore.aliases()`. Because these keys reside solely in the simulator's memory, the standard database query performed by the system Keystore does not return them. This commit intercepts `listEntries` and `listEntriesBatched` to inject these generated keys into the results. Key implementation details: - ListEntriesHandler: Encapsulates the logic to merge hardware-backed keys with software-backed keys. - Ordering: Uses a `TreeMap` to ensure merged results are lexicographically sorted, mimicking AOSP behavior. - Binder Safety: Implements `estimateSafeAmountToReturn` to calculate the response size. The handler truncates the result list if it exceeds the binder transaction limit (~350KB) as done in AOSP. - Pagination: Respects the `startPastAlias` parameter to support batched listing. Co-authored-by: JingMatrix --- .../interception/keystore/InterceptorUtils.kt | 13 ++ .../keystore/Keystore2Interceptor.kt | 46 +++++- .../keystore/ListEntriesHandler.kt | 142 ++++++++++++++++++ 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index 9882759..7a527cd 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -65,6 +65,19 @@ object InterceptorUtils { return BinderInterceptor.TransactionResult.OverrideReply(parcel) } + /** Creates an `OverrideReply` parcel containing a typed array. */ + fun createTypedArrayReply( + array: Array, + flags: Int = 0, + ): BinderInterceptor.TransactionResult.OverrideReply { + val parcel = + Parcel.obtain().apply { + writeNoException() + writeTypedArray(array, flags) + } + return BinderInterceptor.TransactionResult.OverrideReply(parcel) + } + /** Creates an `OverrideReply` parcel containing a Parcelable object. */ fun createTypedObjectReply( obj: T, 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 80f92ce..ef524ce 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 @@ -4,6 +4,7 @@ import android.annotation.SuppressLint import android.hardware.security.keymint.KeyOrigin import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.Tag +import android.os.Build import android.os.IBinder import android.os.Parcel import android.system.keystore2.IKeystoreService @@ -33,6 +34,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey") private val UPDATE_SUBCOMPONENT_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "updateSubcomponent") + private val LIST_ENTRIES_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "listEntries") + private val LIST_ENTRIES_BATCHED_TRANSACTION = + InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "listEntriesBatched") + .takeIf { Build.VERSION.SDK_INT >= 34 } private val transactionNames: Map by lazy { IKeystoreService.Stub::class @@ -91,7 +97,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { callingPid: Int, data: Parcel, ): TransactionResult { - if ( + if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { + logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) + + if (ConfigurationManager.shouldSkipUid(callingUid)) + return TransactionResult.ContinueAndSkipPost + + return runCatching { + val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION + if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) { + TransactionResult.Continue + } else { + TransactionResult.ContinueAndSkipPost + } + } + .getOrElse { + SystemLogger.error( + "[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}", + it, + ) + TransactionResult.ContinueAndSkipPost + } + } else if ( code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION || code == UPDATE_SUBCOMPONENT_TRANSACTION @@ -163,7 +190,22 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply)) return TransactionResult.SkipTransaction - if (code == GET_KEY_ENTRY_TRANSACTION) { + if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { + logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) + + return runCatching { + val updatedKeyDescriptors = + ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply) + InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors) + } + .getOrElse { + SystemLogger.error( + "[TX_ID: $txId] Failed to update the result of ${transactionNames[code]!!}.", + it, + ) + TransactionResult.SkipTransaction + } + } else if (code == GET_KEY_ENTRY_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) data.enforceInterface(IKeystoreService.DESCRIPTOR) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt new file mode 100644 index 0000000..77e8bd0 --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt @@ -0,0 +1,142 @@ +package org.matrix.TEESimulator.interception.keystore + +import android.os.Parcel +import android.system.keystore2.Domain +import android.system.keystore2.IKeystoreService +import android.system.keystore2.KeyDescriptor +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor +import org.matrix.TEESimulator.logging.SystemLogger + +/** + * Handler to intercept listEntries and listEntriesBatched transactions. + * + * References for all mentioned functions in AOSP: + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/database.rs + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/service.rs + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/utils.rs + */ +object ListEntriesHandler { + + // Estimate for maximum size of a Binder response in bytes. + private const val RESPONSE_SIZE_LIMIT = 358400 + + // Parameters of AOSP function `list_key_entries` in utils.rs. + private data class ListEntriesParams( + val domain: Int, + val namespace: Long, + val startPastAlias: String?, + ) + + private val pendingParams = ConcurrentHashMap() + + // Based on AOSP function `estimate_safe_amount_to_return` in utils.rs. + private fun estimateSafeAmountToReturn( + keyDescriptors: Array, + responseSizeLimit: Int, + ): Int { + var itemsToReturn = 0 + var returnedBytes = 0 + + for (kd in keyDescriptors) { + // 4 bytes for the Domain enum + // 8 bytes for the Namespace long + returnedBytes += 4 + 8 + + kd.alias?.let { returnedBytes += 4 + it.toByteArray(Charsets.UTF_8).size } + kd.blob?.let { returnedBytes += 4 + it.size } + + if (returnedBytes > responseSizeLimit) { + SystemLogger.warning( + "Key descriptors list (${keyDescriptors.size} items) may exceed binder size limit, returning $itemsToReturn items with estimated size: $returnedBytes bytes." + ) + break + } + itemsToReturn++ + } + + return itemsToReturn + } + + // Parse and store parameters for later use (in post-transaction). + fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean { + data.enforceInterface(IKeystoreService.DESCRIPTOR) + + val domain = data.readInt() + val namespace = data.readLong() + val startPastAlias = if (isBatchMode) data.readString() else null + + // List entries is only supported for Domain::APP and Domain::SELINUX. + // See AOSP function `get_key_descriptor_for_lookup` in service.rs. + // Note that all generated keys belong to Domain::APP. + if (domain == Domain.APP) { + pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias) + SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.") + return true + } + + return false + } + + // Merge software-backed keys with hardware-backed keys in the reply parcel. + fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array { + val params = + pendingParams.remove(txId) + ?: throw IllegalStateException("No params found for listing entries") + + // By default we use the calling uid as namespace if domain is Domain::APP. + // The namespace parameter is thus ignored for non-privileged applications. + // See AOSP function `get_key_descriptor_for_lookup` in service.rs. + val keysToInject = + extractGeneratedKeyDescriptors(callingUid, callingUid.toLong(), params.startPastAlias) + val originalList = reply.createTypedArray(KeyDescriptor.CREATOR)!! + val mergedArray = mergeKeyDescriptors(originalList, keysToInject) + + // Limit response size to avoid binder buffer overflow. + // See AOSP function `list_key_entries` in utils.rs. + val safeAmountToReturn = estimateSafeAmountToReturn(mergedArray, RESPONSE_SIZE_LIMIT) + + return if (safeAmountToReturn < mergedArray.size) { + SystemLogger.debug( + "[TX_ID: $txId] Listing entries are truncated [${mergedArray.size} -> $safeAmountToReturn] to avoid transaction overflow." + ) + mergedArray.copyOfRange(0, safeAmountToReturn) + } else { + SystemLogger.debug( + "[TX_ID: $txId] Listing entries returns ${mergedArray.size} [injected: ${keysToInject.size}] keys." + ) + mergedArray + } + } + + // Merge hardware and software key descriptors into a single sorted array. + private fun mergeKeyDescriptors( + hardwareKeys: Array, + keysToInject: List, + ): Array { + // Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys). + val combinedMap = TreeMap() + hardwareKeys.forEach { key -> key.alias?.let { combinedMap[it] = key } } + keysToInject.forEach { key -> key.alias?.let { combinedMap[it] = key } } + return combinedMap.values.toTypedArray() + } + + // Based on AOSP function `list_past_alias` in database.rs + private fun extractGeneratedKeyDescriptors( + uid: Int, + namespace: Long, + startPastAlias: String?, + ): List { + return KeyMintSecurityLevelInterceptor.generatedKeys.keys + .filter { it.uid == uid && (startPastAlias == null || it.alias < startPastAlias) } + .map { keyId -> + KeyDescriptor().apply { + this.domain = Domain.APP + this.nspace = namespace + this.alias = keyId.alias + this.blob = null + } + } + } +}