From 5c300ff47b4f8ae3e4725bc1a60b78c419f1b31b Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Thu, 4 Jun 2026 12:55:07 +0100 Subject: [PATCH] style: apply ktfmt formatting pass Run the project ktfmt kotlinLangStyle formatter over app/ to bring the tree into canonical form. Formatting only -- no logic change. Verified semantic-neutral: ktfmt(working tree) is byte-identical to ktfmt(committed HEAD) across all of app/src, so the prior uncommitted WIP carried zero behavioral change. --- app/build.gradle.kts | 97 +- .../main/java/org/matrix/TEESimulator/App.kt | 10 +- .../attestation/AttestationBuilder.kt | 69 +- .../attestation/AttestationPatcher.kt | 8 +- .../attestation/KeyMintAttestation.kt | 3 +- .../config/ConfigurationManager.kt | 21 +- .../interception/core/BinderInterceptor.kt | 26 +- .../interception/keystore/InterceptorUtils.kt | 13 +- .../keystore/Keystore2Interceptor.kt | 189 +- .../Keystore2MaintenanceInterceptor.kt | 19 +- .../keystore/shim/AuthorizeCreate.kt | 19 +- .../keystore/shim/GeneratedKeyPersistence.kt | 449 +++-- .../shim/KeyMintSecurityLevelInterceptor.kt | 1562 ++++++++++------- .../keystore/shim/SoftwareOperation.kt | 137 +- .../TEESimulator/logging/SystemLogger.kt | 28 +- .../TEESimulator/pki/CertificateGenerator.kt | 145 +- .../matrix/TEESimulator/pki/NativeCertGen.kt | 17 +- .../TEESimulator/util/AndroidDeviceUtils.kt | 43 +- .../util/AndroidPermissionUtils.kt | 37 +- .../matrix/TEESimulator/util/Extensions.kt | 5 +- 20 files changed, 1688 insertions(+), 1209 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3cde637..b3feb47 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -66,11 +66,7 @@ android { } } -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_21) - } -} +kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } } dependencies { compileOnly(project(":stub")) @@ -79,27 +75,35 @@ dependencies { } // --- 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." +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") + workingDir = rootProject.projectDir.resolve("native-certgen") - commandLine( - "cargo", "ndk", - "-t", "arm64-v8a", - "-o", rootProject.projectDir.resolve("app/src/main/jniLibs").absolutePath, - "build", "--release" - ) + 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")) + 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) - environment("PATH", "${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}") -} + environment("ANDROID_NDK_HOME", android.ndkDirectory.absolutePath) + environment( + "PATH", + "${System.getProperty("user.home")}/.cargo/bin:${System.getenv("PATH") ?: ""}", + ) + } // AGP auto-detects jniLibs/ as an input to mergeJniLibFolders — wire the dependency tasks.configureEach { @@ -110,31 +114,32 @@ tasks.configureEach { // Auto-rewrite module/update.json on every packaging build so versionCode and // zipUrl track gitCommitCount automatically, matching module.prop. -val refreshUpdateJson by tasks.registering { - group = "TEESimulator-RS Module Packaging" - description = "Rewrite module/update.json to match current verName and gitCommitCount." +val refreshUpdateJson by + tasks.registering { + group = "TEESimulator-RS Module Packaging" + description = "Rewrite module/update.json to match current verName and gitCommitCount." - val updateJsonFile = rootProject.projectDir.resolve("module/update.json") - val capturedVerName = verName - val capturedCount = gitCommitCount + val updateJsonFile = rootProject.projectDir.resolve("module/update.json") + val capturedVerName = verName + val capturedCount = gitCommitCount - inputs.property("verName", capturedVerName) - inputs.property("gitCommitCount", capturedCount) - outputs.file(updateJsonFile) + inputs.property("verName", capturedVerName) + inputs.property("gitCommitCount", capturedCount) + outputs.file(updateJsonFile) - doLast { - val fullVer = "$capturedVerName-$capturedCount" - updateJsonFile.writeText( - """{ + doLast { + val fullVer = "$capturedVerName-$capturedCount" + updateJsonFile.writeText( + """{ "version": "$fullVer", "versionCode": $capturedCount, "zipUrl": "https://github.com/Enginex0/TEESimulator-RS/releases/download/$fullVer/TEESimulator-RS-$fullVer-Release.zip", "changelog": "https://raw.githubusercontent.com/Enginex0/TEESimulator-RS/main/module/changelog.md" } """ - ) + ) + } } -} androidComponents { onVariants(selector().all()) { variant -> @@ -177,14 +182,20 @@ 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" - } + 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") + include( + "**/libinject.so", + "**/libTEESimulator.so", + "**/libsupervisor.so", + "**/libcertgen.so", + ) } // Now, copy and process the files from 'module' directory. diff --git a/app/src/main/java/org/matrix/TEESimulator/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt index be44db0..581318c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/App.kt +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -74,9 +74,9 @@ object App { } /** - * Release builds never emit diagnostics. Sweep any `.bin` dumps a prior - * debug install left in the world-readable temp dir so they can't act as a - * detection artifact for apps that probe /data/local/tmp. + * Release builds never emit diagnostics. Sweep any `.bin` dumps a prior debug install left in + * the world-readable temp dir so they can't act as a detection artifact for apps that probe + * /data/local/tmp. */ private fun purgeDebugDiagnostics() { if (SystemLogger.isDebugBuild) return @@ -88,7 +88,9 @@ object App { if (stale.isNotEmpty()) { // warning() bypasses the rate limiter, so this once-per-boot audit // line survives the noisy startup window. - SystemLogger.warning("Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp") + SystemLogger.warning( + "Purged ${stale.size} stale debug diagnostic(s) from /data/local/tmp" + ) } } 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 17acc1c..d07e033 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -44,9 +44,10 @@ object AttestationBuilder { ): Extension { val keyDescription = buildKeyDescription(params, uid, securityLevel) SystemLogger.verbose { - val formattedString = keyDescription.joinToString(separator = ", ") { - AttestationPatcher.formatAsn1Primitive(it) - } + val formattedString = + keyDescription.joinToString(separator = ", ") { + AttestationPatcher.formatAsn1Primitive(it) + } "Forged attestation data: $formattedString" } return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded)) @@ -116,7 +117,9 @@ object AttestationBuilder { } val bootPatch = AndroidDeviceUtils.getBootPatchLevelLong(uid) - SystemLogger.info("Attestation patch levels for uid=$uid: os=$osPatch, vendor=$vendorPatch, boot=$bootPatch") + 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( @@ -268,7 +271,11 @@ object AttestationBuilder { if (params.rollbackResistance == true && attestVersion >= 3) { list.add( - DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE) + DERTaggedObject( + true, + AttestationConstants.TAG_ROLLBACK_RESISTANCE, + DERNull.INSTANCE, + ) ) } @@ -286,19 +293,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, + ) ) } @@ -449,33 +468,51 @@ object AttestationBuilder { } if (params.callerNonce == true) { - list.add( - DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE) - ) + list.add(DERTaggedObject(true, AttestationConstants.TAG_CALLER_NONCE, DERNull.INSTANCE)) } params.activeDateTime?.let { list.add( - DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time)) + DERTaggedObject( + true, + AttestationConstants.TAG_ACTIVE_DATETIME, + ASN1Integer(it.time), + ) ) } 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, + ) ) } diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt index 097462b..9adfc8b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt @@ -4,6 +4,7 @@ import android.security.keystore.KeyProperties import java.nio.charset.StandardCharsets import java.security.cert.Certificate import java.security.cert.X509Certificate +import java.util.Date import org.bouncycastle.asn1.* import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.cert.X509CertificateHolder @@ -16,7 +17,6 @@ import org.matrix.TEESimulator.logging.SystemLogger import org.matrix.TEESimulator.pki.KeyBox import org.matrix.TEESimulator.pki.KeyBoxManager import org.matrix.TEESimulator.util.toHex -import java.util.Date /** * Handles the modification (patching) of Android Key Attestation extensions within certificates. @@ -287,7 +287,8 @@ object AttestationPatcher { val (allFields, teeEnforcedMap, originalRootOfTrust) = parsed SystemLogger.verbose { - val formattedString = allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) } + val formattedString = + allFields.joinToString(separator = ", ") { formatAsn1Primitive(it) } "Original attestation data: $formattedString" } @@ -317,7 +318,8 @@ object AttestationPatcher { allFields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX] = sortedTeeEnforced val patchedSequence = DERSequence(allFields) SystemLogger.verbose { - val formattedString = patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) } + val formattedString = + patchedSequence.joinToString(separator = ", ") { formatAsn1Primitive(it) } "Patched attestation data: $formattedString" } val patchedOctets = DEROctetString(patchedSequence) 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 5496212..4664a6b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -142,7 +142,8 @@ data class KeyMintAttestation( fun isAttestKey(): Boolean = purpose.size == 1 && purpose.contains(KeyPurpose.ATTEST_KEY) - fun isImportKey(): Boolean = origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED + fun isImportKey(): Boolean = + origin == KeyOrigin.IMPORTED || origin == KeyOrigin.SECURELY_IMPORTED } // --- Private helper extension functions for parsing KeyParameter arrays --- 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 91f1e45..c6b1a33 100644 --- a/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt +++ b/app/src/main/java/org/matrix/TEESimulator/config/ConfigurationManager.kt @@ -96,7 +96,8 @@ object ConfigurationManager { fun isAutoMode(uid: Int): Boolean { for (pkg in getPackagesForUid(uid)) { when (packageModes[pkg]) { - Mode.GENERATE, Mode.PATCH -> return false + Mode.GENERATE, + Mode.PATCH -> return false Mode.AUTO -> return true null -> continue } @@ -112,7 +113,9 @@ object ConfigurationManager { when (packageModes[pkg]) { Mode.GENERATE -> return Mode.GENERATE Mode.PATCH -> return Mode.PATCH - Mode.AUTO -> return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH else Mode.GENERATE + Mode.AUTO -> + return if (DeviceAttestationService.isTeeFunctional) Mode.PATCH + else Mode.GENERATE null -> continue } } @@ -260,7 +263,9 @@ object ConfigurationManager { // 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})") + 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") } contextLines.remove("") // Remove global context to iterate over packages next @@ -293,10 +298,12 @@ 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 -> + 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.") // 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 -> 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..104469e 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 @@ -110,16 +110,20 @@ abstract class BinderInterceptor : Binder() { */ final override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { val txId = data.readLong() - val result = try { - when (code) { - PRE_TRANSACT_CODE -> handlePreTransact(txId, data) - POST_TRANSACT_CODE -> handlePostTransact(txId, data) - else -> return super.onTransact(code, data, reply, flags) + val result = + try { + when (code) { + 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 } - } catch (e: Throwable) { - SystemLogger.error("[TX_ID: $txId] Interceptor exception, falling through to HAL", e) - TransactionResult.ContinueAndSkipPost - } writeResultToReply(result, reply!!) return true } @@ -307,7 +311,9 @@ abstract class BinderInterceptor : Binder() { data.writeInt(filteredCodes.size) for (code in filteredCodes) data.writeInt(code) backdoor.transact(REGISTER_INTERCEPTOR_CODE, data, reply, 0) - SystemLogger.info("Registered interceptor for target: $target (${filteredCodes.size} filtered codes)") + SystemLogger.info( + "Registered interceptor for target: $target (${filteredCodes.size} filtered codes)" + ) } catch (e: Exception) { SystemLogger.error("Failed to register binder interceptor.", e) } finally { diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/InterceptorUtils.kt index 6d975b5..5097f9b 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 @@ -37,12 +37,13 @@ object InterceptorUtils { } fun createErrorReply(errorCode: Int): BinderInterceptor.TransactionResult.OverrideReply { - val parcel = Parcel.obtain().apply { - writeInt(EX_SERVICE_SPECIFIC) - writeString(synthesizeSseMessage(errorCode)) - writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196) - writeInt(errorCode) - } + val parcel = + Parcel.obtain().apply { + writeInt(EX_SERVICE_SPECIFIC) + writeString(synthesizeSseMessage(errorCode)) + writeInt(0) // empty remote stack trace header (AOSP Status.cpp:196) + writeInt(errorCode) + } return BinderInterceptor.TransactionResult.OverrideReply(parcel) } 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 9b4c6ec..c98ac67 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 @@ -120,7 +120,10 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { Keystore2MaintenanceInterceptor, Keystore2MaintenanceInterceptor.interceptedCodes, ) - } ?: SystemLogger.warning("Maintenance binder not found; skipping lifecycle parity.") + } + ?: SystemLogger.warning( + "Maintenance binder not found; skipping lifecycle parity." + ) } .onFailure { SystemLogger.error("Failed to intercept maintenance binder.", it) } } @@ -219,17 +222,23 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ?: return TransactionResult.ContinueAndSkipPost // Domain.GRANT read (Android 16+ KeyStoreManager grant). Served for ANY grantee uid — - // including isolated services (bindIsolatedService) with no package mapping — so resolve - // it before the package-scoped skip; caller-binding in resolveGrant() is the real access - // gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants is + // including isolated services (bindIsolatedService) with no package mapping — so + // resolve + // it before the package-scoped skip; caller-binding in resolveGrant() is the real + // access + // gate. On Android <= 15 no grants are ever issued (grant() denies), so softwareGrants + // is // empty and this falls through to the real keystore2. if (code == GET_KEY_ENTRY_TRANSACTION && descriptor.domain == Domain.GRANT) { val grant = KeyMintSecurityLevelInterceptor.resolveGrant(descriptor.nspace, callingUid) if (grant == null) { - // Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real keystore2. + // Ours but wrong caller -> KEY_NOT_FOUND (caller-binding); not ours -> real + // keystore2. return if ( - KeyMintSecurityLevelInterceptor.softwareGrants.containsKey(descriptor.nspace) + KeyMintSecurityLevelInterceptor.softwareGrants.containsKey( + descriptor.nspace + ) ) InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND) else TransactionResult.ContinueAndSkipPost @@ -253,12 +262,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { KeyIdentifier(callingUid, descriptor.alias) } else if (descriptor.domain == Domain.KEY_ID) { KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( - callingUid, descriptor.nspace - )?.let { info -> - KeyMintSecurityLevelInterceptor.generatedKeys.entries - .find { it.value.nspace == info.nspace && it.key.uid == callingUid } - ?.key - } + callingUid, + descriptor.nspace, + ) + ?.let { info -> + KeyMintSecurityLevelInterceptor.generatedKeys.entries + .find { + it.value.nspace == info.nspace && it.key.uid == callingUid + } + ?.key + } } else null if (keyId != null) { @@ -289,18 +302,22 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { // "Captured private binder exception during timing skip". // Resolving by KEY_ID and returning the cached response keeps // the call on the happy path, eliminating the warmup signal. - val info = KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( - callingUid, descriptor.nspace - ) + val info = + KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( + callingUid, + descriptor.nspace, + ) if (info?.response != null) { SystemLogger.info( "[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}" ) return InterceptorUtils.createTypedObjectReply(info.response) } - val teeResp = KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId( - callingUid, descriptor.nspace - ) + val teeResp = + KeyMintSecurityLevelInterceptor.findTeeResponseByKeyId( + callingUid, + descriptor.nspace, + ) if (teeResp != null) { SystemLogger.info( "[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}" @@ -309,7 +326,8 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } } // Domain.GRANT is handled earlier (before the package-scoped skip); an alias-less - // read reaching here is KEY_ID or unknown, so it falls through to the real keystore2. + // read reaching here is KEY_ID or unknown, so it falls through to the real + // keystore2. return TransactionResult.ContinueAndSkipPost } val keyId = KeyIdentifier(callingUid, descriptor.alias) @@ -317,7 +335,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { 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}") + SystemLogger.info( + "[TX_ID: $txId] Returning KEY_NOT_FOUND for deleted key ${descriptor.alias}" + ) return InterceptorUtils.createErrorReply(RESPONSE_KEY_NOT_FOUND) } return TransactionResult.Continue @@ -339,14 +359,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ?: return TransactionResult.ContinueAndSkipPost val granteeUid = data.readInt() val accessVector = data.readInt() - // Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must grant - // coherently so the Domain.GRANT readback returns the same chain the owner read returns. + // Synthetic (generatedKeys) AND patch-mode (teeResponses) keys are ours; both must + // grant + // coherently so the Domain.GRANT readback returns the same chain the owner read + // returns. // Real hardware keys fall through to the real keystore2, which applies the same SELinux // gate the platform would. val ownerKeyId = - resolveOwnerKeyId(key, callingUid) - ?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) } - ?: return TransactionResult.ContinueAndSkipPost + resolveOwnerKeyId(key, callingUid)?.takeIf { + KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) + } ?: return TransactionResult.ContinueAndSkipPost // Version-gated to mirror the real TEE 1:1. Pre-Android-16, grant was a hidden API and // SELinux denied untrusted_app, so keystore2 returns PERMISSION_DENIED. Android 16 // (API 36) exposes KeyStoreManager.grantKeyAccess(), so an app grants its own key: @@ -373,9 +395,9 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { ?: return TransactionResult.ContinueAndSkipPost val granteeUid = data.readInt() val ownerKeyId = - resolveOwnerKeyId(key, callingUid) - ?.takeIf { KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) } - ?: return TransactionResult.ContinueAndSkipPost + resolveOwnerKeyId(key, callingUid)?.takeIf { + KeyMintSecurityLevelInterceptor.ownsKeyResponse(it) + } ?: return TransactionResult.ContinueAndSkipPost // Same version gate as grant(): denied pre-36, revoke the virtualized grant on 36+. if (Build.VERSION.SDK_INT < GRANT_PUBLIC_API_SDK) { return InterceptorUtils.createErrorReply(RESPONSE_PERMISSION_DENIED) @@ -423,10 +445,11 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { it.uid == callingUid } val totalCount = hardwareCount + softwareCount - val parcel = Parcel.obtain().apply { - writeNoException() - writeInt(totalCount) - } + val parcel = + Parcel.obtain().apply { + writeNoException() + writeInt(totalCount) + } TransactionResult.OverrideReply(parcel) } .getOrElse { @@ -469,8 +492,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) if (userUpdatedKeys.remove(keyId)) { - SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" } - SystemLogger.debug("[TX_ID: $txId] Skipping cert patch for user-updated key $keyId.") + SystemLogger.trace { + "[TRACE-$txId] getKeyEntry $keyId: userUpdated=true, skipping patch" + } + SystemLogger.debug( + "[TX_ID: $txId] Skipping cert patch for user-updated key $keyId." + ) return TransactionResult.SkipTransaction } @@ -480,18 +507,29 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray() ) - SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" } + SystemLogger.trace { + "[TRACE-$txId] getKeyEntry $keyId: isImport=${parsedParameters.isImportKey()} origin=${parsedParameters.origin} inImportedKeys=${KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)} hasPatchedChain=${KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) != null} isAttestKey=${parsedParameters.isAttestKey()}" + } if (parsedParameters.isImportKey()) { val retainedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId) if (retainedChain == null) { - SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" } - SystemLogger.info("[TX_ID: $txId] Skip patching for imported key (no prior attestation).") + SystemLogger.trace { + "[TRACE-$txId] getKeyEntry $keyId: imported, no retained chain, skip" + } + SystemLogger.info( + "[TX_ID: $txId] Skip patching for imported key (no prior attestation)." + ) return TransactionResult.SkipTransaction } - SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" } - SystemLogger.info("[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId") - CertificateHelper.updateCertificateChain(response.metadata, retainedChain).getOrThrow() + SystemLogger.trace { + "[TRACE-$txId] getKeyEntry $keyId: imported, SERVING RETAINED CHAIN (detection vector!)" + } + SystemLogger.info( + "[TX_ID: $txId] Imported key overwrote attested alias, serving retained chain for $keyId" + ) + CertificateHelper.updateCertificateChain(response.metadata, retainedChain) + .getOrThrow() response.metadata.authorizations = InterceptorUtils.patchAuthorizations( response.metadata.authorizations, @@ -501,8 +539,12 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } if (KeyMintSecurityLevelInterceptor.importedKeys.contains(keyId)) { - SystemLogger.trace { "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" } - SystemLogger.debug("[TX_ID: $txId] Skipping attest-key override for imported key $keyId") + SystemLogger.trace { + "[TRACE-$txId] getKeyEntry $keyId: in importedKeys set, skip" + } + SystemLogger.debug( + "[TX_ID: $txId] Skipping attest-key override for imported key $keyId" + ) return TransactionResult.SkipTransaction } @@ -545,17 +587,19 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { // Snapshot metadata bytes for the same reason as the // primary doSoftwareKeyGen path — loss-less restore // after reboot. - val metadataBytesForPersist = response.metadata?.let { md -> - runCatching { - val parcel = android.os.Parcel.obtain() - try { - md.writeToParcel(parcel, 0) - parcel.marshall() - } finally { - parcel.recycle() - } - }.getOrNull() - } + val metadataBytesForPersist = + response.metadata?.let { md -> + runCatching { + val parcel = android.os.Parcel.obtain() + try { + md.writeToParcel(parcel, 0) + parcel.marshall() + } finally { + parcel.recycle() + } + } + .getOrNull() + } GeneratedKeyPersistence.save( keyId = keyId, keyPair = keyData.first, @@ -623,18 +667,23 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } /** - * Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map - * directly; KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns - * null for anything not addressable, so callers fall through to the real keystore2. + * Resolves the owner [KeyIdentifier] a grant/ungrant call targets. APP/alias keys map directly; + * KEY_ID keys are looked up by nspace (mirrors the deleteKey resolver). Returns null for + * anything not addressable, so callers fall through to the real keystore2. */ private fun resolveOwnerKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? = when { descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias) descriptor.domain == Domain.KEY_ID -> - KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId(callingUid, descriptor.nspace) + KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( + callingUid, + descriptor.nspace, + ) ?.let { info -> KeyMintSecurityLevelInterceptor.generatedKeys.entries - .firstOrNull { it.value.nspace == info.nspace && it.key.uid == callingUid } + .firstOrNull { + it.value.nspace == info.nspace && it.key.uid == callingUid + } ?.key } else -> null @@ -642,14 +691,16 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult { data.enforceInterface(IKeystoreService.DESCRIPTOR) - val descriptor = data.readTypedObject(KeyDescriptor.CREATOR) - ?: return TransactionResult.ContinueAndSkipPost + val descriptor = + data.readTypedObject(KeyDescriptor.CREATOR) + ?: return TransactionResult.ContinueAndSkipPost val generatedKeyInfo = when (descriptor.domain) { Domain.KEY_ID -> KeyMintSecurityLevelInterceptor.findGeneratedKeyByKeyId( - callingUid, descriptor.nspace + callingUid, + descriptor.nspace, ) Domain.APP -> descriptor.alias?.let { @@ -659,22 +710,30 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { } if (generatedKeyInfo == null) { - // Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 applies + // Patch-mode key (cached in teeResponses, not generatedKeys): the real keystore2 + // applies // the update, so drop our stale cached chain. Otherwise getKeyEntry replays the // pre-update generated attestation (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE). when (descriptor.domain) { Domain.KEY_ID -> - KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId(callingUid, descriptor.nspace) + KeyMintSecurityLevelInterceptor.evictTeeResponseByKeyId( + callingUid, + descriptor.nspace, + ) Domain.APP -> descriptor.alias?.let { - KeyMintSecurityLevelInterceptor.evictTeeResponse(KeyIdentifier(callingUid, it)) + KeyMintSecurityLevelInterceptor.evictTeeResponse( + KeyIdentifier(callingUid, it) + ) } else -> {} } descriptor.alias?.let { val kid = KeyIdentifier(callingUid, it) userUpdatedKeys.add(kid) - SystemLogger.trace { "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" } + SystemLogger.trace { + "[TRACE] updateSubcomponent $kid: not generated key, added to userUpdatedKeys" + } } return TransactionResult.ContinueAndSkipPost } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2MaintenanceInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2MaintenanceInterceptor.kt index 9cb88a6..f061c65 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2MaintenanceInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2MaintenanceInterceptor.kt @@ -7,18 +7,18 @@ import android.system.keystore2.Domain import android.system.keystore2.KeyDescriptor import org.matrix.TEESimulator.interception.core.BinderInterceptor import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor -import org.matrix.TEESimulator.logging.SystemLogger /** * Intercepts the keystore2 daemon's `android.security.maintenance` binder so our synthetic key * state follows the same lifecycle events the platform applies to real keys. * * This is a pure side-effect hook: every handled transaction mutates only our own synthetic state - * and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs the - * real operation. We never fabricate a maintenance reply, so real key lifecycle is never disturbed. + * and then returns [TransactionResult.ContinueAndSkipPost], so the real keystore2 still performs + * the real operation. We never fabricate a maintenance reply, so real key lifecycle is never + * disturbed. * - * Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder is - * hosted by the same keystore2 process, so the already-injected native hook reaches it too. + * Mounted via `register()` from [Keystore2Interceptor.onInterceptorReady]; the maintenance binder + * is hosted by the same keystore2 process, so the already-injected native hook reaches it too. */ object Keystore2MaintenanceInterceptor : BinderInterceptor() { private val stubClass = IKeystoreMaintenance.Stub::class.java @@ -93,13 +93,18 @@ object Keystore2MaintenanceInterceptor : BinderInterceptor() { descriptor.alias != null -> KeyIdentifier(callingUid, descriptor.alias) descriptor.domain == Domain.KEY_ID -> KeyMintSecurityLevelInterceptor.generatedKeys.entries - .firstOrNull { it.key.uid == callingUid && it.value.nspace == descriptor.nspace } + .firstOrNull { + it.key.uid == callingUid && it.value.nspace == descriptor.nspace + } ?.key else -> null } /** Destination must be an addressable Domain.APP alias for us to keep tracking the key. */ - private fun resolveDestinationKeyId(descriptor: KeyDescriptor, callingUid: Int): KeyIdentifier? { + private fun resolveDestinationKeyId( + descriptor: KeyDescriptor, + callingUid: Int, + ): KeyIdentifier? { val alias = descriptor.alias ?: return null if (descriptor.domain != Domain.APP) return null val uid = if (descriptor.nspace > 0) descriptor.nspace.toInt() else callingUid diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt index 3963689..99ae55a 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/AuthorizeCreate.kt @@ -1,8 +1,8 @@ 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.KeyPurpose import android.hardware.security.keymint.Tag import org.matrix.TEESimulator.attestation.KeyMintAttestation @@ -24,8 +24,9 @@ object AuthorizeCreate { 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) + if ( + (algo == Algorithm.EC || algo == Algorithm.RSA) && + (purpose == KeyPurpose.VERIFY || purpose == KeyPurpose.ENCRYPT) ) { return KeystoreErrorCodes.unsupportedPurpose } @@ -35,10 +36,8 @@ object AuthorizeCreate { } private fun checkPurpose(keyParams: KeyMintAttestation, purpose: Int): Int? { - if (purpose == KeyPurpose.WRAP_KEY) - return KeystoreErrorCodes.incompatiblePurpose - if (purpose !in keyParams.purpose) - return KeystoreErrorCodes.incompatiblePurpose + if (purpose == KeyPurpose.WRAP_KEY) return KeystoreErrorCodes.incompatiblePurpose + if (purpose !in keyParams.purpose) return KeystoreErrorCodes.incompatiblePurpose return null } @@ -64,7 +63,11 @@ object AuthorizeCreate { return null } - private fun checkCallerNonce(keyParams: KeyMintAttestation, purpose: Int, rawOpParams: Array?): Int? { + private fun checkCallerNonce( + keyParams: KeyMintAttestation, + purpose: Int, + rawOpParams: Array?, + ): Int? { if (purpose != KeyPurpose.SIGN && purpose != KeyPurpose.ENCRYPT) return null if (keyParams.callerNonce == true) return null if (rawOpParams?.any { it.tag == Tag.NONCE } == true) diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt index 3508875..181cc6a 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/GeneratedKeyPersistence.kt @@ -33,19 +33,17 @@ data class PersistedKeyData( val privateKeyBytes: ByteArray, val certChainBytes: List, /** - * Byte-identical KeyMetadata parcel snapshot. Restoring authorizations - * directly from these bytes preserves tag count, order, and exact - * security-level annotations across reboots — the kind of structural - * details apps fingerprint to decide whether the alias is still - * "the same key". + * Byte-identical KeyMetadata parcel snapshot. Restoring authorizations directly from these + * bytes preserves tag count, order, and exact security-level annotations across reboots — the + * kind of structural details apps fingerprint to decide whether the alias is still "the same + * key". */ val metadataBytes: ByteArray, /** - * Raw secret material for symmetric records (AES, HMAC, 3DES). Empty - * for asymmetric. Critical for AndroidX security crypto MasterKey - * (AES-GCM-256) — without this every reboot regenerates a fresh AES - * key and EncryptedSharedPreferences becomes undecryptable, which is - * what banking apps interpret as session expiry and force a relogin. + * Raw secret material for symmetric records (AES, HMAC, 3DES). Empty for asymmetric. Critical + * for AndroidX security crypto MasterKey (AES-GCM-256) — without this every reboot regenerates + * a fresh AES key and EncryptedSharedPreferences becomes undecryptable, which is what banking + * apps interpret as session expiry and force a relogin. */ val symmetricKeyBytes: ByteArray, val symmetricAlgorithm: String, @@ -54,20 +52,15 @@ data class PersistedKeyData( object GeneratedKeyPersistence { /** - * Single source of truth for the on-disk format. Bump this every time - * the layout changes; older numbers are silently skipped on read so - * stale dev artifacts and pre-fix upstream files can't be partially - * rehydrated into broken in-memory state. + * Single source of truth for the on-disk format. Bump this every time the layout changes; older + * numbers are silently skipped on read so stale dev artifacts and pre-fix upstream files can't + * be partially rehydrated into broken in-memory state. * - * History: - * 1 — original upstream layout (no metadata snapshot, no symmetric - * block; restored keys lose authorization tags and AES master - * keys altogether — apps relying on persisted keystore state - * across reboots get logged out) - * 2 — transitional dev-only format that added metadata but still - * missed the symmetric block; never shipped - * 3 — current: byte-identical KeyMetadata snapshot + raw symmetric - * key material so AES/HMAC keys survive reboots + * History: 1 — original upstream layout (no metadata snapshot, no symmetric block; restored + * keys lose authorization tags and AES master keys altogether — apps relying on persisted + * keystore state across reboots get logged out) 2 — transitional dev-only format that added + * metadata but still missed the symmetric block; never shipped 3 — current: byte-identical + * KeyMetadata snapshot + raw symmetric key material so AES/HMAC keys survive reboots */ private const val FORMAT_VERSION = 3 private val PERSISTENCE_DIR = File(CONFIG_PATH, "persistent_keys") @@ -104,77 +97,80 @@ object GeneratedKeyPersistence { try { SystemLogger.debug("[Persistence] Lock acquired for $filename") runCatching { - PERSISTENCE_DIR.mkdirs() - val finalFile = File(PERSISTENCE_DIR, filename) - val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp") + PERSISTENCE_DIR.mkdirs() + val finalFile = File(PERSISTENCE_DIR, filename) + val tmpFile = File(PERSISTENCE_DIR, "$filename.tmp") - try { - DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out -> - out.writeInt(FORMAT_VERSION) - out.writeInt(securityLevel) - out.writeInt(keyId.uid) - out.writeUTF(keyId.alias) - out.writeLong(nspace) - out.writeBoolean(isAttestationKey) - out.writeInt(algorithm) - out.writeInt(keySize) - out.writeInt(ecCurve) + try { + DataOutputStream(BufferedOutputStream(FileOutputStream(tmpFile))).use { out + -> + out.writeInt(FORMAT_VERSION) + out.writeInt(securityLevel) + out.writeInt(keyId.uid) + out.writeUTF(keyId.alias) + out.writeLong(nspace) + out.writeBoolean(isAttestationKey) + out.writeInt(algorithm) + out.writeInt(keySize) + out.writeInt(ecCurve) - out.writeInt(purposes.size) - purposes.forEach { out.writeInt(it) } + out.writeInt(purposes.size) + purposes.forEach { out.writeInt(it) } - out.writeInt(digests.size) - digests.forEach { out.writeInt(it) } + out.writeInt(digests.size) + digests.forEach { out.writeInt(it) } - // Asymmetric key block (empty for symmetric-only). - val pkBytes = keyPair?.private?.encoded ?: ByteArray(0) - out.writeInt(pkBytes.size) - out.write(pkBytes) + // Asymmetric key block (empty for symmetric-only). + val pkBytes = keyPair?.private?.encoded ?: ByteArray(0) + out.writeInt(pkBytes.size) + out.write(pkBytes) - out.writeInt(certChain.size) - certChain.forEach { cert -> - val encoded = cert.encoded - out.writeInt(encoded.size) - out.write(encoded) - } + out.writeInt(certChain.size) + certChain.forEach { cert -> + val encoded = cert.encoded + out.writeInt(encoded.size) + out.write(encoded) + } - // Metadata snapshot (always present, may be empty - // if the live KeyMetadata could not be marshalled). - val mdBytes = metadataBytes ?: ByteArray(0) - out.writeInt(mdBytes.size) - if (mdBytes.isNotEmpty()) out.write(mdBytes) + // Metadata snapshot (always present, may be empty + // if the live KeyMetadata could not be marshalled). + val mdBytes = metadataBytes ?: ByteArray(0) + out.writeInt(mdBytes.size) + if (mdBytes.isNotEmpty()) out.write(mdBytes) - // Symmetric key block (empty for asymmetric keys). - if (secretKey != null) { - val skBytes = secretKey.encoded - out.writeUTF(secretKey.algorithm) - out.writeInt(skBytes.size) - out.write(skBytes) - } else { - out.writeUTF("") - out.writeInt(0) + // Symmetric key block (empty for asymmetric keys). + if (secretKey != null) { + val skBytes = secretKey.encoded + out.writeUTF(secretKey.algorithm) + out.writeInt(skBytes.size) + out.write(skBytes) + } else { + out.writeUTF("") + out.writeInt(0) + } } + } catch (e: Exception) { + tmpFile.delete() + throw e } - } catch (e: Exception) { - tmpFile.delete() - throw e - } - // Atomic rename — if this fails the tmp is left behind and cleaned on next deleteAll - if (!tmpFile.renameTo(finalFile)) { - tmpFile.delete() - throw IllegalStateException("Failed to atomically rename $tmpFile -> $finalFile") - } + // Atomic rename — if this fails the tmp is left behind and cleaned on next + // deleteAll + if (!tmpFile.renameTo(finalFile)) { + tmpFile.delete() + throw IllegalStateException( + "Failed to atomically rename $tmpFile -> $finalFile" + ) + } - // Verify write succeeded - catches disk-full or filesystem errors - if (!finalFile.exists() || finalFile.length() < 20) { - throw IOException("File write verification failed - possible disk full") - } + // Verify write succeeded - catches disk-full or filesystem errors + if (!finalFile.exists() || finalFile.length() < 20) { + throw IOException("File write verification failed - possible disk full") + } - SystemLogger.debug("Persisted key: $keyId") - }.onFailure { e -> - SystemLogger.error("Failed to persist key $keyId", e) - } + SystemLogger.debug("Persisted key: $keyId") + } + .onFailure { e -> SystemLogger.error("Failed to persist key $keyId", e) } } finally { lock.unlock() SystemLogger.debug("[Persistence] Lock released for $filename") @@ -183,44 +179,42 @@ object GeneratedKeyPersistence { fun delete(keyId: KeyIdentifier) { runCatching { - val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias)) - if (file.exists()) { - if (file.delete()) { - fileLocks.remove(keyFileName(keyId.uid, keyId.alias)) - SystemLogger.debug("Deleted persisted key: $keyId") + val file = File(PERSISTENCE_DIR, keyFileName(keyId.uid, keyId.alias)) + if (file.exists()) { + if (file.delete()) { + fileLocks.remove(keyFileName(keyId.uid, keyId.alias)) + SystemLogger.debug("Deleted persisted key: $keyId") + } else { + SystemLogger.warning("Failed to delete persisted key file: ${file.name}") + } } else { - SystemLogger.warning("Failed to delete persisted key file: ${file.name}") + SystemLogger.debug("No persisted file to delete for: $keyId") } - } else { - SystemLogger.debug("No persisted file to delete for: $keyId") } - }.onFailure { e -> - SystemLogger.error("Failed to delete persisted key $keyId", e) - } + .onFailure { e -> SystemLogger.error("Failed to delete persisted key $keyId", e) } } fun deleteAll() { runCatching { - if (!PERSISTENCE_DIR.exists()) { - SystemLogger.debug("No persistent_keys directory, nothing to delete") - return - } - val files = PERSISTENCE_DIR.listFiles() - if (files == null) { - SystemLogger.warning("Cannot list persistent_keys directory") - return - } - var count = 0 - files.forEach { file -> - if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) { - if (file.delete()) count++ + if (!PERSISTENCE_DIR.exists()) { + SystemLogger.debug("No persistent_keys directory, nothing to delete") + return } + val files = PERSISTENCE_DIR.listFiles() + if (files == null) { + SystemLogger.warning("Cannot list persistent_keys directory") + return + } + var count = 0 + files.forEach { file -> + if (file.name.endsWith(".bin") || file.name.endsWith(".tmp")) { + if (file.delete()) count++ + } + } + fileLocks.clear() + SystemLogger.info("Deleted $count persisted key files") } - fileLocks.clear() - SystemLogger.info("Deleted $count persisted key files") - }.onFailure { e -> - SystemLogger.error("Failed to delete all persisted keys", e) - } + .onFailure { e -> SystemLogger.error("Failed to delete all persisted keys", e) } } fun loadAll(securityLevel: Int): List { @@ -243,87 +237,86 @@ object GeneratedKeyPersistence { for (file in files) { runCatching { - DataInputStream(BufferedInputStream(FileInputStream(file))).use { input -> - val version = input.readInt() - if (version != FORMAT_VERSION) { - // Old upstream files (v1) and dev-only intermediate - // files (v2) are missing the metadata snapshot - // and/or symmetric key block — restoring them - // would put broken state in memory (apps relying - // on those records get logged out). Skip and let - // the next generateKey re-create cleanly with the - // new format. Affected apps re-login once after - // upgrade, then never again. - SystemLogger.info( - "Skipping ${file.name}: legacy format version $version. " + - "It will be replaced on next generateKey for this alias." - ) - return@runCatching - } - - val storedSecLevel = input.readInt() - val uid = input.readInt() - val alias = input.readUTF() - val nspace = input.readLong() - val isAttestKey = input.readBoolean() - val algo = input.readInt() - val kSize = input.readInt() - val curve = input.readInt() - - val purposeCount = requireBounds(input.readInt(), 64, "purposeCount") - val purposes = (0 until purposeCount).map { input.readInt() } - - val digestCount = requireBounds(input.readInt(), 64, "digestCount") - val digests = (0 until digestCount).map { input.readInt() } - - val pkLen = requireBounds(input.readInt(), 8192, "pkLen") - val pkBytes = ByteArray(pkLen) - if (pkLen > 0) input.readFully(pkBytes) - - val certCount = requireBounds(input.readInt(), 10, "certCount") - val certChainBytes = (0 until certCount).map { - val certLen = requireBounds(input.readInt(), 65536, "certLen") - val certBytes = ByteArray(certLen) - input.readFully(certBytes) - certBytes - } - - val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen") - val metadataBytes = ByteArray(metaLen).also { - if (metaLen > 0) input.readFully(it) - } - - val skAlgo = input.readUTF() - val skLen = requireBounds(input.readInt(), 8192, "skLen") - val skBytes = ByteArray(skLen).also { - if (skLen > 0) input.readFully(it) - } - - if (storedSecLevel == securityLevel) { - result.add( - PersistedKeyData( - uid = uid, - alias = alias, - nspace = nspace, - securityLevel = storedSecLevel, - isAttestationKey = isAttestKey, - algorithm = algo, - keySize = kSize, - ecCurve = curve, - purposes = purposes, - digests = digests, - privateKeyBytes = pkBytes, - certChainBytes = certChainBytes, - metadataBytes = metadataBytes, - symmetricKeyBytes = skBytes, - symmetricAlgorithm = skAlgo, + DataInputStream(BufferedInputStream(FileInputStream(file))).use { input -> + val version = input.readInt() + if (version != FORMAT_VERSION) { + // Old upstream files (v1) and dev-only intermediate + // files (v2) are missing the metadata snapshot + // and/or symmetric key block — restoring them + // would put broken state in memory (apps relying + // on those records get logged out). Skip and let + // the next generateKey re-create cleanly with the + // new format. Affected apps re-login once after + // upgrade, then never again. + SystemLogger.info( + "Skipping ${file.name}: legacy format version $version. " + + "It will be replaced on next generateKey for this alias." ) - ) + return@runCatching + } + + val storedSecLevel = input.readInt() + val uid = input.readInt() + val alias = input.readUTF() + val nspace = input.readLong() + val isAttestKey = input.readBoolean() + val algo = input.readInt() + val kSize = input.readInt() + val curve = input.readInt() + + val purposeCount = requireBounds(input.readInt(), 64, "purposeCount") + val purposes = (0 until purposeCount).map { input.readInt() } + + val digestCount = requireBounds(input.readInt(), 64, "digestCount") + val digests = (0 until digestCount).map { input.readInt() } + + val pkLen = requireBounds(input.readInt(), 8192, "pkLen") + val pkBytes = ByteArray(pkLen) + if (pkLen > 0) input.readFully(pkBytes) + + val certCount = requireBounds(input.readInt(), 10, "certCount") + val certChainBytes = + (0 until certCount).map { + val certLen = requireBounds(input.readInt(), 65536, "certLen") + val certBytes = ByteArray(certLen) + input.readFully(certBytes) + certBytes + } + + val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen") + val metadataBytes = + ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) } + + val skAlgo = input.readUTF() + val skLen = requireBounds(input.readInt(), 8192, "skLen") + val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) } + + if (storedSecLevel == securityLevel) { + result.add( + PersistedKeyData( + uid = uid, + alias = alias, + nspace = nspace, + securityLevel = storedSecLevel, + isAttestationKey = isAttestKey, + algorithm = algo, + keySize = kSize, + ecCurve = curve, + purposes = purposes, + digests = digests, + privateKeyBytes = pkBytes, + certChainBytes = certChainBytes, + metadataBytes = metadataBytes, + symmetricKeyBytes = skBytes, + symmetricAlgorithm = skAlgo, + ) + ) + } } } - }.onFailure { e -> - SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e) - } + .onFailure { e -> + SystemLogger.warning("Skipping corrupted persisted key file: ${file.name}", e) + } } SystemLogger.info("Loaded ${result.size} persisted keys for security level $securityLevel") @@ -345,11 +338,14 @@ object GeneratedKeyPersistence { } val secLevel = metadata.keySecurityLevel - val entry = KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) -> - id.uid == callingUid && info.nspace == generatedKeyInfo.nspace - } + val entry = + KeyMintSecurityLevelInterceptor.generatedKeys.entries.find { (id, info) -> + id.uid == callingUid && info.nspace == generatedKeyInfo.nspace + } if (entry == null) { - SystemLogger.debug("rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}") + SystemLogger.debug( + "rePersist: key not found in map for uid=$callingUid nspace=${generatedKeyInfo.nspace}" + ) return } @@ -368,16 +364,20 @@ object GeneratedKeyPersistence { return } - val persisted = runCatching { - DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input -> - val version = input.readInt() - if (version != FORMAT_VERSION) { - SystemLogger.warning("rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)") - return + val persisted = + runCatching { + DataInputStream(BufferedInputStream(FileInputStream(existing))).use { input -> + val version = input.readInt() + if (version != FORMAT_VERSION) { + SystemLogger.warning( + "rePersist: legacy format version $version for $keyId, will not re-persist (next generateKey replaces it)" + ) + return + } + readPersistedKeyData(input) + } } - readPersistedKeyData(input) - } - }.getOrNull() + .getOrNull() if (persisted == null) { SystemLogger.warning("rePersist: failed to read existing data for $keyId") return @@ -392,16 +392,18 @@ object GeneratedKeyPersistence { // Serialize the live KeyMetadata (now contains the user-installed cert // chain via updateSubcomponent) so the next boot restores byte-identical // metadata. KeyMetadata is binder-free, so marshall() is safe here. - val metadataBytes = runCatching { - android.os.Parcel.obtain().let { parcel -> - try { - metadata.writeToParcel(parcel, 0) - parcel.marshall() - } finally { - parcel.recycle() + val metadataBytes = + runCatching { + android.os.Parcel.obtain().let { parcel -> + try { + metadata.writeToParcel(parcel, 0) + parcel.marshall() + } finally { + parcel.recycle() + } + } } - } - }.getOrNull() + .getOrNull() save( keyId = keyId, keyPair = keyPair, @@ -427,8 +429,8 @@ object GeneratedKeyPersistence { } private fun keyFileName(uid: Int, alias: String): String { - val digest = MessageDigest.getInstance("SHA-256") - .digest("$uid:$alias".toByteArray(Charsets.UTF_8)) + val digest = + MessageDigest.getInstance("SHA-256").digest("$uid:$alias".toByteArray(Charsets.UTF_8)) return digest.joinToString("") { "%02x".format(it) } + ".bin" } @@ -455,23 +457,20 @@ object GeneratedKeyPersistence { if (pkLen > 0) input.readFully(pkBytes) val certCount = requireBounds(input.readInt(), 10, "certCount") - val certChainBytes = (0 until certCount).map { - val certLen = requireBounds(input.readInt(), 65536, "certLen") - val certBytes = ByteArray(certLen) - input.readFully(certBytes) - certBytes - } + val certChainBytes = + (0 until certCount).map { + val certLen = requireBounds(input.readInt(), 65536, "certLen") + val certBytes = ByteArray(certLen) + input.readFully(certBytes) + certBytes + } val metaLen = requireBounds(input.readInt(), 256 * 1024, "metaLen") - val metadataBytes = ByteArray(metaLen).also { - if (metaLen > 0) input.readFully(it) - } + val metadataBytes = ByteArray(metaLen).also { if (metaLen > 0) input.readFully(it) } val skAlgo = input.readUTF() val skLen = requireBounds(input.readInt(), 8192, "skLen") - val skBytes = ByteArray(skLen).also { - if (skLen > 0) input.readFully(it) - } + val skBytes = ByteArray(skLen).also { if (skLen > 0) input.readFully(it) } return PersistedKeyData( uid = uid, 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 5ec7afb..0262110 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -3,10 +3,9 @@ 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.SecurityLevel import android.hardware.security.keymint.Tag import android.os.IBinder @@ -85,8 +84,9 @@ 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) + ?: return TransactionResult.ContinueAndSkipPost SystemLogger.info( "[TX_ID: $txId] Forward to post-importKey hook for ${keyDescriptor.alias}[${keyDescriptor.nspace}]" ) @@ -130,7 +130,8 @@ class KeyMintSecurityLevelInterceptor( // generate/patch cache for this alias is stale. Drop it: a non-attested import then // falls through to the real keystore2 (origin=IMPORTED, imported leaf), and the // attested-import branch below re-caches the fresh patched chain. Without this, - // getKeyEntry replays the prior generated attestation (duck STALE_GENERATED_AFTER_IMPORT). + // getKeyEntry replays the prior generated attestation (duck + // STALE_GENERATED_AFTER_IMPORT). val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) if (generatedKeys.remove(keyId) != null) { SystemLogger.debug("Remove generated key on importKey $keyId") @@ -140,25 +141,33 @@ class KeyMintSecurityLevelInterceptor( patchedChains.remove(keyId) attestationKeys.remove(keyId) importedKeys.add(keyId) - SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" } + SystemLogger.trace { + "[TRACE-$txId] post-importKey $keyId: added to importedKeys, skipUid=${ConfigurationManager.shouldSkipUid(callingUid)}" + } if (!ConfigurationManager.shouldSkipUid(callingUid)) { val metadata: KeyMetadata = reply.readTypedObject(KeyMetadata.CREATOR) ?: return TransactionResult.SkipTransaction val originalChain = CertificateHelper.getCertificateChain(metadata) - SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: chainSize=${originalChain?.size ?: 0}" } + SystemLogger.trace { + "[TRACE-$txId] post-importKey $keyId: chainSize=${originalChain?.size ?: 0}" + } 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 + teeResponses[keyId] = + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + SystemLogger.trace { + "[TRACE-$txId] post-importKey $keyId: PATCHED chain (chainSize=${newChain.size})" } - SystemLogger.trace { "[TRACE-$txId] post-importKey $keyId: PATCHED chain (chainSize=${newChain.size})" } SystemLogger.debug("Cached patched certificate chain for imported key $keyId.") return InterceptorUtils.createTypedObjectReply(metadata) } @@ -167,10 +176,12 @@ 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) + ?: return TransactionResult.SkipTransaction + val params = + data.createTypedArray(KeyParameter.CREATOR) + ?: return TransactionResult.SkipTransaction val parsedParams = KeyMintAttestation(params) val forced = data.readBoolean() if (forced) @@ -193,7 +204,12 @@ class KeyMintSecurityLevelInterceptor( if (backdoor != null) { val isAead = parsedParams.blockMode.firstOrNull() == BlockMode.GCM val interceptor = OperationInterceptor(operation, backdoor, isAead) - register(backdoor, operationBinder, interceptor, OperationInterceptor.INTERCEPTED_CODES) + register( + backdoor, + operationBinder, + interceptor, + OperationInterceptor.INTERCEPTED_CODES, + ) interceptedOperations[operationBinder] = interceptor } else { SystemLogger.error( @@ -210,8 +226,9 @@ class KeyMintSecurityLevelInterceptor( ?: return TransactionResult.SkipTransaction data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR) - ?: return TransactionResult.SkipTransaction + val keyDescriptor = + data.readTypedObject(KeyDescriptor.CREATOR) + ?: return TransactionResult.SkipTransaction val keyId = KeyIdentifier(callingUid, keyDescriptor.alias) val originalChain = CertificateHelper.getCertificateChain(metadata) @@ -221,32 +238,49 @@ class KeyMintSecurityLevelInterceptor( // the forwarded non-attested path takes ~1.5ms, and // TimingSideChannelProbe flags the 1.55x ratio. cleanupKeyData(keyId) - teeResponses[keyId] = KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } + teeResponses[keyId] = + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } return TransactionResult.SkipTransaction } data.readTypedObject(KeyDescriptor.CREATOR) // skip attestationKey val keyParams = data.createTypedArray(KeyParameter.CREATOR) - val certNotBefore = keyParams?.find { it.tag == Tag.CERTIFICATE_NOT_BEFORE }?.value?.dateTime?.let { Date(it) } - val certNotAfter = keyParams?.find { it.tag == Tag.CERTIFICATE_NOT_AFTER }?.value?.dateTime?.let { Date(it) } + val certNotBefore = + keyParams + ?.find { it.tag == Tag.CERTIFICATE_NOT_BEFORE } + ?.value + ?.dateTime + ?.let { Date(it) } + val certNotAfter = + keyParams + ?.find { it.tag == Tag.CERTIFICATE_NOT_AFTER } + ?.value + ?.dateTime + ?.let { Date(it) } - val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid, certNotBefore, certNotAfter) + val newChain = + AttestationPatcher.patchCertificateChain( + originalChain, + callingUid, + certNotBefore, + certNotAfter, + ) - val key = metadata.key - ?: return TransactionResult.SkipTransaction + val key = metadata.key ?: return TransactionResult.SkipTransaction CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() metadata.authorizations = InterceptorUtils.patchAuthorizations(metadata.authorizations, callingUid) cleanupKeyData(keyId) patchedChains[keyId] = newChain - teeResponses[keyId] = KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } + teeResponses[keyId] = + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } SystemLogger.debug( "Cached patched certificate chain for $keyId. (${key.alias} [${key.domain}, ${key.nspace}])" ) @@ -256,7 +290,11 @@ class KeyMintSecurityLevelInterceptor( return TransactionResult.SkipTransaction } - private fun pruneOpsForUid(uid: Int, newOp: SoftwareOperation, maxOps: Int = MAX_CONCURRENT_OPS_PER_UID) { + 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 } @@ -264,12 +302,16 @@ class KeyMintSecurityLevelInterceptor( while (ops.size >= maxOps) { val oldest = ops.pollFirst() ?: break if (!oldest.finalized) { - SystemLogger.info("[LRU] Pruning operation for uid=$uid (active=${ops.size}/$maxOps)") + 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}") + SystemLogger.debug( + "[LRU] uid=$uid ops: before=$before cleaned=${before - afterClean} active=${ops.size}" + ) } private fun trackAndEnforceOpLimit(callingUid: Int, txId: Long): TransactionResult? { @@ -279,7 +321,9 @@ class KeyMintSecurityLevelInterceptor( 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)") + 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()) @@ -290,144 +334,204 @@ class KeyMintSecurityLevelInterceptor( txId: Long, callingUid: Int, data: Parcel, - ): TransactionResult = runCatching { - SystemLogger.debug("[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}") - data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR) - val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!! + ): TransactionResult = + runCatching { + SystemLogger.debug( + "[TX_ID: $txId] createOperation parcel: dataSize=${data.dataSize()} dataAvail=${data.dataAvail()} dataPos=${data.dataPosition()}" + ) + 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}") + 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 = - 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 + // 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 = + 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 + .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.ContinueAndSkipPost + } + } + else -> { + SystemLogger.info( + "[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, 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 + 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 (forced) { + return InterceptorUtils.createServiceSpecificErrorReply( + RESPONSE_PERMISSION_DENIED + ) + } + + AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { + errorCode -> + SystemLogger.info( + "[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode" + ) + return InterceptorUtils.createServiceSpecificErrorReply(errorCode) + } + + val keyParams = generatedKeyInfo.keyParams + val effectiveParams = + if (keyParams != null) { + keyParams.copy( + purpose = parsedParams.purpose, + digest = parsedParams.digest.ifEmpty { keyParams.digest }, + blockMode = parsedParams.blockMode.ifEmpty { keyParams.blockMode }, + padding = parsedParams.padding.ifEmpty { keyParams.padding }, + nonce = parsedParams.nonce, + minMacLength = parsedParams.minMacLength ?: keyParams.minMacLength, + ) + } else parsedParams + + val opLatency = + when (securityLevel) { + SecurityLevel.STRONGBOX -> STRONGBOX_OP_LATENCY_FLOOR_MS + SecurityLevel.TRUSTED_ENVIRONMENT -> TEE_OP_LATENCY_FLOOR_MS + else -> 0L + } + val softwareOperation = + SoftwareOperation( + txId, + generatedKeyInfo.keyPair, + generatedKeyInfo.secretKey, + effectiveParams, + opLatency, + ) + + if (keyParams?.usageCountLimit != null) { + val limit = keyParams.usageCountLimit + val remaining = + usageCounters.getOrPut(resolvedKeyId) { + java.util.concurrent.atomic.AtomicInteger(limit) + } + if (remaining.get() <= 0) { + cleanupKeyData(resolvedKeyId) + usageCounters.remove(resolvedKeyId) + 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)." + ) + } } } - Domain.KEY_ID -> { - val nspace = keyDescriptor.nspace - val entry = if (nspace == null || nspace == 0L) null - else generatedKeys.entries - .filter { it.key.uid == callingUid } - .find { it.value.nspace == nspace } - entry ?: run { - trackAndEnforceOpLimit(callingUid, txId)?.let { return it } - SystemLogger.info("[TX_ID: $txId] createOperation KeyId(${keyDescriptor.nspace}) NOT FOUND for uid=$callingUid. Forwarding to HAL.") - return TransactionResult.ContinueAndSkipPost + + 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 } - } - else -> { - SystemLogger.info("[TX_ID: $txId] createOperation domain=${keyDescriptor.domain}, forwarding to HAL") - return TransactionResult.ContinueAndSkipPost - } + + InterceptorUtils.createTypedObjectReply(response) } - 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 (forced) { - return InterceptorUtils.createServiceSpecificErrorReply(RESPONSE_PERMISSION_DENIED) - } - - AuthorizeCreate.check(generatedKeyInfo.keyParams, parsedParams, params)?.let { errorCode -> - SystemLogger.info("[TX_ID: $txId] authorize_create rejected: errorCode=$errorCode") - return InterceptorUtils.createServiceSpecificErrorReply(errorCode) - } - - val keyParams = generatedKeyInfo.keyParams - val effectiveParams = if (keyParams != null) { - keyParams.copy( - purpose = parsedParams.purpose, - digest = parsedParams.digest.ifEmpty { keyParams.digest }, - blockMode = parsedParams.blockMode.ifEmpty { keyParams.blockMode }, - padding = parsedParams.padding.ifEmpty { keyParams.padding }, - nonce = parsedParams.nonce, - minMacLength = parsedParams.minMacLength ?: keyParams.minMacLength, - ) - } else parsedParams - - val opLatency = when (securityLevel) { - SecurityLevel.STRONGBOX -> STRONGBOX_OP_LATENCY_FLOOR_MS - SecurityLevel.TRUSTED_ENVIRONMENT -> TEE_OP_LATENCY_FLOOR_MS - else -> 0L - } - val softwareOperation = SoftwareOperation(txId, generatedKeyInfo.keyPair, generatedKeyInfo.secretKey, effectiveParams, opLatency) - - if (keyParams?.usageCountLimit != null) { - val limit = keyParams.usageCountLimit - val remaining = usageCounters.getOrPut(resolvedKeyId) { - java.util.concurrent.atomic.AtomicInteger(limit) - } - if (remaining.get() <= 0) { - cleanupKeyData(resolvedKeyId) - usageCounters.remove(resolvedKeyId) - 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).") - } - } - } - - 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 + .getOrElse { + SystemLogger.error("Error during createOperation for UID $callingUid.", it) + InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_UNKNOWN_ERROR) } - InterceptorUtils.createTypedObjectReply(response) - }.getOrElse { - SystemLogger.error("Error during createOperation for UID $callingUid.", it) - InterceptorUtils.createServiceSpecificErrorReply(KEYMINT_UNKNOWN_ERROR) - } - - private fun handleGenerateKey(txId: Long, callingUid: Int, callingPid: Int, data: Parcel): TransactionResult { + private fun handleGenerateKey( + txId: Long, + callingUid: Int, + callingPid: Int, + data: Parcel, + ): TransactionResult { if (SystemLogger.isDebugBuild) { val savedPos = data.dataPosition() val req = data.marshall() data.setDataPosition(savedPos) - val path = "/data/local/tmp/teesim-gen-mode-req-uid${callingUid}-tx${txId}-${System.nanoTime()}.bin" + val path = + "/data/local/tmp/teesim-gen-mode-req-uid${callingUid}-tx${txId}-${System.nanoTime()}.bin" runCatching { java.io.File(path).writeBytes(req) } - SystemLogger.debug("[gen-mode-req] uid=$callingUid txId=$txId len=${req.size} path=$path") + SystemLogger.debug( + "[gen-mode-req] uid=$callingUid txId=$txId len=${req.size} path=$path" + ) } val oversized = data.dataSize() > MAX_ALIAS_LENGTH @@ -443,29 +547,38 @@ class KeyMintSecurityLevelInterceptor( var parsedParams = KeyMintAttestation(params) val isAttestKeyRequest = parsedParams.isAttestKey() - 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 - } + 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 + } // Debug-only probe trail: one greppable line per generateKey carrying the resolving // package and the outcome. Release builds short-circuit before any string is built, // so this is silent and artifact-free in production. fun logProbe(outcome: String) { if (!SystemLogger.isDebugBuild) return - val pkg = ConfigurationManager.getPackagesForUid(callingUid).firstOrNull() - ?: "uid:$callingUid" - val tags = buildList { - if (parsedParams.attestationChallenge != null) add("challenge") - if (parsedParams.brand != null || parsedParams.device != null || - parsedParams.product != null || parsedParams.manufacturer != null || - parsedParams.model != null) add("props") - if (hasDeviceIdAttestation) add("ids") - if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) add("unique_id") - }.joinToString(",") + val pkg = + ConfigurationManager.getPackagesForUid(callingUid).firstOrNull() + ?: "uid:$callingUid" + val tags = + buildList { + if (parsedParams.attestationChallenge != null) add("challenge") + if ( + parsedParams.brand != null || + parsedParams.device != null || + parsedParams.product != null || + parsedParams.manufacturer != null || + parsedParams.model != null + ) + add("props") + if (hasDeviceIdAttestation) add("ids") + if (params.any { it.tag == Tag.INCLUDE_UNIQUE_ID }) add("unique_id") + } + .joinToString(",") SystemLogger.debug( "[probe] tx=$txId uid=$callingUid pkg=$pkg alias=${keyDescriptor.alias} " + "algo=${parsedParams.algorithm} sb=${securityLevel == SecurityLevel.STRONGBOX} " + @@ -473,32 +586,50 @@ class KeyMintSecurityLevelInterceptor( ) } - if (ConfigurationManager.shouldSkipUid(callingUid) - && attestationKey == null && !isAttestKeyRequest) { + if ( + ConfigurationManager.shouldSkipUid(callingUid) && + attestationKey == null && + !isAttestKeyRequest + ) { logProbe("SKIP") return TransactionResult.ContinueAndSkipPost } - SystemLogger.trace { "[TRACE-$txId] generateKey alias=${keyDescriptor.alias} algo=${parsedParams.algorithm} challenge=${parsedParams.attestationChallenge?.size ?: "null"} serial=${parsedParams.serial != null} imei=${parsedParams.imei != null} noAuth=${parsedParams.noAuthRequired} purposes=${parsedParams.purpose}" } - if (SystemLogger.isDebugBuild) params.forEach { p -> - SystemLogger.trace { "[TRACE-$txId] tag=${p.tag} value=${p.value}" } + SystemLogger.trace { + "[TRACE-$txId] generateKey alias=${keyDescriptor.alias} algo=${parsedParams.algorithm} challenge=${parsedParams.attestationChallenge?.size ?: "null"} serial=${parsedParams.serial != null} imei=${parsedParams.imei != null} noAuth=${parsedParams.noAuthRequired} purposes=${parsedParams.purpose}" } + if (SystemLogger.isDebugBuild) + params.forEach { p -> + SystemLogger.trace { "[TRACE-$txId] tag=${p.tag} value=${p.value}" } + } 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})") + 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})" + ) logProbe("REJECT:challenge_len") return InterceptorUtils.createErrorReply(KEYMINT_INVALID_INPUT_LENGTH) } if (params.any { it.tag == Tag.CREATION_DATETIME }) { - SystemLogger.warning("[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params") + SystemLogger.warning( + "[TX_ID: $txId] Rejecting CREATION_DATETIME in generateKey params" + ) logProbe("REJECT:creation_datetime") return InterceptorUtils.createErrorReply(RESPONSE_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") + if ( + params.any { it.tag == Tag.DEVICE_UNIQUE_ATTESTATION } && + !AndroidPermissionUtils.hasUniqueIdAttestationPermission(callingUid) + ) { + SystemLogger.warning( + "[TX_ID: $txId] Rejecting DEVICE_UNIQUE_ATTESTATION for uid=$callingUid" + ) logProbe("REJECT:cannot_attest_unique") return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) } @@ -507,9 +638,15 @@ class KeyMintSecurityLevelInterceptor( // get it, ordinary apps get CANNOT_ATTEST_IDS. The permission check below is that // rule. Device-property attestation (BRAND/MODEL/...) is honored unconditionally — // genuine devices universally attest it, and it is what Play Integrity's hardware - // path needs. Capability is keyed to the device we present, not the real (dead) TEE. - if(hasDeviceIdAttestation && !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid)) { - SystemLogger.warning("[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid") + // path needs. Capability is keyed to the device we present, not the real (dead) + // TEE. + if ( + hasDeviceIdAttestation && + !AndroidPermissionUtils.hasDeviceAttestationPermission(callingUid) + ) { + SystemLogger.warning( + "[TX_ID: $txId] Rejecting DEVICE_ID_ATTESTATION for uid=$callingUid" + ) logProbe("REJECT:cannot_attest_ids") return InterceptorUtils.createErrorReply(KEYMINT_CANNOT_ATTEST_IDS) } @@ -525,25 +662,35 @@ class KeyMintSecurityLevelInterceptor( // attestation simply omits the unique_id field. This mirrors // the pre-PR157 behavior where the tag had no effect. 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.debug("[TX_ID: $txId] Stripping INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid (no permission)") + SystemLogger.debug( + "[TX_ID: $txId] Stripping INCLUDE_UNIQUE_ID for uid=$callingUid pid=$callingPid (no permission)" + ) params = params.filter { it.tag != Tag.INCLUDE_UNIQUE_ID }.toTypedArray() parsedParams = KeyMintAttestation(params) } } - val isSymmetric = parsedParams.algorithm == Algorithm.AES || - parsedParams.algorithm == Algorithm.HMAC || - parsedParams.algorithm == Algorithm.TRIPLE_DES + 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") + SystemLogger.info( + "[TX_ID: $txId] StrongBox-unsupported params (algo=${parsedParams.algorithm} size=${parsedParams.keySize}) → forwarding to HAL for rejection" + ) logProbe("FORWARD_HAL") return TransactionResult.ContinueAndSkipPost } @@ -556,12 +703,21 @@ class KeyMintSecurityLevelInterceptor( isAttestKeyRequest || attestationKey != null - SystemLogger.trace { "[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" } + SystemLogger.trace { + "[TRACE-$txId] dispatch: forceGen=$forceGenerate hasChallenge=${challenge != null} isSymmetric=$isSymmetric isAttestKey=$isAttestKeyRequest" + } when { forceGenerate -> { logProbe("FORGE") - doSoftwareKeyGen(callingUid, keyDescriptor, attestationKey, parsedParams, keyId, isAttestKeyRequest) + doSoftwareKeyGen( + callingUid, + keyDescriptor, + attestationKey, + parsedParams, + keyId, + isAttestKeyRequest, + ) } parsedParams.attestationChallenge != null -> { logProbe("PATCH") @@ -590,12 +746,14 @@ class KeyMintSecurityLevelInterceptor( ): 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}]." + ) cleanupKeyData(keyId) - val isSymmetric = parsedParams.algorithm != Algorithm.EC && - parsedParams.algorithm != Algorithm.RSA + val isSymmetric = + parsedParams.algorithm != Algorithm.EC && parsedParams.algorithm != Algorithm.RSA if (isSymmetric) { if (attestationKey != null) { @@ -604,36 +762,42 @@ class KeyMintSecurityLevelInterceptor( "ATTEST_KEY tag is not supported for symmetric algorithms (algo=${parsedParams.algorithm})", ) } - val algoName = when (parsedParams.algorithm) { - Algorithm.AES -> "AES" - Algorithm.HMAC -> "HmacSHA256" - else -> throw android.os.ServiceSpecificException( - KEYMINT_INVALID_ARGUMENT, - "Unsupported symmetric algorithm: ${parsedParams.algorithm}", - ) - } + val algoName = + when (parsedParams.algorithm) { + Algorithm.AES -> "AES" + Algorithm.HMAC -> "HmacSHA256" + else -> + throw android.os.ServiceSpecificException( + KEYMINT_INVALID_ARGUMENT, + "Unsupported symmetric algorithm: ${parsedParams.algorithm}", + ) + } val keyGen = javax.crypto.KeyGenerator.getInstance(algoName) keyGen.init(parsedParams.keySize) val secretKey = keyGen.generateKey() - val metadata = KeyMetadata().apply { - keySecurityLevel = securityLevel - key = KeyDescriptor().apply { - domain = Domain.KEY_ID - nspace = keyDescriptor.nspace - alias = null - blob = null + val metadata = + KeyMetadata().apply { + keySecurityLevel = securityLevel + key = + KeyDescriptor().apply { + domain = Domain.KEY_ID + nspace = keyDescriptor.nspace + alias = null + blob = null + } + certificate = null + certificateChain = null + authorizations = parsedParams.toAuthorizations(callingUid, securityLevel) + modificationTimeMs = System.currentTimeMillis() } - certificate = null - certificateChain = null - authorizations = parsedParams.toAuthorizations(callingUid, securityLevel) - modificationTimeMs = System.currentTimeMillis() - } - val response = KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } - generatedKeys[keyId] = GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams) + val response = + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + generatedKeys[keyId] = + GeneratedKeyInfo(null, secretKey, keyDescriptor.nspace, response, parsedParams) Keystore2Interceptor.forgetDeletedKey(keyId) // Persist symmetric keys too. Without this, AndroidX security @@ -643,15 +807,17 @@ class KeyMintSecurityLevelInterceptor( // EncryptedSharedPreferences interpret as session expiry. // Snapshot the metadata bytes alongside the raw secret // material so authorizations restore byte-identical. - val metadataBytesForSymmetric = runCatching { - val parcel = android.os.Parcel.obtain() - try { - metadata.writeToParcel(parcel, 0) - parcel.marshall() - } finally { - parcel.recycle() - } - }.getOrNull() + val metadataBytesForSymmetric = + runCatching { + val parcel = android.os.Parcel.obtain() + try { + metadata.writeToParcel(parcel, 0) + parcel.marshall() + } finally { + parcel.recycle() + } + } + .getOrNull() persistExecutor.execute { GeneratedKeyPersistence.save( keyId = keyId, @@ -671,28 +837,44 @@ class KeyMintSecurityLevelInterceptor( } if (securityLevel == SecurityLevel.STRONGBOX) { - val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000 + 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(metadata, diagnosticTag = "gen-mode-sym") } - val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { - generateAttestedKeyPairNative(callingUid, parsedParams) - ?: CertificateGenerator.generateAttestedKeyPair( - callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel, + val keyData = + if (NativeCertGen.isAvailable && attestationKey == null) { + generateAttestedKeyPairNative(callingUid, parsedParams) + ?: CertificateGenerator.generateAttestedKeyPair( + callingUid, + keyDescriptor.alias, + attestationKey?.alias, + parsedParams, + securityLevel, + ) + } else { + CertificateGenerator.generateAttestedKeyPair( + callingUid, + keyDescriptor.alias, + attestationKey?.alias, + parsedParams, + securityLevel, ) - } else { - CertificateGenerator.generateAttestedKeyPair( - callingUid, keyDescriptor.alias, attestationKey?.alias, parsedParams, securityLevel, - ) - } ?: throw Exception("Both native and BouncyCastle cert gen failed.") + } ?: throw Exception("Both native and BouncyCastle cert gen failed.") - 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) Keystore2Interceptor.forgetDeletedKey(keyId) if (isAttestKeyRequest) attestationKeys.add(keyId) @@ -701,9 +883,9 @@ class KeyMintSecurityLevelInterceptor( val leaf = chain.firstOrNull() as? java.security.cert.X509Certificate SystemLogger.trace { "[certchain] ${keyDescriptor.alias}: depth=${chain.size} " + - "issuer=${leaf?.issuerX500Principal?.name} " + - "subject=${leaf?.subjectX500Principal?.name} " + - "hasAttest=${leaf?.getExtensionValue("1.3.6.1.4.1.11129.2.1.17") != null}" + "issuer=${leaf?.issuerX500Principal?.name} " + + "subject=${leaf?.subjectX500Principal?.name} " + + "hasAttest=${leaf?.getExtensionValue("1.3.6.1.4.1.11129.2.1.17") != null}" } } @@ -714,17 +896,19 @@ class KeyMintSecurityLevelInterceptor( // captured into PersistedKeyData primitive fields (origin, block // mode, padding, expiry timestamps...), which broke session pinning // for apps that fingerprint metadata across keystore calls. - val metadataBytesForPersist = response.metadata?.let { md -> - runCatching { - val parcel = android.os.Parcel.obtain() - try { - md.writeToParcel(parcel, 0) - parcel.marshall() - } finally { - parcel.recycle() - } - }.getOrNull() - } + val metadataBytesForPersist = + response.metadata?.let { md -> + runCatching { + val parcel = android.os.Parcel.obtain() + try { + md.writeToParcel(parcel, 0) + parcel.marshall() + } finally { + parcel.recycle() + } + } + .getOrNull() + } persistExecutor.execute { GeneratedKeyPersistence.save( keyId = keyId, @@ -744,13 +928,20 @@ class KeyMintSecurityLevelInterceptor( } if (securityLevel == SecurityLevel.STRONGBOX) { - val delayMs = STRONGBOX_KEYGEN_LATENCY_FLOOR_MS - (System.nanoTime() - genStartNanos) / 1_000_000 + 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, diagnosticTag = "gen-mode-asym") + return InterceptorUtils.createTypedObjectReply( + response.metadata, + diagnosticTag = "gen-mode-asym", + ) } private fun generateAttestedKeyPairNative( @@ -758,75 +949,88 @@ class KeyMintSecurityLevelInterceptor( params: KeyMintAttestation, ): AndroidPair>? { return runCatching { - val algorithmName = when (params.algorithm) { - Algorithm.EC -> "EC" - Algorithm.RSA -> "RSA" - else -> return null + 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 hasChallenge = params.attestationChallenge != null + val appId = + if (hasChallenge) AttestationBuilder.createApplicationId(callingUid) else null + + 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 ?: ByteArray(0), + 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) } - 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 hasChallenge = params.attestationChallenge != null - val appId = if (hasChallenge) AttestationBuilder.createApplicationId(callingUid) else null - - 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 ?: ByteArray(0), - 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() + .onFailure { + SystemLogger.error( + "NativeCertGen: generation failed, falling back to BouncyCastle", + it, + ) + } + .getOrNull() } private fun buildKeyEntryResponse( @@ -863,160 +1067,206 @@ class KeyMintSecurityLevelInterceptor( return } - SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel") + 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 keyId = KeyIdentifier(record.uid, record.alias) + if (generatedKeys.containsKey(keyId)) { + SystemLogger.debug("Skipping already-loaded key: $keyId") + return@runCatching + } - // Symmetric (AES/HMAC/3DES) keys take a separate path: - // there is no PKCS8 private key, no certificate chain, just - // raw secret material plus the metadata snapshot. - val isSymmetric = record.symmetricKeyBytes.isNotEmpty() - if (isSymmetric) { - val secretKey = javax.crypto.spec.SecretKeySpec( - record.symmetricKeyBytes, - record.symmetricAlgorithm, - ) - val response = if (record.metadataBytes.isNotEmpty()) { - runCatching { - val parcel = android.os.Parcel.obtain() - try { - parcel.unmarshall(record.metadataBytes, 0, record.metadataBytes.size) - parcel.setDataPosition(0) - val metadata = KeyMetadata.CREATOR.createFromParcel(parcel) - KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } - } finally { - parcel.recycle() - } - }.getOrElse { e -> - SystemLogger.warning( - "Failed to restore symmetric metadata for ${record.alias}, falling back to primitive rebuild", - e, + // Symmetric (AES/HMAC/3DES) keys take a separate path: + // there is no PKCS8 private key, no certificate chain, just + // raw secret material plus the metadata snapshot. + val isSymmetric = record.symmetricKeyBytes.isNotEmpty() + if (isSymmetric) { + val secretKey = + javax.crypto.spec.SecretKeySpec( + record.symmetricKeyBytes, + record.symmetricAlgorithm, ) - rebuildSymmetricResponse(record) - } - } else { - // Pre-v3 file with symmetric key — should not happen - // because v3 always saves metadata, but be defensive: - // rebuild a minimal KeyMetadata from primitives so - // the secret material is still restored. Without - // this, dropping the record would silently log the - // user out the next time the alias is used. - SystemLogger.info( - "Symmetric record ${record.alias} missing metadata bytes, rebuilding from primitives" + val response = + if (record.metadataBytes.isNotEmpty()) { + runCatching { + val parcel = android.os.Parcel.obtain() + try { + parcel.unmarshall( + record.metadataBytes, + 0, + record.metadataBytes.size, + ) + parcel.setDataPosition(0) + val metadata = + KeyMetadata.CREATOR.createFromParcel(parcel) + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + } finally { + parcel.recycle() + } + } + .getOrElse { e -> + SystemLogger.warning( + "Failed to restore symmetric metadata for ${record.alias}, falling back to primitive rebuild", + e, + ) + rebuildSymmetricResponse(record) + } + } else { + // Pre-v3 file with symmetric key — should not happen + // because v3 always saves metadata, but be defensive: + // rebuild a minimal KeyMetadata from primitives so + // the secret material is still restored. Without + // this, dropping the record would silently log the + // user out the next time the alias is used. + SystemLogger.info( + "Symmetric record ${record.alias} missing metadata bytes, rebuilding from primitives" + ) + rebuildSymmetricResponse(record) + } + generatedKeys[keyId] = + GeneratedKeyInfo( + keyPair = null, + secretKey = secretKey, + nspace = record.nspace, + response = response, + keyParams = + response.metadata?.let { md -> + KeyMintAttestation( + md.authorizations + ?.map { it.keyParameter } + ?.toTypedArray() ?: emptyArray() + ) + }, + ) + SystemLogger.debug( + "Restored symmetric persisted key: $keyId (${record.symmetricAlgorithm}/${record.symmetricKeyBytes.size * 8}bit)" ) - rebuildSymmetricResponse(record) + return@runCatching } - generatedKeys[keyId] = GeneratedKeyInfo( - keyPair = null, - secretKey = secretKey, - nspace = record.nspace, - response = response, - keyParams = response.metadata?.let { md -> - KeyMintAttestation(md.authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()) - }, + + 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 + } + + // Prefer the byte-identical metadata snapshot persisted by v3 + // saves so apps that fingerprint the metadata (e.g. they + // pin algorithm/purpose/digest/origin/authorization order + // across reboots) keep their session valid. Fall back to + // rebuilding + // from primitive fields for v1-era files (which lose + // authorization tags that weren't captured then). + val response = + if (record.metadataBytes.isNotEmpty()) { + runCatching { + val parcel = android.os.Parcel.obtain() + try { + parcel.unmarshall( + record.metadataBytes, + 0, + record.metadataBytes.size, + ) + parcel.setDataPosition(0) + val metadata = KeyMetadata.CREATOR.createFromParcel(parcel) + // Make sure the descriptor's nspace matches the + // KEY_ID we will hand callers. updateSubcomponent + // and getKeyEntry both index by nspace. + metadata.key = + metadata.key + ?: KeyDescriptor().apply { + domain = Domain.KEY_ID + nspace = record.nspace + alias = null + blob = null + } + KeyEntryResponse().apply { + this.metadata = metadata + iSecurityLevel = original + } + } finally { + parcel.recycle() + } + } + .getOrElse { e -> + SystemLogger.warning( + "Failed to restore metadata bytes for $record.alias, falling back to rebuild", + e, + ) + rebuildResponseFromRecord(record, certChain, descriptor) + } + } else { + rebuildResponseFromRecord(record, certChain, descriptor) + } + + val keyIdRestored = KeyIdentifier(record.uid, record.alias) + generatedKeys[keyIdRestored] = + GeneratedKeyInfo( + keyPair, + null, + record.nspace, + response, + response.metadata?.let { md -> + // Re-derive an attestation summary from authorizations so + // any code path that reads keyParams (e.g. logging) still + // works. This does not feed back into the metadata bytes. + KeyMintAttestation( + md.authorizations?.map { it.keyParameter }?.toTypedArray() + ?: emptyArray() + ) + }, + ) + if (record.isAttestationKey) attestationKeys.add(keyIdRestored) + + SystemLogger.debug("Restored persisted key: $keyIdRestored") + } + .onFailure { + SystemLogger.error( + "Failed to restore key: uid=${record.uid} alias=${record.alias}", + it, ) - SystemLogger.debug("Restored symmetric persisted key: $keyId (${record.symmetricAlgorithm}/${record.symmetricKeyBytes.size * 8}bit)") - 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 - } - - // Prefer the byte-identical metadata snapshot persisted by v3 - // saves so apps that fingerprint the metadata (e.g. they - // pin algorithm/purpose/digest/origin/authorization order - // across reboots) keep their session valid. Fall back to - // rebuilding - // from primitive fields for v1-era files (which lose - // authorization tags that weren't captured then). - val response = if (record.metadataBytes.isNotEmpty()) { - runCatching { - val parcel = android.os.Parcel.obtain() - try { - parcel.unmarshall(record.metadataBytes, 0, record.metadataBytes.size) - parcel.setDataPosition(0) - val metadata = KeyMetadata.CREATOR.createFromParcel(parcel) - // Make sure the descriptor's nspace matches the - // KEY_ID we will hand callers. updateSubcomponent - // and getKeyEntry both index by nspace. - metadata.key = metadata.key ?: KeyDescriptor().apply { - domain = Domain.KEY_ID - nspace = record.nspace - alias = null - blob = null - } - KeyEntryResponse().apply { - this.metadata = metadata - iSecurityLevel = original - } - } finally { - parcel.recycle() - } - }.getOrElse { e -> - SystemLogger.warning( - "Failed to restore metadata bytes for $record.alias, falling back to rebuild", - e, - ) - rebuildResponseFromRecord(record, certChain, descriptor) - } - } else { - rebuildResponseFromRecord(record, certChain, descriptor) - } - - val keyIdRestored = KeyIdentifier(record.uid, record.alias) - generatedKeys[keyIdRestored] = GeneratedKeyInfo(keyPair, null, record.nspace, response, response.metadata?.let { md -> - // Re-derive an attestation summary from authorizations so - // any code path that reads keyParams (e.g. logging) still - // works. This does not feed back into the metadata bytes. - KeyMintAttestation(md.authorizations?.map { it.keyParameter }?.toTypedArray() ?: emptyArray()) - }) - if (record.isAttestationKey) attestationKeys.add(keyIdRestored) - - SystemLogger.debug("Restored persisted key: $keyIdRestored") - }.onFailure { - SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it) - } } SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}") } /** - * Fallback rebuild path used when no v3 metadata snapshot is available - * (key was saved by an older build, or the snapshot failed to deserialize). - * Rebuilds KeyEntryResponse from primitive fields. This loses any - * authorization tags that weren't captured at save time, which is why we + * Fallback rebuild path used when no v3 metadata snapshot is available (key was saved by an + * older build, or the snapshot failed to deserialize). Rebuilds KeyEntryResponse from primitive + * fields. This loses any authorization tags that weren't captured at save time, which is why we * prefer the byte-identical v3 snapshot whenever possible. */ private fun rebuildResponseFromRecord( @@ -1024,125 +1274,127 @@ class KeyMintSecurityLevelInterceptor( certChain: List, descriptor: KeyDescriptor, ): KeyEntryResponse { - 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, - nonce = 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 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, + nonce = 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(), + ) return buildKeyEntryResponse(record.uid, certChain, attestation, descriptor) } /** - * Defensive fallback for symmetric key records that somehow ended up - * without a metadata snapshot (e.g. a save where Parcel.marshall() - * threw and persisted an empty mdBytes, or a future format where the - * snapshot is lazily populated). Without this fallback, loadAll would - * skip the record and the secret material would be effectively lost, - * silently logging the user out the next time the alias is used. + * Defensive fallback for symmetric key records that somehow ended up without a metadata + * snapshot (e.g. a save where Parcel.marshall() threw and persisted an empty mdBytes, or a + * future format where the snapshot is lazily populated). Without this fallback, loadAll would + * skip the record and the secret material would be effectively lost, silently logging the user + * out the next time the alias is used. * - * The rebuilt KeyMetadata is structurally minimal — only the primitive - * authorization tags we captured at save time. That's worse than a - * byte-identical snapshot for apps that fingerprint metadata, but it - * still keeps the AES key alive across reboots, which is the - * dominant correctness concern. + * The rebuilt KeyMetadata is structurally minimal — only the primitive authorization tags we + * captured at save time. That's worse than a byte-identical snapshot for apps that fingerprint + * metadata, but it still keeps the AES key alive across reboots, which is the dominant + * correctness concern. */ private fun rebuildSymmetricResponse(record: PersistedKeyData): KeyEntryResponse { - 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, - nonce = 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 metadata = KeyMetadata().apply { - keySecurityLevel = securityLevel - key = KeyDescriptor().apply { - domain = Domain.KEY_ID - nspace = record.nspace - alias = null - 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, + nonce = 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 metadata = + KeyMetadata().apply { + keySecurityLevel = securityLevel + key = + KeyDescriptor().apply { + domain = Domain.KEY_ID + nspace = record.nspace + alias = null + blob = null + } + certificate = null + certificateChain = null + authorizations = attestation.toAuthorizations(record.uid, securityLevel) + modificationTimeMs = System.currentTimeMillis() } - certificate = null - certificateChain = null - authorizations = attestation.toAuthorizations(record.uid, securityLevel) - modificationTimeMs = System.currentTimeMillis() - } return KeyEntryResponse().apply { this.metadata = metadata iSecurityLevel = original @@ -1171,11 +1423,13 @@ class KeyMintSecurityLevelInterceptor( 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 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 val GENERATE_KEY_TRANSACTION = InterceptorUtils.getTransactCode(IKeystoreSecurityLevel.Stub::class.java, "generateKey") @@ -1188,7 +1442,11 @@ class KeyMintSecurityLevelInterceptor( ) val INTERCEPTED_CODES = - intArrayOf(GENERATE_KEY_TRANSACTION, IMPORT_KEY_TRANSACTION, CREATE_OPERATION_TRANSACTION) + intArrayOf( + GENERATE_KEY_TRANSACTION, + IMPORT_KEY_TRANSACTION, + CREATE_OPERATION_TRANSACTION, + ) private val transactionNames: Map by lazy { IKeystoreSecurityLevel.Stub::class @@ -1208,7 +1466,8 @@ class KeyMintSecurityLevelInterceptor( val patchedChains = ConcurrentHashMap>() val attestationKeys: MutableSet = ConcurrentHashMap.newKeySet() val importedKeys: MutableSet = ConcurrentHashMap.newKeySet() - private val usageCounters = ConcurrentHashMap() + private val usageCounters = + ConcurrentHashMap() private val interceptedOperations = ConcurrentHashMap() /** @@ -1229,13 +1488,16 @@ class KeyMintSecurityLevelInterceptor( /** Mint or reuse a grant id (random, non-zero, non -1 Long). Re-grant reuses the id. */ fun issueGrant(ownerKeyId: KeyIdentifier, granteeUid: Int, accessVector: Int): Long { softwareGrants.entries - .firstOrNull { it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid } + .firstOrNull { + it.value.ownerKeyId == ownerKeyId && it.value.granteeUid == granteeUid + } ?.let { existing -> softwareGrants[existing.key] = existing.value.copy(accessVector = accessVector) return existing.key } var id = secureRandom.nextLong() - while (id == 0L || id == -1L || softwareGrants.containsKey(id)) id = secureRandom.nextLong() + while (id == 0L || id == -1L || softwareGrants.containsKey(id)) id = + secureRandom.nextLong() softwareGrants[id] = SoftwareGrant(ownerKeyId, granteeUid, accessVector) return id } @@ -1251,8 +1513,9 @@ class KeyMintSecurityLevelInterceptor( * (`generatedKeys`) OR patch-mode (`teeResponses`, a real TEE key whose attestation we * patched). The grant plane must virtualize both: gating on `generatedKeys` alone left * patch-mode keys' `Domain.GRANT` readback falling through to the real keystore2 unpatched, - * splitting the grant chain against the owner's patched read (duck SELF_/ISOLATED_CHAIN_SPLIT, - * surfaced once Android 16 made KeyStoreManager.grantKeyAccess a public API). + * splitting the grant chain against the owner's patched read (duck + * SELF_/ISOLATED_CHAIN_SPLIT, surfaced once Android 16 made KeyStoreManager.grantKeyAccess + * a public API). */ fun ownsKeyResponse(keyId: KeyIdentifier): Boolean = getGeneratedKeyResponse(keyId) != null @@ -1289,9 +1552,8 @@ class KeyMintSecurityLevelInterceptor( /** * Drops the cached TEE/patched response (and patched chain) addressed by KEY_ID so a - * post-mutation getKeyEntry falls through to the now-updated real keystore2 key. Used - * after updateSubcomponent re-keys a patched chain (duck - * STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE). + * post-mutation getKeyEntry falls through to the now-updated real keystore2 key. Used after + * updateSubcomponent re-keys a patched chain (duck STALE_TEE_RESPONSE_AFTER_KEY_ID_UPDATE). */ fun evictTeeResponseByKeyId(callingUid: Int, nspace: Long?) { if (nspace == null || nspace == 0L) return @@ -1355,7 +1617,9 @@ class KeyMintSecurityLevelInterceptor( .filter { it.value.ownerKeyId == srcId } .forEach { softwareGrants[it.key] = it.value.copy(ownerKeyId = dstId) } GeneratedKeyPersistence.delete(srcId) - SystemLogger.info("Migrated synthetic key $srcId -> $dstId (maintenance.migrateKeyNamespace)") + SystemLogger.info( + "Migrated synthetic key $srcId -> $dstId (maintenance.migrateKeyNamespace)" + ) } fun removeOperationInterceptor(operationBinder: IBinder, backdoor: IBinder) { @@ -1422,11 +1686,20 @@ private fun KeyMintAttestation.toAuthorizations( if (this.ecCurve != null) { authList.add(createAuth(Tag.EC_CURVE, KeyParameterValue.ecCurve(this.ecCurve))) } - this.blockMode.forEach { authList.add(createAuth(Tag.BLOCK_MODE, KeyParameterValue.blockMode(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))) } + this.padding.forEach { + authList.add(createAuth(Tag.PADDING, KeyParameterValue.paddingMode(it))) + } if (this.rsaPublicExponent != null) { - authList.add(createAuth(Tag.RSA_PUBLIC_EXPONENT, KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()))) + authList.add( + createAuth( + Tag.RSA_PUBLIC_EXPONENT, + KeyParameterValue.longInteger(this.rsaPublicExponent.toLong()), + ) + ) } if (this.callerNonce == true) { authList.add(createAuth(Tag.CALLER_NONCE, KeyParameterValue.boolValue(true))) @@ -1444,13 +1717,19 @@ 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))) @@ -1459,8 +1738,12 @@ private fun KeyMintAttestation.toAuthorizations( 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) { @@ -1475,55 +1758,66 @@ private fun KeyMintAttestation.toAuthorizations( // extension via AttestationBuilder, so attestation content is unchanged. /** - * Keystore-enforced authorizations (CREATION_DATETIME, ACTIVE_DATETIME, - * USER_ID, etc.) are tagged by real KeyMint HAL with - * SecurityLevel.KEYSTORE (= 100, byte 0x64), not SOFTWARE (= 0, byte - * 0x00). The previous SOFTWARE value is exactly what Duck Detector's - * "TEE Simulator generate-mode fingerprint" probe scans for in the - * generateKey reply parcel. Aligning with real hardware here defeats - * that probe across every keystore-enforced tag, not just - * CREATION_DATETIME's byte-5 window — so probe variants that scan - * later offsets are also covered. + * Keystore-enforced authorizations (CREATION_DATETIME, ACTIVE_DATETIME, USER_ID, etc.) are + * tagged by real KeyMint HAL with SecurityLevel.KEYSTORE (= 100, byte 0x64), not SOFTWARE (= 0, + * byte 0x00). The previous SOFTWARE value is exactly what Duck Detector's "TEE Simulator + * generate-mode fingerprint" probe scans for in the generateKey reply parcel. Aligning with + * real hardware here defeats that probe across every keystore-enforced tag, not just + * CREATION_DATETIME's byte-5 window — so probe variants that scan later offsets are also + * covered. */ fun createKeystoreAuth(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.KEYSTORE } } - authList.add(createKeystoreAuth(Tag.CREATION_DATETIME, KeyParameterValue.dateTime(System.currentTimeMillis()))) + authList.add( + createKeystoreAuth( + Tag.CREATION_DATETIME, + KeyParameterValue.dateTime(System.currentTimeMillis()), + ) + ) this.activeDateTime?.let { authList.add(createKeystoreAuth(Tag.ACTIVE_DATETIME, KeyParameterValue.dateTime(it.time))) } this.originationExpireDateTime?.let { - authList.add(createKeystoreAuth(Tag.ORIGINATION_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time))) + authList.add( + createKeystoreAuth(Tag.ORIGINATION_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)) + ) } this.usageExpireDateTime?.let { - authList.add(createKeystoreAuth(Tag.USAGE_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time))) + authList.add( + createKeystoreAuth(Tag.USAGE_EXPIRE_DATETIME, KeyParameterValue.dateTime(it.time)) + ) } this.usageCountLimit?.let { authList.add(createKeystoreAuth(Tag.USAGE_COUNT_LIMIT, KeyParameterValue.integer(it))) } if (this.unlockedDeviceRequired == true) { - authList.add(createKeystoreAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true))) + authList.add( + createKeystoreAuth(Tag.UNLOCKED_DEVICE_REQUIRED, KeyParameterValue.boolValue(true)) + ) } // Captured real keystore2 tags USER_ID at SecurityLevel.SOFTWARE (0), even though // CREATION_DATETIME above is KEYSTORE (100). Mirror that split exactly. authList.add( Authorization().apply { - this.keyParameter = KeyParameter().apply { - this.tag = Tag.USER_ID - this.value = KeyParameterValue.integer(callingUid / 100000) - } + this.keyParameter = + KeyParameter().apply { + this.tag = Tag.USER_ID + this.value = KeyParameterValue.integer(callingUid / 100000) + } this.securityLevel = SecurityLevel.SOFTWARE - }, + } ) return authList.toTypedArray() diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt index 5189d6b..18d063c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/SoftwareOperation.kt @@ -9,12 +9,12 @@ import android.hardware.security.keymint.KeyPurpose import android.hardware.security.keymint.PaddingMode import android.hardware.security.keymint.Tag import android.os.ServiceSpecificException -import java.util.concurrent.locks.LockSupport import android.system.keystore2.IKeystoreOperation import android.system.keystore2.KeyParameters import java.security.KeyPair import java.security.Signature import java.security.SignatureException +import java.util.concurrent.locks.LockSupport import javax.crypto.Cipher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.logging.KeyMintParameterLogger @@ -24,9 +24,13 @@ private sealed interface CryptoPrimitive { fun updateAad(aadInput: ByteArray?) { throw ServiceSpecificException(KeystoreErrorCodes.invalidTag) } + fun update(data: ByteArray?): ByteArray? + fun finish(data: ByteArray?, signature: ByteArray?): ByteArray? + fun abort() + fun getBeginParameters(): Array? = null } @@ -118,10 +122,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") + throw ServiceSpecificException( + KeystoreErrorCodes.verificationFailed, + "Signature to verify is null", + ) } if (!this.signature.verify(signature)) { - throw ServiceSpecificException(KeystoreErrorCodes.verificationFailed, "Signature verification failed") + throw ServiceSpecificException( + KeystoreErrorCodes.verificationFailed, + "Signature verification failed", + ) } return null } @@ -201,7 +211,8 @@ class SoftwareOperation( private val latencyFloorMs: Long = 0L, ) { private val primitive: CryptoPrimitive - @Volatile var finalized = false + @Volatile + var finalized = false private set var onFinishCallback: (() -> Unit)? = null @@ -229,9 +240,9 @@ class SoftwareOperation( // silently corrupt their session. SystemLogger.warning( "[SoftwareOp TX_ID: $txId] Purpose missing on restored key " + - "(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " + - "secretKey=${if (secretKey != null) "present" else "null"}). " + - "Returning unsupportedPurpose." + "(authorizations=${params.purpose}, keyPair=${if (keyPair != null) "present" else "null"}, " + + "secretKey=${if (secretKey != null) "present" else "null"}). " + + "Returning unsupportedPurpose." ) throw ServiceSpecificException( KeystoreErrorCodes.unsupportedPurpose, @@ -242,40 +253,50 @@ class SoftwareOperation( primitive = when (purpose) { KeyPurpose.SIGN -> { - val kp = keyPair ?: throw ServiceSpecificException( - KeystoreErrorCodes.invalidArgument, - "[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null", - ) + val kp = + keyPair + ?: throw ServiceSpecificException( + KeystoreErrorCodes.invalidArgument, + "[SoftwareOp TX_ID: $txId] SIGN requested but keyPair is null", + ) Signer(kp, params) } KeyPurpose.VERIFY -> { - val kp = keyPair ?: throw ServiceSpecificException( - KeystoreErrorCodes.invalidArgument, - "[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null", - ) + val kp = + keyPair + ?: throw ServiceSpecificException( + KeystoreErrorCodes.invalidArgument, + "[SoftwareOp TX_ID: $txId] VERIFY requested but keyPair is null", + ) Verifier(kp, params) } KeyPurpose.ENCRYPT -> { - val key: java.security.Key = secretKey ?: keyPair?.public - ?: throw ServiceSpecificException( - KeystoreErrorCodes.unsupportedPurpose, - "[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public", - ) + val key: java.security.Key = + secretKey + ?: keyPair?.public + ?: throw ServiceSpecificException( + KeystoreErrorCodes.unsupportedPurpose, + "[SoftwareOp TX_ID: $txId] ENCRYPT requires either secretKey or keyPair.public", + ) CipherPrimitive(key, params, Cipher.ENCRYPT_MODE) } KeyPurpose.DECRYPT -> { - val key: java.security.Key = secretKey ?: keyPair?.private - ?: throw ServiceSpecificException( - KeystoreErrorCodes.unsupportedPurpose, - "[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private", - ) + val key: java.security.Key = + secretKey + ?: keyPair?.private + ?: throw ServiceSpecificException( + KeystoreErrorCodes.unsupportedPurpose, + "[SoftwareOp TX_ID: $txId] DECRYPT requires either secretKey or keyPair.private", + ) CipherPrimitive(key, params, Cipher.DECRYPT_MODE) } KeyPurpose.AGREE_KEY -> { - val kp = keyPair ?: throw ServiceSpecificException( - KeystoreErrorCodes.invalidArgument, - "[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null", - ) + val kp = + keyPair + ?: throw ServiceSpecificException( + KeystoreErrorCodes.invalidArgument, + "[SoftwareOp TX_ID: $txId] AGREE_KEY requested but keyPair is null", + ) KeyAgreementPrimitive(kp) } else -> @@ -288,29 +309,39 @@ class SoftwareOperation( private fun checkActive() { if (finalized) { - SystemLogger.debug("[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)") + SystemLogger.debug( + "[SoftwareOp TX_ID: $txId] Rejected: operation already finalized (pruned or completed)" + ) throw ServiceSpecificException(KeystoreErrorCodes.invalidOperationHandle) } } 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})") + 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.info("[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}") + SystemLogger.info( + "[SoftwareOp TX_ID: $txId] updateAad() ENTRY inputSize=${aadInput?.size ?: 0} primitive=${primitive::class.simpleName}" + ) checkActive() checkInputLength(aadInput) try { primitive.updateAad(aadInput) - SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)") + SystemLogger.info( + "[SoftwareOp TX_ID: $txId] updateAad() RETURNED_NORMALLY (unexpected for non-AEAD)" + ) } catch (throwable: Throwable) { val top = throwable.stackTrace.firstOrNull()?.toString() ?: "" val code = (throwable as? ServiceSpecificException)?.errorCode - SystemLogger.info("[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top") + SystemLogger.info( + "[SoftwareOp TX_ID: $txId] updateAad() THREW class=${throwable::class.java.name} code=$code msg=${throwable.message} top=$top" + ) throw throwable } } @@ -358,13 +389,18 @@ class SoftwareOperation( 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) - } + 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) + } companion object { private const val MAX_RECEIVE_DATA = 0x8000 @@ -429,12 +465,11 @@ internal object KeystoreErrorCodes { } 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 - } + runCatching { Class.forName(className).getField(fieldName).getInt(null) } + .getOrElse { + SystemLogger.debug("Resolved $className.$fieldName via fallback: $fallback") + fallback + } } class SoftwareOperationBinder(private val operation: SoftwareOperation) : @@ -442,13 +477,17 @@ class SoftwareOperationBinder(private val operation: SoftwareOperation) : @Synchronized override fun updateAad(aadInput: ByteArray?) { - SystemLogger.info("[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}") + SystemLogger.info( + "[SoftwareOpBinder] updateAad() ENTRY callingUid=${android.os.Binder.getCallingUid()} size=${aadInput?.size ?: 0}" + ) try { operation.updateAad(aadInput) SystemLogger.info("[SoftwareOpBinder] updateAad() RETURNED_NORMALLY") } catch (throwable: Throwable) { val code = (throwable as? ServiceSpecificException)?.errorCode - SystemLogger.info("[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}") + SystemLogger.info( + "[SoftwareOpBinder] updateAad() PROPAGATING class=${throwable::class.java.name} code=$code msg=${throwable.message}" + ) throw throwable } } diff --git a/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt b/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt index 109a9e7..1646d12 100644 --- a/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt +++ b/app/src/main/java/org/matrix/TEESimulator/logging/SystemLogger.kt @@ -26,10 +26,11 @@ object SystemLogger { private val suppressedCount = AtomicInteger(0) /** - * Returns true if this message should be emitted. Resets the window if expired - * and emits a suppression summary for the previous window. + * Returns true if this message should be emitted. Resets the window if expired and emits a + * suppression summary for the previous window. */ - @PublishedApi internal fun acquireLogPermit(): Boolean { + @PublishedApi + internal fun acquireLogPermit(): Boolean { val now = System.currentTimeMillis() val start = windowStart.get() if (now - start > RATE_LIMIT_WINDOW_MS) { @@ -38,7 +39,10 @@ object SystemLogger { val suppressed = suppressedCount.getAndSet(0) windowCount.set(1) // this call counts as #1 in the new window if (suppressed > 0) { - Log.i(TAG, "[rate-limit] suppressed $suppressed log messages in previous window") + Log.i( + TAG, + "[rate-limit] suppressed $suppressed log messages in previous window", + ) } return true } @@ -49,9 +53,7 @@ object SystemLogger { return false } - /** - * Logs a debug message. Use this for fine-grained information that is useful for debugging. - */ + /** Logs a debug message. Use this for fine-grained information that is useful for debugging. */ fun debug(message: String) { if (!isDebugBuild) return if (!acquireLogPermit()) return @@ -65,9 +67,7 @@ object SystemLogger { Log.d(TAG, message()) } - /** - * Logs an informational message. Use this to report major application lifecycle events. - */ + /** Logs an informational message. Use this to report major application lifecycle events. */ fun info(message: String) { if (!acquireLogPermit()) return Log.i(TAG, message) @@ -79,9 +79,7 @@ object SystemLogger { Log.i(TAG, message()) } - /** - * Logs a warning message. Warnings are never rate-limited. - */ + /** Logs a warning message. Warnings are never rate-limited. */ fun warning(message: String, throwable: Throwable? = null) { if (throwable != null) { Log.w(TAG, message, throwable) @@ -90,9 +88,7 @@ object SystemLogger { } } - /** - * Logs an error message. Errors are never rate-limited. - */ + /** Logs an error message. Errors are never rate-limited. */ fun error(message: String, throwable: Throwable? = null) { if (throwable != null) { Log.e(TAG, message, throwable) 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 04f85ef..488b69a 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -93,37 +93,39 @@ object CertificateGenerator { ) return try { - // AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1 - if (challenge == null && attestKeyAlias == null) { - SystemLogger.trace { "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" } - return listOf(buildSelfSignedCertificate(subjectKeyPair, params)) + // AOSP ta/src/keys.rs:451-478: no challenge + no attestKey = self-signed, depth 1 + if (challenge == null && attestKeyAlias == null) { + SystemLogger.trace { + "[certgen] no-challenge key: self-signed, depth=1, purposes=${params.purpose}" } + return listOf(buildSelfSignedCertificate(subjectKeyPair, params)) + } - val keybox = getKeyboxForAlgorithm(uid, params.algorithm) + val keybox = getKeyboxForAlgorithm(uid, params.algorithm) - val attestKeyInfo = - if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - getAttestationKeyInfo(uid, attestKeyAlias) - } else null + val attestKeyInfo = + if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + getAttestationKeyInfo(uid, attestKeyAlias) + } else null - val (signingKey, issuer) = attestKeyInfo - ?.let { it.first to it.second } + val (signingKey, issuer) = + attestKeyInfo?.let { it.first to it.second } ?: (keybox.keyPair to getIssuerFromKeybox(keybox)) - val leafCert = - buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel) + val leafCert = + buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel) - if (attestKeyInfo != null) { - listOf(leafCert) - } else { - listOf(leafCert) + keybox.certificates - } - } catch (e: android.os.ServiceSpecificException) { - throw e - } catch (e: Exception) { - SystemLogger.error("Failed to generate certificate chain.", e) - null + if (attestKeyInfo != null) { + listOf(leafCert) + } else { + listOf(leafCert) + keybox.certificates } + } catch (e: android.os.ServiceSpecificException) { + throw e + } catch (e: Exception) { + SystemLogger.error("Failed to generate certificate chain.", e) + null + } } /** @@ -138,27 +140,23 @@ object CertificateGenerator { securityLevel: Int, ): Pair>? { return try { - SystemLogger.info( - "Generating new attested key pair for alias: '$alias' (UID: $uid)" - ) - val newKeyPair = - generateSoftwareKeyPair(params) - ?: throw Exception("Failed to generate underlying software key pair.") + SystemLogger.info("Generating new attested key pair for alias: '$alias' (UID: $uid)") + val newKeyPair = + generateSoftwareKeyPair(params) + ?: throw Exception("Failed to generate underlying software key pair.") - val chain = - generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel) - ?: throw Exception("Failed to generate certificate chain for new key pair.") + val chain = + generateCertificateChain(uid, newKeyPair, attestKeyAlias, params, securityLevel) + ?: throw Exception("Failed to generate certificate chain for new key pair.") - SystemLogger.info( - "Successfully generated new certificate chain for alias: '$alias'." - ) - Pair(newKeyPair, chain) - } catch (e: android.os.ServiceSpecificException) { - throw e - } catch (e: Exception) { - SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e) - null - } + SystemLogger.info("Successfully generated new certificate chain for alias: '$alias'.") + Pair(newKeyPair, chain) + } catch (e: android.os.ServiceSpecificException) { + throw e + } catch (e: Exception) { + SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e) + null + } } fun getIssuerFromKeybox(keybox: KeyBox) = @@ -205,14 +203,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 } @@ -253,9 +253,13 @@ object CertificateGenerator { val signerAlgorithm = when (signingKeyPair.private.algorithm) { - "EC", "ECDSA" -> "SHA256withECDSA" + "EC", + "ECDSA" -> "SHA256withECDSA" "RSA" -> "SHA256withRSA" - else -> throw IllegalArgumentException("Unsupported signing key: ${signingKeyPair.private.algorithm}") + else -> + throw IllegalArgumentException( + "Unsupported signing key: ${signingKeyPair.private.algorithm}" + ) } val contentSigner = JcaContentSignerBuilder(signerAlgorithm) @@ -274,28 +278,33 @@ object CertificateGenerator { val notBefore = params.certificateNotBefore ?: Date(0) val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER) - val builder = JcaX509v3CertificateBuilder( - subject, - params.certificateSerial ?: BigInteger.ONE, - notBefore, - notAfter, - subject, - keyPair.public, - ) + val builder = + JcaX509v3CertificateBuilder( + subject, + params.certificateSerial ?: BigInteger.ONE, + notBefore, + notAfter, + subject, + keyPair.public, + ) val keyUsageBits = buildKeyUsageFromPurposes(params.purpose) if (keyUsageBits != 0) { builder.addExtension(Extension.keyUsage, true, KeyUsage(keyUsageBits)) } - val signerAlgorithm = when (keyPair.private.algorithm) { - "EC", "ECDSA" -> "SHA256withECDSA" - "RSA" -> "SHA256withRSA" - else -> throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}") - } - val contentSigner = JcaContentSignerBuilder(signerAlgorithm) - .setProvider(BouncyCastleProvider.PROVIDER_NAME) - .build(keyPair.private) + val signerAlgorithm = + when (keyPair.private.algorithm) { + "EC", + "ECDSA" -> "SHA256withECDSA" + "RSA" -> "SHA256withRSA" + else -> + throw IllegalArgumentException("Unsupported key: ${keyPair.private.algorithm}") + } + val contentSigner = + JcaContentSignerBuilder(signerAlgorithm) + .setProvider(BouncyCastleProvider.PROVIDER_NAME) + .build(keyPair.private) return JcaX509CertificateConverter().getCertificate(builder.build(contentSigner)) } diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/NativeCertGen.kt b/app/src/main/java/org/matrix/TEESimulator/pki/NativeCertGen.kt index 649ce64..c47cd5c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/NativeCertGen.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/NativeCertGen.kt @@ -69,7 +69,10 @@ object NativeCertGen { isAvailable = true SystemLogger.info("NativeCertGen: loaded libcertgen.so successfully") } catch (e: UnsatisfiedLinkError) { - SystemLogger.error("NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", e) + SystemLogger.error( + "NativeCertGen: failed to load libcertgen.so, falling back to BouncyCastle", + e, + ) } } @@ -111,11 +114,13 @@ object NativeCertGen { throw IllegalStateException("No certificates in native result") } - val algorithmName = when (certs[0].publicKey.algorithm) { - "EC", "ECDSA" -> "EC" - "RSA" -> "RSA" - else -> certs[0].publicKey.algorithm - } + val algorithmName = + when (certs[0].publicKey.algorithm) { + "EC", + "ECDSA" -> "EC" + "RSA" -> "RSA" + else -> certs[0].publicKey.algorithm + } val keyFactory = KeyFactory.getInstance(algorithmName) val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkBytes)) val publicKey = certs[0].publicKey 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 f6883a3..d791f9c 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt @@ -186,11 +186,12 @@ object AndroidDeviceUtils { 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 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 { @@ -294,7 +295,10 @@ object AndroidDeviceUtils { // 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) + parsePatchLevelValue( + SystemProperties.get("ro.build.version.security_patch", ""), + isLong, + ) resolvedValue.equals("no", ignoreCase = true) -> DO_NOT_REPORT else -> parsePatchLevelValue(resolvedValue, isLong) } @@ -396,23 +400,23 @@ object AndroidDeviceUtils { /** * Retrieves the attestation version for the given security level. The value follows the device - * OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort. - * A static StrongBox=300 floor would force a major-version mismatch with the TEE chain on - * Android 16 devices that report keymaster 400 across both security levels. + * OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort. A + * static StrongBox=300 floor would force a major-version mismatch with the TEE chain on Android + * 16 devices that report keymaster 400 across both security levels. * * @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox). * @return The appropriate attestation version number. */ fun getAttestVersion(securityLevel: Int): Int { val cached = DeviceAttestationService.CachedAttestationData?.attestVersion - val version = cached - ?: attestVersionMap[Build.VERSION.SDK_INT] - ?: 400 // Default to a recent version - val source = when { - cached != null -> "cache" - attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map" - else -> "default" - } + val version = + cached ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 // Default to a recent version + val source = + when { + cached != null -> "cache" + attestVersionMap.containsKey(Build.VERSION.SDK_INT) -> "map" + else -> "default" + } SystemLogger.debug("attestVersion=$version source=$source securityLevel=$securityLevel") return version } @@ -519,10 +523,7 @@ object AndroidDeviceUtils { val moduleHash: ByteArray by lazy { DeviceAttestationService.CachedAttestationData?.moduleHash ?: runCatching { - data class ModuleEntry( - val nameEncoded: ByteArray, - val fullEncoded: ByteArray, - ) + data class ModuleEntry(val nameEncoded: ByteArray, val fullEncoded: ByteArray) val modules = apexInfos.map { (packageName, versionCode) -> diff --git a/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt b/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt index d5877f3..373575d 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidPermissionUtils.kt @@ -12,14 +12,17 @@ object AndroidPermissionUtils { 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") + val currentActivityThreadMethod = + activityThreadClass.getDeclaredMethod("currentActivityThread") currentActivityThreadMethod.isAccessible = true val activityThread = currentActivityThreadMethod.invoke(null) - + if (activityThread == null) { - SystemLogger.warning("Reflection: ActivityThread.currentActivityThread() returned null") + SystemLogger.warning( + "Reflection: ActivityThread.currentActivityThread() returned null" + ) return null } @@ -27,29 +30,31 @@ object AndroidPermissionUtils { 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) + // 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. - */ + /** 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 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 } @@ -69,4 +74,4 @@ object AndroidPermissionUtils { 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/Extensions.kt b/app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt index cb59624..8d89b1d 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/Extensions.kt @@ -7,10 +7,7 @@ package org.matrix.TEESimulator.util * @return A new string with each line individually trimmed. */ fun String.trimLines(): String = - this.trim() - .lines() - .filter { !it.trim().startsWith("