diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt index b1b5112..0b5f1e3 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -447,6 +447,11 @@ object AttestationBuilder { ) } + if (params.callerNonce == true) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE) + ) + } params.activeDateTime?.let { list.add( DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time)) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt index b2f8d40..afa2c57 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt @@ -14,40 +14,38 @@ object AuthorizeCreate { rawOpParams: Array? = null, ): Int? { if (keyParams == null) return null - return checkPurpose(keyParams, opParams) - ?: checkAlgorithmPurpose(keyParams, opParams) - ?: checkTemporalValidity(keyParams, opParams) - ?: checkCallerNonce(keyParams, rawOpParams) + val purpose = opParams.purpose.firstOrNull() ?: return null + // Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior) + return checkAlgorithmPurpose(keyParams, purpose) + ?: checkPurpose(keyParams, purpose) + ?: checkTemporalValidity(keyParams, purpose) + ?: checkCallerNonce(keyParams, purpose, rawOpParams) } - private fun checkPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { - val requestedPurpose = opParams.purpose.firstOrNull() ?: return null - if (requestedPurpose == KeyPurpose.WRAP_KEY) + private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? { + val algo = keyParams.algorithm + if ((algo == Algorithm.EC || algo == Algorithm.RSA) && + (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT) + ) { + return KeystoreErrorCodes.unsupportedPurpose + } + if (algo == Algorithm.EC && purpose == KeyPurpose.DECRYPT) + return KeystoreErrorCodes.unsupportedPurpose + if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY) + return KeystoreErrorCodes.unsupportedPurpose + return null + } + + private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? { + if (purpose == KeyPurpose.WRAP_KEY) return KeystoreErrorCodes.incompatiblePurpose - if (requestedPurpose !in keyParams.purpose) + if (purpose !in keyParams.purpose) return KeystoreErrorCodes.incompatiblePurpose return null } - private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { - val purpose = opParams.purpose.firstOrNull() ?: return null - return when (keyParams.algorithm) { - Algorithm.EC -> when (purpose) { - KeyPurpose.ENCRYPT, KeyPurpose.DECRYPT -> KeystoreErrorCodes.unsupportedPurpose - KeyPurpose.AGREE_KEY -> null - else -> null - } - Algorithm.RSA -> when (purpose) { - KeyPurpose.AGREE_KEY -> KeystoreErrorCodes.unsupportedPurpose - else -> null - } - else -> null - } - } - - private fun checkTemporalValidity(keyParams: KeyMintAttestation, opParams: KeyMintAttestation): Int? { + private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? { val now = System.currentTimeMillis() - val purpose = opParams.purpose.firstOrNull() keyParams.activeDateTime?.let { activeDate -> if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid @@ -68,10 +66,11 @@ object AuthorizeCreate { return null } - private fun checkCallerNonce(keyParams: KeyMintAttestation, rawOpParams: Array?): Int? { + private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array?): Int? { + if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null if (keyParams.callerNonce == true) return null - val hasNonce = rawOpParams?.any { it.tag == Tag.NONCE } == true - if (hasNonce) return KeystoreErrorCodes.callerNonceProhibited + if (rawOpParams?.any { it.tag == Tag.NONCE } == true) + return KeystoreErrorCodes.callerNonceProhibited return null } } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index 69f3d57..795ac73 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -70,7 +70,7 @@ class KeyMintSecurityLevelInterceptor( GENERATE_KEY_TRANSACTION -> { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) - if (!shouldSkip) return handleGenerateKey(txId, callingUid, data) + if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data) } CREATE_OPERATION_TRANSACTION -> { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) @@ -247,29 +247,38 @@ class KeyMintSecurityLevelInterceptor( // 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 + val resolvedEntry: Map.Entry = + 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 + } + val key = KeyIdentifier(callingUid, alias) + generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } ?: run { + SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, 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") + Domain.KEY_ID -> { + val nspace = keyDescriptor.nspace + val entry = if (nspace == null || nspace == 0L) null + else generatedKeys.entries + .filter { it.key.uid == callingUid } + .find { it.value.nspace == nspace } + entry ?: 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 } } - 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 generatedKeyInfo = resolvedEntry.value + val resolvedKeyId = resolvedEntry.key trackAndEnforceOpLimit(callingUid, txId)?.let { return it } @@ -284,14 +293,52 @@ class KeyMintSecurityLevelInterceptor( else -> p.algorithm }) } + val forced = data.readBoolean() + + val requestedPurpose = parsedParams.purpose.firstOrNull() + if (requestedPurpose == null) { + return InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_INVALID_ARGUMENT) + } + + if (forced) { + return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED) + } AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode -> SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode") - return InterceptorUtils.createErrorReply(errorCode) + return InterceptorUtils.createServiceSpecificErrorReply(errorCode) } + val keyParams = generatedKeyInfo.keyParams + val effectiveParams = if (keyParams != null) { + keyParams.copy( + purpose = parsedParams.purpose, + digest = parsedParams.digest.ifEmpty { keyParams.digest }, + ) + } else parsedParams + val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams, opLatency) + val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, effectiveParams, opLatency) + + if (keyParams?.usageCountLimit != null) { + val limit = keyParams.usageCountLimit + val remaining = usageCounters.getOrPut(resolvedKeyId) { + java.util.concurrent.atomic.AtomicInteger(limit) + } + if (remaining.get() <= 0) { + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) + throw android.os.ServiceSpecificException(RESPONSE_KEY_NOT_FOUND) + } + softwareOperation.onFinishCallback = { + if (remaining.decrementAndGet() <= 0) { + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) + SystemLogger.info("Key $resolvedKeyId exhausted (USAGE_COUNT_LIMIT=$limit).") + } + } + } + val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID pruneOpsForUid(callingUid, softwareOperation, maxOps) val operationBinder = SoftwareOperationBinder(softwareOperation) @@ -315,7 +362,7 @@ class KeyMintSecurityLevelInterceptor( return InterceptorUtils.createTypedObjectReply(response) } - private fun handleGenerateKey(txId: Long, callingUid: Int, data: Parcel): TransactionResult { + private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { if (data.dataSize() > MAX_ALIAS_LENGTH) { SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes") return TransactionResult.ContinueAndSkipPost @@ -361,6 +408,21 @@ class KeyMintSecurityLevelInterceptor( return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) } + // AOSP security_level.rs:478-485: INCLUDE_UNIQUE_ID requires + // SELinux gen_unique_id OR Android REQUEST_UNIQUE_ID_ATTESTATION + if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) { + val hasSELinux = ConfigurationManager.checkSELinuxPermission( + callingPid, "keystore_key", "gen_unique_id", + ) + val hasAndroid = ConfigurationManager.hasPermissionForUid( + callingUid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION", + ) + if (!hasSELinux && !hasAndroid) { + SystemLogger.warning("[TX_ID: $txId] Rejecting INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid") + return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED) + } + } + val isSymmetric = parsedParams.algorithm == Algorithm.AES || parsedParams.algorithm == Algorithm.HMAC || parsedParams.algorithm == Algorithm.TRIPLE_DES @@ -668,7 +730,10 @@ class KeyMintSecurityLevelInterceptor( // Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead private const val MAX_ALIAS_LENGTH = 256 * 1024 private const val KEYMINT_INVALID_INPUT_LENGTH = -21 + private const val KEYMINT_INVALID_ARGUMENT = -38 private const val RESPONSE_INVALID_ARGUMENT = 20 + private const val RESPONSE_PERMISSION_DENIED = 6 + private const val RESPONSE_KEY_NOT_FOUND = 7 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 @@ -742,6 +807,7 @@ class KeyMintSecurityLevelInterceptor( val patchedChains = ConcurrentHashMap>() val attestationKeys: MutableSet = ConcurrentHashMap.newKeySet() val importedKeys: MutableSet = ConcurrentHashMap.newKeySet() + private val usageCounters = ConcurrentHashMap() private val interceptedOperations = ConcurrentHashMap() fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = @@ -771,6 +837,7 @@ class KeyMintSecurityLevelInterceptor( SystemLogger.debug("Remove cached attestaion key ${keyId}") } importedKeys.remove(keyId) + usageCounters.remove(keyId) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { @@ -796,6 +863,7 @@ class KeyMintSecurityLevelInterceptor( patchedChains.clear() attestationKeys.clear() importedKeys.clear() + usageCounters.clear() GeneratedKeyPersistence.deleteAll() SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt index bd3b32a..0b8af43 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt @@ -162,6 +162,8 @@ class SoftwareOperation( @Volatile var finalized = false private set + var onFinishCallback: (() -> Unit)? = null + val iv: ByteArray? get() = primitive.getIv() @@ -231,6 +233,7 @@ class SoftwareOperation( if (delayMs > 0) Thread.sleep(delayMs) } finalized = true + onFinishCallback?.invoke() SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") return result } catch (e: ServiceSpecificException) {