diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ac18c93 --- /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 e855cbd..7b87329 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java) val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt() val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir) -val verName = "v5.0" +val verName = "v3.2" android { namespace = "org.matrix.TEESimulator" @@ -71,35 +71,6 @@ dependencies { implementation(libs.bcpkix) } -// --- Rust native cert gen build task --- -val buildRustCertgen by tasks.registering(Exec::class) { - group = "TEESimulator-RS Native Build" - description = "Builds libcertgen.so via cargo-ndk for arm64-v8a." - - workingDir = rootProject.projectDir.resolve("native-certgen") - - commandLine( - "cargo", "ndk", - "-t", "arm64-v8a", - "-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath, - "build", "--release" - ) - - inputs.dir(rootProject.projectDir.resolve("native-certgen/src")) - inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.toml")) - inputs.file(rootProject.projectDir.resolve("native-certgen/Cargo.lock")) - outputs.dir(rootProject.projectDir.resolve("app/src/main/jniLibs")) - - environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath) -} - -// AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency -tasks.configureEach { - if (name.endsWith("JniLibFolders") && name.startsWith("merge")) { - dependsOn(buildRustCertgen) - } -} - androidComponents { onVariants(selector().all()) { variant -> val capitalized = variant.name.replaceFirstChar { it.uppercase() } @@ -108,22 +79,21 @@ androidComponents { // --- Define output locations and file names --- // Stage all files in a temporary directory inside 'build' before zipping val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}") - val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$capitalized.zip" + val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip" // Task 1: Prepare all module files in the temporary build directory. // Using Sync ensures that stale files from previous runs are removed. val prepareModuleFilesTask = tasks.register("prepareModuleFiles${capitalized}") { - group = "TEESimulator-RS Module Packaging" + group = "TEESimulator Module Packaging" description = "Prepares all files for the ${variant.name} module zip." if (isDebug) { dependsOn("package${capitalized}") } else { dependsOn("minify${capitalized}WithR8") - dependsOn("strip${capitalized}DebugSymbols") } - dependsOn(buildRustCertgen) + dependsOn("strip${capitalized}DebugSymbols") if (isDebug) { from(variant.artifacts.get(SingleArtifact.APK)) { @@ -140,14 +110,13 @@ androidComponents { } } - val nativeLibsDir = if (isDebug) { - "intermediates/merged_native_libs/${variant.name}/merge${capitalized}NativeLibs/out/lib" - } else { - "intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib" - } - from(project.layout.buildDirectory.dir(nativeLibsDir)) { - into("lib") - include("**/libinject.so", "**/libTEESimulator.so", "**/libsupervisor.so", "**/libcertgen.so") + from( + project.layout.buildDirectory.dir( + "intermediates/stripped_native_libs/${variant.name}/strip${capitalized}DebugSymbols/out/lib" + ) + ) { + into("lib") // Place them in the 'lib' subfolder of the staging directory. + include("**/libinject.so", "**/libTEESimulator.so") } // Now, copy and process the files from 'module' directory. @@ -162,7 +131,8 @@ androidComponents { // Use expand() for simple key-value replacement. expand( "REPLACEMEVERCODE" to gitCommitCount.toString(), - "REPLACEMEVER" to "$verName-$gitCommitCount", + "REPLACEMEVER" to + "$verName ($gitCommitCount-$gitCommitHash-${variant.name})", ) } @@ -173,7 +143,7 @@ androidComponents { // Task 2: Zip the prepared files from the temporary directory. val zipTask = tasks.register("zip${capitalized}") { - group = "TEESimulator-RS Module Packaging" + group = "TEESimulator Module Packaging" description = "Creates the flashable zip for the ${variant.name} module." dependsOn(prepareModuleFilesTask) @@ -186,7 +156,7 @@ androidComponents { fun createInstallTasks(rootProvider: String, installCli: String) { val pushTask = tasks.register("push${rootProvider}Module${capitalized}") { - group = "TEESimulator-RS Module Installation" + group = "TEESimulator Module Installation" description = "Pushes the ${variant.name} module to the device for $rootProvider." dependsOn(zipTask) @@ -200,7 +170,7 @@ androidComponents { val installTask = tasks.register("install${rootProvider}${capitalized}") { - group = "TEESimulator-RS Module Installation" + group = "TEESimulator Module Installation" description = "Installs the ${variant.name} module via $rootProvider." dependsOn(pushTask) commandLine( @@ -213,7 +183,7 @@ androidComponents { } tasks.register("install${rootProvider}AndReboot${capitalized}") { - group = "TEESimulator-RS Module Installation" + group = "TEESimulator Module Installation" description = "Installs the ${variant.name} module via $rootProvider and reboots." dependsOn(installTask) commandLine("adb", "reboot") diff --git a/app/src/main/cpp/binder_interceptor.cpp b/app/src/main/cpp/binder_interceptor.cpp index ff0d66c..8eef4ed 100644 --- a/app/src/main/cpp/binder_interceptor.cpp +++ b/app/src/main/cpp/binder_interceptor.cpp @@ -235,15 +235,20 @@ 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) mutable std::shared_mutex registry_mutex_; std::map, RegistrationEntry> registry_; public: BinderInterceptor() = default; + // 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_); auto it = registry_.find(target); @@ -350,22 +355,15 @@ static sp g_stub_instance = nullptr; namespace { -constexpr binder_size_t kMaxInterceptableDataSize = 256 * 1024; - +/** + * @brief Analyses a binder transaction. If the target is monitored, + * hijacks the transaction by rewriting its destination to our BinderStub. + * @param txn_data Pointer to the transaction data within the ioctl buffer. + */ void inspectAndRewriteTransaction(binder_transaction_data *txn_data) { if (!txn_data || txn_data->target.ptr == 0) return; - // Bypass interception for oversized payloads to prevent thread starvation from flood attacks - if (txn_data->data_size > kMaxInterceptableDataSize) - return; - - // AIDL methods use codes in [FIRST_CALL_TRANSACTION, LAST_CALL_TRANSACTION] (1..0x00ffffff). - // System transactions (PING, INTERFACE, DUMP, SHELL_COMMAND) use codes above that range. - // Skip those — intercepting a ping adds measurable latency that timing detectors flag. - if (txn_data->code > 0x00ffffffu && txn_data->code != intercept::kBackdoorCode) - return; - bool hijack = false; ThreadTransactionInfo info; @@ -540,11 +538,14 @@ status_t BinderInterceptor::handleRegister(const Parcel &data) { if (data.readStrongBinder(&callback) != OK || !callback) return BAD_VALUE; + // We can only intercept local Binders (BBinder), not remote proxies (BpBinder) if (target->localBinder() == nullptr) { LOGE("Cannot intercept remote binder proxies."); 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) { @@ -612,16 +613,9 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sptransact(intercept::kPreTransact, pre_req, &pre_resp); - if (pre_status != OK) { - // Block when interceptor is dead to prevent privacy leak to third-party apps - if (callback->pingBinder() != OK) { - LOGE("[TX_ID: %" PRIu64 "] Interceptor DEAD. Blocking to prevent attestation leak.", tx_id); - result = DEAD_OBJECT; - return true; - } - LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed (not dead). Forwarding.", tx_id); - return false; + if (callback->transact(intercept::kPreTransact, pre_req, &pre_resp) != OK) { + LOGW("[TX_ID: %" PRIu64 "] Pre-transaction callback failed. Forwarding original call.", tx_id); + return false; // Callback failed, proceed as if not intercepted } int32_t action = pre_resp.readInt32(); @@ -674,8 +668,7 @@ bool BinderInterceptor::processInterceptedTransaction(uint64_t tx_id, sptransact(intercept::kPostTransact, post_req, &post_resp); - if (post_status == OK) { + if (callback->transact(intercept::kPostTransact, post_req, &post_resp) == OK) { int32_t post_action = post_resp.readInt32(); if (post_action == intercept::kActionOverrideReply && reply) { result = post_resp.readInt32(); // Read new status diff --git a/app/src/main/java/org/matrix/TEESimulator/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt index d031482..0ff5675 100644 --- a/app/src/main/java/org/matrix/TEESimulator/App.kt +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -13,7 +13,6 @@ import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor import org.matrix.TEESimulator.logging.SystemLogger -import org.matrix.TEESimulator.pki.NativeCertGen import org.matrix.TEESimulator.util.AndroidDeviceUtils /** @@ -23,6 +22,8 @@ import org.matrix.TEESimulator.util.AndroidDeviceUtils object App { // The delay in milliseconds before retrying to initialize the interceptor. private const val RETRY_DELAY_MS = 1000L + // The sleep duration in milliseconds for the main service loop to keep the process alive. + private const val SERVICE_SLEEP_MS = 1000000L /** * The main entry point of the TEESimulator application. @@ -33,18 +34,13 @@ object App { fun main(args: Array) { SystemLogger.info("Welcome to TEESimulator!") - Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> - SystemLogger.error("Uncaught exception on ${thread.name}", throwable) - } - try { + // Initialize the Android framework environment prepareEnvironment() // 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". @@ -53,8 +49,6 @@ object App { Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) Security.addProvider(BouncyCastleProvider()) - NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so") - // This starts the message queue processing. It blocks here indefinitely // processing messages until Looper.myLooper().quit() is called. Looper.loop() 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 0b5f1e3..499a14d 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -115,7 +115,6 @@ object AttestationBuilder { } val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid) - SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch") properties[AttestationConstants.TAG_BOOT_PATCHLEVEL] = if (bootPatch != DO_NOT_REPORT) { DERTaggedObject( @@ -130,6 +129,7 @@ object AttestationBuilder { return properties } + /** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */ private fun buildKeyDescription( params: KeyMintAttestation, uid: Int, @@ -148,11 +148,15 @@ object AttestationBuilder { val fields = arrayOf( - ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()), - ASN1Enumerated(securityLevel), - ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()), - ASN1Enumerated(securityLevel), - DEROctetString(params.attestationChallenge ?: ByteArray(0)), + ASN1Integer( + AndroidDeviceUtils.getAttestVersion(securityLevel).toLong() + ), // attestationVersion + ASN1Enumerated(securityLevel), // attestationSecurityLevel + ASN1Integer( + AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong() + ), // keymasterVersion + ASN1Enumerated(securityLevel), // keymasterSecurityLevel + DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge DEROctetString(uniqueId), softwareEnforced, teeEnforced, @@ -160,24 +164,37 @@ object AttestationBuilder { 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) + .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) } } @@ -260,14 +277,20 @@ object AttestationBuilder { DERTaggedObject( true, AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST, - DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()), + DERSet( + params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray() + ), ) ) } if (params.rollbackResistance == true && attestVersion >= 3) { list.add( - DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE) + DERTaggedObject( + true, + AttestationConstants.TAG_ROLLBACK_RESISTANCE, + DERNull.INSTANCE, + ) ) } @@ -285,19 +308,31 @@ object AttestationBuilder { if (params.allowWhileOnBody == true) { list.add( - DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE) + 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) + 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) + DERTaggedObject( + true, + AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, + DERNull.INSTANCE, + ) ) } @@ -427,6 +462,7 @@ object AttestationBuilder { ) ) + // ATTESTATION_APPLICATION_ID is only included when an attestation challenge is present. if (params.attestationChallenge != null) { list.add( DERTaggedObject( @@ -436,7 +472,6 @@ object AttestationBuilder { ) ) } - if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) { list.add( DERTaggedObject( @@ -447,11 +482,8 @@ object AttestationBuilder { ) } - if (params.callerNonce == true) { - list.add( - DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE) - ) - } + // 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)) @@ -459,22 +491,38 @@ object AttestationBuilder { } params.originationExpireDateTime?.let { list.add( - DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time)) + 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)) + 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())) + 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) + DERTaggedObject( + true, + AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, + DERNull.INSTANCE, + ) ) } @@ -505,10 +553,16 @@ object AttestationBuilder { * retrieved. */ @Throws(Throwable::class) - internal fun createApplicationId(uid: Int): DEROctetString { + 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()) + return buildApplicationIdDer( + listOf("AndroidSystem" to 1L), + emptySet(), + ) } val pm = 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 3a56c0b..2ba04c7 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt @@ -95,5 +95,5 @@ object AttestationConstants { // --- Other Constants --- // https://cs.android.com/android/platform/superproject/main/+/main:system/keymaster/km_openssl/attestation_record.cpp - const val CHALLENGE_LENGTH_LIMIT = 128 + const val CHALLENGE_LENGTH_LIMIT = 128 // kMaximumAttestationChallengeLength } 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 114ad3d..4bad10a 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 0eaf31a..92b0d30 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -1,7 +1,6 @@ package org.matrix.TEESimulator.attestation import android.hardware.security.keymint.* -import android.hardware.security.keymint.KeyOrigin import java.math.BigInteger import java.util.Date import javax.security.auth.x500.X500Principal @@ -17,11 +16,12 @@ import org.matrix.TEESimulator.logging.KeyMintParameterLogger // Reference: // https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/key_parameter.rs data class KeyMintAttestation( - val keySize: Int, val algorithm: Int, val ecCurve: Int?, val ecCurveName: String, + val keySize: Int, val origin: Int?, + val noAuthRequired: Boolean?, val blockMode: List, val padding: List, val purpose: List, @@ -41,6 +41,7 @@ data class KeyMintAttestation( val manufacturer: ByteArray?, val model: ByteArray?, val secondImei: ByteArray?, + // Enforcement tags val activeDateTime: Date?, val originationExpireDateTime: Date?, val usageExpireDateTime: Date?, @@ -53,7 +54,6 @@ data class KeyMintAttestation( val allowWhileOnBody: Boolean?, val trustedUserPresenceRequired: Boolean?, val trustedConfirmationRequired: Boolean?, - val noAuthRequired: Boolean?, val maxUsesPerBoot: Int?, val maxBootLevel: Int?, val minMacLength: Int?, @@ -63,11 +63,13 @@ data class KeyMintAttestation( constructor( params: Array ) : this( - keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(), - // AOSP: [key_param(tag = ALGORITHM, field = Algorithm)] algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0, + // AOSP: [key_param(tag = KEY_SIZE, field = Integer)] + // For EC keys, derive keySize from EC_CURVE when KEY_SIZE is absent. + keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(), + // AOSP: [key_param(tag = EC_CURVE, field = EcCurve)] ecCurve = params.findEcCurve(Tag.EC_CURVE), ecCurveName = params.deriveEcCurveName(), @@ -75,6 +77,9 @@ data class KeyMintAttestation( // AOSP: [key_param(tag = ORIGIN, field = Origin)] origin = params.findOrigin(Tag.ORIGIN), + // AOSP: [key_param(tag = NO_AUTH_REQUIRED, field = BoolValue)] + noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED), + // AOSP: [key_param(tag = BLOCK_MODE, field = BlockMode)] blockMode = params.findAllBlockMode(Tag.BLOCK_MODE), @@ -116,6 +121,8 @@ 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), @@ -128,7 +135,6 @@ data class KeyMintAttestation( allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY), trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED), trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED), - noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED), maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT), maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL), minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH), @@ -138,13 +144,21 @@ data class KeyMintAttestation( params.forEach { KeyMintParameterLogger.logParameter(it) } } - fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY) + fun isAttestKey(): Boolean { + return purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY) + } - fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED + fun isImportKey(): Boolean { + return origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED + } } // --- Private helper extension functions for parsing KeyParameter arrays --- +/** Maps to AOSP field = Integer */ +private fun Array.findBoolean(tag: Int): Boolean? = + this.find { it.tag == tag }?.value?.boolValue + /** Maps to AOSP field = Integer */ private fun Array.findInteger(tag: Int): Int? = this.find { it.tag == tag }?.value?.integer @@ -177,7 +191,7 @@ private fun Array.findBlob(tag: Int): ByteArray? = private fun Array.findAllBlockMode(tag: Int): List = this.filter { it.tag == tag }.map { it.value.blockMode } -/** Maps to AOSP field = BlockMode (Repeated) */ +/** Maps to AOSP field = PaddingMode (Repeated) */ private fun Array.findAllPaddingMode(tag: Int): List = this.filter { it.tag == tag }.map { it.value.paddingMode } @@ -189,9 +203,7 @@ private fun Array.findAllKeyPurpose(tag: Int): List = private fun Array.findAllDigests(tag: Int): List = this.filter { it.tag == tag }.map { it.value.digest } -private fun Array.findBoolean(tag: Int): Boolean? = - if (this.any { it.tag == tag }) true else null - +/** 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) { 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 77b698c..f65ff53 100644 --- a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -65,6 +65,7 @@ object ConfigurationManager { // Initial load of all configuration files. loadTargetPackages(File(configRoot, TARGET_PACKAGES_FILE)) loadPatchLevelConfig(File(configRoot, PATCH_LEVEL_FILE)) + // Start watching for any subsequent file changes. ConfigObserver.startWatching() SystemLogger.info("Configuration initialized and file observer started.") @@ -82,6 +83,7 @@ object ConfigurationManager { return packages.firstNotNullOfOrNull { pkg -> packageKeyboxes[pkg] } ?: DEFAULT_KEYBOX_FILE } + /** Determines if the certificate for a given UID needs to be patched. */ fun shouldPatch(uid: Int): Boolean { val mode = getPackageModeForUid(uid) return mode == Mode.PATCH || mode == Mode.AUTO @@ -90,10 +92,13 @@ object ConfigurationManager { /** Determines if a new certificate needs to be generated for a given UID. */ fun shouldGenerate(uid: Int): Boolean = getPackageModeForUid(uid) == Mode.GENERATE + /** 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 @@ -151,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. @@ -246,14 +251,7 @@ object ConfigurationManager { } // Parse global and per-package configurations. - var newGlobalLevel = parseLines(contextLines[""]) - // TrickyAddon writes Pixel bulletin dates for boot/vendor but system=prop - // resolves to the real device prop — force boot/vendor through the same path - // to prevent cross-component date mismatches on non-Pixel devices. - if (newGlobalLevel?.system.equals("prop", ignoreCase = true)) { - SystemLogger.info("system=prop: forcing boot/vendor to derive from device props (were: boot=${newGlobalLevel?.boot}, vendor=${newGlobalLevel?.vendor})") - newGlobalLevel = newGlobalLevel?.copy(boot = "prop", vendor = "prop") - } + val newGlobalLevel = parseLines(contextLines[""]) contextLines.remove("") // Remove global context to iterate over packages next for ((pkg, lines) in contextLines) { @@ -284,10 +282,8 @@ object ConfigurationManager { val file = if (event != DELETE) File(configRoot, path) else null when (path) { - TARGET_PACKAGES_FILE -> file?.let { loadTargetPackages(it) } - ?: SystemLogger.warning("$TARGET_PACKAGES_FILE was deleted.") - PATCH_LEVEL_FILE -> file?.let { loadPatchLevelConfig(it) } - ?: SystemLogger.warning("$PATCH_LEVEL_FILE was deleted.") + TARGET_PACKAGES_FILE -> loadTargetPackages(file!!) + PATCH_LEVEL_FILE -> loadPatchLevelConfig(file!!) // Any change to an XML file is assumed to be a keybox. // The cache in KeyBoxManager will handle reloading it on its next use. else -> @@ -330,6 +326,8 @@ 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 = @@ -342,6 +340,7 @@ object ConfigurationManager { } } + /** 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 -> @@ -353,6 +352,7 @@ object ConfigurationManager { } } + /** Retrieves the package names associated with a UID. */ fun getPackagesForUid(uid: Int): Array { return uidToPackagesCache.getOrPut(uid) { try { 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 20e2803..b5bc390 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 @@ -109,17 +109,17 @@ abstract class BinderInterceptor : Binder() { * `handlePostTransact`). */ final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + // The native hook prepends a transaction ID to the data parcel. val txId = data.readLong() - val result = try { + val result = when (code) { + // These codes are defined in the native layer to distinguish hook types. PRE_TRANSACT_CODE -> handlePreTransact(txId, data) POST_TRANSACT_CODE -> handlePostTransact(txId, data) else -> return super.onTransact(code, data, reply, flags) } - } catch (e: Throwable) { - SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e) - TransactionResult.ContinueAndSkipPost - } + + // The reply parcel is guaranteed to be non-null for our custom transactions. writeResultToReply(result, reply!!) return true } @@ -293,6 +293,12 @@ abstract class BinderInterceptor : Binder() { } } + /** + * 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, 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 c2aac08..826dbab 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,8 +68,13 @@ 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") 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 b5ccb8c..d404eef 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 @@ -17,18 +17,6 @@ data class KeyIdentifier(val uid: Int, val alias: String) /** A collection of utility functions to support binder interception. */ object InterceptorUtils { - private const val EX_SERVICE_SPECIFIC = -8 - - fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply { - val parcel = Parcel.obtain().apply { - writeInt(EX_SERVICE_SPECIFIC) - writeString(null) - writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196) - writeInt(errorCode) - } - return BinderInterceptor.TransactionResult.OverrideReply(parcel) - } - /** * Uses reflection to get the integer transaction code for a given method name from a Stub * class. This is necessary for older Android versions where codes are not public constants. @@ -130,6 +118,10 @@ object InterceptorUtils { return exception != null } + /** + * Creates an `OverrideReply` that writes a `ServiceSpecificException` with the given error + * code via EX_SERVICE_SPECIFIC. + */ fun createServiceSpecificErrorReply( errorCode: Int ): BinderInterceptor.TransactionResult.OverrideReply { @@ -140,6 +132,14 @@ object InterceptorUtils { 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, 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 1e61f24..65917f4 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 @@ -15,7 +15,6 @@ import java.util.concurrent.ConcurrentHashMap import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager -import org.matrix.TEESimulator.interception.keystore.shim.GeneratedKeyPersistence import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor import org.matrix.TEESimulator.logging.KeyMintParameterLogger import org.matrix.TEESimulator.logging.SystemLogger @@ -58,8 +57,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } } - private const val RESPONSE_KEY_NOT_FOUND = 7 - private val deletedSoftwareKeys: MutableSet = ConcurrentHashMap.newKeySet() + // 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" @@ -100,7 +98,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { interceptor, KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES, ) - interceptor.loadPersistedKeys() } } .onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) } @@ -117,7 +114,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { interceptor, KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES, ) - interceptor.loadPersistedKeys() } } .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } @@ -145,23 +141,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { if (isGMS || ConfigurationManager.shouldSkipUid(callingUid)) { return TransactionResult.ContinueAndSkipPost + } else { + return TransactionResult.Continue } - - return runCatching { - val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION - if (ListEntriesHandler.cacheParameters(txId, data, isBatchMode)) { - TransactionResult.Continue - } else { - TransactionResult.ContinueAndSkipPost - } - } - .getOrElse { - SystemLogger.error( - "[TX_ID: $txId] Failed to parse parameters for ${transactionNames[code]!!}", - it, - ) - TransactionResult.ContinueAndSkipPost - } } else if ( code == GET_KEY_ENTRY_TRANSACTION || code == DELETE_KEY_TRANSACTION || @@ -181,6 +163,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ?: return TransactionResult.ContinueAndSkipPost if (code == DELETE_KEY_TRANSACTION) { + // Handle delete by alias (APP domain) or nspace (KEY_ID domain). val keyId = if (descriptor.alias != null) { KeyIdentifier(callingUid, descriptor.alias) @@ -199,7 +182,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { KeyMintSecurityLevelInterceptor.generatedKeys.containsKey(keyId) KeyMintSecurityLevelInterceptor.cleanupKeyData(keyId) if (isSoftwareKey) { - deletedSoftwareKeys.add(keyId) SystemLogger.info( "[TX_ID: $txId] Deleted cached keypair ${keyId.alias}, replying with empty response." ) @@ -214,14 +196,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } val keyId = KeyIdentifier(callingUid, descriptor.alias) - val response = KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) - if (response == null) { - if (deletedSoftwareKeys.remove(keyId)) { - SystemLogger.info("[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}") - return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND) - } - return TransactionResult.Continue - } + val response = + KeyMintSecurityLevelInterceptor.getGeneratedKeyResponse(keyId) + ?: return TransactionResult.Continue if (KeyMintSecurityLevelInterceptor.isAttestationKey(keyId)) SystemLogger.info("${descriptor.alias} was an attestation key") @@ -282,8 +259,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) return runCatching { + val isBatchMode = code == LIST_ENTRIES_BATCHED_TRANSACTION + val params = + ListEntriesHandler.cacheParameters(txId, data, isBatchMode) + ?: throw Exception("Abort updating entries for invalid parameters.") val updatedKeyDescriptors = - ListEntriesHandler.injectGeneratedKeys(txId, callingUid, reply) + ListEntriesHandler.injectGeneratedKeys(txId, callingUid, params, reply) InterceptorUtils.createTypedArrayReply(updatedKeyDescriptors) } .getOrElse { @@ -306,13 +287,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { callingPid, ) - if (!ConfigurationManager.shouldPatch(callingUid)) - return TransactionResult.SkipTransaction - runCatching { 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 @@ -324,26 +303,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray() ) - if (parsedParameters.isImportKey()) { - val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) - if (retainedChain == null) { - SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).") - return TransactionResult.SkipTransaction - } - SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId") - CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow() - return InterceptorUtils.createTypedObjectReply(response) - } - - if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) { - SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId") - 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." ) + // Attest keys that are not under our control should be overriden. val keyData = CertificateGenerator.generateAttestedKeyPair( callingUid, @@ -375,26 +341,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { parsedParameters, ) KeyMintSecurityLevelInterceptor.attestationKeys.add(keyId) - - GeneratedKeyPersistence.save( - keyId = keyId, - keyPair = keyData.first, - nspace = newNspace, - securityLevel = response.metadata.keySecurityLevel, - certChain = keyData.second, - algorithm = parsedParameters.algorithm, - keySize = parsedParameters.keySize, - ecCurve = parsedParameters.ecCurve ?: 0, - purposes = parsedParameters.purpose, - digests = parsedParameters.digest, - isAttestationKey = true, - ) - return InterceptorUtils.createTypedObjectReply(response) } val originalChain = CertificateHelper.getCertificateChain(response) + // Check if we should perform attestation patch. if (originalChain == null || originalChain.size < 2) { SystemLogger.info( "[TX_ID: $txId] Skip patching short certificate chain of length ${originalChain?.size}." @@ -402,6 +354,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { return TransactionResult.SkipTransaction } + // First, try to retrieve the already-patched chain from our cache to ensure + // consistency. val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) val finalChain: Array @@ -411,12 +365,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ) finalChain = cachedChain } else { + // If no chain is cached (e.g., key existed before simulator started), + // perform a live patch as a fallback. This may still be detectable. SystemLogger.info( "[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback." ) finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) + KeyMintSecurityLevelInterceptor.patchedChains[keyId] = finalChain + SystemLogger.debug("Cached patched certificate chain for $keyId.") } CertificateHelper.updateCertificateChain(response.metadata, finalChain) @@ -445,11 +403,13 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return TransactionResult.ContinueAndSkipPost + // Resolve by nspace (KEY_ID) or alias (APP), same as createOperation. val generatedKeyInfo = when (descriptor.domain) { Domain.KEY_ID -> KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( - callingUid, descriptor.nspace + callingUid, + descriptor.nspace, ) Domain.APP -> descriptor.alias?.let { @@ -459,6 +419,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } if (generatedKeyInfo == null) { + // Hardware key: mark so getKeyEntry skips cert re-patching. descriptor.alias?.let { userUpdatedKeys.add(KeyIdentifier(callingUid, it)) } return TransactionResult.ContinueAndSkipPost } @@ -470,9 +431,6 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { metadata.certificate = publicCert metadata.certificateChain = certificateChain - - GeneratedKeyPersistence.rePersistIfNeeded(callingUid, generatedKeyInfo) - SystemLogger.verbose( "Key updated with sizes: [publicCert, certificateChain] = [${publicCert?.size}, ${certificateChain?.size}]" ) 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 33bb559..8fc0300 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 @@ -399,17 +399,18 @@ private data class LegacyKeygenParameters( /** * Converts the legacy parameters into the modern [KeyMintAttestation] data structure, which is - * required by the refactored [AttestationBuilder] and [CertificateGenerator]. + * required by [AttestationBuilder] and [CertificateGenerator]. */ fun toKeyMintAttestation(): KeyMintAttestation { // This conversion acts as a bridge, allowing our new generic components // to be used by the legacy interceptor. return KeyMintAttestation( - keySize = this.keySize, algorithm = this.algorithm, - ecCurve = 0, + ecCurve = 0, // Not explicitly available in legacy args, but not critical ecCurveName = this.ecCurveName ?: "", - origin = null, + keySize = this.keySize, + origin = null, // Not needed to build attestaion + noAuthRequired = null, blockMode = listOf(), padding = listOf(), purpose = this.purpose, @@ -443,7 +444,6 @@ private data class LegacyKeygenParameters( allowWhileOnBody = null, trustedUserPresenceRequired = null, trustedConfirmationRequired = null, - noAuthRequired = null, maxUsesPerBoot = null, maxBootLevel = null, minMacLength = null, 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 05e4469..2cad788 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 @@ -5,7 +5,6 @@ import android.system.keystore2.Domain import android.system.keystore2.IKeystoreService import android.system.keystore2.KeyDescriptor import java.util.TreeMap -import java.util.concurrent.ConcurrentHashMap import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor import org.matrix.TEESimulator.logging.SystemLogger @@ -22,15 +21,6 @@ object ListEntriesHandler { // Estimate for maximum size of a Binder response in bytes. private const val RESPONSE_SIZE_LIMIT = 358400 - // Parameters of AOSP function `list_key_entries` in utils.rs. - private data class ListEntriesParams( - val domain: Int, - val namespace: Long, - val startPastAlias: String?, - ) - - private val pendingParams = ConcurrentHashMap() - // Based on AOSP function `estimate_safe_amount_to_return` in utils.rs. private fun estimateSafeAmountToReturn( keyDescriptors: Array, @@ -60,7 +50,7 @@ object ListEntriesHandler { } // Parse and store parameters for later use (in post-transaction). - fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): Boolean { + fun cacheParameters(txId: Long, data: Parcel, isBatchMode: Boolean): ListEntriesParams? { data.enforceInterface(IKeystoreService.DESCRIPTOR) val domain = data.readInt() @@ -71,20 +61,21 @@ object ListEntriesHandler { // See AOSP function `get_key_descriptor_for_lookup` in service.rs. // Note that all generated keys belong to Domain::APP. if (domain == Domain.APP) { - pendingParams[txId] = ListEntriesParams(domain, namespace, startPastAlias) - SystemLogger.debug("[TX_ID: $txId] Cached ${pendingParams[txId]}.") - return true + val params = ListEntriesParams(domain, namespace, startPastAlias) + SystemLogger.debug("[TX_ID: $txId] Cached $params.") + return params } - return false + return null } // Merge software-backed keys with hardware-backed keys in the reply parcel. - fun injectGeneratedKeys(txId: Long, callingUid: Int, reply: Parcel): Array { - val params = - pendingParams.remove(txId) - ?: throw IllegalStateException("No params found for listing entries") - + fun injectGeneratedKeys( + txId: Long, + callingUid: Int, + params: ListEntriesParams, + reply: Parcel, + ): Array { // By default we use the calling uid as namespace if domain is Domain::APP. // The namespace parameter is thus ignored for non-privileged applications. // See AOSP function `get_key_descriptor_for_lookup` in service.rs. @@ -140,3 +131,6 @@ object ListEntriesHandler { } } } + +// Parameters of AOSP function `list_key_entries` in utils.rs. +data class ListEntriesParams(val domain: Int, val namespace: Long, val startPastAlias: String?) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt deleted file mode 100644 index afa2c57..0000000 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt +++ /dev/null @@ -1,76 +0,0 @@ -package org.matrix.TEESimulator.interception.keystore.shim - -import android.hardware.security.keymint.Algorithm -import android.hardware.security.keymint.KeyPurpose -import android.hardware.security.keymint.KeyParameter -import android.hardware.security.keymint.Tag -import org.matrix.TEESimulator.attestation.KeyMintAttestation - -object AuthorizeCreate { - - fun check( - keyParams: KeyMintAttestation?, - opParams: KeyMintAttestation, - rawOpParams: Array? = null, - ): Int? { - if (keyParams == null) return null - val purpose = opParams.purpose.firstOrNull() ?: return null - // Algorithm-level rejection runs before purpose-list check (AOSP HAL behavior) - return checkAlgorithmPurpose(keyParams, purpose) - ?: checkPurpose(keyParams, purpose) - ?: checkTemporalValidity(keyParams, purpose) - ?: checkCallerNonce(keyParams, purpose, rawOpParams) - } - - private fun checkAlgorithmPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? { - val algo = keyParams.algorithm - if ((algo == Algorithm.EC || algo == Algorithm.RSA) && - (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT) - ) { - return KeystoreErrorCodes.unsupportedPurpose - } - if (algo == Algorithm.EC && purpose == KeyPurpose.DECRYPT) - return KeystoreErrorCodes.unsupportedPurpose - if (algo == Algorithm.RSA && purpose == KeyPurpose.AGREE_KEY) - return KeystoreErrorCodes.unsupportedPurpose - return null - } - - private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? { - if (purpose == KeyPurpose.WRAP_KEY) - return KeystoreErrorCodes.incompatiblePurpose - if (purpose !in keyParams.purpose) - return KeystoreErrorCodes.incompatiblePurpose - return null - } - - private fun checkTemporalValidity(keyParams: KeyMintAttestation, purpose: Int): Int? { - val now = System.currentTimeMillis() - - keyParams.activeDateTime?.let { activeDate -> - if (now < activeDate.time) return KeystoreErrorCodes.keyNotYetValid - } - - keyParams.originationExpireDateTime?.let { expireDate -> - if (purpose == KeyPurpose.SIGN || purpose == KeyPurpose.ENCRYPT) { - if (now > expireDate.time) return KeystoreErrorCodes.keyExpired - } - } - - keyParams.usageExpireDateTime?.let { expireDate -> - if (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.DECRYPT) { - if (now > expireDate.time) return KeystoreErrorCodes.keyExpired - } - } - - return null - } - - private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array?): Int? { - if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null - if (keyParams.callerNonce == true) return null - if (rawOpParams?.any { it.tag == Tag.NONCE } == true) - return KeystoreErrorCodes.callerNonceProhibited - return null - } -} diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index e198d9c..5b64fed 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,33 +1,20 @@ package org.matrix.TEESimulator.interception.keystore.shim import android.hardware.security.keymint.Algorithm -import android.hardware.security.keymint.BlockMode -import android.hardware.security.keymint.EcCurve -import android.hardware.security.keymint.KeyParameter -import android.hardware.security.keymint.KeyPurpose -import android.hardware.security.keymint.KeyParameterValue 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 import android.system.keystore2.* -import android.util.Pair as AndroidPair -import java.io.ByteArrayInputStream -import java.security.KeyFactory import java.security.KeyPair import java.security.SecureRandom import java.security.cert.Certificate -import java.security.cert.CertificateFactory -import java.security.spec.PKCS8EncodedKeySpec import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedDeque -import java.util.concurrent.Executors -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.LockSupport -import org.matrix.TEESimulator.attestation.AttestationBuilder -import org.matrix.TEESimulator.attestation.AttestationConstants import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager @@ -35,31 +22,29 @@ import org.matrix.TEESimulator.interception.core.BinderInterceptor import org.matrix.TEESimulator.interception.keystore.InterceptorUtils import org.matrix.TEESimulator.interception.keystore.KeyIdentifier import org.matrix.TEESimulator.logging.SystemLogger -import org.matrix.TEESimulator.pki.CertGenConfig import org.matrix.TEESimulator.pki.CertificateGenerator import org.matrix.TEESimulator.pki.CertificateHelper -import org.matrix.TEESimulator.pki.KeyBoxManager -import org.matrix.TEESimulator.pki.NativeCertGen import org.matrix.TEESimulator.util.AndroidDeviceUtils -import org.matrix.TEESimulator.util.AndroidPermissionUtils import org.matrix.TEESimulator.util.TeeLatencySimulator +/** + * Intercepts calls to an `IKeystoreSecurityLevel` service (e.g., TEE or StrongBox). This is where + * the core logic for key generation and import handling for modern Android resides. + */ class KeyMintSecurityLevelInterceptor( private val original: IKeystoreSecurityLevel, private val securityLevel: Int, ) : BinderInterceptor() { + // --- Data Structures for State Management --- data class GeneratedKeyInfo( val keyPair: KeyPair?, val secretKey: javax.crypto.SecretKey?, val nspace: Long, val response: KeyEntryResponse, - val keyParams: KeyMintAttestation? = null, + val keyParams: KeyMintAttestation, ) - private val activeOps = ConcurrentHashMap>() - private val recentOps = ConcurrentHashMap>() - override fun onPreTransact( txId: Long, target: IBinder, @@ -75,7 +60,7 @@ class KeyMintSecurityLevelInterceptor( GENERATE_KEY_TRANSACTION -> { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) - if (!shouldSkip) return handleGenerateKey(txId, callingUid, callingPid, data) + if (!shouldSkip) return handleGenerateKey(callingUid, callingPid, data) } CREATE_OPERATION_TRANSACTION -> { logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) @@ -86,8 +71,7 @@ class KeyMintSecurityLevelInterceptor( logTransaction(txId, transactionNames[code]!!, callingUid, callingPid) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) - ?: return TransactionResult.ContinueAndSkipPost + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! SystemLogger.info( "[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]" ) @@ -117,6 +101,7 @@ class KeyMintSecurityLevelInterceptor( reply: Parcel?, resultCode: Int, ): TransactionResult { + // We only care about successful transactions. if (resultCode != 0 || reply == null || InterceptorUtils.hasException(reply)) return TransactionResult.SkipTransaction @@ -127,31 +112,23 @@ class KeyMintSecurityLevelInterceptor( val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) ?: return TransactionResult.SkipTransaction - // Evict generated key data but retain patched chains so detectors - // can't use importKey to force unpatched getKeyEntry responses. val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) - if (generatedKeys.remove(keyId) != null) { - SystemLogger.debug("Remove generated key on importKey $keyId") - GeneratedKeyPersistence.delete(keyId) - } - attestationKeys.remove(keyId) + cleanupKeyData(keyId) importedKeys.add(keyId) + // Patch imported key certificates the same way as generated keys. if (!ConfigurationManager.shouldSkipUid(callingUid)) { val metadata: KeyMetadata = reply.readTypedObject(KeyMetadata.CREATOR) ?: return TransactionResult.SkipTransaction val originalChain = CertificateHelper.getCertificateChain(metadata) if (originalChain != null && originalChain.size > 1) { - val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) + val newChain = + AttestationPatcher.patchCertificateChain(originalChain, callingUid) CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() metadata.authorizations = InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid) patchedChains[keyId] = newChain - teeResponses[keyId] = KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } SystemLogger.debug("Cached patched certificate chain for imported key $keyId.") return InterceptorUtils.createTypedObjectReply(metadata) } @@ -160,10 +137,8 @@ class KeyMintSecurityLevelInterceptor( logTransaction(txId, "post-${transactionNames[code]!!}", callingUid, callingPid) data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) - ?: return TransactionResult.SkipTransaction - val params = data.createTypedArray(KeyParameter.CREATOR) - ?: return TransactionResult.SkipTransaction + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + val params = data.createTypedArray(KeyParameter.CREATOR)!! val parsedParams = KeyMintAttestation(params) val forced = data.readBoolean() if (forced) @@ -171,8 +146,7 @@ class KeyMintSecurityLevelInterceptor( "[TX_ID: $txId] Current operation has a very high pruning power." ) val response: CreateOperationResponse = - reply.readTypedObject(CreateOperationResponse.CREATOR) - ?: return TransactionResult.SkipTransaction + reply.readTypedObject(CreateOperationResponse.CREATOR)!! SystemLogger.verbose( "[TX_ID: $txId] CreateOperationResponse: ${response.iOperation} ${response.operationChallenge}" ) @@ -185,7 +159,12 @@ class KeyMintSecurityLevelInterceptor( val backdoor = getBackdoor(target) if (backdoor != null) { val interceptor = OperationInterceptor(operation, backdoor) - register(backdoor, operationBinder, interceptor, OperationInterceptor.INTERCEPTED_CODES) + register( + backdoor, + operationBinder, + interceptor, + OperationInterceptor.INTERCEPTED_CODES, + ) interceptedOperations[operationBinder] = interceptor } else { SystemLogger.error( @@ -200,6 +179,9 @@ class KeyMintSecurityLevelInterceptor( val metadata: KeyMetadata = reply.readTypedObject(KeyMetadata.CREATOR) ?: return TransactionResult.SkipTransaction + KeyMintAttestation( + metadata.authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray() + ) val originalChain = CertificateHelper.getCertificateChain(metadata) ?: return TransactionResult.SkipTransaction @@ -208,10 +190,8 @@ class KeyMintSecurityLevelInterceptor( // Cache the newly patched chain to ensure consistency across subsequent API calls. data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) - ?: return TransactionResult.SkipTransaction - val key = metadata.key - ?: return TransactionResult.SkipTransaction + val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + val key = metadata.key!! val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() metadata.authorizations = @@ -220,10 +200,6 @@ class KeyMintSecurityLevelInterceptor( // We must clean up cached generated keys before storing the patched chain cleanupKeyData(keyId) patchedChains[keyId] = newChain - teeResponses[keyId] = KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } SystemLogger.debug( "Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])" ) @@ -234,233 +210,254 @@ class KeyMintSecurityLevelInterceptor( return TransactionResult.SkipTransaction } - private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) { - val ops = activeOps.computeIfAbsent(uid) { ConcurrentLinkedDeque() } - val before = ops.size - ops.removeIf { it.finalized } - val afterClean = ops.size - while (ops.size >= maxOps) { - val oldest = ops.pollFirst() ?: break - if (!oldest.finalized) { - SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)") - oldest.abort() - } - } - ops.addLast(newOp) - SystemLogger.debug("[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}") - } - - private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? { - if (securityLevel != SecurityLevel.STRONGBOX) return null - val timestamps = recentOps.computeIfAbsent(callingUid) { ConcurrentLinkedDeque() } - val cutoff = System.nanoTime() - STRONGBOX_OP_WINDOW_NS - timestamps.removeIf { it < cutoff } - val swOps = activeOps[callingUid]?.count { !it.finalized } ?: 0 - if (timestamps.size + swOps >= STRONGBOX_MAX_CONCURRENT_OPS) { - SystemLogger.info("[TX_ID: $txId] StrongBox op limit reached for uid=$callingUid (hw=${timestamps.size} sw=$swOps max=$STRONGBOX_MAX_CONCURRENT_OPS)") - return InterceptorUtils.createErrorReply(KEYMINT_TOO_MANY_OPERATIONS) - } - timestamps.addLast(System.nanoTime()) - return null - } - + /** + * Handles the `createOperation` transaction. It checks if the operation is for a key that was + * generated in software. If so, it creates a software-based operation handler. Otherwise, it + * lets the call proceed to the real hardware service. + */ private fun handleCreateOperation( txId: Long, callingUid: Int, data: Parcel, - ): TransactionResult = runCatching { - SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}") + ): TransactionResult { data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! - SystemLogger.debug("[TX_ID: $txId] createOperation descriptor: domain=${keyDescriptor.domain} nspace=${keyDescriptor.nspace} alias=${keyDescriptor.alias}") - - // Android framework calls createOperation with domain=APP+alias; - // keystore2 internally resolves to KEY_ID — but software keys never - // reach keystore2's database, so we must handle both lookup paths. - val resolvedEntry: Map.Entry = + // Resolve key descriptor to a generated key via nspace (KEY_ID) or alias (APP). + val resolvedEntry: Map.Entry? = when (keyDescriptor.domain) { - Domain.APP -> { - val alias = keyDescriptor.alias ?: run { - SystemLogger.info("[TX_ID: $txId] createOperation domain=APP with null alias, forwarding to HAL") - return TransactionResult.ContinueAndSkipPost - } - val key = KeyIdentifier(callingUid, alias) - generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } ?: run { - SystemLogger.info("[TX_ID: $txId] createOperation alias=$alias not in generatedKeys, forwarding to HAL") - return TransactionResult.ContinueAndSkipPost - } - } Domain.KEY_ID -> { val nspace = keyDescriptor.nspace - val entry = if (nspace == null || nspace == 0L) null - else generatedKeys.entries + if (nspace == 0L) null + else + generatedKeys.entries .filter { it.key.uid == callingUid } .find { it.value.nspace == nspace } - entry ?: run { - trackAndEnforceOpLimit(callingUid, txId)?.let { return it } - SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.") - return TransactionResult.Continue + } + Domain.APP -> + keyDescriptor.alias?.let { alias -> + val key = KeyIdentifier(callingUid, alias) + generatedKeys[key]?.let { java.util.AbstractMap.SimpleEntry(key, it) } } - } - else -> { - SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL") - return TransactionResult.ContinueAndSkipPost - } + else -> null } - val generatedKeyInfo = resolvedEntry.value - val resolvedKeyId = resolvedEntry.key + val generatedKeyInfo = resolvedEntry?.value + val resolvedKeyId = resolvedEntry?.key - trackAndEnforceOpLimit(callingUid, txId)?.let { return it } - - SystemLogger.info("[TX_ID: $txId] Creating SOFTWARE operation for uid=$callingUid.") - - val params = data.createTypedArray(KeyParameter.CREATOR)!! - val parsedParams = KeyMintAttestation(params).let { p -> - if (p.algorithm != 0) p - else { - val keyAlgo = generatedKeyInfo.keyPair?.private?.algorithm - p.copy(algorithm = when (keyAlgo) { - "EC", "ECDSA" -> Algorithm.EC - "RSA" -> Algorithm.RSA - else -> generatedKeyInfo.keyParams?.algorithm ?: p.algorithm - }) - } - } - val forced = data.readBoolean() - - val requestedPurpose = parsedParams.purpose.firstOrNull() - if (requestedPurpose == null) { - return InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_INVALID_ARGUMENT) + if (generatedKeyInfo == null) { + SystemLogger.debug( + "[TX_ID: $txId] Operation for unknown/hardware key (domain=${keyDescriptor.domain}, " + + "alias=${keyDescriptor.alias}, nspace=${keyDescriptor.nspace}). Forwarding." + ) + return TransactionResult.Continue } - if (forced) { - return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED) - } + SystemLogger.info( + "[TX_ID: $txId] Creating SOFTWARE operation for key ${generatedKeyInfo.nspace}." + ) - AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode -> - SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode") - return InterceptorUtils.createServiceSpecificErrorReply(errorCode) - } + val opParams = data.createTypedArray(KeyParameter.CREATOR)!! + val parsedOpParams = KeyMintAttestation(opParams) + data.readBoolean() // forced: no-op for sw ops val keyParams = generatedKeyInfo.keyParams - val effectiveParams = if (keyParams != null) { - keyParams.copy( - purpose = parsedParams.purpose, - digest = parsedParams.digest.ifEmpty { keyParams.digest }, + + val requestedPurpose = parsedOpParams.purpose.firstOrNull() + if (requestedPurpose == null) { + return InterceptorUtils.createServiceSpecificErrorReply( + KeystoreErrorCode.INVALID_ARGUMENT ) - } else parsedParams + } - val opLatency = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_OP_LATENCY_FLOOR_MS else 0L - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, generatedKeyInfo.secretKey, effectiveParams, opLatency) + 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 (keyParams?.usageCountLimit != null) { - val limit = keyParams.usageCountLimit - val remaining = usageCounters.getOrPut(resolvedKeyId) { - java.util.concurrent.atomic.AtomicInteger(limit) - } - if (remaining.get() <= 0) { - cleanupKeyData(resolvedKeyId) - usageCounters.remove(resolvedKeyId) - return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_KEY_NOT_FOUND) - } - softwareOperation.onFinishCallback = { - if (remaining.decrementAndGet() <= 0) { - cleanupKeyData(resolvedKeyId) - usageCounters.remove(resolvedKeyId) - SystemLogger.info("Key $resolvedKeyId exhausted (USAGE_COUNT_LIMIT=$limit).") - } + 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 + ) } } - val maxOps = if (securityLevel == SecurityLevel.STRONGBOX) STRONGBOX_MAX_CONCURRENT_OPS else MAX_CONCURRENT_OPS_PER_UID - pruneOpsForUid(callingUid, softwareOperation, maxOps) - val operationBinder = SoftwareOperationBinder(softwareOperation) - - val response = - CreateOperationResponse().apply { - iOperation = operationBinder - operationChallenge = null - parameters = softwareOperation.beginParameters + // 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 + ) } + } - InterceptorUtils.createTypedObjectReply(response) - }.getOrElse { - SystemLogger.error("Error during createOperation for UID $callingUid.", it) - InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_UNKNOWN_ERROR) + // 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 + ) + } + } + + 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 }) + val softwareOperation = + SoftwareOperation( + txId, + generatedKeyInfo.keyPair, + generatedKeyInfo.secretKey, + effectiveParams, + ) + + // 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 + ) + } } - private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { - if (data.dataSize() > MAX_ALIAS_LENGTH) { - SystemLogger.warning("Skipping oversized transaction: ${data.dataSize()} bytes") - return TransactionResult.ContinueAndSkipPost - } - + /** + * 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, callingPid: Int, data: Parcel): TransactionResult { return runCatching { data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! val attestationKey = data.readTypedObject(KeyDescriptor.CREATOR) - SystemLogger.debug( "Handling generateKey ${keyDescriptor.alias}, attestKey=${attestationKey?.alias}" ) + val params = data.createTypedArray(KeyParameter.CREATOR)!! - val parsedParams = KeyMintAttestation(params) - - val challenge = parsedParams.attestationChallenge - if (challenge != null && challenge.size > AttestationConstants.CHALLENGE_LENGTH_LIMIT) { - SystemLogger.warning("[TX_ID: $txId] Rejecting oversized attestation challenge: ${challenge.size} bytes (max ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})") - return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH) - } + // Caller-provided CREATION_DATETIME is not allowed. if (params.any { it.tag == Tag.CREATION_DATETIME }) { - SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params") - return InterceptorUtils.createErrorReply(RESPONSE_INVALID_ARGUMENT) + return@runCatching InterceptorUtils.createServiceSpecificErrorReply( + INVALID_ARGUMENT + ) } - if (params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid)) { - SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid") - return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) + // Device ID attestation requires READ_PRIVILEGED_PHONE_STATE. + val hasDeviceIdTags = + params.any { + it.tag == Tag.ATTESTATION_ID_SERIAL || + it.tag == Tag.ATTESTATION_ID_IMEI || + it.tag == Tag.ATTESTATION_ID_MEID || + it.tag == Tag.DEVICE_UNIQUE_ATTESTATION + } + if ( + hasDeviceIdTags && + !ConfigurationManager.hasPermissionForUid( + callingUid, + "android.permission.READ_PRIVILEGED_PHONE_STATE", + ) + ) { + return@runCatching InterceptorUtils.createServiceSpecificErrorReply( + CANNOT_ATTEST_IDS + ) } - val hasDeviceIdAttestation = params.any { - it.tag == Tag.ATTESTATION_ID_IMEI || - it.tag == Tag.ATTESTATION_ID_MEID || - it.tag == Tag.ATTESTATION_ID_SERIAL || - it.tag == Tag.DEVICE_UNIQUE_ATTESTATION || - it.tag == Tag.ATTESTATION_ID_SECOND_IMEI - } - - if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) { - SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid") - return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) - } - - // AOSP security_level.rs:478-485: INCLUDE_UNIQUE_ID requires - // SELinux gen_unique_id OR Android REQUEST_UNIQUE_ID_ATTESTATION + // 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", - ) + val hasSELinux = + ConfigurationManager.checkSELinuxPermission( + callingPid, + "keystore_key", + "gen_unique_id", + ) + val hasAndroid = + ConfigurationManager.hasPermissionForUid( + callingUid, + "android.permission.REQUEST_UNIQUE_ID_ATTESTATION", + ) if (!hasSELinux && !hasAndroid) { - SystemLogger.warning("[TX_ID: $txId] Rejecting INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid") - return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED) + return@runCatching InterceptorUtils.createServiceSpecificErrorReply( + PERMISSION_DENIED + ) } } - val isSymmetric = parsedParams.algorithm == Algorithm.AES || - parsedParams.algorithm == Algorithm.HMAC || - parsedParams.algorithm == Algorithm.TRIPLE_DES - - if (securityLevel == SecurityLevel.STRONGBOX && !isStrongBoxCapable(parsedParams)) { - SystemLogger.info("[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection") - return TransactionResult.ContinueAndSkipPost - } - - val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) + val parsedParams = KeyMintAttestation(params) val isAttestKeyRequest = parsedParams.isAttestKey() val forceGenerate = @@ -472,47 +469,55 @@ class KeyMintSecurityLevelInterceptor( val isAuto = ConfigurationManager.isAutoMode(callingUid) when { - forceGenerate -> doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) - isAuto && !teeFunctional -> raceTeePatch(callingUid, keyDescriptor, attestationKey, params, parsedParams, keyId, isAttestKeyRequest) + forceGenerate -> doSoftwareGeneration( + callingUid, keyDescriptor, attestationKey, parsedParams, isAttestKeyRequest + ) + isAuto && !teeFunctional -> raceTeePatch( + callingUid, keyDescriptor, attestationKey, params, parsedParams, isAttestKeyRequest + ) parsedParams.attestationChallenge != null -> TransactionResult.Continue - else -> { - cleanupKeyData(keyId) - TransactionResult.ContinueAndSkipPost - } + else -> TransactionResult.ContinueAndSkipPost } } - .getOrElse { - SystemLogger.error("Error during generateKey handling for UID $callingUid.", it) - InterceptorUtils.createServiceSpecificErrorReply(SECURE_HW_COMMUNICATION_FAILED) + .getOrElse { e -> + SystemLogger.error("No key pair generated for UID $callingUid.", e) + val code = + if (e is android.os.ServiceSpecificException) e.errorCode + else SECURE_HW_COMMUNICATION_FAILED + InterceptorUtils.createServiceSpecificErrorReply(code) } } - private fun doSoftwareKeyGen( + /** Performs software key generation and caches the result. */ + private fun doSoftwareGeneration( callingUid: Int, keyDescriptor: KeyDescriptor, attestationKey: KeyDescriptor?, parsedParams: KeyMintAttestation, - keyId: KeyIdentifier, isAttestKeyRequest: Boolean, ): TransactionResult { val genStartNanos = System.nanoTime() keyDescriptor.nspace = secureRandom.nextLong() - SystemLogger.info("Generating software key for ${keyDescriptor.alias}[${keyDescriptor.nspace}].") + 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) - val isSymmetric = parsedParams.algorithm != Algorithm.EC && - parsedParams.algorithm != Algorithm.RSA - 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 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() @@ -534,67 +539,46 @@ class KeyMintSecurityLevelInterceptor( this.metadata = metadata iSecurityLevel = original } - generatedKeys[keyId] = GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams) - - if (securityLevel == SecurityLevel.STRONGBOX) { - val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000 - if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000) - } else { - TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos) - } - + generatedKeys[keyId] = + GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams) + TeeLatencySimulator.simulateGenerateKeyDelay( + parsedParams.algorithm, System.nanoTime() - genStartNanos + ) return InterceptorUtils.createTypedObjectReply(metadata) } - val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { - generateAttestedKeyPairNative(callingUid, parsedParams) - ?: CertificateGenerator.generateAttestedKeyPair( - callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel, - ) - } else { + val keyData = CertificateGenerator.generateAttestedKeyPair( - callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel, - ) - } ?: throw Exception("Both native and BouncyCastle cert gen failed.") + 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) + val response = + buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor) + generatedKeys[keyId] = + GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams) if (isAttestKeyRequest) attestationKeys.add(keyId) - val certChainCopy = keyData.second.toList() - persistExecutor.execute { - GeneratedKeyPersistence.save( - keyId = keyId, - keyPair = keyData.first, - nspace = keyDescriptor.nspace, - securityLevel = securityLevel, - certChain = certChainCopy, - algorithm = parsedParams.algorithm, - keySize = parsedParams.keySize, - ecCurve = parsedParams.ecCurve ?: 0, - purposes = parsedParams.purpose, - digests = parsedParams.digest, - isAttestationKey = isAttestKeyRequest, - ) - } - - if (securityLevel == SecurityLevel.STRONGBOX) { - val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000 - if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000) - } else { - TeeLatencySimulator.simulateGenerateKeyDelay(parsedParams.algorithm, System.nanoTime() - genStartNanos) - } - + TeeLatencySimulator.simulateGenerateKeyDelay( + parsedParams.algorithm, System.nanoTime() - genStartNanos + ) 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, - keyId: KeyIdentifier, isAttestKeyRequest: Boolean, ): TransactionResult { SystemLogger.info("AUTO: racing TEE vs software for ${keyDescriptor.alias}") @@ -605,14 +589,15 @@ class KeyMintSecurityLevelInterceptor( 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 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()) @@ -624,10 +609,11 @@ class KeyMintSecurityLevelInterceptor( alias = keyDescriptor.alias blob = keyDescriptor.blob } - val swKeyId = KeyIdentifier(callingUid, keyDescriptor.alias) val threadB = CompletableFuture.supplyAsync { - doSoftwareKeyGen(callingUid, swDescriptor, attestationKey, parsedParams, swKeyId, isAttestKeyRequest) + doSoftwareGeneration( + callingUid, swDescriptor, attestationKey, parsedParams, isAttestKeyRequest + ) } return try { @@ -642,14 +628,19 @@ class KeyMintSecurityLevelInterceptor( CertificateHelper.updateCertificateChain(teeMetadata, newChain).getOrThrow() teeMetadata.authorizations = InterceptorUtils.patchAuthorizations(teeMetadata.authorizations, callingUid) + val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) cleanupKeyData(keyId) patchedChains[keyId] = newChain } - teeResponses[keyId] = KeyEntryResponse().apply { + // 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) { @@ -667,81 +658,9 @@ class KeyMintSecurityLevelInterceptor( } } - private fun generateAttestedKeyPairNative( - callingUid: Int, - params: KeyMintAttestation, - ): AndroidPair>? { - return runCatching { - val algorithmName = when (params.algorithm) { - Algorithm.EC -> "EC" - Algorithm.RSA -> "RSA" - else -> return null - } - val keyboxFile = ConfigurationManager.getKeyboxFileForUid(callingUid) - val keybox = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) ?: return null - - val keyboxPrivateKeyBytes = keybox.keyPair.private.encoded - val keyboxCertChainBytes = keybox.certificates - .map { it.encoded } - .fold(ByteArray(0)) { acc, der -> acc + der } - - val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel) - val keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel) - val appId = AttestationBuilder.createApplicationId(callingUid) - - val config = CertGenConfig( - algorithm = params.algorithm, - keySize = params.keySize, - ecCurve = params.ecCurve ?: 0, - rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L, - attestationChallenge = params.attestationChallenge, - purposes = params.purpose.toIntArray(), - digests = params.digest.toIntArray(), - certSerial = params.certificateSerial?.toByteArray(), - certSubject = params.certificateSubject?.encoded, - certNotBefore = params.certificateNotBefore?.time ?: -1L, - certNotAfter = params.certificateNotAfter?.time ?: -1L, - keyboxPrivateKey = keyboxPrivateKeyBytes, - keyboxCertChain = keyboxCertChainBytes, - securityLevel = securityLevel, - attestVersion = attestVersion, - keymasterVersion = keymasterVersion, - osVersion = AndroidDeviceUtils.osVersion, - osPatchLevel = AndroidDeviceUtils.getPatchLevel(callingUid), - vendorPatchLevel = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid), - bootPatchLevel = AndroidDeviceUtils.getBootPatchLevelLong(callingUid), - bootKey = AndroidDeviceUtils.bootKey, - bootHash = AndroidDeviceUtils.bootHash, - creationDatetime = System.currentTimeMillis(), - attestationApplicationId = appId.octets, - moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null, - idBrand = params.brand, - idDevice = params.device, - idProduct = params.product, - idSerial = params.serial, - idImei = params.imei, - idMeid = params.meid, - idManufacturer = params.manufacturer, - idModel = params.model, - idSecondImei = if (attestVersion >= 300) params.secondImei else null, - activeDatetime = params.activeDateTime?.time ?: -1L, - originationExpireDatetime = params.originationExpireDateTime?.time ?: -1L, - usageExpireDatetime = params.usageExpireDateTime?.time ?: -1L, - usageCountLimit = params.usageCountLimit ?: -1, - callerNonce = params.callerNonce == true, - unlockedDeviceRequired = params.unlockedDeviceRequired == true, - noAuthRequired = params.noAuthRequired != false, - ) - - val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null - val (keyPair, certs) = NativeCertGen.parseNativeResult(resultBytes) - SystemLogger.info("NativeCertGen: generated key pair successfully (${certs.size} certs)") - AndroidPair(keyPair, certs) - }.onFailure { - SystemLogger.error("NativeCertGen: generation failed, falling back to BouncyCastle", it) - }.getOrNull() - } - + /** + * Constructs a fake `KeyEntryResponse` that mimics a real response from the Keystore service. + */ private fun buildKeyEntryResponse( callingUid: Int, chain: List, @@ -769,133 +688,18 @@ class KeyMintSecurityLevelInterceptor( } } - fun loadPersistedKeys() { - val records = GeneratedKeyPersistence.loadAll(securityLevel) - if (records.isEmpty()) { - SystemLogger.debug("No persisted keys to restore for security level $securityLevel") - return - } - - SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel") - - for (record in records) { - runCatching { - val keyId = KeyIdentifier(record.uid, record.alias) - if (generatedKeys.containsKey(keyId)) { - SystemLogger.debug("Skipping already-loaded key: $keyId") - return@runCatching - } - - val algorithmName = when (record.algorithm) { - Algorithm.EC -> "EC" - Algorithm.RSA -> "RSA" - else -> throw IllegalArgumentException("Unknown algorithm: ${record.algorithm}") - } - - val keyFactory = KeyFactory.getInstance(algorithmName) - val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(record.privateKeyBytes)) - - val certFactory = CertificateFactory.getInstance("X.509") - val certChain = record.certChainBytes.map { bytes -> - certFactory.generateCertificate(ByteArrayInputStream(bytes)) - } - require(certChain.isNotEmpty()) { "Persisted key has empty certificate chain" } - - val publicKey = certChain[0].publicKey - val keyPair = KeyPair(publicKey, privateKey) - - val descriptor = KeyDescriptor().apply { - domain = Domain.APP - nspace = record.nspace - alias = record.alias - blob = null - } - - val attestation = KeyMintAttestation( - keySize = record.keySize, - algorithm = record.algorithm, - ecCurve = record.ecCurve, - ecCurveName = "", - origin = null, - blockMode = emptyList(), - padding = emptyList(), - purpose = record.purposes, - digest = record.digests, - rsaPublicExponent = null, - certificateSerial = null, - certificateSubject = null, - certificateNotBefore = null, - certificateNotAfter = null, - attestationChallenge = null, - brand = null, - device = null, - product = null, - serial = null, - imei = null, - meid = null, - 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, - noAuthRequired = null, - maxUsesPerBoot = null, - maxBootLevel = null, - minMacLength = null, - rsaOaepMgfDigest = emptyList(), - ) - - val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor) - generatedKeys[keyId] = GeneratedKeyInfo(keyPair, null, record.nspace, response, attestation) - if (record.isAttestationKey) attestationKeys.add(keyId) - - SystemLogger.debug("Restored persisted key: $keyId") - }.onFailure { - SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it) - } - } - - SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}") - } - companion object { private val secureRandom = SecureRandom() + + /** Once set to true, AUTO mode skips the race and uses PATCH directly. */ @Volatile var teeFunctional = false - // Maximum alias length to prevent binder buffer exhaustion (Issue #109) - // Binder buffer is ~1MB; 256KB provides 4x safety margin for transaction overhead - private const val MAX_ALIAS_LENGTH = 256 * 1024 - private const val KEYMINT_INVALID_INPUT_LENGTH = -21 - private const val KEYMINT_INVALID_ARGUMENT = -38 - private const val RESPONSE_INVALID_ARGUMENT = 20 - private const val RESPONSE_PERMISSION_DENIED = 6 - private const val RESPONSE_KEY_NOT_FOUND = 7 - private const val TEE_LATENCY_FLOOR_MS = 15L - private const val STRONGBOX_KEYGEN_LATENCY_FLOOR_MS = 250L - private const val STRONGBOX_OP_LATENCY_FLOOR_MS = 80L - private const val KEYMINT_TOO_MANY_OPERATIONS = -29 - private const val KEYMINT_CANNOT_ATTEST_IDS = -66 - private const val KEYMINT_UNKNOWN_ERROR = -1000 + private const val INVALID_ARGUMENT = 20 + private const val PERMISSION_DENIED = 6 private const val SECURE_HW_COMMUNICATION_FAILED = -49 - private const val MAX_CONCURRENT_OPS_PER_UID = 15 - private const val STRONGBOX_MAX_CONCURRENT_OPS = 4 - private const val STRONGBOX_OP_WINDOW_NS = 10_000_000_000L // 10s - private fun isStrongBoxCapable(params: KeyMintAttestation): Boolean = when (params.algorithm) { - Algorithm.RSA -> params.keySize <= 2048 - Algorithm.EC -> params.ecCurve == null || params.ecCurve == EcCurve.P_256 - else -> true - } + private const val CANNOT_ATTEST_IDS = -66 + // Transaction codes for IKeystoreSecurityLevel interface. private val GENERATE_KEY_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") private val IMPORT_KEY_TRANSACTION = @@ -906,6 +710,7 @@ class KeyMintSecurityLevelInterceptor( "createOperation", ) + /** Only these transaction codes need native-level interception. */ val INTERCEPTED_CODES = intArrayOf(GENERATE_KEY_TRANSACTION, IMPORT_KEY_TRANSACTION, CREATE_OPERATION_TRANSACTION) @@ -920,20 +725,37 @@ class KeyMintSecurityLevelInterceptor( .associate { field -> (field.get(null) as Int) to field.name.split("_")[1] } } - private val persistExecutor = Executors.newSingleThreadExecutor() - + // Stores keys generated entirely in software. val generatedKeys = ConcurrentHashMap() - val teeResponses = ConcurrentHashMap() + // A set to quickly identify keys that were generated for attestation purposes. + val attestationKeys = ConcurrentHashMap.newKeySet() + // Caches patched certificate chains to prevent re-generation and signature inconsistencies. val patchedChains = ConcurrentHashMap>() - val attestationKeys: MutableSet = ConcurrentHashMap.newKeySet() + // Keys imported via importKey; getKeyEntry must not override these. val importedKeys: MutableSet = ConcurrentHashMap.newKeySet() - private val usageCounters = ConcurrentHashMap() + // 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 ?: teeResponses[keyId] + /** + * Finds a software-generated key by first filtering all known keys by the caller's UID, and + * then matching the specific nspace. + * + * @param callingUid The UID of the process that initiated the createOperation call. + * @param nspace The unique key identifier from the operation's KeyDescriptor. + * @return The matching GeneratedKeyInfo if found, otherwise null. + */ fun findGeneratedKeyByKeyId(callingUid: Int, nspace: Long?): GeneratedKeyInfo? { + // Iterate through all entries in the map to check both the key (for UID) and value (for + // nspace). if (nspace == null || nspace == 0L) return null return generatedKeys.entries .filter { (keyIdentifier, _) -> keyIdentifier.uid == callingUid } @@ -948,9 +770,7 @@ class KeyMintSecurityLevelInterceptor( fun cleanupKeyData(keyId: KeyIdentifier) { if (generatedKeys.remove(keyId) != null) { SystemLogger.debug("Remove generated key ${keyId}") - GeneratedKeyPersistence.delete(keyId) } - teeResponses.remove(keyId) if (patchedChains.remove(keyId) != null) { SystemLogger.debug("Remove patched chain for ${keyId}") } @@ -959,9 +779,11 @@ class KeyMintSecurityLevelInterceptor( } importedKeys.remove(keyId) usageCounters.remove(keyId) + teeResponses.remove(keyId) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { + // Unregister from the native hook layer first. unregister(backdoor, operationBinder) if (interceptedOperations.remove(operationBinder) != null) { @@ -969,35 +791,38 @@ class KeyMintSecurityLevelInterceptor( } } - fun invalidatePatchedChains(reason: String? = null) { - val count = patchedChains.size - if (count == 0) return - val reasonMessage = reason?.let { " due to $it" } ?: "" - patchedChains.clear() - SystemLogger.info("Invalidated $count patched cert chains$reasonMessage.") - } - + // Clears all cached keys. fun clearAllGeneratedKeys(reason: String? = null) { val count = generatedKeys.size val reasonMessage = reason?.let { " due to $it" } ?: "" generatedKeys.clear() - teeResponses.clear() patchedChains.clear() attestationKeys.clear() importedKeys.clear() usageCounters.clear() - GeneratedKeyPersistence.deleteAll() + teeResponses.clear() SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") } } } +/** + * Extension function to convert parsed `KeyMintAttestation` parameters back into an array of + * `Authorization` objects for the fake `KeyMetadata`. + */ private fun KeyMintAttestation.toAuthorizations( callingUid: Int, securityLevel: Int, ): Array { val authList = mutableListOf() + /** + * Helper function to create a fully-formed Authorization object. + * + * @param tag The KeyMint tag (e.g., Tag.ALGORITHM). + * @param value The value for the tag, wrapped in a KeyParameterValue. + * @return A populated Authorization object. + */ fun createAuth(tag: Int, value: KeyParameterValue): Authorization { val param = KeyParameter().apply { @@ -1011,17 +836,37 @@ private fun KeyMintAttestation.toAuthorizations( } authList.add(createAuth(Tag.ALGORITHM, KeyParameterValue.algorithm(this.algorithm))) + if (this.ecCurve != null) { authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve))) } + 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))) } - authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize))) - if (this.rsaPublicExponent != null) { - authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()))) + 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))) + } + + authList.add(createAuth(Tag.KEY_SIZE, KeyParameterValue.integer(this.keySize))) + + if (this.rsaPublicExponent != null) { + authList.add( + createAuth( + Tag.RSA_PUBLIC_EXPONENT, + KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()), + ) + ) + } + + if (this.noAuthRequired != null) { + authList.add( + createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(this.noAuthRequired)) + ) + } + if (this.callerNonce == true) { authList.add(createAuth(Tag.CALLER_NONCE, KeyParameterValue.boolValue(true))) } @@ -1038,67 +883,91 @@ private fun KeyMintAttestation.toAuthorizations( 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))) + 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))) + 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))) + 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.MAX_BOOT_LEVEL, KeyParameterValue.integer(this.maxBootLevel)) + ) } - if (this.noAuthRequired != false) { - authList.add(createAuth(Tag.NO_AUTH_REQUIRED, KeyParameterValue.boolValue(true))) - } - authList.add(createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED))) - authList.add(createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion))) + authList.add( + createAuth(Tag.ORIGIN, KeyParameterValue.origin(this.origin ?: KeyOrigin.GENERATED)) + ) + + authList.add( + createAuth(Tag.OS_VERSION, KeyParameterValue.integer(AndroidDeviceUtils.osVersion)) + ) val osPatch = AndroidDeviceUtils.getPatchLevel(callingUid) if (osPatch != AndroidDeviceUtils.DO_NOT_REPORT) { authList.add(createAuth(Tag.OS_PATCHLEVEL, KeyParameterValue.integer(osPatch))) } + val vendorPatch = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid) if (vendorPatch != AndroidDeviceUtils.DO_NOT_REPORT) { authList.add(createAuth(Tag.VENDOR_PATCHLEVEL, KeyParameterValue.integer(vendorPatch))) } + val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(callingUid) if (bootPatch != AndroidDeviceUtils.DO_NOT_REPORT) { 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 - } + val param = + KeyParameter().apply { + this.tag = tag + this.value = value + } return Authorization().apply { this.keyParameter = param this.securityLevel = SecurityLevel.SOFTWARE } } - authList.add(createSwAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis()))) + authList.add( + createSwAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis())) + ) 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))) + 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))) + 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.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true)) + ) } - authList.add(createSwAuth(Tag.USER_ID, KeyParameterValue.integer(callingUid / 100000))) + 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 c8236a3..adfd18d 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/OperationInterceptor.kt @@ -44,6 +44,7 @@ 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 { 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 59a78ff..bc36cb9 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 @@ -8,8 +8,8 @@ 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 java.util.concurrent.locks.LockSupport import android.system.keystore2.IKeystoreOperation import android.system.keystore2.KeyParameters import java.security.KeyPair @@ -20,16 +20,48 @@ 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(aadInput: ByteArray?) { - throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) - } + 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. private object JcaAlgorithmMapper { fun mapSignatureAlgorithm(params: KeyMintAttestation): String { val digest = @@ -39,18 +71,17 @@ private object JcaAlgorithmMapper { Digest.SHA_2_512 -> "SHA512" else -> "NONE" } - return when (params.algorithm) { - Algorithm.EC -> "${digest}withECDSA" - Algorithm.RSA -> { - val isPss = params.padding.firstOrNull() == PaddingMode.RSA_PSS - if (isPss) "${digest}withRSA/PSS" else "${digest}withRSA" + val keyAlgo = + when (params.algorithm) { + Algorithm.EC -> "ECDSA" + Algorithm.RSA -> "RSA" + else -> + throw ServiceSpecificException( + KeystoreErrorCode.SYSTEM_ERROR, + "Unsupported signature algorithm: ${params.algorithm}", + ) } - else -> - throw ServiceSpecificException( - KeystoreErrorCodes.incompatibleAlgorithm, - "Unsupported signature algorithm: ${params.algorithm}", - ) - } + return "${digest}with${keyAlgo}" } fun mapCipherAlgorithm(params: KeyMintAttestation): String { @@ -60,7 +91,7 @@ private object JcaAlgorithmMapper { Algorithm.AES -> "AES" else -> throw ServiceSpecificException( - KeystoreErrorCodes.incompatibleAlgorithm, + KeystoreErrorCode.SYSTEM_ERROR, "Unsupported cipher algorithm: ${params.algorithm}", ) } @@ -68,29 +99,32 @@ private object JcaAlgorithmMapper { when (params.blockMode.firstOrNull()) { BlockMode.ECB -> "ECB" BlockMode.CBC -> "CBC" - BlockMode.CTR -> "CTR" BlockMode.GCM -> "GCM" - else -> "ECB" + else -> "ECB" // Default for RSA } val padding = when (params.padding.firstOrNull()) { PaddingMode.NONE -> "NoPadding" PaddingMode.PKCS7 -> "PKCS7Padding" PaddingMode.RSA_PKCS1_1_5_ENCRYPT -> "PKCS1Padding" - PaddingMode.RSA_PKCS1_1_5_SIGN -> "PKCS1Padding" PaddingMode.RSA_OAEP -> "OAEPPadding" - else -> "NoPadding" + else -> "NoPadding" // Default for GCM } return "$keyAlgo/$blockMode/$padding" } } +// Concrete implementation for Signing. private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private val signature: Signature = Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { initSign(keyPair.private) } + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } + override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) return null @@ -104,12 +138,17 @@ private class Signer(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimi override fun abort() {} } +// Concrete implementation for Verification. private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPrimitive { private val signature: Signature = Signature.getInstance(JcaAlgorithmMapper.mapSignatureAlgorithm(params)).apply { initVerify(keyPair.public) } + override fun updateAad(data: ByteArray?) { + throw ServiceSpecificException(KeystoreErrorCode.INVALID_TAG) + } + override fun update(data: ByteArray?): ByteArray? { if (data != null) signature.update(data) return null @@ -117,11 +156,16 @@ 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 ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature to verify is null") - } + if (signature == null) + throw ServiceSpecificException( + KeystoreErrorCode.VERIFICATION_FAILED, + "Signature to verify is null", + ) if (!this.signature.verify(signature)) { - throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed") + throw ServiceSpecificException( + KeystoreErrorCode.VERIFICATION_FAILED, + "Signature/MAC verification failed", + ) } return null } @@ -129,20 +173,19 @@ private class Verifier(keyPair: KeyPair, params: KeyMintAttestation) : CryptoPri override fun abort() {} } +// Concrete implementation for Encryption/Decryption. private class CipherPrimitive( cryptoKey: java.security.Key, params: KeyMintAttestation, private val opMode: Int, ) : CryptoPrimitive { - private val isAead = params.blockMode.firstOrNull() == BlockMode.GCM private val cipher: Cipher = Cipher.getInstance(JcaAlgorithmMapper.mapCipherAlgorithm(params)).apply { init(opMode, cryptoKey) } - override fun updateAad(aadInput: ByteArray?) { - if (!isAead) throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) - if (aadInput != null) cipher.updateAAD(aadInput) + override fun updateAad(data: ByteArray?) { + if (data != null) cipher.updateAAD(data) } override fun update(data: ByteArray?): ByteArray? = @@ -151,6 +194,9 @@ private class CipherPrimitive( override fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? = if (data != null) cipher.doFinal(data) else cipher.doFinal() + override fun abort() {} + + /** Returns the cipher IV as a NONCE parameter for GCM operations. */ override fun getBeginParameters(): Array? { val iv = cipher.iv ?: return null return arrayOf( @@ -160,20 +206,23 @@ private class CipherPrimitive( } ) } - - override fun abort() {} } +// 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( - KeystoreErrorCodes.invalidArgument, + KeystoreErrorCode.INVALID_ARGUMENT, "Peer public key required for key agreement", ) val peerKey = @@ -186,25 +235,23 @@ private class KeyAgreementPrimitive(keyPair: KeyPair) : CryptoPrimitive { override fun abort() {} } +/** + * A software-only implementation of a cryptographic operation. This class acts as a controller, + * delegating to a specific cryptographic primitive based on the operation's purpose. + * + * 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?, secretKey: javax.crypto.SecretKey?, params: KeyMintAttestation, - private val latencyFloorMs: Long = 0L, + var onFinishCallback: (() -> Unit)? = null, ) { private val primitive: CryptoPrimitive - @Volatile var finalized = false - private set - var onFinishCallback: (() -> Unit)? = null - - val beginParameters: KeyParameters? - get() { - val params = primitive.getBeginParameters() ?: return null - if (params.isEmpty()) return null - return KeyParameters().apply { keyParameter = params } - } + @Volatile private var finalized = false init { val purpose = params.purpose.firstOrNull() @@ -226,175 +273,121 @@ class SoftwareOperation( KeyPurpose.AGREE_KEY -> KeyAgreementPrimitive(keyPair!!) else -> throw ServiceSpecificException( - KeystoreErrorCodes.unsupportedPurpose, + 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) { - SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)") - throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle) - } + if (finalized) + throw ServiceSpecificException( + KeystoreErrorCode.INVALID_OPERATION_HANDLE, + "Operation already finalized.", + ) } - private fun checkInputLength(data: ByteArray?) { - if (data != null && data.size > MAX_RECEIVE_DATA) { - SystemLogger.info("[SoftwareOp TX_ID: $txId] Input too large: ${data.size} > $MAX_RECEIVE_DATA, throwing TOO_MUCH_DATA(${KeystoreErrorCodes.tooMuchData})") - throw ServiceSpecificException(KeystoreErrorCodes.tooMuchData) - } - } - - fun updateAad(aadInput: ByteArray?) { - SystemLogger.debug("[SoftwareOp TX_ID: $txId] updateAad() inputSize=${aadInput?.size ?: 0}") + fun updateAad(data: ByteArray?) { checkActive() - checkInputLength(aadInput) - primitive.updateAad(aadInput) + 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? { - SystemLogger.debug("[SoftwareOp TX_ID: $txId] update() inputSize=${data?.size ?: 0}") checkActive() - checkInputLength(data) 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 mapToServiceSpecificException(e) + throw ServiceSpecificException(KeystoreErrorCode.SYSTEM_ERROR, e.message) } } fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? { checkActive() - checkInputLength(data) try { - val startNs = if (latencyFloorMs > 0) System.nanoTime() else 0L val result = primitive.finish(data, signature) - if (latencyFloorMs > 0) { - val elapsedMs = (System.nanoTime() - startNs) / 1_000_000 - val delayMs = latencyFloorMs - elapsedMs - if (delayMs > 0) LockSupport.parkNanos(delayMs * 1_000_000) - } - finalized = true - onFinishCallback?.invoke() SystemLogger.info("[SoftwareOp TX_ID: $txId] Finished operation successfully.") + onFinishCallback?.invoke() return result } catch (e: ServiceSpecificException) { throw e } catch (e: Exception) { SystemLogger.error("[SoftwareOp TX_ID: $txId] Failed to finish operation.", e) - throw mapToServiceSpecificException(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.") } +} - private fun mapToServiceSpecificException(e: Exception): ServiceSpecificException = when (e) { - is SignatureException -> ServiceSpecificException(KeystoreErrorCodes.verificationFailed, e.message) - is javax.crypto.BadPaddingException -> ServiceSpecificException(KeystoreErrorCodes.invalidArgument, e.message) - is javax.crypto.IllegalBlockSizeException -> ServiceSpecificException(KeystoreErrorCodes.invalidInputLength, e.message) - is java.security.InvalidKeyException -> ServiceSpecificException(KeystoreErrorCodes.incompatibleKey, e.message) - else -> ServiceSpecificException(KeystoreErrorCodes.unknownError, e.message) +/** 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? { + synchronized(this) { + checkInputLength(input) + return operation.update(input) + } + } + + @Throws(RemoteException::class) + override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { + synchronized(this) { + checkInputLength(input) + checkInputLength(signature) + return operation.finish(input, signature) + } + } + + @Throws(RemoteException::class) + override fun abort() { + synchronized(this) { operation.abort() } } companion object { private const val MAX_RECEIVE_DATA = 0x8000 } } - -internal object KeystoreErrorCodes { - val tooMuchData: Int by lazy { - resolveField("android.system.keystore2.ResponseCode", "TOO_MUCH_DATA", 21) - } - - val invalidOperationHandle: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_OPERATION_HANDLE", -28) - } - - val invalidTag: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_TAG", -76) - } - - val verificationFailed: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "VERIFICATION_FAILED", -30) - } - - val invalidArgument: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_ARGUMENT", -38) - } - - val invalidInputLength: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INVALID_INPUT_LENGTH", -21) - } - - val incompatibleKey: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_KEY", -31) - } - - val incompatiblePurpose: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_PURPOSE", -13) - } - - val unsupportedPurpose: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "UNSUPPORTED_PURPOSE", -14) - } - - val incompatibleAlgorithm: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "INCOMPATIBLE_ALGORITHM", -18) - } - - val keyNotYetValid: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "KEY_NOT_YET_VALID", -39) - } - - val keyExpired: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "KEY_EXPIRED", -40) - } - - val callerNonceProhibited: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "CALLER_NONCE_PROHIBITED", -55) - } - - val unknownError: Int by lazy { - resolveField("android.hardware.security.keymint.ErrorCode", "UNKNOWN_ERROR", -1000) - } - - fun resolveField(className: String, fieldName: String, fallback: Int): Int = - runCatching { - Class.forName(className).getField(fieldName).getInt(null) - }.getOrElse { - SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback") - fallback - } -} - -class SoftwareOperationBinder(private val operation: SoftwareOperation) : - IKeystoreOperation.Stub() { - - @Synchronized - override fun updateAad(aadInput: ByteArray?) { - operation.updateAad(aadInput) - } - - @Synchronized - override fun update(input: ByteArray?): ByteArray? { - return operation.update(input) - } - - @Synchronized - override fun finish(input: ByteArray?, signature: ByteArray?): ByteArray? { - return operation.finish(input, signature) - } - - @Synchronized - override fun abort() { - operation.abort() - } -} diff --git a/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt b/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt index 0940cdd..9eddc87 100644 --- a/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt +++ b/app/src/main/java/org/matrix/TEESimulator/logging/KeyMintParameterLogger.kt @@ -37,6 +37,22 @@ object KeyMintParameterLogger { .associate { field -> (field.get(null) as Int) to field.name } } + val hardwareAuthenticatorTypeNames: Map by lazy { + HardwareAuthenticatorType::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + + val keyOriginNames: Map by lazy { + KeyOrigin::class + .java + .fields + .filter { it.type == Int::class.java } + .associate { field -> (field.get(null) as Int) to field.name } + } + val paddingNames: Map by lazy { PaddingMode::class .java @@ -81,22 +97,33 @@ object KeyMintParameterLogger { when (param.tag) { Tag.ALGORITHM -> algorithmNames[value.algorithm] Tag.BLOCK_MODE -> blockModeNames[value.blockMode] + Tag.DIGEST -> digestNames[value.digest] Tag.EC_CURVE -> ecCurveNames[value.ecCurve] + Tag.ORIGIN -> keyOriginNames[value.origin] Tag.PADDING -> paddingNames[value.paddingMode] Tag.PURPOSE -> purposeNames[value.keyPurpose] - Tag.DIGEST -> digestNames[value.digest] + Tag.USER_AUTH_TYPE -> + hardwareAuthenticatorTypeNames[value.hardwareAuthenticatorType] Tag.AUTH_TIMEOUT, + Tag.BOOT_PATCHLEVEL, Tag.KEY_SIZE, - Tag.MIN_MAC_LENGTH -> value.integer.toString() + Tag.MAC_LENGTH, + Tag.MIN_MAC_LENGTH, + Tag.OS_VERSION, + Tag.OS_PATCHLEVEL, + Tag.USER_ID, + Tag.VENDOR_PATCHLEVEL -> value.integer.toString() Tag.CERTIFICATE_SERIAL -> BigInteger(value.blob).toString() Tag.ACTIVE_DATETIME, Tag.CERTIFICATE_NOT_AFTER, Tag.CERTIFICATE_NOT_BEFORE, + Tag.CREATION_DATETIME, Tag.ORIGINATION_EXPIRE_DATETIME, Tag.USAGE_EXPIRE_DATETIME -> Date(value.dateTime).toString() Tag.CERTIFICATE_SUBJECT -> X500Name(X500Principal(value.blob).name).toString() + Tag.USER_SECURE_ID, Tag.RSA_PUBLIC_EXPONENT -> value.longInteger.toString() - Tag.NO_AUTH_REQUIRED -> "true" + Tag.NO_AUTH_REQUIRED -> value.boolValue.toString() Tag.ATTESTATION_CHALLENGE, Tag.ATTESTATION_ID_BRAND, Tag.ATTESTATION_ID_DEVICE, 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 4b27058..50a92f3 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -35,6 +35,7 @@ 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 /** @@ -198,14 +199,16 @@ object CertificateGenerator { private fun buildKeyUsageFromPurposes(purposes: List): Int { var bits = 0 for (purpose in purposes) { - bits = bits or when (purpose) { - KeyPurpose.SIGN -> KeyUsage.digitalSignature - KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment - KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment - KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement - KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign - else -> 0 - } + bits = + bits or + when (purpose) { + KeyPurpose.SIGN -> KeyUsage.digitalSignature + KeyPurpose.DECRYPT -> KeyUsage.dataEncipherment + KeyPurpose.WRAP_KEY -> KeyUsage.keyEncipherment + KeyPurpose.AGREE_KEY -> KeyUsage.keyAgreement + KeyPurpose.ATTEST_KEY -> KeyUsage.keyCertSign + else -> 0 + } } return bits } @@ -220,6 +223,8 @@ object CertificateGenerator { securityLevel: Int, ): Certificate { 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) @@ -243,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 (signingKeyPair.private.algorithm) { - "EC", "ECDSA" -> "SHA256withECDSA" - "RSA" -> "SHA256withRSA" - else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.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/AndroidDeviceUtils.kt b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt index bed1d6e..a1ccd21 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt @@ -91,33 +91,27 @@ object AndroidDeviceUtils { attestationValueProvider: () -> ByteArray?, expectedSize: Int, ): ByteArray { + // 1. Attempt to get the value from the system property. getProperty(propertyName, expectedSize)?.let { SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}") - persistToFile(propertyName, it) return it } + // 2. Fallback to the value from a cached TEE attestation. try { attestationValueProvider()?.let { SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}") - setProperty(propertyName, it) - persistToFile(propertyName, it) + setProperty(propertyName, it) // Persist for consistency return it } } catch (e: Exception) { SystemLogger.error("Failed to get $propertyName from attestation.", e) } - readFromFile(propertyName, expectedSize)?.let { - SystemLogger.debug("Using $propertyName from persistent file: ${it.toHex()}") - setProperty(propertyName, it) - return it - } - + // 3. As a final fallback, generate a random value. return generateRandomBytes(expectedSize).also { SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}") setProperty(propertyName, it) - persistToFile(propertyName, it) } } @@ -164,37 +158,10 @@ object AndroidDeviceUtils { } } + /** Generates a cryptographically random byte array of a specified length. */ private fun generateRandomBytes(size: Int): ByteArray = ByteArray(size).also { ThreadLocalRandom.current().nextBytes(it) } - private val PERSIST_DIR = File("/data/adb/tricky_store") - - private fun fileForProperty(propertyName: String): File = when (propertyName) { - "ro.boot.vbmeta.digest" -> File(PERSIST_DIR, "boot_hash.bin") - "ro.boot.vbmeta.public_key_digest" -> File(PERSIST_DIR, "boot_key.bin") - else -> File(PERSIST_DIR, "${propertyName.replace('.', '_')}.bin") - } - - private fun persistToFile(propertyName: String, bytes: ByteArray) { - try { - fileForProperty(propertyName).writeBytes(bytes) - } catch (e: Exception) { - SystemLogger.error("Failed to persist $propertyName to file.", e) - } - } - - private fun readFromFile(propertyName: String, expectedSize: Int): ByteArray? { - return try { - val file = fileForProperty(propertyName) - if (!file.exists()) return null - val bytes = file.readBytes() - if (bytes.size == expectedSize) bytes else null - } catch (e: Exception) { - SystemLogger.error("Failed to read $propertyName from file.", e) - null - } - } - // --- Patch Level Properties --- fun getPatchLevel(uid: Int): Int { @@ -273,12 +240,11 @@ object AndroidDeviceUtils { val resolvedValue = resolveDateKeywords(value) return when { + // "device_default" indicates falling back to the system property. resolvedValue.equals("device_default", ignoreCase = true) -> null - // Resolve from live system prop — matches what detectors see via getprop, - // even when PIF has spoofed ro.build.version.security_patch via resetprop - resolvedValue.equals("prop", ignoreCase = true) -> - parsePatchLevelValue(SystemProperties.get("ro.build.version.security_patch", ""), isLong) + // "no" indicates this value should not be reported. resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT + // Otherwise, parse the resolved date string. else -> parsePatchLevelValue(resolvedValue, isLong) } } @@ -405,7 +371,10 @@ object AndroidDeviceUtils { // --- APEX and Module Hash Properties --- - // Minimal protobuf parser for apex_manifest.pb (field 1: name, field 2: version) + // https://cs.android.com/android/platform/superproject/+/android-latest-release:system/apex/proto/apex_manifest.proto + // --- Minimal Protobuf Parser for ApexManifest --- + // Field 1: name (string) + // Field 2: version (int64) private class MinimalApexManifestParser(private val data: ByteArray) { var pos = 0 @@ -419,13 +388,13 @@ object AndroidDeviceUtils { val wireType = (tag and 0x07).toInt() when (fieldNum) { - 1L -> { + 1L -> { // name val length = readVarint().toInt() if (pos + length > data.size) return null name = String(data, pos, length, Charsets.UTF_8) pos += length } - 2L -> { + 2L -> { // version version = readVarint() } else -> skipField(wireType) @@ -453,18 +422,19 @@ object AndroidDeviceUtils { private fun skipField(wireType: Int) { when (wireType) { - 0 -> readVarint() - 1 -> pos += 8 - 2 -> { + 0 -> readVarint() // Varint + 1 -> pos += 8 // 64-bit + 2 -> { // Length-delimited val len = readVarint().toInt() pos += len } - 5 -> pos += 4 + 5 -> pos += 4 // 32-bit else -> throw IllegalStateException("Unknown wire type $wireType") } } } + // https://cs.android.com/android/platform/superproject/main/+/main:system/apex/libs/libapexutil/apexutil.cpp private val apexInfos: List> by lazy { val results = mutableListOf>() val apexRoot = File("/apex") @@ -473,14 +443,22 @@ object AndroidDeviceUtils { return@lazy emptyList() } + // Logic from: GetActivePackages in apexutil.cpp apexRoot.listFiles()?.forEach { file -> if (!file.isDirectory) return@forEach val name = file.name + // 1. Ignore "." (and implicitly "..") if (name.startsWith(".")) return@forEach + + // 2. Ignore directories containing '@' (active mounts usually don't have version in + // path) if (name.contains("@")) return@forEach + + // 3. Ignore "sharedlibs" if (name == "sharedlibs") return@forEach + // 4. Parse apex_manifest.pb val manifestFile = File(file, "apex_manifest.pb") if (manifestFile.exists()) { runCatching { @@ -491,46 +469,59 @@ object AndroidDeviceUtils { } } + // Ensure uniqueness (though filesystem scan usually prevents exact dupes, + // strictly speaking we want to behave like a Map keyed by package name) results.distinctBy { it.first } } + // https://cs.android.com/android/platform/superproject/main/+/main:system/security/keystore2/src/maintenance.rs val moduleHash: ByteArray by lazy { DeviceAttestationService.CachedAttestationData?.moduleHash ?: runCatching { + // 1. Create a container to hold the sort key (name encoded) and the full data + // (sequence encoded) data class ModuleEntry( - val nameEncoded: ByteArray, - val fullEncoded: ByteArray, + val nameEncoded: ByteArray, // The sort key + val fullEncoded: ByteArray, // The data to hash ) val modules = apexInfos.map { (packageName, versionCode) -> + // Create the components val nameOctet = DEROctetString(packageName.toByteArray(Charsets.UTF_8)) val versionInt = ASN1Integer(versionCode) + // Create the Sequence: SEQUENCE { packageName, version } val vec = ASN1EncodableVector() vec.add(nameOctet) vec.add(versionInt) val sequence = DERSequence(vec) - // AOSP sorts by encoded name only, not full sequence + // We store the encoded name separately because Rust sorts ONLY by this ModuleEntry( nameEncoded = nameOctet.encoded, fullEncoded = sequence.encoded, ) } + // 2. Sort manually based on the encoded Package Name (lexicographically) + // This mimics the Rust 'impl DerOrd for ModuleInfo' which delegates to + // 'self.name' val sortedModules = modules.sortedWith { m1, m2 -> compareByteArrays(m1.nameEncoded, m2.nameEncoded) } + // 3. Concatenate the full sequences in the specific sorted order val payloadStream = ByteArrayOutputStream() sortedModules.forEach { payloadStream.write(it.fullEncoded) } val payload = payloadStream.toByteArray() - // Wrap in DER SET tag manually — DERSet() re-sorts by full encoding + // 4. Wrap manually in a DER SET tag (0x31) + // We cannot use DERSet(vector) because it would re-sort incorrectly. val finalDerSet = encodeAsDerSet(payload) + // 5. Compute SHA-256 MessageDigest.getInstance("SHA-256").digest(finalDerSet) } .getOrElse { @@ -539,6 +530,7 @@ object AndroidDeviceUtils { } } + /** Compares two byte arrays lexicographically (unsigned). */ private fun compareByteArrays(a: ByteArray, b: ByteArray): Int { val length = minOf(a.size, b.size) for (i in 0 until length) { @@ -551,25 +543,31 @@ object AndroidDeviceUtils { return a.size - b.size } + /** Manually wraps the payload in an ASN.1 SET (0x31) tag with correct length encoding. */ private fun encodeAsDerSet(payload: ByteArray): ByteArray { val out = ByteArrayOutputStream() - out.write(0x31) + out.write(0x31) // ASN.1 Tag for SET writeDerLength(out, payload.size) out.write(payload) return out.toByteArray() } + /** Writes the ASN.1 length field to the stream. */ private fun writeDerLength(out: ByteArrayOutputStream, length: Int) { if (length < 128) { + // Short form out.write(length) } else { + // Long form var size = length val bytes = ArrayList() while (size > 0) { bytes.add((size and 0xFF).toByte()) size = size ushr 8 } + // First byte: 0x80 | number of length bytes out.write(0x80 or bytes.size) + // Write length bytes in big-endian (reverse of how we extracted them) for (i in bytes.indices.reversed()) { out.write(bytes[i].toInt()) } diff --git a/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt b/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt deleted file mode 100644 index d5877f3..0000000 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt +++ /dev/null @@ -1,72 +0,0 @@ -package org.matrix.TEESimulator.util - -import android.annotation.SuppressLint -import android.content.Context -import android.content.pm.PackageManager -import org.matrix.TEESimulator.logging.SystemLogger - -object AndroidPermissionUtils { - - @SuppressLint("PrivateApi", "DiscouragedPrivateApi") - private fun getGlobalContext(): Context? { - return try { - // 1. Get the hidden ActivityThread class via reflection - val activityThreadClass = Class.forName("android.app.ActivityThread") - - // 2. Invoke the static currentActivityThread() method - val currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread") - currentActivityThreadMethod.isAccessible = true - val activityThread = currentActivityThreadMethod.invoke(null) - - if (activityThread == null) { - SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null") - return null - } - - // 3. Try to get the application context - val getApplicationMethod = activityThreadClass.getDeclaredMethod("getApplication") - getApplicationMethod.isAccessible = true - val application = getApplicationMethod.invoke(activityThread) as? Context - - if (application != null) return application - - // 4. Fallback to getSystemContext() if application is null (often happens in system_server) - val getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext") - getSystemContextMethod.isAccessible = true - getSystemContextMethod.invoke(activityThread) as? Context - - } catch (e: Exception) { - SystemLogger.error("Reflection failed to get global context for permission check", e) - null - } - } - - /** - * Core permission check. - */ - fun hasPermission(uid: Int, permission: String): Boolean { - val context = getGlobalContext() ?: run { - SystemLogger.warning("AndroidPermissionUtils: Context is null, failing permission check safely.") - return false - } - - val result = context.checkPermission(permission, -1, uid) - return result == PackageManager.PERMISSION_GRANTED - } - - fun hasDeviceAttestationPermission(uid: Int): Boolean { - return hasPermission(uid, "android.permission.READ_PRIVILEGED_PHONE_STATE") - } - - fun hasUniqueIdAttestationPermission(uid: Int): Boolean { - return hasPermission(uid, "android.permission.REQUEST_UNIQUE_ID_ATTESTATION") - } - - fun hasManageUsersPermission(uid: Int): Boolean { - return hasPermission(uid, "android.permission.MANAGE_USERS") - } - - fun hasDumpPermission(uid: Int): Boolean { - return hasPermission(uid, "android.permission.DUMP") - } -} \ No newline at end of file diff --git a/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt b/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt index 33f9952..0106984 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/TeeLatencySimulator.kt @@ -8,6 +8,21 @@ 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() @@ -41,6 +56,11 @@ object TeeLatencySimulator { 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) { diff --git a/module/customize.sh b/module/customize.sh index a4ce5a5..7d6d62f 100644 --- a/module/customize.sh +++ b/module/customize.sh @@ -15,7 +15,7 @@ fi # --- Version Info --- VERSION=$(grep_prop version "${TMPDIR}/module.prop") -ui_print "- Installing TEESimulator-RS $VERSION" +ui_print "- Installing TEESimulator $VERSION" ui_print "" # --- Architecture Handling --- @@ -48,7 +48,7 @@ install_file() { # --- Installation --- ui_print "- Extracting module files" -for file in customize.sh module.prop service.sh sepolicy.rule daemon action.sh uninstall.sh; do +for file in customize.sh module.prop service.sh sepolicy.rule daemon; do install_file "$file" "$MODPATH" done @@ -67,14 +67,10 @@ ui_print "" ui_print "- Extracting $ARCH libraries" install_file "lib/$ABI_DIR/libTEESimulator.so" "$MODPATH" install_file "lib/$ABI_DIR/libinject.so" "$MODPATH" -install_file "lib/$ABI_DIR/libsupervisor.so" "$MODPATH" -install_file "lib/$ABI_DIR/libcertgen.so" "$MODPATH" ui_print "" mv "$MODPATH/libinject.so" "$MODPATH/inject" -mv "$MODPATH/libsupervisor.so" "$MODPATH/supervisor" chmod 755 "$MODPATH/inject" -chmod 755 "$MODPATH/supervisor" # --- Configuration Files --- if [ ! -d "$CONFIG_DIR" ]; then @@ -92,6 +88,7 @@ if [ ! -f "$CONFIG_DIR/target.txt" ]; then 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 diff --git a/stub/src/main/java/android/hardware/security/keymint/HardwareAuthenticatorType.java b/stub/src/main/java/android/hardware/security/keymint/HardwareAuthenticatorType.java new file mode 100644 index 0000000..73c2f1d --- /dev/null +++ b/stub/src/main/java/android/hardware/security/keymint/HardwareAuthenticatorType.java @@ -0,0 +1,8 @@ +package android.hardware.security.keymint; + +public @interface HardwareAuthenticatorType { + int NONE = 0; + int PASSWORD = 1; + int FINGERPRINT = 2; + int ANY = -1; +} diff --git a/stub/src/main/java/android/os/SELinux.java b/stub/src/main/java/android/os/SELinux.java index ffaaf10..6191b90 100644 --- a/stub/src/main/java/android/os/SELinux.java +++ b/stub/src/main/java/android/os/SELinux.java @@ -1,5 +1,6 @@ package android.os; +/** Stub for android.os.SELinux. */ public class SELinux { public static boolean checkSELinuxAccess( String scon, String tcon, String tclass, String perm) { diff --git a/stub/src/main/java/android/os/ServiceSpecificException.java b/stub/src/main/java/android/os/ServiceSpecificException.java index 6082d49..f0a9343 100644 --- a/stub/src/main/java/android/os/ServiceSpecificException.java +++ b/stub/src/main/java/android/os/ServiceSpecificException.java @@ -1,14 +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) { - this.errorCode = errorCode; - } - public ServiceSpecificException(int errorCode, String message) { super(message); this.errorCode = errorCode; } + + public ServiceSpecificException(int errorCode) { + this(errorCode, null); + } }