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)
|
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,
|
||||||
|
|||||||
+44
-2
@@ -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)
|
||||||
|
|||||||
+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