SecureFlagCoverageTest.kt

3.9 KB · raw

1package com.peachpasswords.security23import javax.xml.parsers.DocumentBuilderFactory4import org.junit.Assert.assertTrue5import org.junit.Test6import org.w3c.dom.Element78class SecureFlagCoverageTest {910    // Compose screens that surface secret material. They live in the MainActivity11    // window, where FLAG_SECURE is already set release-wide, but they must also12    // enforce it during the lifetime of the screen in debug builds — which is13    // what SecureFlagEffect() does. (H-05.)14    private val secretScreens = listOf(15        "app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt",16        "app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt",17        "app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt",18        "app/src/main/java/com/peachpasswords/ui/vault/ExportScreen.kt",19        "app/src/main/java/com/peachpasswords/ui/vault/RecoveryWordsScreen.kt",20        "app/src/main/java/com/peachpasswords/ui/recovery/RecoveryWordEntryScreen.kt",21        "app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt",22    )2324    // Activities whose own window is separate from MainActivity (overlay / system25    // API surfaces). They do NOT inherit FLAG_SECURE from MainActivity, so each26    // one must set it directly in its own onCreate. (H-05.)27    private val secretActivities = listOf(28        "com.peachpasswords.autofill.AutofillEntryPickerActivity",29        "com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity",30    )3132    @Test fun secret_screens_keep_flag_secure_coverage() {33        secretScreens.forEach { relativePath ->34            val file = java.io.File(relativePath).takeIf { it.exists() }35                ?: java.io.File("../$relativePath")36            val source = file.readText()37            assertTrue(38                "$relativePath must protect screenshots while visible",39                source.contains("SecureFlagEffect()") ||40                    source.contains("WindowManager.LayoutParams.FLAG_SECURE"),41            )42        }43    }4445    @Test fun secret_bearing_activities_set_flag_secure() {46        // Every secret-bearing activity listed in the manifest must set47        // FLAG_SECURE itself — MainActivity's release-wide set does not reach48        // these windows. (H-05.)49        val manifestActivities = parseActivityNames(50            "app/src/main/AndroidManifest.xml",51        )52        secretActivities.forEach { fqcn ->53            assertTrue(54                "$fqcn must be declared in the manifest",55                manifestActivities.contains(fqcn),56            )57            val relativePath = fqcnToSourcePath(fqcn)58            val file = java.io.File(relativePath).takeIf { it.exists() }59                ?: java.io.File("../$relativePath")60            val source = file.readText()61            assertTrue(62                "$fqcn must set WindowManager.LayoutParams.FLAG_SECURE " +63                    "(separate window from MainActivity)",64                source.contains("WindowManager.LayoutParams.FLAG_SECURE"),65            )66        }67    }6869    private fun parseActivityNames(relativePath: String): Set<String> {70        val file = java.io.File(relativePath).takeIf { it.exists() }71            ?: java.io.File("../$relativePath")72        val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()73            .parse(file)74        val activities = doc.getElementsByTagName("activity")75        return (0 until activities.length)76            .mapNotNull { (activities.item(it) as? Element)?.getAttribute("android:name") }77            .toSet()78    }7980    // Activities in the manifest are declared as ".Foo" (relative to the81    // application package). The activity source lives at the matching package82    // path under app/src/main/java.83    private fun fqcnToSourcePath(fqcn: String): String {84        val pkg = fqcn.substringBeforeLast('.')85        val cls = fqcn.substringAfterLast('.')86        return "app/src/main/java/" + pkg.replace('.', '/') + "/$cls.kt"87    }88}