From b27a33b444d4d439b1843d19dadba0499308069b Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Sat, 30 May 2026 13:42:05 +0100 Subject: [PATCH] refactor(app): drop PIF resolvers + dump purge Remove PatchLevelManager (auto-resolved the security-patch date from an installed PlayIntegrityFix module into security_patch.txt, with a FileObserver hot-reload) and BulletinPoller (scheduled bulletin refresh), and their App.kt init/start calls. Add purgeDebugDiagnostics(): release builds sweep stale teesim-*.bin dumps from /data/local/tmp at boot so a prior debug install can't leave a detection artifact. Stabilize the InterceptorUtils diagnostic dump path to a single file instead of one per call. --- .../main/java/org/matrix/TEESimulator/App.kt | 34 ++- .../TEESimulator/config/BulletinPoller.kt | 193 ------------------ .../TEESimulator/config/PatchLevelManager.kt | 192 ----------------- .../interception/keystore/InterceptorUtils.kt | 2 +- 4 files changed, 24 insertions(+), 397 deletions(-) delete mode 100644 app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt delete mode 100644 app/src/main/java/org/matrix/TEESimulator/config/PatchLevelManager.kt diff --git a/app/src/main/java/org/matrix/TEESimulator/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt index 94e14fe..be44db0 100644 --- a/app/src/main/java/org/matrix/TEESimulator/App.kt +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -6,12 +6,11 @@ import android.content.Context import android.content.ContextWrapper import android.os.Build import android.os.Looper +import java.io.File import java.security.Security import org.bouncycastle.jce.provider.BouncyCastleProvider import org.matrix.TEESimulator.config.BootStateManager -import org.matrix.TEESimulator.config.BulletinPoller import org.matrix.TEESimulator.config.ConfigurationManager -import org.matrix.TEESimulator.config.PatchLevelManager import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor @@ -41,12 +40,12 @@ object App { } try { + purgeDebugDiagnostics() prepareEnvironment() - // Spoof boot-state and patch-level props before any hook attaches, - // so keystore2's cached snapshot reflects the spoofed values. + // Spoof boot-state props before any hook attaches, so keystore2's + // cached snapshot reflects the spoofed values. BootStateManager.apply() - PatchLevelManager.initialize() // Load the package configuration. ConfigurationManager.initialize() @@ -65,12 +64,6 @@ object App { NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so") - try { - BulletinPoller.start() - } catch (e: Throwable) { - SystemLogger.error("Failed to start BulletinPoller", e) - } - // This starts the message queue processing. It blocks here indefinitely // processing messages until Looper.myLooper().quit() is called. Looper.loop() @@ -80,6 +73,25 @@ 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. + */ + private fun purgeDebugDiagnostics() { + if (SystemLogger.isDebugBuild) return + val stale = + File("/data/local/tmp").listFiles { _, name -> + name.startsWith("teesim-") && name.endsWith(".bin") + } ?: return + stale.forEach { runCatching { it.delete() } } + 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") + } + } + /** Initializes the necessary Android framework internals to satisfy KeyStore requirements. */ private fun prepareEnvironment() { // 1. Prepare Main Looper diff --git a/app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt b/app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt deleted file mode 100644 index bf5a477..0000000 --- a/app/src/main/java/org/matrix/TEESimulator/config/BulletinPoller.kt +++ /dev/null @@ -1,193 +0,0 @@ -package org.matrix.TEESimulator.config - -import android.os.Handler -import android.os.HandlerThread -import java.io.File -import java.net.URL -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import javax.net.ssl.HttpsURLConnection -import org.json.JSONArray -import org.json.JSONObject -import org.matrix.TEESimulator.BuildConfig -import org.matrix.TEESimulator.logging.SystemLogger - -object BulletinPoller { - private const val BULLETIN_URL = "https://source.android.com/docs/security/bulletin/pixel" - private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt" - private const val HISTORY_FILE = "/data/adb/tricky_store/last_bulletin_fetch.json" - private const val HISTORY_STAGING = "/data/adb/tricky_store/last_bulletin_fetch.json.next" - private const val HISTORY_CAP = 10 - private const val CONNECT_TIMEOUT_MS = 10_000 - private const val READ_TIMEOUT_MS = 15_000 - private const val STEADY_INTERVAL_MS = 24L * 60 * 60 * 1000 - - private val BOOTSTRAP_INTERVALS = longArrayOf(5_000, 30_000, 120_000, 600_000, 1_800_000) - private val DATE_REGEX = Regex("(\\d{4}-\\d{2}-\\d{2})") - private val PATCH_DATE_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}$") - - private lateinit var handler: Handler - @Volatile private var bootstrapStep = 0 - @Volatile private var steadyArmed = false - - fun start() { - val thread = HandlerThread("BulletinPoller").apply { start() } - handler = Handler(thread.looper) - handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[0]) - } - - private fun pollOnce() { - try { - val result = fetchAndParse() - appendHistory(result) - scheduleNext(result.status == "success") - } catch (t: Throwable) { - SystemLogger.error("BulletinPoller: pollOnce failed", t) - scheduleNext(false) - } - } - - private fun scheduleNext(success: Boolean) { - if (success || steadyArmed) { - steadyArmed = true - handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS) - return - } - bootstrapStep++ - if (bootstrapStep >= BOOTSTRAP_INTERVALS.size) { - steadyArmed = true - handler.postDelayed(::pollOnce, STEADY_INTERVAL_MS) - } else { - handler.postDelayed(::pollOnce, BOOTSTRAP_INTERVALS[bootstrapStep]) - } - } - - private data class FetchResult( - val ts: Long, - val status: String, - val httpCode: Int?, - val parsedDate: String?, - val applied: Boolean, - val error: String?, - ) - - private fun fetchAndParse(): FetchResult { - val ts = System.currentTimeMillis() - var conn: HttpsURLConnection? = null - return try { - conn = - (URL(BULLETIN_URL).openConnection() as HttpsURLConnection).apply { - connectTimeout = CONNECT_TIMEOUT_MS - readTimeout = READ_TIMEOUT_MS - setRequestProperty( - "User-Agent", - "TEESimulator/${BuildConfig.VERSION_NAME}", - ) - requestMethod = "GET" - } - val code = conn.responseCode - if (code != 200) { - return FetchResult(ts, "network_error", code, null, false, "HTTP $code") - } - val html = conn.inputStream.bufferedReader().use { it.readText() } - val date = DATE_REGEX.find(html)?.groupValues?.get(1) - if (date == null) { - return FetchResult( - ts, - "parse_error", - code, - null, - false, - "no YYYY-MM-DD match", - ) - } - val current = currentPatch() - if (current == null || date <= current) { - return FetchResult(ts, "success", code, date, false, null) - } - if (PatchLevelManager.updateTo(date)) { - FetchResult(ts, "success", code, date, true, null) - } else { - FetchResult( - ts, - "validation_rejected", - code, - date, - false, - "PatchLevelManager.updateTo rejected $date", - ) - } - } catch (e: Exception) { - FetchResult(ts, "network_error", null, null, false, e.toString()) - } finally { - conn?.disconnect() - } - } - - private fun currentPatch(): String? { - val f = File(PATCH_FILE) - if (!f.exists()) return null - val raw = try { - f.readLines() - .firstOrNull { it.startsWith("system=") } - ?.substringAfter("system=") - ?.trim() - ?.takeIf { it != "prop" && it.isNotEmpty() } - } catch (_: Exception) { - null - } - if (raw == null) return null - if (PATCH_DATE_PATTERN.matches(raw)) return raw - SystemLogger.warning( - "BulletinPoller: ignoring malformed system='$raw' in $PATCH_FILE" - ) - return null - } - - private fun appendHistory(result: FetchResult) { - try { - val target = File(HISTORY_FILE) - val staging = File(HISTORY_STAGING) - val existing = if (target.exists()) runCatching { target.readText() }.getOrNull() else null - val history = - existing - ?.let { runCatching { JSONObject(it).optJSONArray("history") }.getOrNull() } - ?: JSONArray() - val entry = - JSONObject().apply { - put("ts", result.ts) - put("status", result.status) - put("http_code", result.httpCode ?: JSONObject.NULL) - put("parsed_date", result.parsedDate ?: JSONObject.NULL) - put("applied", result.applied) - put("error", result.error ?: JSONObject.NULL) - } - history.put(entry) - while (history.length() > HISTORY_CAP) history.remove(0) - - val latestKnown = - (0 until history.length()) - .mapNotNull { - history.optJSONObject(it)?.optString("parsed_date", "")?.takeIf { d -> - d.isNotBlank() - } - } - .lastOrNull() - - val root = - JSONObject().apply { - put("latest_known_date", latestKnown ?: JSONObject.NULL) - put("history", history) - } - staging.writeText(root.toString(2)) - Files.move( - staging.toPath(), - target.toPath(), - StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING, - ) - } catch (e: Exception) { - SystemLogger.error("BulletinPoller: failed to persist history", e) - } - } -} diff --git a/app/src/main/java/org/matrix/TEESimulator/config/PatchLevelManager.kt b/app/src/main/java/org/matrix/TEESimulator/config/PatchLevelManager.kt deleted file mode 100644 index 50dc679..0000000 --- a/app/src/main/java/org/matrix/TEESimulator/config/PatchLevelManager.kt +++ /dev/null @@ -1,192 +0,0 @@ -package org.matrix.TEESimulator.config - -import android.os.Build -import android.os.FileObserver -import android.os.SystemProperties -import java.io.File -import java.nio.file.Files -import java.nio.file.StandardCopyOption -import java.time.LocalDate -import org.json.JSONObject -import org.matrix.TEESimulator.logging.SystemLogger -import org.matrix.TEESimulator.util.AndroidDeviceUtils - -object PatchLevelManager { - private const val PATCH_FILE = "/data/adb/tricky_store/security_patch.txt" - private const val STAGING_FILE = "/data/adb/tricky_store/security_patch.txt.next" - private const val PIF_DIR = "/data/adb/modules/playintegrityfix" - private const val FLOOR_YYYYMMDD = 20200101 - private const val MAX_PAST_OFFSET = 10000 - - /** - * Pixel security bulletins publish monthly; pre-announced dates occasionally - * slip by 2-4 weeks. 60 days covers that window without admitting a - * far-future date from a hostile or mis-parsed bulletin response. - */ - private const val MAX_FUTURE_DAYS = 60L - - private val PIF_FILENAMES = - setOf("pif.json", "pif.prop", "custom.pif.json", "custom.pif.prop") - - private val DATE_PATTERN = Regex("^\\d{4}-\\d{2}-\\d{2}$") - private val PROP_PATTERN = Regex("^SECURITY_PATCH=(.+)$", RegexOption.MULTILINE) - private val SECTION_HEADER = Regex("^\\[[a-zA-Z0-9_.-]+]$") - private val GLOBAL_KEYS = setOf("system", "boot", "vendor", "all") - - private val PIF_SOURCES = - listOf( - "/data/adb/modules/playintegrityfix/pif.json", - "/data/adb/pif.json", - "/data/adb/modules/playintegrityfix/pif.prop", - "/data/adb/pif.prop", - "/data/adb/modules/playintegrityfix/custom.pif.json", - "/data/adb/modules/playintegrityfix/custom.pif.prop", - ) - - fun initialize() { - refreshFromSources() - startPifObserver() - } - - private fun refreshFromSources() { - val date = - resolvePifPatch() - ?: SystemProperties.get( - "ro.build.version.security_patch", - Build.VERSION.SECURITY_PATCH, - ) - SystemLogger.info("PatchLevelManager: resolved patch date = $date") - applyToProps(date) - } - - private fun startPifObserver() { - if (!File(PIF_DIR).exists()) { - SystemLogger.debug("PatchLevelManager: PIF dir absent, hot-reload disabled") - return - } - PifObserver.startWatching() - } - - @Synchronized - private fun applyToProps(date: String) { - if (!DATE_PATTERN.matches(date)) { - SystemLogger.warning( - "PatchLevelManager: skip resetprop for invalid date: $date" - ) - return - } - AndroidDeviceUtils.setProperty("ro.build.version.security_patch", date) - AndroidDeviceUtils.setProperty("ro.vendor.build.security_patch", date) - } - - fun updateTo(date: String): Boolean { - if (!DATE_PATTERN.matches(date)) { - SystemLogger.warning("PatchLevelManager: invalid date format: $date") - return false - } - val dateInt = date.replace("-", "").toInt() - if (dateInt < FLOOR_YYYYMMDD) { - SystemLogger.warning("PatchLevelManager: $date below floor $FLOOR_YYYYMMDD") - return false - } - val now = LocalDate.now() - val today = now.year * 10000 + now.monthValue * 100 + now.dayOfMonth - if (today >= dateInt + MAX_PAST_OFFSET) { - SystemLogger.warning( - "PatchLevelManager: $date more than 1y older than today ($today)" - ) - return false - } - val maxFuture = - now.plusDays(MAX_FUTURE_DAYS).let { - it.year * 10000 + it.monthValue * 100 + it.dayOfMonth - } - if (dateInt > maxFuture) { - SystemLogger.warning( - "PatchLevelManager: $date more than $MAX_FUTURE_DAYS days in future ($maxFuture)" - ) - return false - } - try { - atomicWrite(date) - } catch (e: Exception) { - SystemLogger.error("PatchLevelManager: atomicWrite failed for $date", e) - return false - } - applyToProps(date) - SystemLogger.info("PatchLevelManager: applied patch date $date") - return true - } - - private fun resolvePifPatch(): String? { - val source = - PIF_SOURCES.map(::File).lastOrNull { it.exists() && it.length() > 0 } - ?: return null - return try { - val text = source.readText() - val parsed = - if (source.name.endsWith(".json")) { - JSONObject(text).optString("SECURITY_PATCH", "") - } else { - PROP_PATTERN.find(text)?.groupValues?.get(1)?.trim().orEmpty() - } - parsed.takeIf { it.isNotBlank() } - } catch (e: Exception) { - SystemLogger.warning( - "PatchLevelManager: failed to parse ${source.path}: ${e.message}" - ) - null - } - } - - private fun atomicWrite(date: String) { - val target = File(PATCH_FILE) - val staging = File(STAGING_FILE) - staging.writeText(mergedContents(target, date)) - Files.move( - staging.toPath(), - target.toPath(), - StandardCopyOption.ATOMIC_MOVE, - StandardCopyOption.REPLACE_EXISTING, - ) - } - - private fun mergedContents(target: File, date: String): String { - val globalBlock = "system=$date\nboot=$date\nvendor=$date\n" - if (!target.exists()) return globalBlock - val tail = stripGlobalAssignments(target.readLines()) - if (tail.isEmpty()) return globalBlock - return globalBlock + tail.joinToString("\n", prefix = "\n", postfix = "\n") - } - - private fun stripGlobalAssignments(lines: List): List { - val kept = mutableListOf() - var inGlobal = true - for (line in lines) { - val trimmed = line.trim() - if (SECTION_HEADER.matches(trimmed)) { - inGlobal = false - kept += line - continue - } - if (inGlobal && isGlobalKeyAssignment(trimmed)) continue - kept += line - } - return kept - } - - private fun isGlobalKeyAssignment(trimmed: String): Boolean { - if (trimmed.isEmpty() || trimmed.startsWith("#") || '=' !in trimmed) return false - val key = trimmed.substringBefore('=').trim().lowercase() - return key in GLOBAL_KEYS - } - - private object PifObserver : - FileObserver(File(PIF_DIR), CLOSE_WRITE or MOVED_TO or DELETE) { - override fun onEvent(event: Int, path: String?) { - if (path == null || path !in PIF_FILENAMES) return - SystemLogger.info("PatchLevelManager: PIF change ($path), refreshing") - refreshFromSources() - } - } -} 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 8563b8f..6d975b5 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 @@ -127,7 +127,7 @@ object InterceptorUtils { val savedPos = parcel.dataPosition() val wire = parcel.marshall() parcel.setDataPosition(savedPos) - val path = "/data/local/tmp/teesim-$diagnosticTag-${System.nanoTime()}.bin" + val path = "/data/local/tmp/teesim-$diagnosticTag.bin" runCatching { java.io.File(path).writeBytes(wire) } SystemLogger.debug("[$diagnosticTag] reply len=${wire.size} path=$path") }