fix(interception): restore key lifecycle tracking and cache invalidation

Restores pre-PR157 custom features:
- deletedSoftwareKeys tracking in Keystore2Interceptor (returns
  KEY_NOT_FOUND for getKeyEntry after software key deletion instead
  of falling through to hardware)
- invalidatePatchedChains() for bulk cert chain cache clearing
- Per-UID LRU operation pruning (MAX_CONCURRENT_OPS_PER_UID=15)
  prevents resource exhaustion from concurrent software operations
- SoftwareOperation.isFinalized made public for LRU eviction checks
This commit is contained in:
Enginex0
2026-03-22 00:29:46 +01:00
parent b63570a3e2
commit 9a7011eb5e
3 changed files with 35 additions and 15 deletions
@@ -61,8 +61,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
.associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] }
} }
// Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry.
private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>() private val userUpdatedKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
private val deletedSoftwareKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
// Backdoor binder for registering new interceptors at runtime. // Backdoor binder for registering new interceptors at runtime.
private var backdoorBinder: IBinder? = null private var backdoorBinder: IBinder? = null
@@ -225,6 +225,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId) KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId)
KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId) KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId)
if (isSoftwareKey) { if (isSoftwareKey) {
deletedSoftwareKeys.add(keyId)
SystemLogger.info( SystemLogger.info(
"[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response." "[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response."
) )
@@ -239,6 +240,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
} }
val keyId = KeyIdentifier(callingUid, descriptor.alias) val keyId = KeyIdentifier(callingUid, descriptor.alias)
if (deletedSoftwareKeys.remove(keyId)) {
return InterceptorUtils.createErrorReply(7) // KEY_NOT_FOUND
}
val response = val response =
KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId)
?: return TransactionResult.Continue ?: return TransactionResult.Continue
@@ -51,8 +51,8 @@ class KeyMintSecurityLevelInterceptor(
private val securityLevel: Int, private val securityLevel: Int,
) : BinderInterceptor() { ) : BinderInterceptor() {
// --- Data Structures for State Management ---
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>() private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
data class GeneratedKeyInfo( data class GeneratedKeyInfo(
val keyPair: KeyPair?, val keyPair: KeyPair?,
@@ -253,11 +253,17 @@ class KeyMintSecurityLevelInterceptor(
timestamps.addLast(System.nanoTime()) timestamps.addLast(System.nanoTime())
} }
/** private fun pruneOpsForUid(callingUid: Int, newOp: SoftwareOperation) {
* Handles the `createOperation` transaction. It checks if the operation is for a key that was val ops = activeOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
* generated in software. If so, it creates a software-based operation handler. Otherwise, it ops.removeIf { it.isFinalized }
* lets the call proceed to the real hardware service. while (ops.size >= MAX_CONCURRENT_OPS_PER_UID) {
*/ val oldest = ops.pollFirst() ?: break
oldest.abort()
SystemLogger.debug("Pruned oldest op for uid=$callingUid (LRU eviction)")
}
ops.addLast(newOp)
}
private fun handleCreateOperation( private fun handleCreateOperation(
txId: Long, txId: Long,
callingUid: Int, callingUid: Int,
@@ -402,6 +408,7 @@ class KeyMintSecurityLevelInterceptor(
effectiveParams, effectiveParams,
opLatency, opLatency,
) )
pruneOpsForUid(callingUid, softwareOperation)
// Decrement usage counter on finish; delete key when exhausted. // Decrement usage counter on finish; delete key when exhausted.
if (keyParams.usageCountLimit != null && resolvedKeyId != null) { if (keyParams.usageCountLimit != null && resolvedKeyId != null) {
@@ -956,6 +963,7 @@ class KeyMintSecurityLevelInterceptor(
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4 private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L
private const val MAX_ALIAS_LENGTH = 256 * 1024 private const val MAX_ALIAS_LENGTH = 256 * 1024
private const val MAX_CONCURRENT_OPS_PER_UID = 15
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) { private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
Algorithm.RSA -> params.keySize <= 2048 Algorithm.RSA -> params.keySize <= 2048
@@ -1056,7 +1064,13 @@ class KeyMintSecurityLevelInterceptor(
} }
} }
// Clears all cached keys. fun invalidatePatchedChains(reason: String? = null) {
val count = patchedChains.size
if (count == 0) return
patchedChains.clear()
SystemLogger.info("Invalidated $count patched cert chains${reason?.let { " due to $it" } ?: ""}.")
}
fun clearAllGeneratedKeys(reason: String? = null) { fun clearAllGeneratedKeys(reason: String? = null) {
val count = generatedKeys.size val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: "" val reasonMessage = reason?.let { " due to $it" } ?: ""
@@ -257,7 +257,8 @@ class SoftwareOperation(
) { ) {
private val primitive: CryptoPrimitive private val primitive: CryptoPrimitive
@Volatile private var finalized = false @Volatile var isFinalized = false
private set
init { init {
val purpose = params.purpose.firstOrNull() val purpose = params.purpose.firstOrNull()
@@ -294,7 +295,7 @@ class SoftwareOperation(
} }
private fun checkActive() { private fun checkActive() {
if (finalized) if (isFinalized)
throw ServiceSpecificException( throw ServiceSpecificException(
KeystoreErrorCode.INVALID_OPERATION_HANDLE, KeystoreErrorCode.INVALID_OPERATION_HANDLE,
"Operation already finalized.", "Operation already finalized.",
@@ -306,10 +307,10 @@ class SoftwareOperation(
try { try {
primitive.updateAad(data) primitive.updateAad(data)
} catch (e: ServiceSpecificException) { } catch (e: ServiceSpecificException) {
finalized = true isFinalized = true
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
finalized = true isFinalized = true
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e) SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e)
throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message)
} }
@@ -320,10 +321,10 @@ class SoftwareOperation(
try { try {
return primitive.update(data) return primitive.update(data)
} catch (e: ServiceSpecificException) { } catch (e: ServiceSpecificException) {
finalized = true isFinalized = true
throw e throw e
} catch (e: Exception) { } catch (e: Exception) {
finalized = true isFinalized = true
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e)
throw mapToServiceSpecificException(e) throw mapToServiceSpecificException(e)
} }
@@ -348,7 +349,7 @@ class SoftwareOperation(
SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e)
throw mapToServiceSpecificException(e) throw mapToServiceSpecificException(e)
} finally { } finally {
finalized = true isFinalized = true
} }
} }