diff --git a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt index dcd1ad5..fbf9586 100644 --- a/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt +++ b/sidecar/src/main/kotlin/dev/sanderling/sidecar/DriverBackend.kt @@ -954,7 +954,7 @@ internal fun treeWithoutKeyboard( // cannot tell a password field from a search box and everything typed anywhere // gets recorded as a credential. The XML the same read produced does carry the // fact, so it is fetched (lazily: a screen with no text field never pays for -// it) and matched back onto the fields by identity and bounds. +// it) and matched back onto the fields by identity, class and bounds. // // A field the XML cannot be matched to is left unstated rather than guessed. // Unstated reads as "may be a credential" downstream, which is the safe way to @@ -976,6 +976,7 @@ internal fun withSecureFacts( for (field in fields) { val key = secureFactKey( nodeAttribute(field, "resource-id"), + nodeAttribute(field, "class"), nodeAttribute(field, "bounds"), ) field.put("secure", facts[key] ?: continue) @@ -1002,6 +1003,7 @@ internal fun secureFactsFromXml(xml: String): Map { val element = nodes.item(index) as? org.w3c.dom.Element ?: continue val key = secureFactKey( element.getAttribute("resource-id"), + element.getAttribute("class"), element.getAttribute("bounds"), ) val password = element.getAttribute("password") == "true" @@ -1011,7 +1013,8 @@ internal fun secureFactsFromXml(xml: String): Map { return facts } -private fun secureFactKey(id: String, bounds: String) = "$id@$bounds" +private fun secureFactKey(id: String, className: String, bounds: String) = + "$id@$className@$bounds" // A text field is what the Go side calls editable off the same two attributes // (internal/hierarchy): stating the fact on a narrower set would leave fields diff --git a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt index 46063d4..f828d53 100644 --- a/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt +++ b/sidecar/src/test/kotlin/dev/sanderling/sidecar/SecureFactsTest.kt @@ -27,22 +27,21 @@ private const val LOGIN_XML = """ """ private fun secureOf(tree: String, resourceId: String): Boolean? { - val field = jacksonTree(tree, resourceId) ?: return null + val field = jacksonTree(tree, "resource-id", resourceId) ?: return null val secure = field.get("secure") ?: return null return secure.asBoolean() } private fun jacksonTree( tree: String, - resourceId: String, + attribute: String, + value: String, ): com.fasterxml.jackson.databind.JsonNode? { val mapper = com.fasterxml.jackson.module.kotlin.jacksonObjectMapper() fun walk( node: com.fasterxml.jackson.databind.JsonNode, ): com.fasterxml.jackson.databind.JsonNode? { - if (node.get("attributes")?.get("resource-id")?.asText() == - resourceId - ) { + if (node.get("attributes")?.get(attribute)?.asText() == value) { return node } node.get("children")?.forEach { child -> @@ -111,6 +110,35 @@ class SecureFactsTest { } } + // A wrapper drawn to the same bounds as the untagged field inside it shares + // the field's empty id and its bounds. The class is what still tells them + // apart; without it the two collide, the field answers for nothing, and + // every value typed into an untagged field is redacted. + @Test fun anUntaggedFieldInsideAWrapperOfItsOwnSizeIsStillStated() { + val wrapped = """ + {"attributes":{"bounds":"[0,0,1080,2340]"},"children":[ + {"attributes":{"class":"android.view.View","bounds":"[51,249][429,321]"},"children":[ + {"attributes":{"class":"android.widget.EditText","bounds":"[51,249][429,321]"},"children":[]} + ]} + ]} + """ + val xml = """ + + + + + + """ + + val field = jacksonTree( + withSecureFacts(wrapped) { xml }, + "class", + "android.widget.EditText", + ) + + assertEquals(true, field?.get("secure")?.asBoolean()) + } + // Two nodes sharing a key answer for neither: taking the first would state // "not secure" about a field that may be the other one. @Test fun anAmbiguousMatchIsLeftUnstated() {