diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..ac18c939 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +# Ensure shell scripts always have LF line endings, even on Windows. +# These get packaged into flashable zips and run on Android devices. +*.sh text eol=lf +module/daemon text eol=lf diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b873290..bf9091be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -122,7 +122,7 @@ androidComponents { // Now, copy and process the files from 'module' directory. val sourceModuleDir = rootProject.projectDir.resolve("module") from(sourceModuleDir) { - exclude("module.prop") // Exclude the template file. + exclude("module.prop", "service.sh") // Exclude templated files. } // Copy and filter the module.prop template separately. @@ -136,6 +136,18 @@ androidComponents { ) } + // Wire DEBUG flag in service.sh to the build variant. + // Cannot use expand() here because shell syntax ${0%/*} conflicts. + // FixCrLfFilter applied last to ensure LF output on Windows. + from(sourceModuleDir) { + include("service.sh") + filter { it.replace("DEBUG=false", "DEBUG=$isDebug") } + filter( + mapOf("eol" to org.apache.tools.ant.filters.FixCrLfFilter.CrLf.newInstance("lf")), + org.apache.tools.ant.filters.FixCrLfFilter::class.java, + ) + } + // The destination for all the above 'from' operations. into(tempModuleDir) } 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/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt index de71061c..0ff56752 100644 --- a/app/src/main/java/org/matrix/TEESimulator/App.kt +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -40,9 +40,7 @@ object App { // Initialize and start the appropriate keystore interceptors. initializeInterceptors() - // Load the package configuration. ConfigurationManager.initialize() - // Set up the device's boot key and hash, which are crucial for attestation. AndroidDeviceUtils.setupBootKeyAndHash() // Android ships with a stripped-down Bouncy Castle provider under the name "BC". 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 0625001d..02dcad34 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation import android.content.pm.PackageManager import android.os.Build +import java.nio.ByteBuffer import java.nio.charset.StandardCharsets import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import org.bouncycastle.asn1.ASN1Boolean import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1Enumerated @@ -132,8 +135,16 @@ object AttestationBuilder { uid: Int, securityLevel: Int, ): ASN1Sequence { + val creationTime = System.currentTimeMillis() val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel) - val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel) + val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime) + + val uniqueId = + if (params.includeUniqueId == true && params.attestationChallenge != null) { + computeUniqueId(creationTime, createApplicationId(uid).octets) + } else { + ByteArray(0) + } val fields = arrayOf( @@ -146,13 +157,49 @@ object AttestationBuilder { ), // keymasterVersion ASN1Enumerated(securityLevel), // keymasterSecurityLevel DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge - DEROctetString(ByteArray(0)), // uniqueId + DEROctetString(uniqueId), softwareEnforced, teeEnforced, ) return DERSequence(fields) } + /** + * Computes the unique ID per the KeyMint HAL spec: + * HMAC-SHA256(T || C || R, HBK) truncated to 128 bits. + * + * T = temporal counter (creationTime / 2592000000, i.e. 30-day periods since epoch) + * C = DER-encoded ATTESTATION_APPLICATION_ID + * R = 0x00 (no factory reset since ID rotation) + * HBK = device-unique secret generated once during module installation + */ + private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray { + val temporalCounter = creationTimeMs / 2592000000L + + val message = + ByteBuffer.allocate(8 + aaidDer.size + 1) + .putLong(temporalCounter) + .put(aaidDer) + .put(0x00) // RESET_SINCE_ID_ROTATION = false + .array() + + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(hbk, "HmacSHA256")) + return mac.doFinal(message).copyOf(16) + } + + /** Device-unique key seed, generated once at module installation. */ + private val hbk: ByteArray by lazy { + val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk") + if (file.exists() && file.length() == 32L) { + file.readBytes() + } else { + // Fallback: generate in-memory (won't persist across reboots) + SystemLogger.warning("hbk not found, generating ephemeral HBK.") + ByteArray(32).also { java.security.SecureRandom().nextBytes(it) } + } + } + /** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */ private fun buildTeeEnforcedList( params: KeyMintAttestation, @@ -193,9 +240,23 @@ object AttestationBuilder { ) } - params.padding.forEach { + if (params.blockMode.isNotEmpty()) { list.add( - DERTaggedObject(true, AttestationConstants.TAG_PADDING, ASN1Integer(it.toLong())) + DERTaggedObject( + true, + AttestationConstants.TAG_BLOCK_MODE, + DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()), + ) + ) + } + + if (params.padding.isNotEmpty()) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_PADDING, + DERSet(params.padding.map { ASN1Integer(it.toLong()) }.toTypedArray()), + ) ) } @@ -209,14 +270,79 @@ object AttestationBuilder { ) } + val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel) + + if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST, + DERSet( + params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray() + ), + ) + ) + } + + if (params.rollbackResistance == true && attestVersion >= 3) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ROLLBACK_RESISTANCE, + DERNull.INSTANCE, + ) + ) + } + + if (params.earlyBootOnly == true && attestVersion >= 4) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE) + ) + } + + if (params.noAuthRequired == true) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE) + ) + } + + if (params.allowWhileOnBody == true) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, + DERNull.INSTANCE, + ) + ) + } + + if (params.trustedUserPresenceRequired == true && attestVersion >= 3) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, + DERNull.INSTANCE, + ) + ) + } + + if (params.trustedConfirmationRequired == true && attestVersion >= 3) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, + DERNull.INSTANCE, + ) + ) + } + list.addAll( listOf( - DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE), DERTaggedObject( true, AttestationConstants.TAG_ORIGIN, - ASN1Integer(0L), - ), // KeyOrigin.GENERATED + ASN1Integer((params.origin ?: 0).toLong()), + ), DERTaggedObject( true, AttestationConstants.TAG_ROOT_OF_TRUST, @@ -320,20 +446,32 @@ object AttestationBuilder { * Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by * Keystore. */ - private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence { - val list = - mutableListOf( - DERTaggedObject( - true, - AttestationConstants.TAG_CREATION_DATETIME, - ASN1Integer(System.currentTimeMillis()), - ), + private fun buildSoftwareEnforcedList( + params: KeyMintAttestation, + uid: Int, + securityLevel: Int, + creationTimeMs: Long = System.currentTimeMillis(), + ): DERSequence { + val list = mutableListOf() + + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_CREATION_DATETIME, + ASN1Integer(creationTimeMs), + ) + ) + + // ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present. + if (params.attestationChallenge != null) { + list.add( DERTaggedObject( true, AttestationConstants.TAG_ATTESTATION_APPLICATION_ID, createApplicationId(uid), - ), + ) ) + } if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) { list.add( DERTaggedObject( @@ -343,7 +481,52 @@ object AttestationBuilder { ) ) } - return DERSequence(list.toTypedArray()) + + // Keystore2-enforced tags belong in softwareEnforced, not teeEnforced. + // The HAL does not enforce these; keystore2's authorize_create handles them. + params.activeDateTime?.let { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time)) + ) + } + params.originationExpireDateTime?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, + ASN1Integer(it.time), + ) + ) + } + params.usageExpireDateTime?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, + ASN1Integer(it.time), + ) + ) + } + params.usageCountLimit?.let { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_USAGE_COUNT_LIMIT, + ASN1Integer(it.toLong()), + ) + ) + } + if (params.unlockedDeviceRequired == true) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, + DERNull.INSTANCE, + ) + ) + } + + return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray()) } /** @@ -371,6 +554,17 @@ object AttestationBuilder { */ @Throws(Throwable::class) private fun createApplicationId(uid: Int): DEROctetString { + // AOSP keystore_attestation_id.cpp: gather_attestation_application_id() + // uses a hardcoded identity for AID_SYSTEM (1000) and AID_ROOT (0): + // packageName = "AndroidSystem", versionCode = 1, no signing digests. + val appUid = uid % 100000 + if (appUid == 0 || appUid == 1000) { + return buildApplicationIdDer( + listOf("AndroidSystem" to 1L), + emptySet(), + ) + } + val pm = ConfigurationManager.getPackageManager() ?: throw IllegalStateException("PackageManager not found!") @@ -378,12 +572,11 @@ object AttestationBuilder { pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") val sha256 = MessageDigest.getInstance("SHA-256") - val packageInfoList = mutableListOf() + val packageInfoList = mutableListOf>() val signatureDigests = mutableSetOf() - // Process all packages associated with the UID in a single loop. + val userId = uid / 100000 packages.forEach { packageName -> - val userId = uid / 100000 val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getPackageInfo( @@ -396,34 +589,38 @@ object AttestationBuilder { pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId) } - // Add package information (name and version code) to our list. - packageInfoList.add( + packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode) + + val certs = packageInfo.signingInfo?.signingCertificateHistory + ?: packageInfo.signingInfo?.apkContentsSigners + certs?.forEach { signature -> + signatureDigests.add(Digest(sha256.digest(signature.toByteArray()))) + } + } + + return buildApplicationIdDer(packageInfoList, signatureDigests) + } + + private fun buildApplicationIdDer( + packages: List>, + digests: Set, + ): DEROctetString { + val packageInfoList = + packages.map { (name, version) -> DERSequence( arrayOf( - DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)), - ASN1Integer(packageInfo.longVersionCode), + DEROctetString(name.toByteArray(StandardCharsets.UTF_8)), + ASN1Integer(version), ) ) - ) - - // Collect unique signature digests from the signing history. - packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature -> - val digest = sha256.digest(signature.toByteArray()) - signatureDigests.add(Digest(digest)) } - } - - // The application ID is a sequence of two sets: - // 1. A set of package information (name and version). - // 2. A set of SHA-256 digests of the signing certificates. val applicationIdSequence = DERSequence( arrayOf( DERSet(packageInfoList.toTypedArray()), - DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()), + DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()), ) ) - return DEROctetString(applicationIdSequence.encoded) } } diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt index 767e8b39..2ba04c75 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt @@ -44,9 +44,11 @@ object AttestationConstants { // --- Key Lifetime and Usage Control --- const val TAG_ROLLBACK_RESISTANCE = 303 + const val TAG_EARLY_BOOT_ONLY = 305 const val TAG_ACTIVE_DATETIME = 400 const val TAG_ORIGINATION_EXPIRE_DATETIME = 401 const val TAG_USAGE_EXPIRE_DATETIME = 402 + const val TAG_MAX_BOOT_LEVEL = 403 const val TAG_MAX_USES_PER_BOOT = 404 const val TAG_USAGE_COUNT_LIMIT = 405 @@ -56,6 +58,10 @@ object AttestationConstants { const val TAG_NO_AUTH_REQUIRED = 503 const val TAG_USER_AUTH_TYPE = 504 const val TAG_AUTH_TIMEOUT = 505 + const val TAG_ALLOW_WHILE_ON_BODY = 506 + const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507 + const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508 + const val TAG_UNLOCKED_DEVICE_REQUIRED = 509 // --- Attestation and Application Info --- const val TAG_APPLICATION_ID = 601 diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt index 114ad3de..4bad10a2 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/DeviceAttestationService.kt @@ -1,13 +1,8 @@ package org.matrix.TEESimulator.attestation import android.annotation.SuppressLint -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import java.security.KeyPairGenerator import java.security.KeyStore -import java.security.SecureRandom import java.security.cert.X509Certificate -import java.security.spec.ECGenParameterSpec import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1OctetString @@ -57,55 +52,14 @@ object DeviceAttestationService { val bootPatchLevel: Int?, ) - // A unique alias for the key used to perform the TEE functionality check. private const val TEE_CHECK_KEY_ALIAS = "TEESimulator_AttestationCheck" - /** - * Lazily determines if the device's TEE is functional by attempting to generate an - * attestation-backed key pair. The result is cached. - */ - val isTeeFunctional: Boolean by lazy { checkTeeFunctionality() } - /** * Lazily fetches and parses attestation data from a genuinely generated certificate. The result * is cached. Returns null if the TEE is not functional or parsing fails. */ val CachedAttestationData: AttestationData? by lazy { fetchAttestationData() } - /** - * Checks if the TEE is working correctly by generating a key in the Android Keystore with an - * attestation challenge. - * - * @return `true` if a key with attestation was generated successfully, `false` otherwise. - */ - private fun checkTeeFunctionality(): Boolean { - SystemLogger.info("Performing TEE functionality check...") - return try { - val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } - val keyPairGenerator = - KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore") - - // A random challenge is required for attestation. - val challenge = ByteArray(16).apply { SecureRandom().nextBytes(this) } - - val spec = - KeyGenParameterSpec.Builder(TEE_CHECK_KEY_ALIAS, KeyProperties.PURPOSE_SIGN) - .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) - .setDigests(KeyProperties.DIGEST_SHA256) - .setAttestationChallenge(challenge) - .build() - - keyPairGenerator.initialize(spec) - keyPairGenerator.generateKeyPair() - - SystemLogger.info("TEE functionality check successful.") - true - } catch (e: Exception) { - SystemLogger.warning("TEE functionality check failed.", e) - false - } - } - /** * Retrieves the attestation certificate generated during the TEE check. The key entry is * deleted after retrieval to clean up. @@ -113,8 +67,6 @@ object DeviceAttestationService { * @return The leaf `X509Certificate` containing the attestation, or `null` if unavailable. */ private fun getAttestationCertificate(): X509Certificate? { - if (!isTeeFunctional) return null - return try { val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) } val certChain = keyStore.getCertificateChain(TEE_CHECK_KEY_ALIAS) 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 baa174d4..92b0d30f 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -41,6 +41,23 @@ data class KeyMintAttestation( val manufacturer: ByteArray?, val model: ByteArray?, val secondImei: ByteArray?, + // Enforcement tags + val activeDateTime: Date?, + val originationExpireDateTime: Date?, + val usageExpireDateTime: Date?, + val usageCountLimit: Int?, + val callerNonce: Boolean?, + val unlockedDeviceRequired: Boolean?, + val includeUniqueId: Boolean?, + val rollbackResistance: Boolean?, + val earlyBootOnly: Boolean?, + val allowWhileOnBody: Boolean?, + val trustedUserPresenceRequired: Boolean?, + val trustedConfirmationRequired: Boolean?, + val maxUsesPerBoot: Int?, + val maxBootLevel: Int?, + val minMacLength: Int?, + val rsaOaepMgfDigest: List, ) { /** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */ constructor( @@ -50,7 +67,8 @@ data class KeyMintAttestation( algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0, // AOSP: [key_param(tag = KEY_SIZE, field = Integer)] - keySize = params.findInteger(Tag.KEY_SIZE) ?: 0, + // For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent. + keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(), // AOSP: [key_param(tag = EC_CURVE, field = EcCurve)] ecCurve = params.findEcCurve(Tag.EC_CURVE), @@ -103,6 +121,24 @@ 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 + 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), + rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST), ) { // Log all parsed parameters for debugging purposes. params.forEach { KeyMintParameterLogger.logParameter(it) } @@ -167,6 +203,19 @@ 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. */ +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 -> 256 + EcCurve.P_384 -> 384 + EcCurve.P_521 -> 521 + EcCurve.CURVE_25519 -> 256 + else -> 0 + } +} + /** * Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back * to KEY_SIZE (field=Integer). 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..f65ff53d 100644 --- a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -7,7 +7,6 @@ import android.os.IBinder import android.os.ServiceManager import java.io.File import java.util.concurrent.ConcurrentHashMap -import org.matrix.TEESimulator.attestation.DeviceAttestationService import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.pki.KeyBoxManager @@ -31,7 +30,6 @@ object ConfigurationManager { // --- Configuration Paths --- const val CONFIG_PATH = "/data/adb/tricky_store" private const val TARGET_PACKAGES_FILE = "target.txt" - private const val TEE_STATUS_FILE = "tee_status.txt" private const val PATCH_LEVEL_FILE = "security_patch.txt" private const val DEFAULT_KEYBOX_FILE = "keybox.xml" private val configRoot = File(CONFIG_PATH) @@ -39,7 +37,6 @@ object ConfigurationManager { // --- In-Memory Configuration State --- @Volatile private var packageModes = mapOf() @Volatile private var packageKeyboxes = mapOf() - @Volatile private var isTeeBroken: Boolean? = null @Volatile private var globalCustomPatchLevel: CustomPatchLevel? = null @Volatile private var packagePatchLevels = mapOf() @@ -68,7 +65,6 @@ object ConfigurationManager { // Initial load of all configuration files. loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE)) loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE)) - storeTeeStatus() // Check and store the current TEE status. // Start watching for any subsequent file changes. ConfigObserver.startWatching() @@ -88,7 +84,10 @@ object ConfigurationManager { } /** Determines if the certificate for a given UID needs to be patched. */ - fun shouldPatch(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.PATCH + fun shouldPatch(uid: Int): Boolean { + val mode = getPackageModeForUid(uid) + return mode == Mode.PATCH || mode == Mode.AUTO + } /** Determines if a new certificate needs to be generated for a given UID. */ fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE @@ -96,24 +95,23 @@ object ConfigurationManager { /** Determines if no operation is needed for a given UID. */ fun shouldSkipUid(uid: Int): Boolean = getPackageModeForUid(uid) == null + /** Determines if the UID is in AUTO mode (no explicit ! or ? suffix). */ + fun isAutoMode(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.AUTO + /** Resolves the operating mode for a given UID based on its packages and the TEE status. */ private fun getPackageModeForUid(uid: Int): Mode? { val packages = getPackagesForUid(uid) if (packages.isEmpty()) return null - // Lazily load TEE status if it hasn't been checked yet. - if (isTeeBroken == null) loadTeeStatus() - - // Find the first configured mode for any of the UID's packages. for (pkg in packages) { when (packageModes[pkg]) { Mode.GENERATE -> return Mode.GENERATE Mode.PATCH -> return Mode.PATCH - Mode.AUTO -> return if (isTeeBroken == true) Mode.GENERATE else Mode.PATCH - null -> continue // No config for this package, check the next one. + Mode.AUTO -> return Mode.AUTO + null -> continue } } - return null // No configuration found for this UID. + return null } /** @@ -158,25 +156,25 @@ object ConfigurationManager { return@forEach } + val mode: Mode + val rawPkg: String when { - // Suffix '!' means force GENERATE mode. trimmedLine.endsWith("!") -> { - val pkg = trimmedLine.removeSuffix("!").trim() - newModes[pkg] = Mode.GENERATE - newKeyboxes[pkg] = currentKeybox + mode = Mode.GENERATE + rawPkg = trimmedLine.removeSuffix("!").trim() } - // Suffix '?' means force PATCH mode. trimmedLine.endsWith("?") -> { - val pkg = trimmedLine.removeSuffix("?").trim() - newModes[pkg] = Mode.PATCH - newKeyboxes[pkg] = currentKeybox + mode = Mode.PATCH + rawPkg = trimmedLine.removeSuffix("?").trim() } - // No suffix means AUTO mode. else -> { - newModes[trimmedLine] = Mode.AUTO - newKeyboxes[trimmedLine] = currentKeybox + mode = Mode.AUTO + rawPkg = trimmedLine } } + + newModes[rawPkg] = mode + newKeyboxes[rawPkg] = currentKeybox } // Atomically update the configuration maps. @@ -273,29 +271,6 @@ object ConfigurationManager { } } - /** Checks the device's TEE status and writes the result to a file for persistence. */ - private fun storeTeeStatus() { - val statusFile = File(configRoot, TEE_STATUS_FILE) - isTeeBroken = !DeviceAttestationService.isTeeFunctional - try { - statusFile.writeText("tee_broken=$isTeeBroken") - SystemLogger.info("TEE status stored: isTeeBroken=$isTeeBroken") - } catch (e: Exception) { - SystemLogger.error("Failed to write TEE status to file.", e) - } - } - - /** Loads the TEE status from the file. */ - private fun loadTeeStatus() { - val statusFile = File(configRoot, TEE_STATUS_FILE) - isTeeBroken = - if (statusFile.exists()) { - statusFile.readText().trim() == "tee_broken=true" - } else { - null // Status is unknown. - } - } - /** * A FileObserver that monitors the configuration directory for changes and triggers reloads of * the relevant settings. @@ -351,6 +326,32 @@ object ConfigurationManager { return iPackageManager } + /** Checks if any package belonging to the UID holds the given permission. */ + /** Checks a SELinux permission for a caller identified by PID against the keystore context. */ + fun checkSELinuxPermission(callingPid: Int, tclass: String, perm: String): Boolean { + return try { + val callerCtx = + java.io.File("/proc/$callingPid/attr/current").readText().trim('\u0000', ' ', '\n') + val selfCtx = + java.io.File("/proc/self/attr/current").readText().trim('\u0000', ' ', '\n') + android.os.SELinux.checkSELinuxAccess(callerCtx, selfCtx, tclass, perm) + } catch (_: Exception) { + false + } + } + + /** 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..fdc75553 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,27 @@ 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..8ea04038 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) @@ -108,8 +113,81 @@ 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 + } + } + + /** + * 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) + } + + /** + * Patches the system-level authorization values (OS_PATCHLEVEL, VENDOR_PATCHLEVEL, + * BOOT_PATCHLEVEL) in an authorization array to match the configured patch levels for the + * given calling UID. Each authorization's original [Authorization.securityLevel] is preserved. + * + * When a patch level is configured as "no" ([AndroidDeviceUtils.DO_NOT_REPORT]), the original + * hardware value is kept as-is. + */ + 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() } } 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 e6358fbe..33e764ec 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 @@ -5,11 +5,13 @@ import android.hardware.security.keymint.SecurityLevel import android.os.Build import android.os.IBinder import android.os.Parcel +import android.system.keystore2.Domain import android.system.keystore2.IKeystoreService import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyEntryResponse import java.security.SecureRandom import java.security.cert.Certificate +import java.util.concurrent.ConcurrentHashMap import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager @@ -43,6 +45,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { if (Build.VERSION.SDK_INT >= 34) InterceptorUtils.getTransactCode(stubBinderClass, "listEntriesBatched") else null + private val GET_NUMBER_OF_ENTRIES_TRANSACTION = + InterceptorUtils.getTransactCode(stubBinderClass, "getNumberOfEntries") private val transactionNames: Map by lazy { stubBinderClass.declaredFields @@ -53,10 +57,26 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } } + // Keys whose certs were updated via updateSubcomponent; skip re-patching on getKeyEntry. + private val userUpdatedKeys = ConcurrentHashMap.newKeySet() + override val serviceName = "android.system.keystore2.IKeystoreService/default" 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, + GET_NUMBER_OF_ENTRIES_TRANSACTION, + ) + .filter { it != -1 } // Exclude methods unavailable on this Android version + .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 +93,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 +109,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) } @@ -99,7 +129,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { callingPid: Int, data: Parcel, ): TransactionResult { - if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { + if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) { + logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true) + return if (ConfigurationManager.shouldSkipUid(callingUid)) + TransactionResult.ContinueAndSkipPost + else TransactionResult.Continue + } else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid, true) val packages = ConfigurationManager.getPackagesForUid(callingUid).joinToString() @@ -117,7 +152,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ) { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) - if (ConfigurationManager.shouldSkipUid(callingUid)) + val skipUid = ConfigurationManager.shouldSkipUid(callingUid) + + if (skipUid && code == UPDATE_SUBCOMPONENT_TRANSACTION) return TransactionResult.ContinueAndSkipPost if (code == UPDATE_SUBCOMPONENT_TRANSACTION) @@ -128,30 +165,51 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { data.readTypedObject(KeyDescriptor.CREATOR) ?: return TransactionResult.ContinueAndSkipPost - if (descriptor.alias != null) { - SystemLogger.info("Handling ${transactionNames[code]!!} ${descriptor.alias}") - } else { - SystemLogger.info( - "Skip ${transactionNames[code]!!} for key [alias, blob, domain, nspace]: [${descriptor.alias}, ${descriptor.blob}, ${descriptor.domain}, ${descriptor.nspace}]" - ) - return TransactionResult.ContinueAndSkipPost - } - val keyId = KeyIdentifier(callingUid, descriptor.alias) - if (code == DELETE_KEY_TRANSACTION) { - if (KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) != null) { + // Handle delete by alias (APP domain) or nspace (KEY_ID domain). + val keyId = + if (descriptor.alias != null) { + KeyIdentifier(callingUid, descriptor.alias) + } else if (descriptor.domain == Domain.KEY_ID) { + KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( + callingUid, descriptor.nspace + )?.let { info -> + KeyMintSecurityLevelInterceptor.generatedKeys.entries + .find { it.value.nspace == info.nspace && it.key.uid == callingUid } + ?.key + } + } else null + + if (keyId != null) { + val isSoftwareKey = + KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId) KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId) - SystemLogger.info( - "[TX_ID: $txId] Deleted cached keypair ${descriptor.alias}, replying with empty response." - ) - return InterceptorUtils.createSuccessReply(writeResultCode = false) + if (isSoftwareKey) { + SystemLogger.info( + "[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response." + ) + return InterceptorUtils.createSuccessReply(writeResultCode = false) + } } return TransactionResult.ContinueAndSkipPost } + if (descriptor.alias == null) { + return TransactionResult.ContinueAndSkipPost + } + val keyId = KeyIdentifier(callingUid, descriptor.alias) + + // Always return software-generated keys from cache, even if UID + // config changed after generation. Without this, a config reload + // mid-session orphans keys that only exist in memory. val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) - ?: return TransactionResult.Continue + if (response == null) { + val action = if (skipUid) "passthrough (skipped UID)" else "forwarding to hardware" + SystemLogger.debug("[TX_ID: $txId] getKeyEntry ${descriptor.alias}: cache miss, $action") + return if (skipUid) TransactionResult.ContinueAndSkipPost + else TransactionResult.Continue + } if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId)) SystemLogger.info("${descriptor.alias} was an attestation key") @@ -186,10 +244,35 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { reply: Parcel?, resultCode: Int, ): TransactionResult { - if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply)) + if (target != keystoreService || reply == null || InterceptorUtils.hasException(reply)) { + if (reply != null && InterceptorUtils.hasException(reply)) { + SystemLogger.debug( + "[TX_ID: $txId] post-${transactionNames[code] ?: code}: hardware returned exception, forwarding as-is" + ) + } return TransactionResult.SkipTransaction + } - if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { + if (code == GET_NUMBER_OF_ENTRIES_TRANSACTION) { + logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) + return runCatching { + val hardwareCount = reply.readInt() + val softwareCount = + KeyMintSecurityLevelInterceptor.generatedKeys.keys.count { + it.uid == callingUid + } + val totalCount = hardwareCount + softwareCount + val parcel = Parcel.obtain().apply { + writeNoException() + writeInt(totalCount) + } + TransactionResult.OverrideReply(parcel) + } + .getOrElse { + SystemLogger.error("[TX_ID: $txId] Failed to modify getNumberOfEntries.", it) + TransactionResult.SkipTransaction + } + } else if (code == LIST_ENTRIES_TRANSACTION || code == LIST_ENTRIES_BATCHED_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) return runCatching { @@ -225,6 +308,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { val response = reply.readTypedObject(KeyEntryResponse.CREATOR)!! val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + // Skip patching for keys whose certs were explicitly set via updateSubcomponent. + if (userUpdatedKeys.remove(keyId)) { + SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.") + return TransactionResult.SkipTransaction + } + val authorizations = response.metadata.authorizations val parsedParameters = KeyMintAttestation( @@ -236,7 +325,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { return TransactionResult.SkipTransaction } - if (parsedParameters.isAttestKey()) { + if (parsedParameters.isAttestKey() && + !KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId) + ) { SystemLogger.warning( "[TX_ID: $txId] Found hardware attest key ${keyId.alias} in the reply." ) @@ -255,13 +346,21 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { keyData.second.toTypedArray(), ) .getOrThrow() + response.metadata.authorizations = + InterceptorUtils.patchAuthorizations( + response.metadata.authorizations, + callingUid, + ) - keyDescriptor.nspace = SecureRandom().nextLong() + val newNspace = SecureRandom().nextLong() + response.metadata.key?.let { it.nspace = newNspace } KeyMintSecurityLevelInterceptor.generatedKeys[keyId] = KeyMintSecurityLevelInterceptor.GeneratedKeyInfo( keyData.first, - keyDescriptor.nspace, + null, + newNspace, response, + parsedParameters, ) KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId) return InterceptorUtils.createTypedObjectReply(response) @@ -302,6 +401,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { CertificateHelper.updateCertificateChain(response.metadata, finalChain) .getOrThrow() + response.metadata.authorizations = + InterceptorUtils.patchAuthorizations( + response.metadata.authorizations, + callingUid, + ) return InterceptorUtils.createTypedObjectReply(response) } @@ -319,9 +423,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult { data.enforceInterface(IKeystoreService.DESCRIPTOR) val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) + ?: return TransactionResult.ContinueAndSkipPost + + // Resolve by nspace (KEY_ID) or alias (APP), same as createOperation. val generatedKeyInfo = - KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor?.nspace) - ?: return TransactionResult.ContinueAndSkipPost + when (descriptor.domain) { + Domain.KEY_ID -> + KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( + callingUid, + descriptor.nspace, + ) + Domain.APP -> + descriptor.alias?.let { + KeyMintSecurityLevelInterceptor.generatedKeys[KeyIdentifier(callingUid, it)] + } + else -> null + } + + if (generatedKeyInfo == null) { + // Hardware key: mark so getKeyEntry skips cert re-patching. + descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) } + return TransactionResult.ContinueAndSkipPost + } SystemLogger.info("Updating sub-component with key[${generatedKeyInfo.nspace}]") val metadata = generatedKeyInfo.response.metadata diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt index 3c7705a5..8fc03001 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/KeystoreInterceptor.kt @@ -432,6 +432,22 @@ private data class LegacyKeygenParameters( manufacturer = null, model = null, secondImei = null, + activeDateTime = null, + originationExpireDateTime = null, + usageExpireDateTime = null, + usageCountLimit = null, + callerNonce = null, + unlockedDeviceRequired = null, + includeUniqueId = null, + rollbackResistance = null, + earlyBootOnly = null, + allowWhileOnBody = null, + trustedUserPresenceRequired = null, + trustedConfirmationRequired = null, + maxUsesPerBoot = null, + maxBootLevel = null, + minMacLength = null, + rsaOaepMgfDigest = emptyList(), ) } 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 f86b1bda..84d0a6b5 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,11 @@ 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 import android.os.Parcel @@ -10,7 +13,10 @@ import android.system.keystore2.* import java.security.KeyPair import java.security.SecureRandom import java.security.cert.Certificate +import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap +import org.matrix.TEESimulator.attestation.ATTESTATION_OID +import org.matrix.TEESimulator.attestation.AttestationConstants import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager @@ -21,6 +27,7 @@ import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.pki.CertificateGenerator import org.matrix.TEESimulator.pki.CertificateHelper import org.matrix.TEESimulator.util.AndroidDeviceUtils +import org.matrix.TEESimulator.util.TeeLatencySimulator /** * Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where @@ -33,9 +40,11 @@ class KeyMintSecurityLevelInterceptor( // --- Data Structures for State Management --- data class GeneratedKeyInfo( - val keyPair: KeyPair, + val keyPair: KeyPair?, + val secretKey: javax.crypto.SecretKey?, val nspace: Long, val response: KeyEntryResponse, + val keyParams: KeyMintAttestation, ) override fun onPreTransact( @@ -53,7 +62,7 @@ class KeyMintSecurityLevelInterceptor( GENERATE_KEY_TRANSACTION -> { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) - if (!shouldSkip) return handleGenerateKey(callingUid, data) + if (!shouldSkip) return handleGenerateKey(callingUid, callingPid, data) } CREATE_OPERATION_TRANSACTION -> { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) @@ -95,8 +104,15 @@ class KeyMintSecurityLevelInterceptor( resultCode: Int, ): TransactionResult { // We only care about successful transactions. - if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) + if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) { + val reason = when { + resultCode != 0 -> "binder error (resultCode=$resultCode)" + reply != null && InterceptorUtils.hasException(reply) -> "service exception in reply" + else -> "null reply" + } + SystemLogger.debug("[TX_ID: $txId] post-${transactionNames[code] ?: code}: skipped ($reason)") return TransactionResult.SkipTransaction + } if (code == IMPORT_KEY_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) @@ -105,7 +121,9 @@ class KeyMintSecurityLevelInterceptor( val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return TransactionResult.SkipTransaction - cleanupKeyData(KeyIdentifier(callingUid, keyDescriptor.alias)) + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + cleanupKeyData(keyId) + importedKeys.add(keyId) } else if (code == CREATE_OPERATION_TRANSACTION) { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) @@ -132,7 +150,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( @@ -162,6 +185,8 @@ class KeyMintSecurityLevelInterceptor( val key = metadata.key!! val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() + metadata.authorizations = + InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid) // We must clean up cached generated keys before storing the patched chain cleanupKeyData(keyId) @@ -189,43 +214,188 @@ 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) + // Resolve key descriptor to a generated key via nspace (KEY_ID) or alias (APP). + 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) + val keyParams = generatedKeyInfo.keyParams - val response = - CreateOperationResponse().apply { - iOperation = operationBinder - operationChallenge = null + val requestedPurpose = parsedOpParams.purpose.firstOrNull() + if (requestedPurpose == null) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INVALID_ARGUMENT + ) + } + + 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 + ) + } + + if (requestedPurpose == KeyPurpose.WRAP_KEY) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INCOMPATIBLE_PURPOSE + ) + } + + if (requestedPurpose !in keyParams.purpose) { + SystemLogger.info( + "[TX_ID: $txId] Rejecting: purpose $requestedPurpose not in ${keyParams.purpose}" + ) + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INCOMPATIBLE_PURPOSE + ) + } + + keyParams.activeDateTime?.let { activeDate -> + if (System.currentTimeMillis() < activeDate.time) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.KEY_NOT_YET_VALID + ) + } + } + + // ORIGINATION_EXPIRE applies to SIGN/ENCRYPT only. + keyParams.originationExpireDateTime?.let { expireDate -> + if ( + (requestedPurpose == KeyPurpose.SIGN || + requestedPurpose == KeyPurpose.ENCRYPT) && + System.currentTimeMillis() > expireDate.time + ) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.KEY_EXPIRED + ) } + } + + // USAGE_EXPIRE applies to DECRYPT/VERIFY only. + keyParams.usageExpireDateTime?.let { expireDate -> + if ( + (requestedPurpose == KeyPurpose.DECRYPT || + requestedPurpose == KeyPurpose.VERIFY) && + System.currentTimeMillis() > expireDate.time + ) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.KEY_EXPIRED + ) + } + } - return InterceptorUtils.createTypedObjectReply(response) + 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 { + // Use key params for crypto properties (algorithm, digest, etc.) but + // override purpose from the operation params. + 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, + ) + + // Decrement usage counter on finish; delete key when exhausted. + 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) + } + } + } + + val operationBinder = SoftwareOperationBinder(softwareOperation) + + val response = + CreateOperationResponse().apply { + iOperation = operationBinder + operationChallenge = null + 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 + ) + } } /** * Handles the `generateKey` transaction. Based on the configuration for the calling UID, it * either generates a key in software or lets the call pass through to the hardware. */ - private fun handleGenerateKey(callingUid: Int, data: Parcel): TransactionResult { + private fun handleGenerateKey(callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { return runCatching { data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! @@ -233,61 +403,294 @@ class KeyMintSecurityLevelInterceptor( SystemLogger.debug( "Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}" ) + val params = data.createTypedArray(KeyParameter.CREATOR)!! + + // Caller-provided CREATION_DATETIME is not allowed. + 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_SECOND_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 + ) + } + + // INCLUDE_UNIQUE_ID requires SELinux gen_unique_id OR Android + // REQUEST_UNIQUE_ID_ATTESTATION (security_level.rs:478-485). + if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) { + val hasSELinux = + ConfigurationManager.checkSELinuxPermission( + callingPid, + "keystore_key", + "gen_unique_id", + ) + val hasAndroid = + ConfigurationManager.hasPermissionForUid( + callingUid, + "android.permission.REQUEST_UNIQUE_ID_ATTESTATION", + ) + if (!hasSELinux && !hasAndroid) { + return@runCatching InterceptorUtils.createServiceSpecificErrorReply( + PERMISSION_DENIED + ) + } + } + val parsedParams = KeyMintAttestation(params) val isAttestKeyRequest = parsedParams.isAttestKey() - // Determine if we need to generate a key based on config or - // if it's an attestation request in patch mode. - val needsSoftwareGeneration = + val forceGenerate = ConfigurationManager.shouldGenerate(callingUid) || (ConfigurationManager.shouldPatch(callingUid) && isAttestKeyRequest) || (attestationKey != null && isAttestationKey(KeyIdentifier(callingUid, attestationKey.alias))) - if (needsSoftwareGeneration) { - keyDescriptor.nspace = secureRandom.nextLong() - SystemLogger.info( - "Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]." + val isAuto = ConfigurationManager.isAutoMode(callingUid) + + when { + forceGenerate -> { + SystemLogger.debug("generateKey ${keyDescriptor.alias}: mode=GENERATE") + doSoftwareGeneration( + callingUid, keyDescriptor, attestationKey, parsedParams, isAttestKeyRequest + ) + } + isAuto && !teeFunctional -> { + SystemLogger.debug("generateKey ${keyDescriptor.alias}: mode=AUTO (racing)") + raceTeePatch( + callingUid, keyDescriptor, attestationKey, params, parsedParams, isAttestKeyRequest + ) + } + parsedParams.attestationChallenge != null -> { + SystemLogger.debug("generateKey ${keyDescriptor.alias}: mode=PATCH (forwarding to TEE)") + TransactionResult.Continue + } + else -> { + SystemLogger.debug("generateKey ${keyDescriptor.alias}: no challenge, passthrough") + TransactionResult.ContinueAndSkipPost + } + } + } + .getOrElse { e -> + SystemLogger.error("generateKey failed for UID $callingUid: ${e.javaClass.simpleName}", e) + val code = + if (e is android.os.ServiceSpecificException) e.errorCode + else SECURE_HW_COMMUNICATION_FAILED + InterceptorUtils.createServiceSpecificErrorReply(code) + } + } + + /** Performs software key generation and caches the result. */ + private fun doSoftwareGeneration( + callingUid: Int, + keyDescriptor: KeyDescriptor, + attestationKey: KeyDescriptor?, + parsedParams: KeyMintAttestation, + isAttestKeyRequest: Boolean, + ): TransactionResult { + val genStartNanos = System.nanoTime() + keyDescriptor.nspace = secureRandom.nextLong() + SystemLogger.info( + "Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}]." + ) + + 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( + SECURE_HW_COMMUNICATION_FAILED, + "Unsupported symmetric algorithm: ${parsedParams.algorithm}", ) + } + val keyGen = javax.crypto.KeyGenerator.getInstance(algoName) + keyGen.init(parsedParams.keySize) + val secretKey = keyGen.generateKey() - // Generate the key pair and certificate chain. - val keyData = - CertificateGenerator.generateAttestedKeyPair( - callingUid, - keyDescriptor.alias, - attestationKey?.alias, - parsedParams, - securityLevel, - ) ?: throw Exception("CertificateGenerator failed to create key pair.") + 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) + TeeLatencySimulator.simulateGenerateKeyDelay( + parsedParams.algorithm, System.nanoTime() - genStartNanos + ) + return InterceptorUtils.createTypedObjectReply(metadata) + } + + val keyData = + CertificateGenerator.generateAttestedKeyPair( + callingUid, + keyDescriptor.alias, + attestationKey?.alias, + parsedParams, + securityLevel, + ) ?: throw Exception("CertificateGenerator failed to create key pair.") + + val response = + buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor) + generatedKeys[keyId] = + GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams) + if (isAttestKeyRequest) attestationKeys.add(keyId) + + TeeLatencySimulator.simulateGenerateKeyDelay( + parsedParams.algorithm, System.nanoTime() - genStartNanos + ) + val elapsedMs = (System.nanoTime() - genStartNanos) / 1_000_000.0 + SystemLogger.debug( + "doSoftwareGeneration ${keyDescriptor.alias}: completed in %.1fms (chain=${keyData.second.size} certs, nspace=${keyDescriptor.nspace})".format(elapsedMs) + ) + return InterceptorUtils.createTypedObjectReply(response.metadata) + } + + /** + * Races TEE hardware generation against software generation concurrently for AUTO mode. + * If TEE succeeds, the software future is cancelled and TEE is marked functional. + * If TEE fails, the already-running software result is used without additional delay. + */ + private fun raceTeePatch( + callingUid: Int, + keyDescriptor: KeyDescriptor, + attestationKey: KeyDescriptor?, + rawParams: Array, + parsedParams: KeyMintAttestation, + isAttestKeyRequest: Boolean, + ): TransactionResult { + SystemLogger.info("AUTO: racing TEE vs software for ${keyDescriptor.alias}") + + val teeDescriptor = KeyDescriptor().apply { + domain = keyDescriptor.domain + nspace = keyDescriptor.nspace + alias = keyDescriptor.alias + blob = keyDescriptor.blob + } + val teeAttestKey = + attestationKey?.let { + KeyDescriptor().apply { + domain = it.domain + nspace = it.nspace + alias = it.alias + blob = it.blob + } + } + val threadA = CompletableFuture.supplyAsync { + original.generateKey(teeDescriptor, teeAttestKey, rawParams, 0, byteArrayOf()) + } + + val swDescriptor = KeyDescriptor().apply { + domain = keyDescriptor.domain + nspace = secureRandom.nextLong() + alias = keyDescriptor.alias + blob = keyDescriptor.blob + } + + val threadB = CompletableFuture.supplyAsync { + doSoftwareGeneration( + callingUid, swDescriptor, attestationKey, parsedParams, isAttestKeyRequest + ) + } + + return try { + val teeMetadata = threadA.join() + threadB.cancel(true) + + val originalChain = CertificateHelper.getCertificateChain(teeMetadata) + + // Verify TEE attestation: the leaf cert must contain the attestation + // extension with the exact challenge we requested. Only then is the TEE + // proven capable of attestation and the chain can be patched. + if (parsedParams.attestationChallenge != null && + originalChain != null && originalChain.size > 1 + ) { + val verified = runCatching { + val leaf = org.bouncycastle.cert.X509CertificateHolder(originalChain[0].encoded) + val ext = leaf.getExtension(ATTESTATION_OID) + if (ext != null) { + val seq = org.bouncycastle.asn1.ASN1Sequence.getInstance(ext.extnValue.octets) + val challenge = (seq.getObjectAt( + AttestationConstants.KEY_DESCRIPTION_ATTESTATION_CHALLENGE_INDEX + ) as org.bouncycastle.asn1.ASN1OctetString).octets + challenge.contentEquals(parsedParams.attestationChallenge) + } else false + }.getOrDefault(false) + + if (verified) { + teeFunctional = true + SystemLogger.info("AUTO: TEE attestation verified for ${keyDescriptor.alias}, marked functional.") + + val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) + CertificateHelper.updateCertificateChain(teeMetadata, newChain).getOrThrow() + teeMetadata.authorizations = + InterceptorUtils.patchAuthorizations(teeMetadata.authorizations, callingUid) 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( - callingUid, - keyData.second, - parsedParams, - keyDescriptor, - ) - generatedKeys[keyId] = - GeneratedKeyInfo(keyData.first, keyDescriptor.nspace, response) - if (isAttestKeyRequest) attestationKeys.add(keyId) - - // Return the metadata of our generated key, skipping the real hardware call. - InterceptorUtils.createTypedObjectReply(response.metadata) - } else if (parsedParams.attestationChallenge != null) { - TransactionResult.Continue - } else { - TransactionResult.ContinueAndSkipPost + patchedChains[keyId] = newChain } } - .getOrElse { - SystemLogger.error("No key pair generated for UID $callingUid.", it) - TransactionResult.ContinueAndSkipPost + + // Cache the patched response for getKeyEntry. Stored in teeResponses + // (not generatedKeys) so createOperation forwards to real hardware. + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + val patchedResponse = KeyEntryResponse().apply { + this.metadata = teeMetadata + iSecurityLevel = original + } + teeResponses[keyId] = patchedResponse + + InterceptorUtils.createTypedObjectReply(teeMetadata) + } catch (_: Exception) { + SystemLogger.info("AUTO: TEE failed for ${keyDescriptor.alias}, using software result.") + try { + threadB.join() + } catch (e: Exception) { + SystemLogger.error("AUTO: both paths failed for ${keyDescriptor.alias}.", e) + val code = + if (e.cause is android.os.ServiceSpecificException) + (e.cause as android.os.ServiceSpecificException).errorCode + else SECURE_HW_COMMUNICATION_FAILED + InterceptorUtils.createServiceSpecificErrorReply(code) } + } } /** @@ -323,6 +726,14 @@ class KeyMintSecurityLevelInterceptor( companion object { private val secureRandom = SecureRandom() + /** Once set to true, AUTO mode skips the race and uses PATCH directly. */ + @Volatile var teeFunctional = false + + private const val INVALID_ARGUMENT = 20 + private const val PERMISSION_DENIED = 6 + private const val SECURE_HW_COMMUNICATION_FAILED = -49 + private const val CANNOT_ATTEST_IDS = -66 + // Transaction codes for IKeystoreSecurityLevel interface. private val GENERATE_KEY_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") @@ -334,6 +745,10 @@ 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 @@ -351,12 +766,19 @@ class KeyMintSecurityLevelInterceptor( val attestationKeys = ConcurrentHashMap.newKeySet() // Caches patched certificate chains to prevent re-generation and signature inconsistencies. val patchedChains = ConcurrentHashMap>() + // Keys imported via importKey; getKeyEntry must not override these. + val importedKeys: MutableSet = ConcurrentHashMap.newKeySet() + // TEE-generated responses cached for getKeyEntry (not for createOperation). + val teeResponses = 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() // --- Public Accessors for Other Interceptors --- fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = - generatedKeys[keyId]?.response + generatedKeys[keyId]?.response ?: teeResponses[keyId] /** * Finds a software-generated key by first filtering all known keys by the caller's UID, and @@ -390,6 +812,9 @@ class KeyMintSecurityLevelInterceptor( if (attestationKeys.remove(keyId)) { SystemLogger.debug("Remove cached attestaion key ${keyId}") } + importedKeys.remove(keyId) + usageCounters.remove(keyId) + teeResponses.remove(keyId) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { @@ -408,6 +833,9 @@ class KeyMintSecurityLevelInterceptor( generatedKeys.clear() patchedChains.clear() attestationKeys.clear() + importedKeys.clear() + usageCounters.clear() + teeResponses.clear() SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") } } @@ -449,6 +877,9 @@ private fun KeyMintAttestation.toAuthorizations( } this.purpose.forEach { authList.add(createAuth(Tag.PURPOSE, KeyParameterValue.keyPurpose(it))) } + this.blockMode.forEach { + authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(it))) + } this.digest.forEach { authList.add(createAuth(Tag.DIGEST, KeyParameterValue.digest(it))) } this.padding.forEach { authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) @@ -471,6 +902,42 @@ 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)) ) @@ -494,12 +961,48 @@ private fun KeyMintAttestation.toAuthorizations( authList.add(createAuth(Tag.BOOT_PATCHLEVEL, KeyParameterValue.integer(bootPatch))) } + // Software-enforced tags: CREATION_DATETIME, enforcement dates, USER_ID. + 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))) + 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..ff2cf3f9 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,15 @@ 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 @@ -15,13 +20,45 @@ import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.SystemLogger +/** Keystore2 error codes for ServiceSpecificException. Negative = KeyMint, positive = Keystore. */ +internal object KeystoreErrorCode { + const val INVALID_OPERATION_HANDLE = -28 + const val VERIFICATION_FAILED = -30 + const val UNSUPPORTED_PURPOSE = -2 + const val INCOMPATIBLE_PURPOSE = -3 + const val SYSTEM_ERROR = 4 + const val TOO_MUCH_DATA = 21 + const val KEY_EXPIRED = -25 + const val KEY_NOT_YET_VALID = -24 + + /** KeyMint ErrorCode::CALLER_NONCE_PROHIBITED */ + const val CALLER_NONCE_PROHIBITED = -55 + + /** KeyMint ErrorCode::INVALID_ARGUMENT */ + const val INVALID_ARGUMENT = -38 + + /** KeyMint ErrorCode::INVALID_TAG */ + const val INVALID_TAG = -40 + + /** Keystore2 ResponseCode::PERMISSION_DENIED */ + const val PERMISSION_DENIED = 6 + + /** Keystore2 ResponseCode::KEY_NOT_FOUND */ + 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. @@ -39,8 +76,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}" @@ -52,14 +90,16 @@ 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 = when (params.blockMode.firstOrNull()) { BlockMode.ECB -> "ECB" BlockMode.CBC -> "CBC" + BlockMode.CTR -> "CTR" BlockMode.GCM -> "GCM" else -> "ECB" // Default for RSA } @@ -82,6 +122,10 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi 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 @@ -102,6 +146,10 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri 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,12 +157,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 } @@ -123,21 +176,96 @@ 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, + 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. +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 +273,165 @@ 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]. */ -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? + 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]. */ +/** Binder interface for [SoftwareOperation]. Synchronized and input-length validated. */ class SoftwareOperationBinder(private val operation: SoftwareOperation) : IKeystoreOperation.Stub() { + private fun checkInputLength(data: ByteArray?) { + 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 { + 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 411c43be..9bd17b11 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.spec.ECGenParameterSpec import java.security.spec.RSAKeyGenParameterSpec import java.util.Date @@ -36,6 +35,9 @@ import org.matrix.TEESimulator.logging.SystemLogger */ object CertificateGenerator { + // RFC 5280 GeneralizedTime maximum: 9999-12-31T23:59:59 UTC (millis since epoch). + private const val UNDEFINED_NOT_AFTER = 253402300799000L + /** * Generates a software-based cryptographic key pair. * @@ -49,7 +51,10 @@ object CertificateGenerator { Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName) Algorithm.RSA -> "RSA" to - RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) + RSAKeyGenParameterSpec( + params.keySize, + params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4, + ) else -> throw IllegalArgumentException( "Unsupported algorithm: ${params.algorithm}" @@ -84,15 +89,13 @@ 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 { + return try { val keybox = getKeyboxForAlgorithm(uid, params.algorithm) - // Determine the signing key and issuer. If an attestKey is provided, use it. - // Otherwise, fall back to the root key from the keybox. val (signingKey, issuer) = if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second } @@ -101,20 +104,20 @@ object CertificateGenerator { keybox.keyPair to getIssuerFromKeybox(keybox) } - // Build the new leaf certificate with the simulated attestation. val leafCert = buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel) - // If not self-attesting, the chain is just the leaf. Otherwise, append the keybox - // chain. if (attestKeyAlias != null) { listOf(leafCert) } else { listOf(leafCert) + keybox.certificates } + } catch (e: android.os.ServiceSpecificException) { + throw e + } catch (e: Exception) { + SystemLogger.error("Failed to generate certificate chain.", e) + null } - .onFailure { SystemLogger.error("Failed to generate certificate chain.", it) } - .getOrNull() } /** @@ -128,7 +131,7 @@ object CertificateGenerator { params: KeyMintAttestation, securityLevel: Int, ): Pair>? { - return runCatching { + return try { SystemLogger.info( "Generating new attested key pair for alias: '$alias' (UID: $uid)" ) @@ -144,11 +147,12 @@ object CertificateGenerator { "Successfully generated new certificate chain for alias: '$alias'." ) Pair(newKeyPair, chain) + } catch (e: android.os.ServiceSpecificException) { + throw e + } catch (e: Exception) { + SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e) + null } - .onFailure { - SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it) - } - .getOrNull() } fun getIssuerFromKeybox(keybox: KeyBox) = @@ -163,7 +167,10 @@ object CertificateGenerator { else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm") } return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) - ?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName") + ?: throw android.os.ServiceSpecificException( + -75, // ATTESTATION_KEYS_NOT_PROVISIONED + "No attestation key for algorithm $algorithmName in $keyboxFile", + ) } /** Retrieves the key pair and issuer name for a given attestation key alias. */ @@ -215,17 +222,18 @@ object CertificateGenerator { uid: Int, securityLevel: Int, ): Certificate { - val subject = params.certificateSubject ?: X500Name("CN=Android KeyStore Key") - val leafNotAfter = - (signingKeyPair.public as? X509Certificate)?.notAfter - ?: Date(System.currentTimeMillis() + 31536000000L) + val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key") + + // Default validity: epoch to 9999-12-31T23:59:59 UTC (matches add_required_parameters). + val notBefore = params.certificateNotBefore ?: Date(0) + val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER) val builder = JcaX509v3CertificateBuilder( issuer, params.certificateSerial ?: BigInteger.ONE, - params.certificateNotBefore ?: Date(), - params.certificateNotAfter ?: leafNotAfter, + notBefore, + notAfter, subject, subjectKeyPair.public, ) @@ -240,11 +248,16 @@ object CertificateGenerator { AttestationBuilder.buildAttestationExtension(params, uid, securityLevel) ) + // The signature algorithm must match the SIGNING key, not the subject key. + // An EC attestation key may sign an RSA subject key's certificate (or vice versa). val signerAlgorithm = - when (params.algorithm) { - Algorithm.EC -> "SHA256withECDSA" - Algorithm.RSA -> "SHA256withRSA" - else -> throw IllegalArgumentException("Unsupported algorithm: ${params.algorithm}") + when (signingKeyPair.private) { + is java.security.interfaces.ECKey -> "SHA256withECDSA" + is java.security.interfaces.RSAKey -> "SHA256withRSA" + else -> + throw IllegalArgumentException( + "Unsupported signing key type: ${signingKeyPair.private.javaClass}" + ) } val contentSigner = JcaContentSignerBuilder(signerAlgorithm) diff --git a/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt b/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt new file mode 100644 index 00000000..0106984f --- /dev/null +++ b/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt @@ -0,0 +1,84 @@ +package org.matrix.TEESimulator.util + +import android.hardware.security.keymint.Algorithm +import java.security.SecureRandom +import java.util.concurrent.locks.LockSupport +import kotlin.math.abs +import kotlin.math.exp +import kotlin.math.ln +import kotlin.math.max + +/** + * Simulates realistic TEE hardware latency for software key generation. + * + * The delay model is derived from 64+ timing measurements across QTEE (Qualcomm) and Trustonic + * (MediaTek) hardware. It combines four independent noise sources that model different physical + * latency origins in a real TrustZone-based TEE: + * + * 1. Base crypto processing (log-normal): hardware RNG + key derivation + cert signing + * 2. Binder/kernel transit (exponential): IPC scheduling, context switches + * 3. TrustZone scheduler jitter (Gaussian): world-switch non-determinism + * 4. Cold-start penalty (half-normal): first operation after idle is slower due to TEE + * secure world re-initialization and TLB/cache warming + * + * Per-boot session bias models manufacturing variance between TEE hardware instances. + */ +object TeeLatencySimulator { + + private val rng = SecureRandom() + + private val sessionBiasMs: Double by lazy { rng.nextGaussian() * 5.0 } + private val coldPenaltyMs: Double by lazy { abs(rng.nextGaussian() * 12.0) } + + @Volatile private var firstCall = true + + fun simulateGenerateKeyDelay(algorithm: Int, elapsedNanos: Long) { + val elapsedMs = elapsedNanos / 1_000_000.0 + val targetMs = sampleTotalDelay(algorithm) + val remainingMs = targetMs - elapsedMs + + if (remainingMs > 1.0) { + LockSupport.parkNanos((remainingMs * 1_000_000).toLong()) + } + } + + private fun sampleTotalDelay(algorithm: Int): Double { + val base = sampleBaseCryptoDelay(algorithm) + val transit = sampleExponential(2.5) + val jitter = (rng.nextGaussian() * 2.5).coerceIn(-8.0, 12.0) + + var cold = 0.0 + if (firstCall) { + firstCall = false + cold = coldPenaltyMs + } + + return max(20.0, base + transit + jitter + sessionBiasMs + cold) + } + + /** + * Log-normal base delay. Parameters tuned to match observed hardware profiles: + * EC P-256 on QTEE averages ~65ms, RSA-2048 ~75ms, AES ~40ms. + * Sigma kept low (0.08) to match the tight clustering seen in real measurements. + */ + private fun sampleBaseCryptoDelay(algorithm: Int): Double { + val (mu, sigma) = + when (algorithm) { + Algorithm.EC -> ln(60.0) to 0.08 + Algorithm.RSA -> ln(70.0) to 0.08 + Algorithm.AES -> ln(35.0) to 0.10 + else -> ln(40.0) to 0.10 + } + return sampleLogNormal(mu, sigma) + } + + private fun sampleLogNormal(mu: Double, sigma: Double): Double { + return exp(mu + sigma * rng.nextGaussian()) + } + + private fun sampleExponential(mean: Double): Double { + var u = rng.nextDouble() + while (u == 0.0) u = rng.nextDouble() + return -mean * ln(u) + } +} diff --git a/module/customize.sh b/module/customize.sh index 373cd427..7d6d62f6 100644 --- a/module/customize.sh +++ b/module/customize.sh @@ -87,3 +87,11 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then ui_print "- Adding default target scope" install_file "target.txt" "$CONFIG_DIR" fi + +# Remove legacy TEE status file; TEE status is now determined at runtime. +rm -f "$CONFIG_DIR/tee_status.txt" + +if [ ! -f "$CONFIG_DIR/hbk" ]; then + ui_print "- Generating device-unique hardware-bound key seed" + head -c 32 /dev/random > "$CONFIG_DIR/hbk" +fi 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/module/service.sh b/module/service.sh index b2a35624..cd3466c3 100644 --- a/module/service.sh +++ b/module/service.sh @@ -4,6 +4,23 @@ MODDIR=${0%/*} cd $MODDIR +# Continuous logcat capture for debug builds. +if [ "$DEBUG" = "true" ]; then + BUGHUNTER_DIR="/data/adb/tricky_store/bughunter" + mkdir -p "$BUGHUNTER_DIR" + LOGFILE="$BUGHUNTER_DIR/bughunter_$(date +%Y%m%d_%H%M%S).log" + # Restart loop: if logcat is killed (lmkd, oom, etc.), it respawns. + # Append mode (>>) preserves data across restarts. + # head -c caps total output at 8MB then the pipeline terminates logcat. + (while true; do + logcat -v threadtime -T 1 -s "TEESimulator:*" 2>/dev/null | head -c 8388608 >> "$LOGFILE" + # head exits after 8MB causing logcat to receive SIGPIPE and die. + # If we reached the cap, the file is full — stop permanently. + [ "$(stat -c%s "$LOGFILE" 2>/dev/null || echo 0)" -ge 8388608 ] && break + sleep 1 + done) & +fi + while true; do ./daemon "$MODDIR" || exit 1 # ensure keystore initialized diff --git a/module/target.txt b/module/target.txt index 9f3ec54e..92e12487 100644 --- a/module/target.txt +++ b/module/target.txt @@ -1,3 +1,5 @@ +android +com.android.shell com.android.vending com.google.android.gms io.github.vvb2060.keyattestation 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/SELinux.java b/stub/src/main/java/android/os/SELinux.java new file mode 100644 index 00000000..6191b903 --- /dev/null +++ b/stub/src/main/java/android/os/SELinux.java @@ -0,0 +1,9 @@ +package android.os; + +/** Stub for android.os.SELinux. */ +public class SELinux { + public static boolean checkSELinuxAccess( + String scon, String tcon, String tclass, String perm) { + throw new UnsupportedOperationException("STUB!"); + } +} diff --git a/stub/src/main/java/android/os/ServiceManager.java b/stub/src/main/java/android/os/ServiceManager.java index 2e810d4e..14990bfa 100644 --- a/stub/src/main/java/android/os/ServiceManager.java +++ b/stub/src/main/java/android/os/ServiceManager.java @@ -17,6 +17,10 @@ public static IBinder checkService(String name) { throw new UnsupportedOperationException("STUB!"); } + public static boolean isDeclared(String name) { + throw new UnsupportedOperationException("STUB!"); + } + public static String[] listServices() { 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); + } +}