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:
committed by
GitHub
co-authored by
JingMatrix
parent
54f68b99b1
commit
129cec06bf
@@ -65,6 +65,19 @@ object InterceptorUtils {
|
||||
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. */
|
||||
fun <T : Parcelable?> createTypedObjectReply(
|
||||
obj: T,
|
||||
|
||||
+44
-2
@@ -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<Int, String> 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)
|
||||
|
||||
+142
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user