From f083824d8a747025dfd6d23439a2b85a67ce7890 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:23:52 +0100 Subject: [PATCH 01/15] Parse enforcement tags and add software operation updateAad scaffolding This commit combines the parser and metadata plumbing that the later software-operation work depends on: - derive KEY_SIZE from EC_CURVE when KEY_SIZE is absent - parse enforcement tags from key generation parameters into KeyMintAttestation - add patchAuthorizations() to rewrite patch-level authorizations alongside patched metadata - add updateAad() plumbing for software-backed operations It also widens GeneratedKeyInfo to carry parsed key parameters for the follow-on createOperation enforcement work. (cherry picked from commit e7676498052017a3c56e34808ed6fdfbbce2538b) (cherry picked from commit 4bc471363f8adbba685e350f1e8ba75efe6fe4ec) (cherry picked from commit 45ebf9a664d68ba34387b17d48c009d8996412ab) (cherry picked from commit c263ab1a7791a3a70a493054a19621fb78302d82) --- .../attestation/KeyMintAttestation.kt | 84 +++++++++++++++---- .../interception/keystore/InterceptorUtils.kt | 40 ++++++++- .../shim/KeyMintSecurityLevelInterceptor.kt | 15 +++- .../keystore/shim/SoftwareOperation.kt | 24 ++++++ .../TEESimulator/pki/CertificateGenerator.kt | 1 - .../TEESimulator/pki/CertificateHelper.kt | 4 + 6 files changed, 146 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt index acdb66a6..8e0dc882 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -41,60 +41,84 @@ data class KeyMintAttestation( val manufacturer: ByteArray?, val model: ByteArray?, val secondImei: ByteArray?, + // key_parameter.rs: l=855..1041 (RSA_OAEP_MGF_DIGEST through MAX_BOOT_LEVEL) + val userAuthType: Int? = null, + val userConfirmationRequired: Boolean? = null, + val activeDateTime: Date? = null, + val originationExpireDateTime: Date? = null, + val usageExpireDateTime: Date? = null, + val usageCountLimit: Int? = null, + val callerNonce: Boolean? = null, + val unlockedDeviceRequired: Boolean? = null, + val includeUniqueId: Boolean? = null, + val rollbackResistance: Boolean? = null, + val earlyBootOnly: Boolean? = null, + val allowWhileOnBody: Boolean? = null, + val trustedUserPresenceRequired: Boolean? = null, + val trustedConfirmationRequired: Boolean? = null, + val maxUsesPerBoot: Int? = null, + val maxBootLevel: Int? = null, + val minMacLength: Int? = null, + val rsaOaepMgfDigest: List = emptyList(), ) { /** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */ constructor( params: Array ) : this( - // AOSP: [key_param(tag = ALGORITHM, field = Algorithm)] + // AOSP: [key_param(tag = ALGORITHM, field = Algorithm)] (key_parameter.rs: l=837) algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0, // AOSP: [key_param(tag = KEY_SIZE, field = Integer)] // For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent. + // https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/ec_key_factory.cpp;l=54 keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(), - // AOSP: [key_param(tag = EC_CURVE, field = EcCurve)] + // AOSP: [key_param(tag = EC_CURVE, field = EcCurve)] (key_parameter.rs: l=871) ecCurve = params.findEcCurve(Tag.EC_CURVE), ecCurveName = params.deriveEcCurveName(), - // AOSP: [key_param(tag = ORIGIN, field = Origin)] + // AOSP: [key_param(tag = ORIGIN, field = Origin)] (key_parameter.rs: l=955) origin = params.findOrigin(Tag.ORIGIN), - // AOSP: [key_param(tag = NO_AUTH_REQUIRED, field = BoolValue)] + // AOSP: [key_param(tag = NO_AUTH_REQUIRED, field = BoolValue)] (key_parameter.rs: l=917) noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED), - // AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)] + // AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)] (key_parameter.rs: l=845) blockMode = params.findAllBlockMode(Tag.BLOCK_MODE), - // AOSP: [key_param(tag = PADDING, field = PaddingMode)] + // AOSP: [key_param(tag = PADDING, field = PaddingMode)] (key_parameter.rs: l=860) padding = params.findAllPaddingMode(Tag.PADDING), - // AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)] + // AOSP: [key_param(tag = PURPOSE, field = KeyPurpose)] (key_parameter.rs: l=832) purpose = params.findAllKeyPurpose(Tag.PURPOSE), - // AOSP: [key_param(tag = DIGEST, field = Digest)] + // AOSP: [key_param(tag = DIGEST, field = Digest)] (key_parameter.rs: l=850) digest = params.findAllDigests(Tag.DIGEST), - // AOSP: [key_param(tag = RSA_PUBLIC_EXPONENT, field = LongInteger)] + // AOSP: [key_param(tag = RSA_PUBLIC_EXPONENT, field = LongInteger)] (key_parameter.rs: + // l=874) rsaPublicExponent = params.findLongInteger(Tag.RSA_PUBLIC_EXPONENT), - // AOSP: [key_param(tag = CERTIFICATE_SERIAL, field = Blob)] + // AOSP: [key_param(tag = CERTIFICATE_SERIAL, field = Blob)] (key_parameter.rs: l=1028) certificateSerial = params.findBlob(Tag.CERTIFICATE_SERIAL)?.let { BigInteger(it) }, - // AOSP: [key_param(tag = CERTIFICATE_SUBJECT, field = Blob)] + // AOSP: [key_param(tag = CERTIFICATE_SUBJECT, field = Blob)] (key_parameter.rs: l=1032) certificateSubject = params.findBlob(Tag.CERTIFICATE_SUBJECT)?.let { X500Name(X500Principal(it).name) }, - // AOSP: [key_param(tag = CERTIFICATE_NOT_BEFORE, field = DateTime)] + // AOSP: [key_param(tag = CERTIFICATE_NOT_BEFORE, field = DateTime)] (key_parameter.rs: + // l=1035) certificateNotBefore = params.findDate(Tag.CERTIFICATE_NOT_BEFORE), - // AOSP: [key_param(tag = CERTIFICATE_NOT_AFTER, field = DateTime)] + // AOSP: [key_param(tag = CERTIFICATE_NOT_AFTER, field = DateTime)] (key_parameter.rs: + // l=1038) certificateNotAfter = params.findDate(Tag.CERTIFICATE_NOT_AFTER), - // AOSP: [key_param(tag = ATTESTATION_CHALLENGE, field = Blob)] + // AOSP: [key_param(tag = ATTESTATION_CHALLENGE, field = Blob)] (key_parameter.rs: l=970) attestationChallenge = params.findBlob(Tag.ATTESTATION_CHALLENGE), - // AOSP: [key_param(tag = ATTESTATION_ID_*, field = Blob)] + // AOSP: [key_param(tag = ATTESTATION_ID_*, field = Blob)] (key_parameter.rs: l=976, 991, + // 1000) brand = params.findBlob(Tag.ATTESTATION_ID_BRAND), device = params.findBlob(Tag.ATTESTATION_ID_DEVICE), product = params.findBlob(Tag.ATTESTATION_ID_PRODUCT), @@ -104,6 +128,27 @@ data class KeyMintAttestation( manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER), model = params.findBlob(Tag.ATTESTATION_ID_MODEL), secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI), + // Enforcement tags. + // key_parameter.rs: l=855..1041 (RSA_OAEP_MGF_DIGEST through MAX_BOOT_LEVEL) + userAuthType = params.findInteger(Tag.USER_AUTH_TYPE), + userConfirmationRequired = params.findBoolean(Tag.USER_SECURE_ID), + activeDateTime = params.findDate(Tag.ACTIVE_DATETIME), + originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME), + usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME), + usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT), + callerNonce = params.findBoolean(Tag.CALLER_NONCE), + unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED), + includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID), + rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE), + earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY), + allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY), + trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED), + trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED), + maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT), + maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL), + minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH), + // key_parameter.rs: l=855 + rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST), ) { // Log all parsed parameters for debugging purposes. params.forEach { KeyMintParameterLogger.logParameter(it) } @@ -168,12 +213,17 @@ private fun Array.findAllKeyPurpose(tag: Int): List = private fun Array.findAllDigests(tag: Int): List = this.filter { it.tag == tag }.map { it.value.digest } -/** Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. */ +/** + * Derives keySize from EC_CURVE tag when KEY_SIZE is not explicitly provided. + * + * https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/ec_key_factory.cpp;l=54 + */ private fun Array.deriveKeySizeFromCurve(): Int { val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0 return when (curveId) { EcCurve.P_224 -> 224 - EcCurve.P_256, EcCurve.CURVE_25519 -> 256 + EcCurve.P_256, + EcCurve.CURVE_25519 -> 256 EcCurve.P_384 -> 384 EcCurve.P_521 -> 521 else -> 0 diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index b0a87a17..d85c0fc0 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -1,11 +1,16 @@ package org.matrix.TEESimulator.interception.keystore +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyParameterValue +import android.hardware.security.keymint.Tag import android.os.Parcel import android.os.Parcelable import android.security.KeyStore import android.security.keystore.KeystoreResponse +import android.system.keystore2.Authorization import org.matrix.TEESimulator.interception.core.BinderInterceptor import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.util.AndroidDeviceUtils data class KeyIdentifier(val uid: Int, val alias: String) @@ -28,7 +33,6 @@ object InterceptorUtils { } } - /** Creates an `KeystoreResponse` parcel that indicates success with no data. */ fun createSuccessKeystoreResponse(): KeystoreResponse { val parcel = Parcel.obtain() try { @@ -41,6 +45,40 @@ object InterceptorUtils { } } + fun patchAuthorizations( + authorizations: Array?, + callingUid: Int, + ): Array? { + if (authorizations == null) return null + val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid) + val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid) + val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid) + + return authorizations + .mapNotNull { auth -> + val replacement = + when (auth.keyParameter.tag) { + Tag.OS_PATCHLEVEL -> osPatch + Tag.VENDOR_PATCHLEVEL -> vendorPatch + Tag.BOOT_PATCHLEVEL -> bootPatch + else -> return@mapNotNull auth + } + if (replacement == AndroidDeviceUtils.DO_NOT_REPORT) { + null + } else { + Authorization().apply { + securityLevel = auth.securityLevel + keyParameter = + KeyParameter().apply { + tag = auth.keyParameter.tag + value = KeyParameterValue.integer(replacement) + } + } + } + } + .toTypedArray() + } + /** Creates an `OverrideReply` parcel that indicates success with no data. */ fun createSuccessReply( writeResultCode: Boolean = true 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 ad9a04f5..394902d7 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 @@ -12,6 +12,7 @@ import java.security.KeyPair import java.security.SecureRandom import java.security.cert.Certificate import java.util.concurrent.ConcurrentHashMap +import javax.crypto.SecretKey import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager @@ -34,10 +35,18 @@ class KeyMintSecurityLevelInterceptor( // --- Data Structures for State Management --- data class GeneratedKeyInfo( - val keyPair: KeyPair, + val keyPair: KeyPair?, + val secretKey: SecretKey?, val nspace: Long, val response: KeyEntryResponse, - ) + val params: KeyMintAttestation, + ) { + constructor( + keyPair: KeyPair?, + nspace: Long, + response: KeyEntryResponse, + ) : this(keyPair, null, nspace, response, KeyMintAttestation(emptyArray())) + } override fun onPreTransact( txId: Long, @@ -211,7 +220,7 @@ class KeyMintSecurityLevelInterceptor( val params = data.createTypedArray(KeyParameter.CREATOR)!! val parsedParams = KeyMintAttestation(params) - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams) + val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair!!, parsedParams) val operationBinder = SoftwareOperationBinder(softwareOperation) val response = 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 79cd2c9a..f6beea8d 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 @@ -17,6 +17,8 @@ import org.matrix.TEESimulator.logging.SystemLogger // A sealed interface to represent the different cryptographic operations we can perform. private sealed interface CryptoPrimitive { + fun updateAad(data: ByteArray?) + fun update(data: ByteArray?): ByteArray? fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? @@ -82,6 +84,8 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi initSign(keyPair.private) } + override fun updateAad(data: ByteArray?) {} + override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) return null @@ -102,6 +106,8 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri initVerify(keyPair.public) } + override fun updateAad(data: ByteArray?) {} + override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) return null @@ -133,6 +139,10 @@ private class CipherPrimitive( init(opMode, key) } + override fun updateAad(data: ByteArray?) { + if (data != null) cipher.updateAAD(data) + } + override fun update(data: ByteArray?): ByteArray? = if (data != null) cipher.update(data) else null @@ -168,6 +178,15 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin } } + fun updateAad(data: ByteArray?) { + try { + primitive.updateAad(data) + } catch (e: Exception) { + SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e) + throw e + } + } + fun update(data: ByteArray?): ByteArray? { try { return primitive.update(data) @@ -199,6 +218,11 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { + @Throws(RemoteException::class) + override fun updateAad(aadInput: ByteArray?) { + operation.updateAad(aadInput) + } + @Throws(RemoteException::class) override fun update(input: ByteArray?): ByteArray? { return operation.update(input) diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt index 321d396b..6c8db053 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -8,7 +8,6 @@ import java.math.BigInteger import java.security.KeyPair import java.security.KeyPairGenerator import java.security.cert.Certificate -import java.security.cert.X509Certificate import java.security.interfaces.ECKey import java.security.interfaces.RSAKey import java.security.spec.ECGenParameterSpec diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt index fa929b70..d7fe547e 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateHelper.kt @@ -194,6 +194,10 @@ object CertificateHelper { * @param chain The new certificate chain to set. The leaf must be at index 0. * @return A [Result] indicating success or failure. */ + fun updateCertificateChain(metadata: KeyMetadata, chain: Array): Result { + return updateCertificateChain(0, metadata, chain) + } + fun updateCertificateChain( callingUid: Int, metadata: KeyMetadata, From af855ca86f3a78da812483285c11e28f14d5ab4e Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:45:24 +0100 Subject: [PATCH 02/15] Add operation lifecycle tracking, concurrency guard, and input limits Software operations now track finalization state and reject calls after finish/abort with INVALID_OPERATION_HANDLE, matching AOSP operation.rs outcome tracking. Errors during update/updateAad also finalize the operation. SoftwareOperationBinder wraps all methods in synchronized blocks to prevent concurrent access, matching AOSP's Mutex-protected KeystoreOperation wrapper that returns OPERATION_BUSY. Input data is validated against MAX_RECEIVE_DATA (32KB) on update, updateAad, and finish to match the AOSP-enforced limit. CryptoPrimitive gains getBeginParameters() for exposing begin-phase output (e.g. GCM nonce/IV) via CreateOperationResponse.parameters. (cherry picked from commit 962cef6aa83f54561c642e284aff58d73e7fb87a) --- .../keystore/shim/SoftwareOperation.kt | 93 +++++++++++++++++-- 1 file changed, 84 insertions(+), 9 deletions(-) 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 f6beea8d..e5dc7b93 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 @@ -3,10 +3,14 @@ package org.matrix.TEESimulator.interception.keystore.shim import android.hardware.security.keymint.Algorithm import android.hardware.security.keymint.BlockMode import android.hardware.security.keymint.Digest +import android.hardware.security.keymint.KeyParameter +import android.hardware.security.keymint.KeyParameterValue import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.PaddingMode +import android.hardware.security.keymint.Tag import android.os.RemoteException import android.system.keystore2.IKeystoreOperation +import android.system.keystore2.KeyParameters import java.security.KeyPair import java.security.Signature import java.security.SignatureException @@ -15,6 +19,12 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.SystemLogger +/* + * References: + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/operation.rs + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/security_level.rs + */ + // A sealed interface to represent the different cryptographic operations we can perform. private sealed interface CryptoPrimitive { fun updateAad(data: ByteArray?) @@ -24,6 +34,9 @@ private sealed interface CryptoPrimitive { fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? fun abort() + + /** Returns parameters from the begin phase (e.g. GCM nonce), or null if none. */ + fun getBeginParameters(): Array? = null } // Helper object to map KeyMint constants to JCA algorithm strings. @@ -150,19 +163,33 @@ private class CipherPrimitive( if (data != null) cipher.doFinal(data) else cipher.doFinal() override fun abort() {} + + /** Returns the cipher IV as a NONCE parameter for GCM operations. */ + override fun getBeginParameters(): Array? { + val iv = cipher.iv ?: return null + return arrayOf( + KeyParameter().apply { + tag = Tag.NONCE + value = KeyParameterValue.blob(iv) + } + ) + } } /** * A software-only implementation of a cryptographic operation. This class acts as a controller, * delegating to a specific cryptographic primitive based on the operation's purpose. + * + * Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls will throw. This + * matches AOSP keystore2 behavior where finalized operations return INVALID_OPERATION_HANDLE + * (operation.rs: l=26, 320). */ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) { - // This now holds the specific strategy object (Signer, Verifier, etc.) private val primitive: CryptoPrimitive + @Volatile private var finalized = false + init { - // The "Strategy" pattern: choose the implementation based on the purpose. - // For simplicity, we only consider the first purpose listed. val purpose = params.purpose.firstOrNull() val purposeName = KeyMintParameterLogger.purposeNames[purpose] ?: "UNKNOWN" SystemLogger.debug("[SoftwareOp TX_ID: $txId] Initializing for purpose: $purposeName.") @@ -178,63 +205,111 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin } } + /** Parameters produced during begin (e.g. GCM nonce), to populate CreateOperationResponse. */ + val beginParameters: KeyParameters? + // security_level.rs: l=402 + get() { + val params = primitive.getBeginParameters() ?: return null + if (params.isEmpty()) return null + return KeyParameters().apply { keyParameter = params } + } + + private fun checkActive() { + if (finalized) throw IllegalStateException("INVALID_OPERATION_HANDLE") + } + fun updateAad(data: ByteArray?) { + checkActive() try { primitive.updateAad(data) } catch (e: Exception) { + finalized = true SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e) throw e } } fun update(data: ByteArray?): ByteArray? { + checkActive() try { return primitive.update(data) } catch (e: Exception) { + finalized = true SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) throw e } } fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { + checkActive() try { val result = primitive.finish(data, signature) SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") return result } catch (e: Exception) { SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) - // Re-throw the exception so the binder can report it to the client. throw e + } finally { + finalized = true } } fun abort() { + checkActive() + finalized = true primitive.abort() SystemLogger.debug("[SoftwareOp TX_ID: $txId] Operation aborted.") } } -/** The Binder interface for our [SoftwareOperation]. */ +/** + * The Binder interface for [SoftwareOperation]. + * + * All methods are synchronized to prevent concurrent access, matching AOSP's Mutex-protected + * KeystoreOperation wrapper. Input data is validated against [MAX_RECEIVE_DATA] (32KB) to match + * AOSP's enforced limit (operation.rs: l=74, 216, 809). + */ class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { + private fun checkInputLength(data: ByteArray?) { + // operation.rs: l=337 + if (data != null && data.size > MAX_RECEIVE_DATA) + throw IllegalStateException("TOO_MUCH_DATA") + } + @Throws(RemoteException::class) override fun updateAad(aadInput: ByteArray?) { - operation.updateAad(aadInput) + synchronized(this) { + checkInputLength(aadInput) + operation.updateAad(aadInput) + } } @Throws(RemoteException::class) override fun update(input: ByteArray?): ByteArray? { - return operation.update(input) + synchronized(this) { + checkInputLength(input) + return operation.update(input) + } } @Throws(RemoteException::class) override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { - return operation.finish(input, signature) + synchronized(this) { + checkInputLength(input) + checkInputLength(signature) + return operation.finish(input, signature) + } } @Throws(RemoteException::class) override fun abort() { - operation.abort() + synchronized(this) { operation.abort() } + } + + companion object { + // operation.rs: l=216 + private const val MAX_RECEIVE_DATA = 0x8000 } } From 7f2c9f86ee08b2e94cf434069aa02ca8309a5dd8 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:46:57 +0100 Subject: [PATCH 03/15] Populate CreateOperationResponse.parameters for GCM operations AOSP returns begin_result.params in CreateOperationResponse.parameters, which contains the IV/nonce for AES-GCM encryption operations. Software operations previously left this field null, so clients expecting the server-generated IV from the response would not receive it. CipherPrimitive now exposes cipher.iv as a NONCE KeyParameter via getBeginParameters(), surfaced through SoftwareOperation.beginParameters and into the CreateOperationResponse. (cherry picked from commit da452cf2be91791c5a4dd5c7eea63fc3e29a8f3e) --- .../keystore/shim/KeyMintSecurityLevelInterceptor.kt | 3 +++ .../interception/keystore/shim/SoftwareOperation.kt | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) 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 394902d7..309b1f83 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 @@ -227,6 +227,9 @@ class KeyMintSecurityLevelInterceptor( CreateOperationResponse().apply { iOperation = operationBinder operationChallenge = null + // AOSP forwards begin_result.params into CreateOperationResponse.parameters. + // https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/security_level.rs;l=402 + parameters = softwareOperation.beginParameters } return InterceptorUtils.createTypedObjectReply(response) 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 e5dc7b93..e28998fd 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 @@ -164,7 +164,11 @@ private class CipherPrimitive( override fun abort() {} - /** Returns the cipher IV as a NONCE parameter for GCM operations. */ + /** + * Returns the cipher IV as a NONCE parameter for GCM operations. + * + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/security_level.rs;l=402 + */ override fun getBeginParameters(): Array? { val iv = cipher.iv ?: return null return arrayOf( From 4aae34d863caa1bb202b221548dffb129933c3e5 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:23:05 +0100 Subject: [PATCH 04/15] Use ServiceSpecificException with AOSP error codes for operation errors Replace ad-hoc operation exceptions with ServiceSpecificException so the software-backed binder path returns AOSP-compatible error codes. This commit also folds in the follow-up error-code cleanup: - set TOO_MUCH_DATA to the correct keystore2 response value (21) - add the missing AOSP error-code constants used by the software operation path - align finish/update/updateAad failure propagation with the later usage-limit and onFinishCallback flow (cherry picked from commit 0ebddedb265a6a25bf1de45da4b5d269240bd6ad) (cherry picked from commit 60d978d9e3138c4c8948a7ed6943ead419937f22) --- .../keystore/shim/SoftwareOperation.kt | 119 ++++++++++++++---- .../android/os/ServiceSpecificException.java | 21 ++++ 2 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 stub/src/main/java/android/os/ServiceSpecificException.java 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 e28998fd..7002cafd 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 @@ -9,11 +9,11 @@ import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.PaddingMode import android.hardware.security.keymint.Tag import android.os.RemoteException +import android.os.ServiceSpecificException import android.system.keystore2.IKeystoreOperation import android.system.keystore2.KeyParameters import java.security.KeyPair import java.security.Signature -import java.security.SignatureException import javax.crypto.Cipher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.logging.KeyMintParameterLogger @@ -24,6 +24,48 @@ import org.matrix.TEESimulator.logging.SystemLogger * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/operation.rs * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/security_level.rs */ +/** + * Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. + * + * Reference: + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/km_compat/km_compat_type_conversion.h + * Reference: + * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/aidl/android/security/authorization/ResponseCode.aidl + */ +private object KeystoreErrorCode { + /** km_compat_type_conversion.h: l=88 */ + const val INVALID_OPERATION_HANDLE = -28 + + /** km_compat_type_conversion.h: l=92 */ + const val VERIFICATION_FAILED = -30 + + /** km_compat_type_conversion.h: l=36 */ + const val UNSUPPORTED_PURPOSE = -2 + + /** ResponseCode.aidl: l=35 */ + const val SYSTEM_ERROR = 4 + + /** Keystore2 ResponseCode::TOO_MUCH_DATA */ + const val TOO_MUCH_DATA = 21 + + /** km_compat_type_conversion.h: l=82 */ + const val KEY_EXPIRED = -25 + + /** km_compat_type_conversion.h: l=80 */ + const val KEY_NOT_YET_VALID = -24 + + /** km_compat_type_conversion.h: l=138 */ + const val CALLER_NONCE_PROHIBITED = -55 + + /** km_compat_type_conversion.h: l=108 */ + const val INVALID_ARGUMENT = -38 + + /** ResponseCode.aidl: l=40 */ + const val PERMISSION_DENIED = 6 + + /** ResponseCode.aidl: l=45 */ + const val KEY_NOT_FOUND = 7 +} // A sealed interface to represent the different cryptographic operations we can perform. private sealed interface CryptoPrimitive { @@ -54,8 +96,9 @@ private object JcaAlgorithmMapper { Algorithm.EC -> "ECDSA" Algorithm.RSA -> "RSA" else -> - throw IllegalArgumentException( - "Unsupported signature algorithm: ${params.algorithm}" + throw ServiceSpecificException( + KeystoreErrorCode.SYSTEM_ERROR, + "Unsupported signature algorithm: ${params.algorithm}", ) } return "${digest}with${keyAlgo}" @@ -67,8 +110,9 @@ private object JcaAlgorithmMapper { Algorithm.RSA -> "RSA" Algorithm.AES -> "AES" else -> - throw IllegalArgumentException( - "Unsupported cipher algorithm: ${params.algorithm}" + throw ServiceSpecificException( + KeystoreErrorCode.SYSTEM_ERROR, + "Unsupported cipher algorithm: ${params.algorithm}", ) } val blockMode = @@ -128,12 +172,17 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { if (data != null) update(data) - if (signature == null) throw SignatureException("Signature to verify is null") + if (signature == null) + throw ServiceSpecificException( + KeystoreErrorCode.VERIFICATION_FAILED, + "Signature to verify is null", + ) if (!this.signature.verify(signature)) { - // Throwing an exception is how Keystore signals verification failure. - throw SignatureException("Signature verification failed") + throw ServiceSpecificException( + KeystoreErrorCode.VERIFICATION_FAILED, + "Signature/MAC verification failed", + ) } - // A successful verification returns no data. return null } @@ -164,11 +213,7 @@ private class CipherPrimitive( override fun abort() {} - /** - * Returns the cipher IV as a NONCE parameter for GCM operations. - * - * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/security_level.rs;l=402 - */ + /** Returns the cipher IV as a NONCE parameter for GCM operations. */ override fun getBeginParameters(): Array? { val iv = cipher.iv ?: return null return arrayOf( @@ -184,11 +229,17 @@ private class CipherPrimitive( * A software-only implementation of a cryptographic operation. This class acts as a controller, * delegating to a specific cryptographic primitive based on the operation's purpose. * - * Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls will throw. This - * matches AOSP keystore2 behavior where finalized operations return INVALID_OPERATION_HANDLE - * (operation.rs: l=26, 320). + * Tracks operation lifecycle: once [finish] or [abort] is called, subsequent calls throw + * [ServiceSpecificException] with [KeystoreErrorCode.INVALID_OPERATION_HANDLE]. This matches AOSP + * keystore2 behavior where finalized operations fail `check_active()` (operation.rs: l=26, 320). */ -class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMintAttestation) { +class SoftwareOperation( + private val txId: Long, + keyPair: KeyPair, + params: KeyMintAttestation, + /** Called after a successful finish(), used for USAGE_COUNT_LIMIT enforcement. */ + var onFinishCallback: (() -> Unit)? = null, +) { private val primitive: CryptoPrimitive @Volatile private var finalized = false @@ -205,7 +256,10 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) else -> - throw UnsupportedOperationException("Unsupported operation purpose: $purpose") + throw ServiceSpecificException( + KeystoreErrorCode.UNSUPPORTED_PURPOSE, + "Unsupported operation purpose: $purpose", + ) } } @@ -219,17 +273,24 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin } private fun checkActive() { - if (finalized) throw IllegalStateException("INVALID_OPERATION_HANDLE") + if (finalized) + throw ServiceSpecificException( + KeystoreErrorCode.INVALID_OPERATION_HANDLE, + "Operation already finalized.", + ) } fun updateAad(data: ByteArray?) { checkActive() try { primitive.updateAad(data) + } catch (e: ServiceSpecificException) { + finalized = true + throw e } catch (e: Exception) { finalized = true SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to updateAad.", e) - throw e + throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) } } @@ -237,10 +298,13 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin checkActive() try { return primitive.update(data) + } catch (e: ServiceSpecificException) { + finalized = true + throw e } catch (e: Exception) { finalized = true SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to update operation.", e) - throw e + throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) } } @@ -249,10 +313,13 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin try { val result = primitive.finish(data, signature) SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") + onFinishCallback?.invoke() return result + } catch (e: ServiceSpecificException) { + throw e } catch (e: Exception) { SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) - throw e + throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) } finally { finalized = true } @@ -271,7 +338,9 @@ class SoftwareOperation(private val txId: Long, keyPair: KeyPair, params: KeyMin * * All methods are synchronized to prevent concurrent access, matching AOSP's Mutex-protected * KeystoreOperation wrapper. Input data is validated against [MAX_RECEIVE_DATA] (32KB) to match - * AOSP's enforced limit (operation.rs: l=74, 216, 809). + * AOSP's enforced limit. All errors are reported as [ServiceSpecificException] with AOSP-compatible + * numeric error codes, matching the wire format produced by AOSP's `into_binder()` (operation.rs: + * l=74, 216, 809). */ class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { @@ -279,7 +348,7 @@ class SoftwareOperationBinder(private val operation: SoftwareOperation) : private fun checkInputLength(data: ByteArray?) { // operation.rs: l=337 if (data != null && data.size > MAX_RECEIVE_DATA) - throw IllegalStateException("TOO_MUCH_DATA") + throw ServiceSpecificException(KeystoreErrorCode.TOO_MUCH_DATA) } @Throws(RemoteException::class) diff --git a/stub/src/main/java/android/os/ServiceSpecificException.java b/stub/src/main/java/android/os/ServiceSpecificException.java new file mode 100644 index 00000000..f0a93432 --- /dev/null +++ b/stub/src/main/java/android/os/ServiceSpecificException.java @@ -0,0 +1,21 @@ +package android.os; + +/** + * Stub for android.os.ServiceSpecificException. + * + *

Used by AIDL-generated binder stubs to report service-specific errors with numeric codes. + * The binder framework serializes this as EX_SERVICE_SPECIFIC on the wire, preserving the integer + * error code for the client. + */ +public class ServiceSpecificException extends RuntimeException { + public final int errorCode; + + public ServiceSpecificException(int errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + public ServiceSpecificException(int errorCode) { + this(errorCode, null); + } +} From e8169d896a5517291837a374867070eeba674e3f Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 17:47:14 +0100 Subject: [PATCH 05/15] Implement AOSP enforcements.rs authorize_create for software operations Software-generated keys now enforce the same operation policies as AOSP keystore2 authorize_create(). - Missing PURPOSE rejected with INVALID_ARGUMENT (-38) - Incompatible PURPOSE rejected with INCOMPATIBLE_PURPOSE (-3) - Forced operations rejected with PERMISSION_DENIED (6) - ACTIVE_DATETIME in future rejected with KEY_NOT_YET_VALID (-24) - ORIGINATION_EXPIRE past rejected with KEY_EXPIRED (-25) for SIGN/ENCRYPT - USAGE_EXPIRE past rejected with KEY_EXPIRED (-25) for DECRYPT/VERIFY - CALLER_NONCE without permission rejected with CALLER_NONCE_PROHIBITED (-55) - USAGE_COUNT_LIMIT enforced on finish via callback; key deleted on exhaustion Store KeyMintAttestation in GeneratedKeyInfo so enforcement checks can access the original key parameters during createOperation. This commit combines four steps from the same authorize_create evolution path: - 3078ea9 introduced the main AOSP authorize_create enforcement flow. - 50cd77f added the earlier purpose validation and caller-provided CREATION_DATETIME rejection that were later folded into the aligned validation path. - 07c98bc contributed the follow-up fixes around operation parameter handling and usage tracking that now live in the final createOperation enforcement implementation. - 2bc46be refined the unsupported-purpose and usage-count-limit enforcement edge cases. (cherry picked from commit 3078ea9db9ac7a246f120a5743fa796e82cee9c1) (cherry picked from commit 50cd77f5f40537de370164e7c9ea62a935640ec9) (cherry picked from commit 07c98bcd06487209e9cc5e640165c11193575cfb) (cherry picked from commit 2bc46beac5e7bf645527e72a78d8437f50691ec6) --- .../interception/keystore/InterceptorUtils.kt | 22 +++ .../keystore/Keystore2Interceptor.kt | 1 + .../shim/KeyMintSecurityLevelInterceptor.kt | 154 ++++++++++++++++-- .../keystore/shim/SoftwareOperation.kt | 5 +- 4 files changed, 167 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index d85c0fc0..c85d430a 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -129,6 +129,28 @@ object InterceptorUtils { return BinderInterceptor.TransactionResult.OverrideReply(parcel) } + /** + * Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error + * code. Uses the C++ binder::Status wire format which includes a remote stack trace header + * between the message and the error code. Java's Parcel.writeException omits this header, + * making it incompatible with native C++ AIDL clients on Android 12+. + * + * Wire format: [int32 exceptionCode] [String16 message] [int32 stackTraceSize=0] [int32 + * errorCode] + */ + fun createServiceSpecificErrorReply( + errorCode: Int + ): BinderInterceptor.TransactionResult.OverrideReply { + val parcel = + Parcel.obtain().apply { + writeInt(-8) // EX_SERVICE_SPECIFIC + writeString(null) // message (null → writeInt(-1) as String16 null marker) + writeInt(0) // remote stack trace header size (empty) + writeInt(errorCode) // service-specific error code + } + return BinderInterceptor.TransactionResult.OverrideReply(parcel) + } + /** * Extracts the base alias from a potentially prefixed alias string. For example, it converts * "USRCERT_my_key" to "my_key". diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index d123a6f2..ab1723d4 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -264,6 +264,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { keyData.first, key.nspace, response, + parsedParameters, ) KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId) return InterceptorUtils.createTypedObjectReply(response) 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 309b1f83..430093b2 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 @@ -3,6 +3,7 @@ package org.matrix.TEESimulator.interception.keystore.shim import android.hardware.security.keymint.KeyOrigin import android.hardware.security.keymint.KeyParameter import android.hardware.security.keymint.KeyParameterValue +import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.Tag import android.os.IBinder @@ -39,8 +40,15 @@ class KeyMintSecurityLevelInterceptor( val secretKey: SecretKey?, val nspace: Long, val response: KeyEntryResponse, - val params: KeyMintAttestation, + val keyParams: KeyMintAttestation, ) { + constructor( + keyPair: KeyPair?, + nspace: Long, + response: KeyEntryResponse, + keyParams: KeyMintAttestation, + ) : this(keyPair, null, nspace, response, keyParams) + constructor( keyPair: KeyPair?, nspace: Long, @@ -191,6 +199,8 @@ class KeyMintSecurityLevelInterceptor( * Handles the `createOperation` transaction. It checks if the operation is for a key that was * generated in software. If so, it creates a software-based operation handler. Otherwise, it * lets the call proceed to the real hardware service. + * + * References: enforcements.rs: l=382 security_level.rs: l=402 */ private fun handleCreateOperation( txId: Long, @@ -217,22 +227,128 @@ class KeyMintSecurityLevelInterceptor( SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for KeyId $nspace.") - val params = data.createTypedArray(KeyParameter.CREATOR)!! - val parsedParams = KeyMintAttestation(params) + val opParams = data.createTypedArray(KeyParameter.CREATOR)!! + val parsedOpParams = KeyMintAttestation(opParams) + val forced = data.readBoolean() + + // AOSP authorize_create parity for purpose checks, date validity, caller nonce, + // and deferred USAGE_COUNT_LIMIT accounting (enforcements.rs: l=382). + val keyParams = generatedKeyInfo.keyParams + + // F14: Missing PURPOSE → INVALID_ARGUMENT (-38) + val requestedPurpose = parsedOpParams.purpose.firstOrNull() + if (requestedPurpose == null) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INVALID_ARGUMENT + ) + } + + // F9: Forced op without permission → PERMISSION_DENIED (6) + if (forced) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.PERMISSION_DENIED + ) + } + + // F1: PURPOSE not in key's allowed purposes → INCOMPATIBLE_PURPOSE (-3) + if (requestedPurpose !in keyParams.purpose) { + SystemLogger.info( + "[TX_ID: $txId] Rejecting: purpose $requestedPurpose not in ${keyParams.purpose}" + ) + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INCOMPATIBLE_PURPOSE + ) + } + + // F4: ACTIVE_DATETIME — KEY_NOT_YET_VALID (-24) + keyParams.activeDateTime?.let { activeDate -> + if (System.currentTimeMillis() < activeDate.time) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.KEY_NOT_YET_VALID + ) + } + } - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair!!, parsedParams) - val operationBinder = SoftwareOperationBinder(softwareOperation) + // F2: ORIGINATION_EXPIRE_DATETIME — KEY_EXPIRED (-25) for SIGN/ENCRYPT only + // enforcements.rs: l=487 + keyParams.originationExpireDateTime?.let { expireDate -> + if ( + (requestedPurpose == KeyPurpose.SIGN || requestedPurpose == KeyPurpose.ENCRYPT) && + System.currentTimeMillis() > expireDate.time + ) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.KEY_EXPIRED + ) + } + } - val response = - CreateOperationResponse().apply { - iOperation = operationBinder - operationChallenge = null - // AOSP forwards begin_result.params into CreateOperationResponse.parameters. - // https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/security_level.rs;l=402 - parameters = softwareOperation.beginParameters + // F3: USAGE_EXPIRE_DATETIME — KEY_EXPIRED (-25) for DECRYPT/VERIFY only + // enforcements.rs: l=494 + keyParams.usageExpireDateTime?.let { expireDate -> + if ( + (requestedPurpose == KeyPurpose.DECRYPT || requestedPurpose == KeyPurpose.VERIFY) && + System.currentTimeMillis() > expireDate.time + ) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.KEY_EXPIRED + ) } + } + + // F7: CALLER_NONCE — CALLER_NONCE_PROHIBITED (-55) + if ( + (requestedPurpose == KeyPurpose.SIGN || requestedPurpose == KeyPurpose.ENCRYPT) && + keyParams.callerNonce != true && + opParams.any { it.tag == Tag.NONCE } + ) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.CALLER_NONCE_PROHIBITED + ) + } + + return runCatching { + val softwareOperation = + SoftwareOperation(txId, generatedKeyInfo.keyPair!!, parsedOpParams) + + // F11: USAGE_COUNT_LIMIT — decrement on finish, delete key when exhausted. + // AOSP tracks this in database via check_and_update_key_usage_count on + // after_finish (enforcements.rs: l=510). + keyParams.usageCountLimit?.let { limit -> + val keyId = + generatedKeys.entries.find { it.value === generatedKeyInfo }?.key + ?: return@let + val remaining = + usageCounters.getOrPut(keyId) { + java.util.concurrent.atomic.AtomicInteger(limit) + } + softwareOperation.onFinishCallback = { + if (remaining.decrementAndGet() <= 0) { + cleanupKeyData(keyId) + usageCounters.remove(keyId) + SystemLogger.info("Key $keyId exhausted (USAGE_COUNT_LIMIT=$limit).") + } + } + } + + val operationBinder = SoftwareOperationBinder(softwareOperation) + val response = + CreateOperationResponse().apply { + iOperation = operationBinder + operationChallenge = null + // AOSP forwards begin_result.params into + // CreateOperationResponse.parameters (security_level.rs: l=402). + parameters = softwareOperation.beginParameters + } - return InterceptorUtils.createTypedObjectReply(response) + InterceptorUtils.createTypedObjectReply(response) + } + .getOrElse { e -> + SystemLogger.error("[TX_ID: $txId] Failed to create software operation.", e) + InterceptorUtils.createServiceSpecificErrorReply( + if (e is android.os.ServiceSpecificException) e.errorCode + else KeystoreErrorCode.SYSTEM_ERROR + ) + } } /** @@ -287,7 +403,12 @@ class KeyMintSecurityLevelInterceptor( keyDescriptor, ) generatedKeys[keyId] = - GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response) + GeneratedKeyInfo( + keyData.first, + keyDescriptor.nspace, + response, + parsedParams, + ) if (isAttestKeyRequest) attestationKeys.add(keyId) // Return the metadata of our generated key, skipping the real hardware call. @@ -366,6 +487,9 @@ class KeyMintSecurityLevelInterceptor( val attestationKeys = ConcurrentHashMap.newKeySet() // Caches patched certificate chains to prevent re-generation and signature inconsistencies. val patchedChains = ConcurrentHashMap>() + // Tracks remaining usage count per key for USAGE_COUNT_LIMIT enforcement. + private val usageCounters = + ConcurrentHashMap() // Stores interceptors for active cryptographic operations. private val interceptedOperations = ConcurrentHashMap() @@ -405,6 +529,7 @@ class KeyMintSecurityLevelInterceptor( if (attestationKeys.remove(keyId)) { SystemLogger.debug("Remove cached attestaion key ${keyId}") } + usageCounters.remove(keyId) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { @@ -423,6 +548,7 @@ class KeyMintSecurityLevelInterceptor( generatedKeys.clear() patchedChains.clear() attestationKeys.clear() + usageCounters.clear() 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 7002cafd..620e1247 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 @@ -32,7 +32,7 @@ import org.matrix.TEESimulator.logging.SystemLogger * Reference: * https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/aidl/android/security/authorization/ResponseCode.aidl */ -private object KeystoreErrorCode { +object KeystoreErrorCode { /** km_compat_type_conversion.h: l=88 */ const val INVALID_OPERATION_HANDLE = -28 @@ -42,6 +42,9 @@ private object KeystoreErrorCode { /** km_compat_type_conversion.h: l=36 */ const val UNSUPPORTED_PURPOSE = -2 + /** km_compat_type_conversion.h: l=38 */ + const val INCOMPATIBLE_PURPOSE = -3 + /** ResponseCode.aidl: l=35 */ const val SYSTEM_ERROR = 4 From 124f9bb39c29db5fbbe3928fc66cafd89d7c7a8c Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 18:10:59 +0100 Subject: [PATCH 06/15] Handle Domain.APP in createOperation for software-generated keys handleCreateOperation only accepted Domain.KEY_ID descriptors, rejecting Domain.APP with ContinueAndSkipPost. Native callers and the Android framework can call createOperation with Domain.APP + alias, which was being forwarded to hardware where the software-generated key doesn't exist, resulting in KEY_NOT_FOUND for all operation enforcement tests. Add alias-based lookup from generatedKeys when domain is APP, matching AOSP's create_operation which resolves all domain types via database. (cherry picked from commit 890ee705152cc33e459d0dac2a87f119f9ec8c5a) --- .../shim/KeyMintSecurityLevelInterceptor.kt | 50 +++++++++++++++---- 1 file changed, 39 insertions(+), 11 deletions(-) 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 430093b2..ee1a94aa 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 @@ -1,5 +1,6 @@ package org.matrix.TEESimulator.interception.keystore.shim +import android.hardware.security.keymint.Algorithm import android.hardware.security.keymint.KeyOrigin import android.hardware.security.keymint.KeyParameter import android.hardware.security.keymint.KeyParameterValue @@ -210,22 +211,30 @@ class KeyMintSecurityLevelInterceptor( 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 - } - - val nspace = keyDescriptor.nspace - val generatedKeyInfo = findGeneratedKeyByKeyId(callingUid, nspace) + // AOSP createOperation accepts Domain::APP (alias), Domain::KEY_ID (nspace), + // Domain::SELINUX, and Domain::BLOB. Resolve to our generated key by trying + // both alias-based and nspace-based lookups (database.rs: l=2060, 2123). + val generatedKeyInfo = + when (keyDescriptor.domain) { + Domain.KEY_ID -> findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) + Domain.APP -> + keyDescriptor.alias?.let { alias -> + generatedKeys[KeyIdentifier(callingUid, alias)] + } + else -> null + } if (generatedKeyInfo == null) { SystemLogger.debug( - "[TX_ID: $txId] Operation for unknown/hardware KeyId ($nspace). Forwarding." + "[TX_ID: $txId] Operation for unknown/hardware key (domain=${keyDescriptor.domain}, " + + "alias=${keyDescriptor.alias}, nspace=${keyDescriptor.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 key ${generatedKeyInfo.nspace}." + ) val opParams = data.createTypedArray(KeyParameter.CREATOR)!! val parsedOpParams = KeyMintAttestation(opParams) @@ -250,6 +259,18 @@ class KeyMintSecurityLevelInterceptor( ) } + // F8/F13: AOSP rejects VERIFY/ENCRYPT for asymmetric keys at the HAL level + // with UNSUPPORTED_PURPOSE (-2), distinct from INCOMPATIBLE_PURPOSE (-3). + val algorithm = keyParams.algorithm + if ( + (algorithm == Algorithm.EC || algorithm == Algorithm.RSA) && + (requestedPurpose == KeyPurpose.VERIFY || requestedPurpose == KeyPurpose.ENCRYPT) + ) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.UNSUPPORTED_PURPOSE + ) + } + // F1: PURPOSE not in key's allowed purposes → INCOMPATIBLE_PURPOSE (-3) if (requestedPurpose !in keyParams.purpose) { SystemLogger.info( @@ -315,12 +336,19 @@ class KeyMintSecurityLevelInterceptor( // after_finish (enforcements.rs: l=510). keyParams.usageCountLimit?.let { limit -> val keyId = - generatedKeys.entries.find { it.value === generatedKeyInfo }?.key - ?: return@let + generatedKeys.entries + .find { it.value.nspace == generatedKeyInfo.nspace } + ?.key ?: return@let val remaining = usageCounters.getOrPut(keyId) { java.util.concurrent.atomic.AtomicInteger(limit) } + // Check if already exhausted before creating the operation + if (remaining.get() <= 0) { + cleanupKeyData(keyId) + usageCounters.remove(keyId) + throw android.os.ServiceSpecificException(KeystoreErrorCode.KEY_NOT_FOUND) + } softwareOperation.onFinishCallback = { if (remaining.decrementAndGet() <= 0) { cleanupKeyData(keyId) From 804a5f85f890089f405fa1694df97d2c5b26e18f Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Wed, 18 Mar 2026 19:02:09 +0100 Subject: [PATCH 07/15] Filter intercepted transaction codes and gate device ID attestation checks This commit combines two related pieces of plumbing around the keystore2 interception path: - filter binder transaction codes at native registration time to avoid unnecessary Java round-trips on unintercepted calls - add READ_PRIVILEGED_PHONE_STATE-based permission checks before rejecting device ID attestation tags during generateKey The native filtering applies to IKeystoreService, IKeystoreSecurityLevel, and IKeystoreOperation registrations. The permission gating adds ConfigurationManager.hasPermissionForUid(), the IPackageManager checkPermission() stub, and the generateKey-side device-ID attestation validation path. (cherry picked from commit ca3fcbc3885a9ddf52a2748b0c6f4fdd6386e45a) (cherry picked from commit ed987685e207d25fecfe0b05e6311c7e8d8f805b) --- app/src/main/cpp/binder_interceptor.cpp | 33 ++++++++++--- .../config/ConfigurationManager.kt | 12 +++++ .../interception/core/BinderInterceptor.kt | 20 ++++++-- .../keystore/AbstractKeystoreInterceptor.kt | 8 +++- .../keystore/Keystore2Interceptor.kt | 25 +++++++++- .../shim/KeyMintSecurityLevelInterceptor.kt | 46 ++++++++++++++++++- .../keystore/shim/OperationInterceptor.kt | 3 ++ .../android/content/pm/IPackageManager.java | 2 + 8 files changed, 136 insertions(+), 13 deletions(-) diff --git a/app/src/main/cpp/binder_interceptor.cpp b/app/src/main/cpp/binder_interceptor.cpp index bc2b2b5f..8eef4ed7 100644 --- a/app/src/main/cpp/binder_interceptor.cpp +++ b/app/src/main/cpp/binder_interceptor.cpp @@ -235,6 +235,8 @@ class BinderInterceptor : public BBinder { struct RegistrationEntry { wp target; sp callback_interface; + // Transaction codes to intercept. Empty = intercept all (legacy behavior). + std::vector filtered_codes; }; // Reader-Writer lock for the registry to allow concurrent reads (lookups) @@ -244,10 +246,15 @@ class BinderInterceptor : public BBinder { public: BinderInterceptor() = default; - // Checks if a specific Binder instance is currently registered for interception - bool isBinderIntercepted(const wp &target) const { + // Checks if a specific Binder+code combination should be intercepted. + // Returns true if the binder is registered AND the code is in its filter + // (or the filter is empty, meaning intercept everything). + bool shouldIntercept(const wp &target, uint32_t code) const { std::shared_lock lock(registry_mutex_); - return registry_.find(target) != registry_.end(); + auto it = registry_.find(target); + if (it == registry_.end()) return false; + const auto &codes = it->second.filtered_codes; + return codes.empty() || std::find(codes.begin(), codes.end(), code) != codes.end(); } // Main entry point for processing the "Man-in-the-Middle" logic @@ -386,7 +393,7 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { // This is safe because we are holding a strong reference. wp wp_target = target_binder_ptr; - if (g_interceptor_instance->isBinderIntercepted(wp_target)) { + if (g_interceptor_instance->shouldIntercept(wp_target, txn_data->code)) { info.transaction_code = txn_data->code; info.target_binder = wp_target; // Assign the valid weak pointer hijack = true; @@ -537,12 +544,26 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) { return BAD_TYPE; } + // Read optional transaction code filter. If present: int32 count + count * uint32 codes. + // If absent or count <= 0: intercept all transaction codes (legacy behavior). + std::vector codes; + int32_t code_count = 0; + if (data.dataAvail() >= sizeof(int32_t) && data.readInt32(&code_count) == OK && code_count > 0) { + codes.reserve(code_count); + for (int32_t i = 0; i < code_count; i++) { + uint32_t c = 0; + if (data.readUint32(&c) == OK) codes.push_back(c); + } + LOGI("Interceptor registered for binder %p with %zu filtered codes", target.get(), codes.size()); + } else { + LOGI("Interceptor registered for binder %p (all codes)", target.get()); + } + wp weak_target = target; std::unique_lock lock(registry_mutex_); - registry_[weak_target] = {weak_target, callback}; + registry_[weak_target] = {weak_target, callback, std::move(codes)}; - LOGI("Interceptor registered for binder %p", target.get()); return OK; } diff --git a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt index dacda945..6847928e 100644 --- a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -351,6 +351,18 @@ object ConfigurationManager { return iPackageManager } + /** Checks if any package belonging to the UID holds the given permission. */ + fun hasPermissionForUid(uid: Int, permission: String): Boolean { + val userId = uid / 100000 + return getPackagesForUid(uid).any { pkg -> + try { + getPackageManager()?.checkPermission(permission, pkg, userId) == 0 + } catch (_: Exception) { + false + } + } + } + /** Retrieves the package names associated with a UID. */ fun getPackagesForUid(uid: Int): Array { return uidToPackagesCache.getOrPut(uid) { diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt index 370141b8..c51c5a8f 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt @@ -293,15 +293,29 @@ abstract class BinderInterceptor : Binder() { } } - /** Uses the backdoor binder to register an interceptor for a specific target service. */ - fun register(backdoor: IBinder, target: IBinder, interceptor: BinderInterceptor) { + /** + * Uses the backdoor binder to register an interceptor for a specific target service. + * + * @param filteredCodes If non-empty, only these transaction codes will be intercepted at + * the native level. All other codes pass through without the round-trip to Java. + */ + fun register( + backdoor: IBinder, + target: IBinder, + interceptor: BinderInterceptor, + filteredCodes: IntArray = intArrayOf(), + ) { val data = Parcel.obtain() val reply = Parcel.obtain() try { data.writeStrongBinder(target) data.writeStrongBinder(interceptor) + data.writeInt(filteredCodes.size) + for (code in filteredCodes) data.writeInt(code) backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0) - SystemLogger.info("Registered interceptor for target: $target") + SystemLogger.info( + "Registered interceptor for target: $target (${filteredCodes.size} filtered codes)" + ) } catch (e: Exception) { SystemLogger.error("Failed to register binder interceptor.", e) } finally { diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt index d080fb68..826dbabe 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/AbstractKeystoreInterceptor.kt @@ -68,11 +68,17 @@ abstract class AbstractKeystoreInterceptor : BinderInterceptor() { } } + /** + * Transaction codes this interceptor needs to handle at the native level. Override in + * subclasses to filter; empty means intercept everything (legacy behavior). + */ + protected open val interceptedCodes: IntArray = intArrayOf() + /** Registers this interceptor with the native hook layer and sets up a death recipient. */ private fun setupInterceptor(service: IBinder, backdoor: IBinder) { keystoreService = service SystemLogger.info("Registering interceptor for service: $serviceName") - register(backdoor, service, this) + register(backdoor, service, this, interceptedCodes) service.linkToDeath(createDeathRecipient(), 0) onInterceptorReady(service, backdoor) } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index ab1723d4..aaebb10f 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -57,6 +57,17 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { override val processName = "keystore2" override val injectionCommand = "exec ./inject `pidof keystore2` libTEESimulator.so entry" + override val interceptedCodes: IntArray by lazy { + listOfNotNull( + GET_KEY_ENTRY_TRANSACTION, + DELETE_KEY_TRANSACTION, + UPDATE_SUBCOMPONENT_TRANSACTION, + LIST_ENTRIES_TRANSACTION, + LIST_ENTRIES_BATCHED_TRANSACTION, + ) + .toIntArray() + } + /** * This method is called once the main service is hooked. It proceeds to find and hook the * security level sub-services (e.g., TEE, StrongBox). @@ -73,7 +84,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { SystemLogger.info("Found TEE SecurityLevel. Registering interceptor...") val interceptor = KeyMintSecurityLevelInterceptor(tee, SecurityLevel.TRUSTED_ENVIRONMENT) - register(backdoor, tee.asBinder(), interceptor) + register( + backdoor, + tee.asBinder(), + interceptor, + KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES, + ) } } .onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) } @@ -84,7 +100,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { SystemLogger.info("Found StrongBox SecurityLevel. Registering interceptor...") val interceptor = KeyMintSecurityLevelInterceptor(strongbox, SecurityLevel.STRONGBOX) - register(backdoor, strongbox.asBinder(), interceptor) + register( + backdoor, + strongbox.asBinder(), + interceptor, + KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES, + ) } } .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } 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 ee1a94aa..4e70dc5b 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 @@ -151,7 +151,12 @@ class KeyMintSecurityLevelInterceptor( val backdoor = getBackdoor(target) if (backdoor != null) { val interceptor = OperationInterceptor(operation, backdoor) - register(backdoor, operationBinder, interceptor) + register( + backdoor, + operationBinder, + interceptor, + OperationInterceptor.INTERCEPTED_CODES, + ) interceptedOperations[operationBinder] = interceptor } else { SystemLogger.error( @@ -392,6 +397,35 @@ class KeyMintSecurityLevelInterceptor( "Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}" ) val params = data.createTypedArray(KeyParameter.CREATOR)!! + + // AOSP add_required_parameters parity for CREATION_DATETIME rejection + // and device-ID attestation permission checks + // (security_level.rs: l=416, 424; utils.rs: l=115). + if (params.any { it.tag == Tag.CREATION_DATETIME }) { + return@runCatching InterceptorUtils.createServiceSpecificErrorReply( + INVALID_ARGUMENT + ) + } + + // Device ID attestation requires READ_PRIVILEGED_PHONE_STATE. + val hasDeviceIdTags = + params.any { + it.tag == Tag.ATTESTATION_ID_SERIAL || + it.tag == Tag.ATTESTATION_ID_IMEI || + it.tag == Tag.ATTESTATION_ID_MEID || + it.tag == Tag.DEVICE_UNIQUE_ATTESTATION + } + if ( + hasDeviceIdTags && + !ConfigurationManager.hasPermissionForUid( + callingUid, + "android.permission.READ_PRIVILEGED_PHONE_STATE", + ) + ) { + return@runCatching InterceptorUtils.createServiceSpecificErrorReply( + CANNOT_ATTEST_IDS + ) + } val parsedParams = KeyMintAttestation(params) val isAttestKeyRequest = parsedParams.isAttestKey() @@ -487,6 +521,8 @@ class KeyMintSecurityLevelInterceptor( companion object { private val secureRandom = SecureRandom() + private const val INVALID_ARGUMENT = 20 + private const val CANNOT_ATTEST_IDS = -66 // Transaction codes for IKeystoreSecurityLevel interface. private val GENERATE_KEY_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") @@ -498,6 +534,14 @@ class KeyMintSecurityLevelInterceptor( "createOperation", ) + /** Only these transaction codes need native-level interception. */ + val INTERCEPTED_CODES = + intArrayOf( + GENERATE_KEY_TRANSACTION, + IMPORT_KEY_TRANSACTION, + CREATE_OPERATION_TRANSACTION, + ) + private val transactionNames: Map by lazy { IKeystoreSecurityLevel.Stub::class .java diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt index 91e0dde6..adfd18db 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt @@ -44,6 +44,9 @@ class OperationInterceptor( private val ABORT_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreOperation.Stub::class.java, "abort") + /** Only intercept finish/abort for cleanup. Other ops pass through without round-trip. */ + val INTERCEPTED_CODES = intArrayOf(FINISH_TRANSACTION, ABORT_TRANSACTION) + private val transactionNames: Map by lazy { IKeystoreOperation.Stub::class .java diff --git a/stub/src/main/java/android/content/pm/IPackageManager.java b/stub/src/main/java/android/content/pm/IPackageManager.java index 2b0b97b6..7356f826 100644 --- a/stub/src/main/java/android/content/pm/IPackageManager.java +++ b/stub/src/main/java/android/content/pm/IPackageManager.java @@ -13,6 +13,8 @@ public interface IPackageManager { ParceledListSlice getInstalledPackages(long flags, int userId); + int checkPermission(String permName, String pkgName, int userId); + class Stub { public static IPackageManager asInterface(IBinder binder) { throw new UnsupportedOperationException("STUB!"); From 4e3b4b9ba6cecc6f3e00f89065698c2f3a159bb9 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Thu, 19 Mar 2026 02:46:06 +0100 Subject: [PATCH 08/15] Reject AGREE_KEY for all non-EC algorithms with UNSUPPORTED_PURPOSE AOSP enforcements.rs rejects AGREE_KEY for any algorithm that is not EC, not just RSA. Restructure the unsupported purpose check to match the exact authorize_create decision tree. (cherry picked from commit 509d15752f581aa7487b9a3b637486aa105da9bf) --- .../shim/KeyMintSecurityLevelInterceptor.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 4e70dc5b..24d88e97 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 @@ -264,13 +264,16 @@ class KeyMintSecurityLevelInterceptor( ) } - // F8/F13: AOSP rejects VERIFY/ENCRYPT for asymmetric keys at the HAL level - // with UNSUPPORTED_PURPOSE (-2), distinct from INCOMPATIBLE_PURPOSE (-3). + // F8/F13: AOSP rejects VERIFY/ENCRYPT for asymmetric keys, and rejects + // AGREE_KEY for any non-EC algorithm, with UNSUPPORTED_PURPOSE (-2). val algorithm = keyParams.algorithm - if ( - (algorithm == Algorithm.EC || algorithm == Algorithm.RSA) && - (requestedPurpose == KeyPurpose.VERIFY || requestedPurpose == KeyPurpose.ENCRYPT) - ) { + val isAsymmetric = algorithm == Algorithm.EC || algorithm == Algorithm.RSA + val unsupported = + (isAsymmetric && + (requestedPurpose == KeyPurpose.VERIFY || + requestedPurpose == KeyPurpose.ENCRYPT)) || + (requestedPurpose == KeyPurpose.AGREE_KEY && algorithm != Algorithm.EC) + if (unsupported) { return InterceptorUtils.createServiceSpecificErrorReply( KeystoreErrorCode.UNSUPPORTED_PURPOSE ) From 7780c7bfce81be85f06fe7bae96c0ebdbe0e4641 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Thu, 19 Mar 2026 03:27:26 +0100 Subject: [PATCH 09/15] Support symmetric key generation (AES, HMAC) in software mode AES and HMAC keys were failing in GENERATE mode because doSoftwareGeneration only handled asymmetric key pairs. Generate symmetric keys via javax.crypto.KeyGenerator and return KeyMetadata without certificates (symmetric keys have no cert chain). Store SecretKey in GeneratedKeyInfo alongside KeyPair. Update SoftwareOperation and CipherPrimitive to accept either key type. (cherry picked from commit d3bf3c8a70aff34ce6f10ca138ecea5cfe9a7cb2) --- .../keystore/Keystore2Interceptor.kt | 1 + .../shim/KeyMintSecurityLevelInterceptor.kt | 75 ++++++++++++++++++- .../keystore/shim/SoftwareOperation.kt | 24 +++--- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index aaebb10f..7b61672b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -283,6 +283,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( keyData.first, + null, key.nspace, response, parsedParameters, 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 24d88e97..8edea87b 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 @@ -336,8 +336,21 @@ class KeyMintSecurityLevelInterceptor( } return runCatching { + val effectiveParams = + keyParams.copy( + purpose = parsedOpParams.purpose, + digest = parsedOpParams.digest.ifEmpty { keyParams.digest }, + blockMode = parsedOpParams.blockMode.ifEmpty { keyParams.blockMode }, + padding = parsedOpParams.padding.ifEmpty { keyParams.padding }, + ) val softwareOperation = - SoftwareOperation(txId, generatedKeyInfo.keyPair!!, parsedOpParams) + SoftwareOperation( + txId, + generatedKeyInfo.keyPair, + generatedKeyInfo.secretKey, + effectiveParams, + opParams, + ) // F11: USAGE_COUNT_LIMIT — decrement on finish, delete key when exhausted. // AOSP tracks this in database via check_and_update_key_usage_count on @@ -446,6 +459,63 @@ class KeyMintSecurityLevelInterceptor( "Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]." ) + // Software generation follows the same high-level generateKey path as + // security_level.rs, but substitutes our local key material and metadata + // (security_level.rs: l=123). + val isSymmetric = + parsedParams.algorithm != Algorithm.EC && + parsedParams.algorithm != Algorithm.RSA + + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + cleanupKeyData(keyId) + + if (isSymmetric) { + val algoName = + when (parsedParams.algorithm) { + Algorithm.AES -> "AES" + Algorithm.HMAC -> "HmacSHA256" + else -> + throw android.os.ServiceSpecificException( + KeystoreErrorCode.SYSTEM_ERROR, + "Unsupported symmetric algorithm: ${parsedParams.algorithm}", + ) + } + val keyGen = javax.crypto.KeyGenerator.getInstance(algoName) + keyGen.init(parsedParams.keySize) + val secretKey = keyGen.generateKey() + + val metadata = + KeyMetadata().apply { + keySecurityLevel = securityLevel + key = + KeyDescriptor().apply { + domain = Domain.KEY_ID + nspace = keyDescriptor.nspace + alias = null + blob = null + } + certificate = null + certificateChain = null + authorizations = + parsedParams.toAuthorizations(callingUid, securityLevel) + modificationTimeMs = System.currentTimeMillis() + } + val response = + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + generatedKeys[keyId] = + GeneratedKeyInfo( + null, + secretKey, + keyDescriptor.nspace, + response, + parsedParams, + ) + return@runCatching InterceptorUtils.createTypedObjectReply(metadata) + } + // Generate the key pair and certificate chain. val keyData = CertificateGenerator.generateAttestedKeyPair( @@ -456,9 +526,6 @@ class KeyMintSecurityLevelInterceptor( securityLevel, ) ?: throw Exception("CertificateGenerator failed to create key pair.") - val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) - // It is unnecessary but a good practice to clean up possible caches - cleanupKeyData(keyId) // Store the generated key data. val response = buildKeyEntryResponse( 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 620e1247..003dcb25 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 @@ -194,14 +194,13 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri // Concrete implementation for Encryption/Decryption. private class CipherPrimitive( - keyPair: KeyPair, + cryptoKey: java.security.Key, params: KeyMintAttestation, private val opMode: Int, ) : CryptoPrimitive { private val cipher: Cipher = Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { - val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private - init(opMode, key) + init(opMode, cryptoKey) } override fun updateAad(data: ByteArray?) { @@ -238,9 +237,10 @@ private class CipherPrimitive( */ class SoftwareOperation( private val txId: Long, - keyPair: KeyPair, + keyPair: KeyPair?, + secretKey: javax.crypto.SecretKey?, params: KeyMintAttestation, - /** Called after a successful finish(), used for USAGE_COUNT_LIMIT enforcement. */ + opParams: Array = emptyArray(), var onFinishCallback: (() -> Unit)? = null, ) { private val primitive: CryptoPrimitive @@ -254,10 +254,16 @@ class SoftwareOperation( primitive = when (purpose) { - KeyPurpose.SIGN -> Signer(keyPair, params) - KeyPurpose.VERIFY -> Verifier(keyPair, params) - KeyPurpose.ENCRYPT -> CipherPrimitive(keyPair, params, Cipher.ENCRYPT_MODE) - KeyPurpose.DECRYPT -> CipherPrimitive(keyPair, params, Cipher.DECRYPT_MODE) + KeyPurpose.SIGN -> Signer(keyPair!!, params) + KeyPurpose.VERIFY -> Verifier(keyPair!!, params) + KeyPurpose.ENCRYPT -> { + val key: java.security.Key = secretKey ?: keyPair!!.public + CipherPrimitive(key, params, Cipher.ENCRYPT_MODE) + } + KeyPurpose.DECRYPT -> { + val key: java.security.Key = secretKey ?: keyPair!!.private + CipherPrimitive(key, params, Cipher.DECRYPT_MODE) + } else -> throw ServiceSpecificException( KeystoreErrorCode.UNSUPPORTED_PURPOSE, From 8e94056fa182443876401dd679bbb39754c41882 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <1@mhmrdd.me> Date: Thu, 19 Mar 2026 06:45:44 +0100 Subject: [PATCH 10/15] Align KeyMetadata authorizations and operation semantics with AOSP (cherry picked from commit 492d6dcf45fc4efe43439f6c2d071039a01e470f) --- .../shim/KeyMintSecurityLevelInterceptor.kt | 86 +++++++++++++++---- .../keystore/shim/SoftwareOperation.kt | 43 +++++++++- 2 files changed, 111 insertions(+), 18 deletions(-) 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 8edea87b..8f4f2a23 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 @@ -243,7 +243,7 @@ class KeyMintSecurityLevelInterceptor( val opParams = data.createTypedArray(KeyParameter.CREATOR)!! val parsedOpParams = KeyMintAttestation(opParams) - val forced = data.readBoolean() + data.readBoolean() // forced: no-op for sw ops // AOSP authorize_create parity for purpose checks, date validity, caller nonce, // and deferred USAGE_COUNT_LIMIT accounting (enforcements.rs: l=382). @@ -257,13 +257,6 @@ class KeyMintSecurityLevelInterceptor( ) } - // F9: Forced op without permission → PERMISSION_DENIED (6) - if (forced) { - return InterceptorUtils.createServiceSpecificErrorReply( - KeystoreErrorCode.PERMISSION_DENIED - ) - } - // F8/F13: AOSP rejects VERIFY/ENCRYPT for asymmetric keys, and rejects // AGREE_KEY for any non-EC algorithm, with UNSUPPORTED_PURPOSE (-2). val algorithm = keyParams.algorithm @@ -699,6 +692,8 @@ class KeyMintSecurityLevelInterceptor( /** * Extension function to convert parsed `KeyMintAttestation` parameters back into an array of * `Authorization` objects for the fake `KeyMetadata`. + * + * References: security_level.rs: l=123, 165 */ private fun KeyMintAttestation.toAuthorizations( callingUid: Int, @@ -754,6 +749,40 @@ private fun KeyMintAttestation.toAuthorizations( ) } + if (this.callerNonce == true) { + authList.add(createAuth(Tag.CALLER_NONCE, KeyParameterValue.boolValue(true))) + } + if (this.minMacLength != null) { + authList.add(createAuth(Tag.MIN_MAC_LENGTH, KeyParameterValue.integer(this.minMacLength))) + } + if (this.rollbackResistance == true) { + authList.add(createAuth(Tag.ROLLBACK_RESISTANCE, KeyParameterValue.boolValue(true))) + } + if (this.earlyBootOnly == true) { + authList.add(createAuth(Tag.EARLY_BOOT_ONLY, KeyParameterValue.boolValue(true))) + } + if (this.allowWhileOnBody == true) { + authList.add(createAuth(Tag.ALLOW_WHILE_ON_BODY, KeyParameterValue.boolValue(true))) + } + if (this.trustedUserPresenceRequired == true) { + authList.add( + createAuth(Tag.TRUSTED_USER_PRESENCE_REQUIRED, KeyParameterValue.boolValue(true)) + ) + } + if (this.trustedConfirmationRequired == true) { + authList.add( + createAuth(Tag.TRUSTED_CONFIRMATION_REQUIRED, KeyParameterValue.boolValue(true)) + ) + } + if (this.maxUsesPerBoot != null) { + authList.add( + createAuth(Tag.MAX_USES_PER_BOOT, KeyParameterValue.integer(this.maxUsesPerBoot)) + ) + } + if (this.maxBootLevel != null) { + authList.add(createAuth(Tag.MAX_BOOT_LEVEL, KeyParameterValue.integer(this.maxBootLevel))) + } + authList.add( createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)) ) @@ -771,18 +800,43 @@ private fun KeyMintAttestation.toAuthorizations( val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid) authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) + // Software-enforced tags: CREATION_DATETIME, enforcement dates, USER_ID + // (security_level.rs: l=165, 436). + fun createSwAuth(tag: Int, value: KeyParameterValue): Authorization { + val param = + KeyParameter().apply { + this.tag = tag + this.value = value + } + return Authorization().apply { + this.keyParameter = param + this.securityLevel = SecurityLevel.SOFTWARE + } + } + authList.add( - createAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())) + createSwAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())) ) - // AOSP class android.os.UserHandle: PER_USER_RANGE = 100000; - authList.add( - createAuth( - Tag.USER_ID, - KeyParameterValue.integer(callingUid / 100000), - SecurityLevel.SOFTWARE, + this.activeDateTime?.let { + authList.add(createSwAuth(Tag.ACTIVE_DATETIME, KeyParameterValue.dateTime(it.time))) + } + this.originationExpireDateTime?.let { + authList.add( + createSwAuth(Tag.ORIGINATION_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)) ) - ) + } + this.usageExpireDateTime?.let { + authList.add(createSwAuth(Tag.USAGE_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time))) + } + this.usageCountLimit?.let { + authList.add(createSwAuth(Tag.USAGE_COUNT_LIMIT, KeyParameterValue.integer(it))) + } + if (this.unlockedDeviceRequired == true) { + authList.add(createSwAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true))) + } + + authList.add(createSwAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000))) return authList.toTypedArray() } 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 003dcb25..93922f41 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 @@ -63,6 +63,13 @@ object KeystoreErrorCode { /** km_compat_type_conversion.h: l=108 */ const val INVALID_ARGUMENT = -38 + /** + * KeyMint ErrorCode::INVALID_TAG + * + * km_compat_type_conversion.h: l=112 + */ + const val INVALID_TAG = -40 + /** ResponseCode.aidl: l=40 */ const val PERMISSION_DENIED = 6 @@ -144,7 +151,9 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi initSign(keyPair.private) } - override fun updateAad(data: ByteArray?) {} + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) @@ -166,7 +175,9 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri initVerify(keyPair.public) } - override fun updateAad(data: ByteArray?) {} + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) @@ -227,6 +238,33 @@ private class CipherPrimitive( } } +// Concrete implementation for ECDH Key Agreement. +private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive { + private val agreement: javax.crypto.KeyAgreement = + javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) } + + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } + + override fun update(data: ByteArray?): ByteArray? = null + + override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { + if (data == null) + throw ServiceSpecificException( + KeystoreErrorCode.INVALID_ARGUMENT, + "Peer public key required for key agreement", + ) + val peerKey = + java.security.KeyFactory.getInstance("EC") + .generatePublic(java.security.spec.X509EncodedKeySpec(data)) + agreement.doPhase(peerKey, true) + return agreement.generateSecret() + } + + override fun abort() {} +} + /** * A software-only implementation of a cryptographic operation. This class acts as a controller, * delegating to a specific cryptographic primitive based on the operation's purpose. @@ -264,6 +302,7 @@ class SoftwareOperation( val key: java.security.Key = secretKey ?: keyPair!!.private CipherPrimitive(key, params, Cipher.DECRYPT_MODE) } + KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!) else -> throw ServiceSpecificException( KeystoreErrorCode.UNSUPPORTED_PURPOSE, From 0ac2a2b3c153d52ab83a0d4b0e910bbd245753b2 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:16:10 +0100 Subject: [PATCH 11/15] Handle IV/nonce, OAEP, GCM tags, CTR mode, and ECDH in software operations (cherry picked from commit 25538dacb153be62a7424d2ef23050f9b07b5160) --- .../keystore/shim/SoftwareOperation.kt | 76 ++++++++++++++++--- 1 file changed, 65 insertions(+), 11 deletions(-) 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 93922f41..8e2694cc 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 @@ -91,8 +91,10 @@ private sealed interface CryptoPrimitive { fun getBeginParameters(): Array? = null } -// Helper object to map KeyMint constants to JCA algorithm strings. +// Helper object to map KeyMint parameters onto the simulated software primitives we expose via JCA. private object JcaAlgorithmMapper { + // AOSP sign operations derive the effective signature behavior from the key algorithm and the + // selected digest (ecdsa_operation.cpp: l=79; rsa_operation.cpp: l=63). fun mapSignatureAlgorithm(params: KeyMintAttestation): String { val digest = when (params.digest.firstOrNull()) { @@ -114,6 +116,8 @@ private object JcaAlgorithmMapper { return "${digest}with${keyAlgo}" } + // AOSP cipher operations derive behavior from algorithm, block mode, and padding tags + // (block_cipher_operation.cpp: l=39, 79; rsa_operation.cpp: l=66). fun mapCipherAlgorithm(params: KeyMintAttestation): String { val keyAlgo = when (params.algorithm) { @@ -129,6 +133,7 @@ private object JcaAlgorithmMapper { when (params.blockMode.firstOrNull()) { BlockMode.ECB -> "ECB" BlockMode.CBC -> "CBC" + BlockMode.CTR -> "CTR" BlockMode.GCM -> "GCM" else -> "ECB" // Default for RSA } @@ -144,7 +149,8 @@ private object JcaAlgorithmMapper { } } -// Concrete implementation for Signing. +// Concrete implementation for Signing +// (ecdsa_operation.cpp: l=138; rsa_operation.cpp: l=305). private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private val signature: Signature = Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { @@ -168,7 +174,8 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi override fun abort() {} } -// Concrete implementation for Verification. +// Concrete implementation for Verification +// (ecdsa_operation.cpp: l=273; rsa_operation.cpp: l=438). private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private val signature: Signature = Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { @@ -203,15 +210,46 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri override fun abort() {} } -// Concrete implementation for Encryption/Decryption. +// Concrete implementation for Encryption/Decryption (block_cipher_operation.cpp: l=79). private class CipherPrimitive( cryptoKey: java.security.Key, params: KeyMintAttestation, private val opMode: Int, + nonce: ByteArray?, + macLength: Int?, ) : CryptoPrimitive { private val cipher: Cipher = Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { - init(opMode, cryptoKey) + val algSpec = + when { + nonce != null && params.blockMode.contains(BlockMode.GCM) -> + javax.crypto.spec.GCMParameterSpec((macLength ?: 128), nonce) + params.padding.contains(PaddingMode.RSA_OAEP) -> { + val mgfDigest = + when (params.rsaOaepMgfDigest.firstOrNull()) { + Digest.SHA_2_256 -> "SHA-256" + Digest.SHA_2_384 -> "SHA-384" + Digest.SHA_2_512 -> "SHA-512" + else -> "SHA-1" + } + val mainDigest = + when (params.digest.firstOrNull()) { + Digest.SHA_2_256 -> "SHA-256" + Digest.SHA_2_384 -> "SHA-384" + Digest.SHA_2_512 -> "SHA-512" + else -> "SHA-1" + } + javax.crypto.spec.OAEPParameterSpec( + mainDigest, + "MGF1", + java.security.spec.MGF1ParameterSpec(mgfDigest), + javax.crypto.spec.PSource.PSpecified.DEFAULT, + ) + } + nonce != null -> javax.crypto.spec.IvParameterSpec(nonce) + else -> null + } + if (algSpec != null) init(opMode, cryptoKey, algSpec) else init(opMode, cryptoKey) } override fun updateAad(data: ByteArray?) { @@ -221,8 +259,16 @@ private class CipherPrimitive( override fun update(data: ByteArray?): ByteArray? = if (data != null) cipher.update(data) else null - override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? = - if (data != null) cipher.doFinal(data) else cipher.doFinal() + override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { + return try { + if (data != null) cipher.doFinal(data) else cipher.doFinal() + } catch (e: javax.crypto.AEADBadTagException) { + throw ServiceSpecificException( + KeystoreErrorCode.VERIFICATION_FAILED, + "GCM tag verification failed", + ) + } + } override fun abort() {} @@ -238,7 +284,7 @@ private class CipherPrimitive( } } -// Concrete implementation for ECDH Key Agreement. +// Concrete implementation for ECDH Key Agreement (ecdh_operation.cpp: l=52). private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive { private val agreement: javax.crypto.KeyAgreement = javax.crypto.KeyAgreement.getInstance("ECDH").apply { init(keyPair.private) } @@ -296,11 +342,15 @@ class SoftwareOperation( KeyPurpose.VERIFY -> Verifier(keyPair!!, params) KeyPurpose.ENCRYPT -> { val key: java.security.Key = secretKey ?: keyPair!!.public - CipherPrimitive(key, params, Cipher.ENCRYPT_MODE) + val nonce = opParams.find { it.tag == Tag.NONCE }?.value?.blob + val macLen = opParams.find { it.tag == Tag.MAC_LENGTH }?.value?.integer + CipherPrimitive(key, params, Cipher.ENCRYPT_MODE, nonce, macLen) } KeyPurpose.DECRYPT -> { val key: java.security.Key = secretKey ?: keyPair!!.private - CipherPrimitive(key, params, Cipher.DECRYPT_MODE) + val nonce = opParams.find { it.tag == Tag.NONCE }?.value?.blob + val macLen = opParams.find { it.tag == Tag.MAC_LENGTH }?.value?.integer + CipherPrimitive(key, params, Cipher.DECRYPT_MODE, nonce, macLen) } KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!) else -> @@ -361,7 +411,11 @@ class SoftwareOperation( try { val result = primitive.finish(data, signature) SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") - onFinishCallback?.invoke() + try { + onFinishCallback?.invoke() + } catch (e: Exception) { + SystemLogger.error("[SoftwareOp TX_ID: $txId] onFinishCallback failed.", e) + } return result } catch (e: ServiceSpecificException) { throw e From 74263c44cae4ab0cf07d206539c9dc961f2063ca Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:16:39 +0100 Subject: [PATCH 12/15] Fix silent error paths, challenge error code, and null handling (cherry picked from commit d839b6932382d3a2d1242df08fba79762ba9c468) --- .../TEESimulator/attestation/AttestationBuilder.kt | 9 +++++---- .../interception/keystore/InterceptorUtils.kt | 11 ++++++++--- .../interception/keystore/ListEntriesHandler.kt | 2 +- .../keystore/shim/OperationInterceptor.kt | 7 ++++++- .../matrix/TEESimulator/pki/CertificateGenerator.kt | 4 ++-- 5 files changed, 22 insertions(+), 11 deletions(-) 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 f4bfc3f5..1f10b88c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -418,10 +418,11 @@ object AttestationBuilder { ) ) - // Collect unique signature digests from the signing history. - packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature -> - val digest = sha256.digest(signature.toByteArray()) - signatureDigests.add(Digest(digest)) + val certs = + packageInfo.signingInfo?.signingCertificateHistory + ?: packageInfo.signingInfo?.apkContentsSigners + certs?.forEach { signature -> + signatureDigests.add(Digest(sha256.digest(signature.toByteArray()))) } } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index c85d430a..8779fe7c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -168,8 +168,13 @@ object InterceptorUtils { /** Checks if a reply parcel contains an exception without consuming it. */ fun hasException(reply: Parcel): Boolean { - val exception = runCatching { reply.readException() }.exceptionOrNull() - if (exception != null) reply.setDataPosition(0) - return exception != null + val pos = reply.dataPosition() + return try { + reply.readException() + false + } catch (_: Exception) { + reply.setDataPosition(pos) + true + } } } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt index 2cad788a..dae4ecbc 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/ListEntriesHandler.kt @@ -81,7 +81,7 @@ object ListEntriesHandler { // 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 originalList = reply.createTypedArray(KeyDescriptor.CREATOR) ?: emptyArray() val mergedArray = mergeKeyDescriptors(originalList, keysToInject) // Limit response size to avoid binder buffer overflow. diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt index adfd18db..b0748dc9 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt @@ -5,6 +5,7 @@ import android.os.Parcel import android.system.keystore2.IKeystoreOperation import org.matrix.TEESimulator.interception.core.BinderInterceptor import org.matrix.TEESimulator.interception.keystore.InterceptorUtils +import org.matrix.TEESimulator.logging.SystemLogger /** * Intercepts calls to an `IKeystoreOperation` service. This is used to log the data manipulation @@ -28,7 +29,11 @@ class OperationInterceptor( logTransaction(txId, methodName, callingUid, callingPid, true) if (code == FINISH_TRANSACTION || code == ABORT_TRANSACTION) { - KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor) + try { + KeyMintSecurityLevelInterceptor.removeOperationInterceptor(target, backdoor) + } catch (e: Exception) { + SystemLogger.error("[TX_ID: $txId] Failed to unregister operation interceptor.", e) + } } return TransactionResult.ContinueAndSkipPost diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt index 6c8db053..92bf635e 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -92,8 +92,8 @@ object CertificateGenerator { ): List? { val challenge = params.attestationChallenge if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) - throw IllegalArgumentException( - "Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})" + throw android.os.ServiceSpecificException( + -21 // INVALID_INPUT_LENGTH (KM_ERROR_INVALID_INPUT_LENGTH) ) return runCatching { From 9a4cbc2b854316b7d75e58b0a20cc4382085ea21 Mon Sep 17 00:00:00 2001 From: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> Date: Sat, 21 Mar 2026 23:16:53 +0100 Subject: [PATCH 13/15] Add dir class to sepolicy and crash safety for binder interceptors (cherry picked from commit 7a7e36222b9f489b3cef735838429a43975daf81) --- app/src/main/cpp/binder_interceptor.cpp | 47 ++++++++++++++++--- .../interception/core/BinderInterceptor.kt | 12 +++++ module/sepolicy.rule | 3 +- 3 files changed, 55 insertions(+), 7 deletions(-) diff --git a/app/src/main/cpp/binder_interceptor.cpp b/app/src/main/cpp/binder_interceptor.cpp index 8eef4ed7..79ded3ec 100644 --- a/app/src/main/cpp/binder_interceptor.cpp +++ b/app/src/main/cpp/binder_interceptor.cpp @@ -314,7 +314,11 @@ class BinderStub : public BBinder { } if (!found_context) { - LOGW("BinderStub received transaction but no context found for thread"); + LOGW("BinderStub received transaction but no context found for thread (code=%u)", code); +#ifndef NDEBUG + std::lock_guard dbg_lock(g_thread_context_mutex); + LOGW(" Thread context map has %zu entries", g_thread_context_map.size()); +#endif return UNKNOWN_TRANSACTION; } @@ -400,6 +404,9 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { } // Manually release the temporary strong reference we acquired at the start. target_binder_ptr->decStrong(nullptr); + } else { + LOGD("[Hook] attemptIncStrong failed for target %p (code=%u, uid=%d) — binder may be dying", + reinterpret_cast(txn_data->target.ptr), txn_data->code, txn_data->sender_euid); } } @@ -416,7 +423,13 @@ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { // Store context for the stub to retrieve later in its onTransact std::lock_guard lock(g_thread_context_mutex); - g_thread_context_map[std::this_thread::get_id()].push(std::move(info)); + auto &queue = g_thread_context_map[std::this_thread::get_id()]; + queue.push(std::move(info)); +#ifndef NDEBUG + if (queue.size() > 8) { + LOGW("[Hook] Thread context queue depth=%zu for thread — possible leak", queue.size()); + } +#endif } } @@ -613,8 +626,24 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sptransact(intercept::kPreTransact, pre_req, &pre_resp) != OK) { - LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id); +#ifndef NDEBUG + struct timespec ts_start{}; + clock_gettime(CLOCK_MONOTONIC, &ts_start); +#endif + + status_t pre_cb_status = callback->transact(intercept::kPreTransact, pre_req, &pre_resp); + +#ifndef NDEBUG + struct timespec ts_end{}; + clock_gettime(CLOCK_MONOTONIC, &ts_end); + double pre_ms = (ts_end.tv_sec - ts_start.tv_sec) * 1000.0 + (ts_end.tv_nsec - ts_start.tv_nsec) / 1e6; + if (pre_ms > 5000.0) { + LOGW("[TX_ID: %" PRIu64 "] Pre-callback took %.0fms (code=%u) — possible hang", tx_id, pre_ms, code); + } +#endif + + if (pre_cb_status != OK) { + LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (status=%d). Forwarding original call.", tx_id, pre_cb_status); return false; // Callback failed, proceed as if not intercepted } @@ -648,8 +677,10 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sptransact(intercept::kPostTransact, post_req, &post_resp) == OK) { + status_t post_cb_status = callback->transact(intercept::kPostTransact, post_req, &post_resp); + if (post_cb_status == OK) { int32_t post_action = post_resp.readInt32(); if (post_action == intercept::kActionOverrideReply && reply) { result = post_resp.readInt32(); // Read new status @@ -676,6 +708,9 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, spsetDataSize(0); // Clear original reply VALIDATE_STATUS(tx_id, reply->appendFrom(&post_resp, post_resp.dataPosition(), new_size)); } + } else { + LOGW("[TX_ID: %" PRIu64 "] Post-transaction callback failed (status=%d, code=%u). Using original reply.", + tx_id, post_cb_status, code); } return true; // We handled the flow, even if we just forwarded it diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt index c51c5a8f..ac85153f 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/core/BinderInterceptor.kt @@ -148,6 +148,12 @@ abstract class BinderInterceptor : Binder() { callingPid, transactionData, ) + } catch (e: Exception) { + SystemLogger.error( + "[TX_ID: $txId] onPreTransact crashed (code=$transactionCode, uid=$callingUid)", + e, + ) + TransactionResult.ContinueAndSkipPost } finally { transactionData.recycle() } @@ -191,6 +197,12 @@ abstract class BinderInterceptor : Binder() { reply, resultCode, ) + } catch (e: Exception) { + SystemLogger.error( + "[TX_ID: $txId] onPostTransact crashed (code=$transactionCode, uid=$callingUid, resultCode=${data.readInt()})", + e, + ) + TransactionResult.SkipTransaction } finally { transactionData.recycle() transactionReply.recycle() diff --git a/module/sepolicy.rule b/module/sepolicy.rule index 21b522ac..50e02fdd 100644 --- a/module/sepolicy.rule +++ b/module/sepolicy.rule @@ -1,2 +1,3 @@ -allow keystore {adb_data_file shell_data_file} file * +allow keystore {adb_data_file shell_data_file} {file dir} * allow crash_dump keystore process * +allow crash_dump keystore {dir file lnk_file} * From b2d4194224dcafe8ff6a4d5f6427f022f557b3a3 Mon Sep 17 00:00:00 2001 From: XiaoTong6666 <3278671549@qq.com> Date: Mon, 30 Mar 2026 07:02:20 +0800 Subject: [PATCH 14/15] Align patched metadata and createOperation bookkeeping Patched certificate chains now update KeyMetadata.authorizations, and createOperation bookkeeping follows the later aligned key-resolution path. - PATCH-mode certificate updates now patch authorizations alongside the certificate chain - createOperation usage tracking now resolves counters by the resolved key id - this builds on the earlier Domain.APP key-resolution path already introduced in the existing createOperation history This commit combines follow-up steps from the same metadata and createOperation alignment path: - 45ebf9a patched authorizations alongside certificate chains in PATCH mode. - 07c98bc contributed the follow-up fixes around operation parameter handling and usage tracking that now live in the aligned createOperation bookkeeping path. (cherry picked from commit 45ebf9a664d68ba34387b17d48c009d8996412ab) (cherry picked from commit 07c98bcd06487209e9cc5e640165c11193575cfb) Co-authored-by: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> --- .../interception/keystore/InterceptorUtils.kt | 23 ++++++----- .../keystore/Keystore2Interceptor.kt | 10 +++++ .../shim/KeyMintSecurityLevelInterceptor.kt | 40 ++++++++++++------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index 8779fe7c..18a83ffe 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt @@ -50,30 +50,35 @@ object InterceptorUtils { callingUid: Int, ): Array? { if (authorizations == null) return null + val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid) val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid) val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid) return authorizations - .mapNotNull { auth -> + .map { auth -> val replacement = when (auth.keyParameter.tag) { - Tag.OS_PATCHLEVEL -> osPatch - Tag.VENDOR_PATCHLEVEL -> vendorPatch - Tag.BOOT_PATCHLEVEL -> bootPatch - else -> return@mapNotNull auth + Tag.OS_PATCHLEVEL -> + if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) osPatch else null + Tag.VENDOR_PATCHLEVEL -> + if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) vendorPatch + else null + Tag.BOOT_PATCHLEVEL -> + if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) bootPatch else null + else -> null } - if (replacement == AndroidDeviceUtils.DO_NOT_REPORT) { - null - } else { + if (replacement != null) { Authorization().apply { - securityLevel = auth.securityLevel keyParameter = KeyParameter().apply { tag = auth.keyParameter.tag value = KeyParameterValue.integer(replacement) } + securityLevel = auth.securityLevel } + } else { + auth } } .toTypedArray() diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index 7b61672b..9e4359c2 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -277,6 +277,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { keyData.second.toTypedArray(), ) .getOrThrow() + response.metadata.authorizations = + InterceptorUtils.patchAuthorizations( + response.metadata.authorizations, + callingUid, + ) val key = response.metadata.key!! key.nspace = SecureRandom().nextLong() @@ -331,6 +336,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { finalChain, ) .getOrThrow() + response.metadata.authorizations = + InterceptorUtils.patchAuthorizations( + response.metadata.authorizations, + callingUid, + ) return InterceptorUtils.createTypedObjectReply(response) } 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 8f4f2a23..cf9f74fb 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 @@ -187,6 +187,8 @@ class KeyMintSecurityLevelInterceptor( val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) CertificateHelper.updateCertificateChain(callingUid, metadata, newChain) .getOrThrow() + metadata.authorizations = + InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid) // We must clean up cached generated keys before storing the patched chain cleanupKeyData(keyId) @@ -219,15 +221,25 @@ class KeyMintSecurityLevelInterceptor( // AOSP createOperation accepts Domain::APP (alias), Domain::KEY_ID (nspace), // Domain::SELINUX, and Domain::BLOB. Resolve to our generated key by trying // both alias-based and nspace-based lookups (database.rs: l=2060, 2123). - val generatedKeyInfo = + val resolvedEntry: Map.Entry? = when (keyDescriptor.domain) { - Domain.KEY_ID -> findGeneratedKeyByKeyId(callingUid, keyDescriptor.nspace) + Domain.KEY_ID -> { + val nspace = keyDescriptor.nspace + if (nspace == 0L) null + else + generatedKeys.entries + .filter { it.key.uid == callingUid } + .find { it.value.nspace == nspace } + } Domain.APP -> keyDescriptor.alias?.let { alias -> - generatedKeys[KeyIdentifier(callingUid, alias)] + val key = KeyIdentifier(callingUid, alias) + generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } } else -> null } + val generatedKeyInfo = resolvedEntry?.value + val resolvedKeyId = resolvedEntry?.key if (generatedKeyInfo == null) { SystemLogger.debug( @@ -348,26 +360,24 @@ class KeyMintSecurityLevelInterceptor( // F11: USAGE_COUNT_LIMIT — decrement on finish, delete key when exhausted. // AOSP tracks this in database via check_and_update_key_usage_count on // after_finish (enforcements.rs: l=510). - keyParams.usageCountLimit?.let { limit -> - val keyId = - generatedKeys.entries - .find { it.value.nspace == generatedKeyInfo.nspace } - ?.key ?: return@let + if (keyParams.usageCountLimit != null && resolvedKeyId != null) { + val limit = keyParams.usageCountLimit val remaining = - usageCounters.getOrPut(keyId) { + usageCounters.getOrPut(resolvedKeyId) { java.util.concurrent.atomic.AtomicInteger(limit) } - // Check if already exhausted before creating the operation if (remaining.get() <= 0) { - cleanupKeyData(keyId) - usageCounters.remove(keyId) + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) throw android.os.ServiceSpecificException(KeystoreErrorCode.KEY_NOT_FOUND) } softwareOperation.onFinishCallback = { if (remaining.decrementAndGet() <= 0) { - cleanupKeyData(keyId) - usageCounters.remove(keyId) - SystemLogger.info("Key $keyId exhausted (USAGE_COUNT_LIMIT=$limit).") + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) + SystemLogger.info( + "Key $resolvedKeyId exhausted (USAGE_COUNT_LIMIT=$limit)." + ) } } } From 6a1230c2d91f5ba925e06173aaa3c3c61f46c295 Mon Sep 17 00:00:00 2001 From: XiaoTong6666 <3278671549@qq.com> Date: Mon, 30 Mar 2026 07:59:50 +0800 Subject: [PATCH 15/15] Hide DO_NOT_REPORT patch levels in generated KeyMetadata Software-generated KeyMetadata now skips OS/VENDOR/BOOT patch level authorizations when the configured value is DO_NOT_REPORT, matching the existing certificate-patching behavior and the later aligned metadata semantics. - OS_PATCHLEVEL is omitted when configured to not report - VENDOR_PATCHLEVEL is omitted when configured to not report - BOOT_PATCHLEVEL is omitted when configured to not report This commit aligns the generated metadata path with the same patch-level hiding semantics already applied when patching certificate chains. (cherry picked from commit 492d6dcf45fc4efe43439f6c2d071039a01e470f) Co-authored-by: Mohammed Riad <52679407+MhmRdd@users.noreply.github.com> --- .../keystore/shim/KeyMintSecurityLevelInterceptor.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) 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 cf9f74fb..6a5c7071 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 @@ -802,13 +802,19 @@ private fun KeyMintAttestation.toAuthorizations( ) val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid) - authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch))) + if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) { + authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch))) + } val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid) - authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch))) + if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) { + authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch))) + } val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid) - authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) + if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) { + authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) + } // Software-enforced tags: CREATION_DATETIME, enforcement dates, USER_ID // (security_level.rs: l=165, 436).