feat(interception): override pre-existing attest keys, skip GMS list hooking

Two changes to Keystore2Interceptor:

1. Hardware attest keys created before TEESimulator loads now get
   detected in the getKeyEntry post-hook via isAttestKey(). A software
   replacement keypair is generated, cached, and persisted so the
   unpatched hardware chain is never served.

2. GMS calls listEntries frequently. Skip the post-hook injection
   for com.google.android.gms to reduce log flooding and unnecessary
   key merging work.

Also adds null-alias guard in onPreTransact to avoid NPE on keys
looked up by domain/nspace without an alias.
This commit is contained in:
Enginex0
2026-03-09 19:59:51 +01:00
parent 14786ecce0
commit 776a7b2343
@@ -1,22 +1,23 @@
package org.matrix.TEESimulator.interception.keystore package org.matrix.TEESimulator.interception.keystore
import android.annotation.SuppressLint import android.annotation.SuppressLint
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.os.Build 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
import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse import android.system.keystore2.KeyEntryResponse
import java.security.SecureRandom
import java.security.cert.Certificate import java.security.cert.Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.attestation.KeyMintAttestation
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.KeyMintParameterLogger
import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.pki.CertificateGenerator
import org.matrix.TEESimulator.pki.CertificateHelper import org.matrix.TEESimulator.pki.CertificateHelper
/** /**
@@ -102,10 +103,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data: Parcel, data: Parcel,
): TransactionResult { ): TransactionResult {
if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) {
logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true)
if (ConfigurationManager.shouldSkipUid(callingUid)) val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString()
val isGMS = packages.contains("com.google.android.gms")
if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) {
return TransactionResult.ContinueAndSkipPost return TransactionResult.ContinueAndSkipPost
}
return runCatching { return runCatching {
val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION
@@ -140,7 +145,14 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.ContinueAndSkipPost ?: return TransactionResult.ContinueAndSkipPost
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}") if (descriptor.alias != null) {
SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}")
} else {
SystemLogger.info(
"Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]"
)
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, descriptor.alias) val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (code == DELETE_KEY_TRANSACTION) { if (code == DELETE_KEY_TRANSACTION) {
@@ -210,78 +222,124 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
TransactionResult.SkipTransaction TransactionResult.SkipTransaction
} }
} else if (code == GET_KEY_ENTRY_TRANSACTION) { } else if (code == GET_KEY_ENTRY_TRANSACTION) {
logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid)
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val keyDescriptor = val keyDescriptor =
data.readTypedObject(KeyDescriptor.CREATOR) data.readTypedObject(KeyDescriptor.CREATOR)
?: return TransactionResult.SkipTransaction ?: return TransactionResult.SkipTransaction
logTransaction(
txId,
"post-${transactionNames[code]!!} ${keyDescriptor.alias}",
callingUid,
callingPid,
)
if (!ConfigurationManager.shouldPatch(callingUid)) if (!ConfigurationManager.shouldPatch(callingUid))
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
SystemLogger.info("Handling post-${transactionNames[code]!!} ${keyDescriptor.alias}") runCatching {
return try { val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!!
val response =
reply.readTypedObject(KeyEntryResponse.CREATOR)
?: return TransactionResult.SkipTransaction
reply.setDataPosition(0) // Reset for potential reuse.
val originalChain = CertificateHelper.getCertificateChain(response)
val authorizations = response.metadata?.authorizations
val origin =
authorizations
?.find { it.keyParameter.tag == Tag.ORIGIN }
?.let { it.keyParameter.value.origin }
if (origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED) {
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) { val authorizations = response.metadata.authorizations
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).") val parsedParameters =
KeyMintAttestation(
authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()
)
if (parsedParameters.isImportKey()) {
val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
if (retainedChain == null) {
SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).")
return TransactionResult.SkipTransaction
}
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow()
return InterceptorUtils.createTypedObjectReply(response)
}
if (parsedParameters.isAttestKey()) {
SystemLogger.warning(
"[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply."
)
val keyData =
CertificateGenerator.generateAttestedKeyPair(
callingUid,
keyId.alias,
null,
parsedParameters,
response.metadata.keySecurityLevel,
) ?: throw Exception("Failed to create overriding attest key pair.")
CertificateHelper.updateCertificateChain(
response.metadata,
keyData.second.toTypedArray(),
)
.getOrThrow()
keyDescriptor.nspace = SecureRandom().nextLong()
KeyMintSecurityLevelInterceptor.generatedKeys[keyId] =
KeyMintSecurityLevelInterceptor.GeneratedKeyInfo(
keyData.first,
keyDescriptor.nspace,
response,
)
KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId)
GeneratedKeyPersistence.save(
keyId = keyId,
keyPair = keyData.first,
nspace = keyDescriptor.nspace,
securityLevel = response.metadata.keySecurityLevel,
certChain = keyData.second,
algorithm = parsedParameters.algorithm,
keySize = parsedParameters.keySize,
ecCurve = parsedParameters.ecCurve,
purposes = parsedParameters.purpose,
digests = parsedParameters.digest,
isAttestationKey = true,
)
return InterceptorUtils.createTypedObjectReply(response)
}
val originalChain = CertificateHelper.getCertificateChain(response)
if (originalChain == null || originalChain.size < 2) {
SystemLogger.info(
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}."
)
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId")
CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow() val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain =
AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain)
.getOrThrow()
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} }
.onFailure {
if (originalChain == null || originalChain.size < 2) { SystemLogger.error(
SystemLogger.info( "[TX_ID: $txId] Failed to modify hardware KeyEntryResponse.",
"[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}." it,
) )
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }
// Perform the attestation patch.
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
// First, try to retrieve the already-patched chain from our cache to ensure
// consistency.
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
// If no chain is cached (e.g., key existed before simulator started),
// perform a live patch as a fallback. This may still be detectable.
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) {
SystemLogger.error("[TX_ID: $txId] Failed to patch certificate chain.", e)
TransactionResult.SkipTransaction
}
} }
return TransactionResult.SkipTransaction return TransactionResult.SkipTransaction
} }