package com.peachpasswords.security import javax.xml.parsers.DocumentBuilderFactory import org.junit.Assert.assertTrue import org.junit.Test import org.w3c.dom.Element class SecureFlagCoverageTest { // Compose screens that surface secret material. They live in the MainActivity // window, where FLAG_SECURE is already set release-wide, but they must also // enforce it during the lifetime of the screen in debug builds — which is // what SecureFlagEffect() does. (H-05.) private val secretScreens = listOf( "app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt", "app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt", "app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt", "app/src/main/java/com/peachpasswords/ui/vault/ExportScreen.kt", "app/src/main/java/com/peachpasswords/ui/vault/RecoveryWordsScreen.kt", "app/src/main/java/com/peachpasswords/ui/recovery/RecoveryWordEntryScreen.kt", "app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt", ) // Activities whose own window is separate from MainActivity (overlay / system // API surfaces). They do NOT inherit FLAG_SECURE from MainActivity, so each // one must set it directly in its own onCreate. (H-05.) private val secretActivities = listOf( "com.peachpasswords.autofill.AutofillEntryPickerActivity", "com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity", ) @Test fun secret_screens_keep_flag_secure_coverage() { secretScreens.forEach { relativePath -> val file = java.io.File(relativePath).takeIf { it.exists() } ?: java.io.File("../$relativePath") val source = file.readText() assertTrue( "$relativePath must protect screenshots while visible", source.contains("SecureFlagEffect()") || source.contains("WindowManager.LayoutParams.FLAG_SECURE"), ) } } @Test fun secret_bearing_activities_set_flag_secure() { // Every secret-bearing activity listed in the manifest must set // FLAG_SECURE itself — MainActivity's release-wide set does not reach // these windows. (H-05.) val manifestActivities = parseActivityNames( "app/src/main/AndroidManifest.xml", ) secretActivities.forEach { fqcn -> assertTrue( "$fqcn must be declared in the manifest", manifestActivities.contains(fqcn), ) val relativePath = fqcnToSourcePath(fqcn) val file = java.io.File(relativePath).takeIf { it.exists() } ?: java.io.File("../$relativePath") val source = file.readText() assertTrue( "$fqcn must set WindowManager.LayoutParams.FLAG_SECURE " + "(separate window from MainActivity)", source.contains("WindowManager.LayoutParams.FLAG_SECURE"), ) } } private fun parseActivityNames(relativePath: String): Set { val file = java.io.File(relativePath).takeIf { it.exists() } ?: java.io.File("../$relativePath") val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder() .parse(file) val activities = doc.getElementsByTagName("activity") return (0 until activities.length) .mapNotNull { (activities.item(it) as? Element)?.getAttribute("android:name") } .toSet() } // Activities in the manifest are declared as ".Foo" (relative to the // application package). The activity source lives at the matching package // path under app/src/main/java. private fun fqcnToSourcePath(fqcn: String): String { val pkg = fqcn.substringBeforeLast('.') val cls = fqcn.substringAfterLast('.') return "app/src/main/java/" + pkg.replace('.', '/') + "/$cls.kt" } }