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")
}