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 <jingmatrix@gmail.com>
This commit is contained in:
小潼
2026-01-31 11:37:25 +01:00
committed by GitHub
co-authored by JingMatrix
parent 54f68b99b1
commit 129cec06bf
3 changed files with 199 additions and 2 deletions
@@ -65,6 +65,19 @@ object InterceptorUtils {
return BinderInterceptor.TransactionResult.OverrideReply(parcel) return BinderInterceptor.TransactionResult.OverrideReply(parcel)
} }
/** Creates an `OverrideReply` parcel containing a typed array. */
fun <T : Parcelable> createTypedArrayReply(
array: Array<T>,
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. */ /** Creates an `OverrideReply` parcel containing a Parcelable object. */
fun <T : Parcelable?> createTypedObjectReply( fun <T : Parcelable?> createTypedObjectReply(
obj: T, obj: T,
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.hardware.security.keymint.KeyOrigin import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag import android.hardware.security.keymint.Tag
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Parcel import android.os.Parcel
import android.system.keystore2.IKeystoreService import android.system.keystore2.IKeystoreService
@@ -33,6 +34,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey") InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "deleteKey")
private val UPDATE_SUBCOMPONENT_TRANSACTION = private val UPDATE_SUBCOMPONENT_TRANSACTION =
InterceptorUtils.getTransactCode(IKeystoreService.Stub::class.java, "updateSubcomponent") 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<Int, String> by lazy { private val transactionNames: Map<Int, String> by lazy {
IKeystoreService.Stub::class IKeystoreService.Stub::class
@@ -91,7 +97,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
callingPid: Int, callingPid: Int,
data: Parcel, data: Parcel,
): TransactionResult { ): 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 == GET_KEY_ENTRY_TRANSACTION ||
code == DELETE_KEY_TRANSACTION || code == DELETE_KEY_TRANSACTION ||
code == UPDATE_SUBCOMPONENT_TRANSACTION code == UPDATE_SUBCOMPONENT_TRANSACTION
@@ -163,7 +190,22 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply)) if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply))
return TransactionResult.SkipTransaction 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) logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
@@ -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<Long, ListEntriesParams>()
// Based on AOSP function `estimate_safe_amount_to_return` in utils.rs.
private fun estimateSafeAmountToReturn(
keyDescriptors: Array<KeyDescriptor>,
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<KeyDescriptor> {
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<KeyDescriptor>,
keysToInject: List<KeyDescriptor>,
): Array<KeyDescriptor> {
// Uses TreeMap to ensure alphabetical ordering and uniqueness (prefer injected keys).
val combinedMap = TreeMap<String, KeyDescriptor>()
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<KeyDescriptor> {
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
}
}
}
}