From d41bd1f27d5f280c31295e4953b63a883ae79796 Mon Sep 17 00:00:00 2001 From: ScreenTinker Date: Mon, 8 Jun 2026 19:06:23 -0500 Subject: [PATCH] fix(android): verify OTA APK signature before install + disable backup (Critical) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The updater fetched download_url from the server JSON and installed it via PackageInstaller with NO verification, over cleartext (usesCleartextTraffic, no pinning). A network MITM or compromised server could return a malicious APK and have it silently installed (REQUEST_INSTALL_PACKAGES) → full device RCE. Fix: before install, verify the downloaded APK (a) is our own package and (b) shares a current signing certificate with the installed app (GET_SIGNING_CERTIFICATES on P+, GET_SIGNATURES below). An attacker can't forge our signing key, so this holds even over an untrusted/cleartext transport. Fail-closed on any parse/verify error; the APK is deleted on mismatch. Gates both the session-install and intent-fallback paths. Also set android:allowBackup="false" so adb backup can't exfiltrate the device token / config. Compile-checked + signed debug APK builds. NOT verified on-device - needs a real update cycle on a device (valid update installs; a wrong-signed APK is rejected) before merge. Co-Authored-By: Claude Opus 4.8 (1M context) --- android/app/src/main/AndroidManifest.xml | 2 +- .../player/service/UpdateChecker.kt | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5a4cc48..16cf27c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ = Build.VERSION_CODES.P) + PackageManager.GET_SIGNING_CERTIFICATES else @Suppress("DEPRECATION") PackageManager.GET_SIGNATURES + val downloaded = pm.getPackageArchiveInfo(apkFile.absolutePath, flags) + if (downloaded == null) { + Log.e(TAG, "Could not parse downloaded APK") + return false + } + if (downloaded.packageName != context.packageName) { + Log.e(TAG, "APK package mismatch: ${downloaded.packageName} != ${context.packageName}") + return false + } + val installed = pm.getPackageInfo(context.packageName, flags) + val downloadedSigs = signingCertHashes(downloaded) + val installedSigs = signingCertHashes(installed) + if (downloadedSigs.isEmpty() || installedSigs.isEmpty()) { + Log.e(TAG, "Missing signing certificates (downloaded=${downloadedSigs.size}, installed=${installedSigs.size})") + return false + } + // Share at least one current signing certificate. + val match = downloadedSigs.any { it in installedSigs } + if (!match) Log.e(TAG, "APK signing certificate does not match installed app") + match + } catch (e: Exception) { + Log.e(TAG, "Signature verification error: ${e.message}", e) + false + } + } + + private fun signingCertHashes(info: PackageInfo): Set { + val sigs: Array? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + info.signingInfo?.apkContentsSigners + } else { + @Suppress("DEPRECATION") info.signatures + } + return sigs?.mapNotNull { sha256(it.toByteArray()) }?.toSet() ?: emptySet() + } + + private fun sha256(bytes: ByteArray): String? { + return try { + MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + null + } + } + private fun getAppVersion(): String { return try { context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "1.0.0"