diff --git a/app/src/main/cpp/binder_interceptor.cpp b/app/src/main/cpp/binder_interceptor.cpp index bc2b2b5f..79ded3ec 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 @@ -307,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; } @@ -386,13 +397,16 @@ 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; } // 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); } } @@ -409,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 } } @@ -537,12 +557,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; } @@ -592,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 } @@ -627,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 @@ -655,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/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/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/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..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() @@ -293,15 +305,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/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index b0a87a17..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 @@ -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,45 @@ 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 + .map { auth -> + val replacement = + when (auth.keyParameter.tag) { + 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 != null) { + Authorization().apply { + keyParameter = + KeyParameter().apply { + tag = auth.keyParameter.tag + value = KeyParameterValue.integer(replacement) + } + securityLevel = auth.securityLevel + } + } else { + auth + } + } + .toTypedArray() + } + /** Creates an `OverrideReply` parcel that indicates success with no data. */ fun createSuccessReply( writeResultCode: Boolean = true @@ -91,6 +134,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". @@ -108,8 +173,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/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index d123a6f2..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 @@ -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) } @@ -256,14 +277,21 @@ 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() KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( keyData.first, + null, key.nspace, response, + parsedParameters, ) KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId) return InterceptorUtils.createTypedObjectReply(response) @@ -308,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/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/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index ad9a04f5..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 @@ -1,8 +1,10 @@ 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 +import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.SecurityLevel import android.hardware.security.keymint.Tag import android.os.IBinder @@ -12,6 +14,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 +37,25 @@ 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 keyParams: KeyMintAttestation, + ) { + constructor( + keyPair: KeyPair?, + nspace: Long, + response: KeyEntryResponse, + keyParams: KeyMintAttestation, + ) : this(keyPair, null, nspace, response, keyParams) + + constructor( + keyPair: KeyPair?, + nspace: Long, + response: KeyEntryResponse, + ) : this(keyPair, null, nspace, response, KeyMintAttestation(emptyArray())) + } override fun onPreTransact( txId: Long, @@ -133,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( @@ -164,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) @@ -182,6 +207,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, @@ -191,36 +218,189 @@ 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 resolvedEntry: Map.Entry? = + when (keyDescriptor.domain) { + 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 -> + 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( - "[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 params = data.createTypedArray(KeyParameter.CREATOR)!! - val parsedParams = KeyMintAttestation(params) + val opParams = data.createTypedArray(KeyParameter.CREATOR)!! + val parsedOpParams = KeyMintAttestation(opParams) + data.readBoolean() // forced: no-op for sw ops - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, parsedParams) - val operationBinder = SoftwareOperationBinder(softwareOperation) + // 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 - val response = - CreateOperationResponse().apply { - iOperation = operationBinder - operationChallenge = null + // F14: Missing PURPOSE → INVALID_ARGUMENT (-38) + val requestedPurpose = parsedOpParams.purpose.firstOrNull() + if (requestedPurpose == null) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INVALID_ARGUMENT + ) + } + + // 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 + 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 + ) + } + + // 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 + ) } + } + + // 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 + ) + } + } - return InterceptorUtils.createTypedObjectReply(response) + // 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 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, + 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 + // after_finish (enforcements.rs: l=510). + if (keyParams.usageCountLimit != null && resolvedKeyId != null) { + val limit = keyParams.usageCountLimit + val remaining = + usageCounters.getOrPut(resolvedKeyId) { + java.util.concurrent.atomic.AtomicInteger(limit) + } + if (remaining.get() <= 0) { + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) + throw android.os.ServiceSpecificException(KeystoreErrorCode.KEY_NOT_FOUND) + } + softwareOperation.onFinishCallback = { + if (remaining.decrementAndGet() <= 0) { + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) + SystemLogger.info( + "Key $resolvedKeyId 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 + } + + 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 + ) + } } /** @@ -236,6 +416,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() @@ -253,6 +462,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( @@ -263,9 +529,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( @@ -275,7 +538,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. @@ -326,6 +594,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") @@ -337,6 +607,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 @@ -354,6 +632,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() @@ -393,6 +674,7 @@ class KeyMintSecurityLevelInterceptor( if (attestationKeys.remove(keyId)) { SystemLogger.debug("Remove cached attestaion key ${keyId}") } + usageCounters.remove(keyId) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { @@ -411,6 +693,7 @@ class KeyMintSecurityLevelInterceptor( generatedKeys.clear() patchedChains.clear() attestationKeys.clear() + usageCounters.clear() SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") } } @@ -419,6 +702,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, @@ -474,6 +759,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)) ) @@ -483,26 +802,57 @@ 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). + 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/OperationInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt index 91e0dde6..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 @@ -44,6 +49,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/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..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 @@ -3,29 +3,98 @@ 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.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 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 + */ +/** + * 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 + */ +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 + + /** km_compat_type_conversion.h: l=38 */ + const val INCOMPATIBLE_PURPOSE = -3 + + /** 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 + + /** + * 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 + + /** 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 { + fun updateAad(data: ByteArray?) + fun update(data: ByteArray?): ByteArray? 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. +// 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()) { @@ -39,27 +108,32 @@ 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}" } + // 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) { 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 = when (params.blockMode.firstOrNull()) { BlockMode.ECB -> "ECB" BlockMode.CBC -> "CBC" + BlockMode.CTR -> "CTR" BlockMode.GCM -> "GCM" else -> "ECB" // Default for RSA } @@ -75,13 +149,18 @@ 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 { initSign(keyPair.private) } + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } + override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) return null @@ -95,13 +174,18 @@ 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 { initVerify(keyPair.public) } + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } + override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) return null @@ -109,35 +193,120 @@ 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 } override fun abort() {} } -// Concrete implementation for Encryption/Decryption. +// Concrete implementation for Encryption/Decryption (block_cipher_operation.cpp: l=79). private class CipherPrimitive( - keyPair: KeyPair, + 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 { - val key = if (opMode == Cipher.ENCRYPT_MODE) keyPair.public else keyPair.private - init(opMode, key) + 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?) { + if (data != null) cipher.updateAAD(data) + } + 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() {} + + /** 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) + } + ) + } +} + +// 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) } + + 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() {} } @@ -145,72 +314,177 @@ 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 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) { - // This now holds the specific strategy object (Signer, Verifier, etc.) +class SoftwareOperation( + private val txId: Long, + keyPair: KeyPair?, + secretKey: javax.crypto.SecretKey?, + params: KeyMintAttestation, + opParams: Array = emptyArray(), + var onFinishCallback: (() -> Unit)? = null, +) { 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.") 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 + 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 + 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 -> - throw UnsupportedOperationException("Unsupported operation purpose: $purpose") + throw ServiceSpecificException( + KeystoreErrorCode.UNSUPPORTED_PURPOSE, + "Unsupported operation purpose: $purpose", + ) } } + /** 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 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 ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) + } + } + fun update(data: ByteArray?): ByteArray? { + 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) } } fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { + checkActive() try { val result = primitive.finish(data, signature) SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") + try { + onFinishCallback?.invoke() + } catch (e: Exception) { + SystemLogger.error("[SoftwareOp TX_ID: $txId] onFinishCallback failed.", e) + } return result + } catch (e: ServiceSpecificException) { + throw e } 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 + throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) + } 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. 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() { + private fun checkInputLength(data: ByteArray?) { + // operation.rs: l=337 + if (data != null && data.size > MAX_RECEIVE_DATA) + throw ServiceSpecificException(KeystoreErrorCode.TOO_MUCH_DATA) + } + + @Throws(RemoteException::class) + override fun updateAad(aadInput: ByteArray?) { + 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 } } 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..92bf635e 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 @@ -93,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 { 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, 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} * 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!"); 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); + } +}