Compare commits

..
9 Commits
Author SHA1 Message Date
Enginex0 258a65ba59 docs(release): add v4.8 changelog for StrongBox hardening and LRU pruning 2026-03-17 19:56:45 +01:00
Enginex0 d2b8a92fbd chore(version): bump to v4.8 2026-03-17 19:48:48 +01:00
Enginex0 0723865eab feat(interception): add StrongBox hardening and LRU operation pruning
DuckDetector flags several behavioral anomalies that real TEE/StrongBox
hardware exhibits but our software interceptor did not:

- LRU pruning: cap concurrent ops at 15 (TEE) / 4 (StrongBox) per UID,
  aborting oldest when exceeded — matches AOSP keystore2 malus scoring
- StrongBox param guard: forward unsupported params (RSA>2048, non-P256)
  to real HAL for proper rejection instead of generating in software
- StrongBox latency floors: 250ms keygen, 80ms sign to match real SE
  timing characteristics
- Sliding-window op limit for hardware-generated StrongBox keys that
  bypass the software pruning path
- Domain.APP lookup path for createOperation to find software-generated
  keys that never reach keystore2's database
2026-03-17 19:48:40 +01:00
Enginex0 7cb44b9999 feat(operation): add LRU pruning support and latency floor to SoftwareOperation
Expose finalized state for pruning, add latency floor parameter for
StrongBox timing simulation, and add trace logging for 32KB test
diagnosis.
2026-03-17 19:48:29 +01:00
Enginex0 eddd9908af fix(certgen): accept ECDSA as EC algorithm alias in JCA key type matching
Some Android 10 devices (e.g. Sony H8296) report EC private key
algorithm as "ECDSA" instead of "EC", causing IllegalArgumentException
in certificate signing and a SIGSEGV crash in the keystore process.

Closes #4
2026-03-17 19:48:19 +01:00
Enginex0 36ccd22cdc fix(operation): correct TOO_MUCH_DATA fallback to match AOSP ResponseCode
AOSP ResponseCode.TOO_MUCH_DATA = 21, not 29.
2026-03-17 14:16:00 +01:00
Enginex0 81e6fbf97e fix(keygen): forward symmetric algorithms to HAL and add missing JCA mappings
Symmetric keys (AES/HMAC/3DES) don't have KeyPairs or attestation
certs — routing them through doSoftwareKeyGen crashes with
"Unsupported algorithm: 32". Skip the software path entirely and
let the real HAL handle them.

Also adds CTR block mode, RSA_PKCS1_1_5_SIGN cipher padding, and
RSA_PSS signature padding to JcaAlgorithmMapper.
2026-03-17 13:44:25 +01:00
Enginex0andGitHub 7f63713f07 fix(interception): add permission checks for device ID attestation tags
fix(interception): Add permission checks for KeyMintSecurityLevelInterceptor and fix some regression
2026-03-17 13:05:36 +01:00
fatalcoder524 5df76eacd1 fix(interception): Add permission checks for KeyMintSecurityLevelInterceptor and fix some regression
1. Add permission checks for KeyMintSecurityLevelInterceptor to ensure that only authorized users can access sensitive information about the security level of the key mint.
2. Fix regression where device id attestation was allowed for all users by adding appropriate permission checks.
3. Update .gitignore to exclude build artifacts and generated files to keep the repository clean and prevent accidental commits of unnecessary files.
2026-03-17 11:49:54 +00:00
8 changed files with 246 additions and 37 deletions
+6
View File
@@ -1 +1,7 @@
out
.gradle
.kotlin
app/build
build
native-certgen/target
app/src/main/jniLibs
+1 -1
View File
@@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java)
val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt()
val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir)
val verName = "v4.7"
val verName = "v4.8"
android {
namespace = "org.matrix.TEESimulator"
@@ -1,9 +1,11 @@
package org.matrix.TEESimulator.interception.keystore.shim
import android.hardware.security.keymint.Algorithm
import android.hardware.security.keymint.EcCurve
import android.hardware.security.keymint.KeyParameter
import android.hardware.security.keymint.KeyParameterValue
import android.hardware.security.keymint.KeyOrigin
import android.hardware.security.keymint.SecurityLevel
import android.hardware.security.keymint.Tag
import android.os.IBinder
import android.os.Parcel
@@ -17,6 +19,7 @@ import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.security.spec.PKCS8EncodedKeySpec
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedDeque
import java.util.concurrent.atomic.AtomicInteger
import org.matrix.TEESimulator.attestation.AttestationBuilder
import org.matrix.TEESimulator.attestation.AttestationConstants
@@ -33,6 +36,7 @@ import org.matrix.TEESimulator.pki.CertificateHelper
import org.matrix.TEESimulator.pki.KeyBoxManager
import org.matrix.TEESimulator.pki.NativeCertGen
import org.matrix.TEESimulator.util.AndroidDeviceUtils
import org.matrix.TEESimulator.util.AndroidPermissionUtils
class KeyMintSecurityLevelInterceptor(
private val original: IKeystoreSecurityLevel,
@@ -45,6 +49,9 @@ class KeyMintSecurityLevelInterceptor(
val response: KeyEntryResponse,
)
private val activeOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<SoftwareOperation>>()
private val recentOps = ConcurrentHashMap<Int, ConcurrentLinkedDeque<Long>>()
override fun onPreTransact(
txId: Long,
target: IBinder,
@@ -192,42 +199,90 @@ class KeyMintSecurityLevelInterceptor(
return TransactionResult.SkipTransaction
}
private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) {
val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() }
val before = ops.size
ops.removeIf { it.finalized }
val afterClean = ops.size
while (ops.size >= maxOps) {
val oldest = ops.pollFirst() ?: break
if (!oldest.finalized) {
SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)")
oldest.abort()
}
}
ops.addLast(newOp)
SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}")
}
private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? {
if (securityLevel != SecurityLevel.STRONGBOX) return null
val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() }
val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS
timestamps.removeIf { it < cutoff }
val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0
if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) {
SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)")
return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS)
}
timestamps.addLast(System.nanoTime())
return null
}
private fun handleCreateOperation(
txId: Long,
callingUid: Int,
data: Parcel,
): TransactionResult {
SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}")
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
// An operation must use the KEY_ID domain.
if (keyDescriptor.domain != Domain.KEY_ID) {
return TransactionResult.ContinueAndSkipPost
SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}")
// Android framework calls createOperation with domain=APP+alias;
// keystore2 internally resolves to KEY_ID — but software keys never
// reach keystore2's database, so we must handle both lookup paths.
val generatedKeyInfo = when (keyDescriptor.domain) {
Domain.APP -> {
val alias = keyDescriptor.alias ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
generatedKeys[KeyIdentifier(callingUid, alias)] ?: run {
SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
}
Domain.KEY_ID -> {
findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) ?: run {
trackAndEnforceOpLimit(callingUid, txId)?.let { return it }
SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.")
return TransactionResult.Continue
}
}
else -> {
SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
}
val nspace = keyDescriptor.nspace
val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace)
if (generatedKeyInfo == null) {
SystemLogger.debug(
"[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding."
)
return TransactionResult.Continue
}
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.")
SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.")
val params = data.createTypedArray(KeyParameter.CREATOR)!!
val parsedParams = KeyMintAttestation(params).let { p ->
if (p.algorithm != 0) p
else p.copy(algorithm = when (generatedKeyInfo.keyPair.private.algorithm) {
"EC" -> Algorithm.EC
"EC", "ECDSA" -> Algorithm.EC
"RSA" -> Algorithm.RSA
else -> p.algorithm
})
}
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams)
val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L
val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency)
val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID
pruneOpsForUid(callingUid, softwareOperation, maxOps)
val operationBinder = SoftwareOperationBinder(softwareOperation)
val response =
@@ -267,11 +322,38 @@ class KeyMintSecurityLevelInterceptor(
return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT)
}
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION }) {
if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
val hasDeviceIdAttestation = params.any {
it.tag == Tag.ATTESTATION_ID_IMEI ||
it.tag == Tag.ATTESTATION_ID_MEID ||
it.tag == Tag.ATTESTATION_ID_SERIAL ||
it.tag == Tag.DEVICE_UNIQUE_ATTESTATION ||
it.tag == Tag.ATTESTATION_ID_SECOND_IMEI
}
if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) {
SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid")
return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS)
}
val isSymmetric = parsedParams.algorithm == Algorithm.AES ||
parsedParams.algorithm == Algorithm.HMAC ||
parsedParams.algorithm == Algorithm.TRIPLE_DES
if (isSymmetric) {
SystemLogger.debug("[TX_ID: $txId] Symmetric algorithm ${parsedParams.algorithm} → forwarding to HAL")
return TransactionResult.ContinueAndSkipPost
}
if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) {
SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection")
return TransactionResult.ContinueAndSkipPost
}
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
val isAttestKeyRequest = parsedParams.isAttestKey()
@@ -357,7 +439,8 @@ class KeyMintSecurityLevelInterceptor(
)
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = TEE_LATENCY_FLOOR_MS - elapsedMs
val floor = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_KEYGEN_LATENCY_FLOOR_MS else TEE_LATENCY_FLOOR_MS
val delayMs = floor - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs)
return InterceptorUtils.createTypedObjectReply(response.metadata)
@@ -549,7 +632,13 @@ class KeyMintSecurityLevelInterceptor(
private const val KEYMINT_INVALID_INPUT_LENGTH = -21
private const val RESPONSE_INVALID_ARGUMENT = 20
private const val TEE_LATENCY_FLOOR_MS = 15L
private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L
private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L
private const val KEYMINT_TOO_MANY_OPERATIONS = -29
private const val KEYMINT_CANNOT_ATTEST_IDS = -66
private const val MAX_CONCURRENT_OPS_PER_UID = 15
private const val STRONGBOX_MAX_CONCURRENT_OPS = 4
private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s
private const val MAX_CONCURRENT_HW_KEYGEN_PER_UID = 2
// Sliding window: max hardware keygen permits per UID within the burst window
private const val MAX_HW_KEYGEN_PER_WINDOW = 2
@@ -558,6 +647,12 @@ class KeyMintSecurityLevelInterceptor(
private val hardwareKeygenTxIds = ConcurrentHashMap.newKeySet<Long>()
private val uidKeygenTimestamps = ConcurrentHashMap<Int, MutableList<Long>>()
private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) {
Algorithm.RSA -> params.keySize <= 2048
Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256
else -> true
}
private fun hardwareKeygenCount(uid: Int): AtomicInteger =
uidHardwareKeygenCount.computeIfAbsent(uid) { AtomicInteger(0) }
@@ -34,16 +34,17 @@ private object JcaAlgorithmMapper {
Digest.SHA_2_512 -> "SHA512"
else -> "NONE"
}
val keyAlgo =
when (params.algorithm) {
Algorithm.EC -> "ECDSA"
Algorithm.RSA -> "RSA"
else ->
throw IllegalArgumentException(
"Unsupported signature algorithm: ${params.algorithm}"
)
return when (params.algorithm) {
Algorithm.EC -> "${digest}withECDSA"
Algorithm.RSA -> {
val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS
if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA"
}
return "${digest}with${keyAlgo}"
else ->
throw IllegalArgumentException(
"Unsupported signature algorithm: ${params.algorithm}"
)
}
}
fun mapCipherAlgorithm(params: KeyMintAttestation): String {
@@ -60,16 +61,18 @@ private object JcaAlgorithmMapper {
when (params.blockMode.firstOrNull()) {
BlockMode.ECB -> "ECB"
BlockMode.CBC -> "CBC"
BlockMode.CTR -> "CTR"
BlockMode.GCM -> "GCM"
else -> "ECB" // Default for RSA
else -> "ECB"
}
val padding =
when (params.padding.firstOrNull()) {
PaddingMode.NONE -> "NoPadding"
PaddingMode.PKCS7 -> "PKCS7Padding"
PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding"
PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding"
PaddingMode.RSA_OAEP -> "OAEPPadding"
else -> "NoPadding" // Default for GCM
else -> "NoPadding"
}
return "$keyAlgo/$blockMode/$padding"
}
@@ -142,9 +145,15 @@ private class CipherPrimitive(
override fun abort() {}
}
class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) {
class SoftwareOperation(
private val txId: Long,
keyPair: KeyPair,
params: KeyMintAttestation,
private val latencyFloorMs: Long = 0L,
) {
private val primitive: CryptoPrimitive
@Volatile private var finalized = false
@Volatile var finalized = false
private set
init {
val purpose = params.purpose.firstOrNull()
@@ -163,21 +172,28 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
}
private fun checkActive() {
if (finalized) throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
if (finalized) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)")
throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle)
}
}
private fun checkInputLength(data: ByteArray?) {
if (data != null && data.size > MAX_RECEIVE_DATA)
if (data != null && data.size > MAX_RECEIVE_DATA) {
SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})")
throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData)
}
}
fun updateAad(aadInput: ByteArray?) {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}")
checkActive()
checkInputLength(aadInput)
primitive.updateAad(aadInput)
}
fun update(data: ByteArray?): ByteArray? {
SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}")
checkActive()
checkInputLength(data)
try {
@@ -194,7 +210,13 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
checkActive()
checkInputLength(data)
try {
val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L
val result = primitive.finish(data, signature)
if (latencyFloorMs > 0) {
val elapsedMs = (System.nanoTime() - startNs) / 1_000_000
val delayMs = latencyFloorMs - elapsedMs
if (delayMs > 0) Thread.sleep(delayMs)
}
finalized = true
SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.")
return result
@@ -220,7 +242,7 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin
private object KeystoreErrorCodes {
val tooMuchData: Int by lazy {
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 29)
resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21)
}
val invalidOperationHandle: Int by lazy {
@@ -240,7 +240,7 @@ object CertificateGenerator {
val signerAlgorithm =
when (signingKeyPair.private.algorithm) {
"EC" -> "SHA256withECDSA"
"EC", "ECDSA" -> "SHA256withECDSA"
"RSA" -> "SHA256withRSA"
else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}")
}
@@ -105,7 +105,7 @@ object NativeCertGen {
}
val algorithmName = when (certs[0].publicKey.algorithm) {
"EC" -> "EC"
"EC", "ECDSA" -> "EC"
"RSA" -> "RSA"
else -> certs[0].publicKey.algorithm
}
@@ -0,0 +1,72 @@
package org.matrix.TEESimulator.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.pm.PackageManager
import org.matrix.TEESimulator.logging.SystemLogger
object AndroidPermissionUtils {
@SuppressLint("PrivateApi", "DiscouragedPrivateApi")
private fun getGlobalContext(): Context? {
return try {
// 1. Get the hidden ActivityThread class via reflection
val activityThreadClass = Class.forName("android.app.ActivityThread")
// 2. Invoke the static currentActivityThread() method
val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread")
currentActivityThreadMethod.isAccessible = true
val activityThread = currentActivityThreadMethod.invoke(null)
if (activityThread == null) {
SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null")
return null
}
// 3. Try to get the application context
val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication")
getApplicationMethod.isAccessible = true
val application = getApplicationMethod.invoke(activityThread) as? Context
if (application != null) return application
// 4. Fallback to getSystemContext() if application is null (often happens in system_server)
val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext")
getSystemContextMethod.isAccessible = true
getSystemContextMethod.invoke(activityThread) as? Context
} catch (e: Exception) {
SystemLogger.error("Reflection failed to get global context for permission check", e)
null
}
}
/**
* Core permission check.
*/
fun hasPermission(uid: Int, permission: String): Boolean {
val context = getGlobalContext() ?: run {
SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.")
return false
}
val result = context.checkPermission(permission, -1, uid)
return result == PackageManager.PERMISSION_GRANTED
}
fun hasDeviceAttestationPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE")
}
fun hasUniqueIdAttestationPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION")
}
fun hasManageUsersPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.MANAGE_USERS")
}
fun hasDumpPermission(uid: Int): Boolean {
return hasPermission(uid, "android.permission.DUMP")
}
}
+14
View File
@@ -1,3 +1,17 @@
## TEESimulator-RS v4.8: StrongBox Hardening & LRU Pruning
Tested against DuckDetector on OnePlus (Android 16, KSU). Tamper score dropped from 32 to 8.
- **LRU operation pruning** — Concurrent software operations capped at 15 per UID (TEE) and 4 per UID (StrongBox), with oldest-first eviction. Pruned operations return `INVALID_OPERATION_HANDLE (-28)`, matching AOSP keystore2 malus-based pruning.
- **StrongBox param guard** — Unsupported StrongBox params (RSA >2048-bit, non-P256 EC curves) forwarded to real HAL for proper rejection instead of generating in software.
- **StrongBox timing** — Key generation floors at 250ms, signing at 80ms on StrongBox security level to match real secure element latency.
- **StrongBox op limit** — Sliding-window enforcer caps concurrent StrongBox operations for both software and hardware key paths, returning `TOO_MANY_OPERATIONS (-29)` when exceeded.
- **ECDSA algorithm alias** — Accept "ECDSA" in addition to "EC" as JCA private key algorithm name. Fixes SIGSEGV crash on Android 10 devices where the provider reports EC keys as "ECDSA". Closes #4.
- **createOperation domain handling** — Software-generated keys now found via both `Domain.APP` (alias) and `Domain.KEY_ID` (nspace) lookup paths.
- **Permission guards** — Device ID attestation tags (IMEI, MEID, serial) require caller permission checks.
---
## TEESimulator-RS v4.7: Operation & Attestation Fixes
Tested against [KeyDetector](https://github.com/XiaoTong6666/KeyDetector) and [Key Attestation](https://github.com/nickel-lang/nickel) on OnePlus (Android 16) and Xiaomi Redmi 14C (Android 14).