933dfea7fdd0
feat: Let users fill any entry from an unverified origin via explicit picker choice (v2.8.7)
- time
- 2026-07-29 23:40 UTC · 2 months ago
- machine
- koh2
- parent
- 3b3fd73ab28f
↓ peach-android-933dfea7.tar.gzatlas at this save
files changed3 files
VERSION+1 −1app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt+24 −43app/src/test/java/com/peachpasswords/autofill/AutofillPickerSearchTest.kt+18 −0app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt+24 −43
@@ -201,16 +201,16 @@201201 busy = false202202 }203203 },204- onPick = { entry, confirmedTarget ->204+ onPick = { entry ->205205 val epoch = unlockedEpoch206206 if (epoch == null || !vault.isSessionEpochCurrent(epoch)) {207207 cancel()208208 } else {209- returnDataset(parsed, entry, confirmedTarget)209+ returnDataset(parsed, entry)210210 }211211 },212- onCreateNew = { confirmedTarget ->213- launchNewEntry(parsed, typedUsername, typedEmail, confirmedTarget)212+ onCreateNew = { searchText ->213+ launchNewEntry(parsed, typedUsername, typedEmail, searchText)214214 },215215 )216216 }@@ -269,15 +269,7 @@269269 private fun returnDataset(270270 parsed: ParsedStructure,271271 entry: VaultEntry,272- confirmedTarget: String?,273272 ) {274- if (275- parsed.originTrust == AutofillOriginTrust.UNKNOWN &&276- AutofillResponseBuilder.matchEntries(listOf(entry), confirmedTarget).hits.isEmpty()277- ) {278- cancel()279- return280- }281273 val dataset = AutofillResponseBuilder.datasetFor(this, parsed, entry, inlineSpec = null)282274 if (dataset == null) { cancel(); return }283275 val reply = Intent().putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, dataset)@@ -295,10 +287,10 @@295287 parsed: ParsedStructure,296288 typedUsername: String?,297289 typedEmail: String?,298- confirmedTarget: String?,290+ searchText: String?,299291 ) {300292 val targetOrigin = if (parsed.originTrust == AutofillOriginTrust.UNKNOWN) {301- confirmedTarget?.let(::extractDomain) ?: return293+ searchText?.takeIf { it.isNotBlank() }?.let(::extractDomain) ?: parsed.origin302294 } else {303295 parsed.origin304296 }@@ -403,7 +395,7 @@403395 canBiometric: Boolean,404396 onBiometric: () -> Unit,405397 onUnlock: (String) -> Unit,406- onPick: (VaultEntry, String?) -> Unit,398+ onPick: (VaultEntry) -> Unit,407399 onCreateNew: (String?) -> Unit,408400 ) {409401 val accent = LocalPeachAccent.current@@ -437,7 +429,7 @@437429 .padding(12.dp),438430 ) {439431 Text(440- text = "Peach couldn't verify this app or site. Enter the site domain yourself; only exact domain matches can be filled.",432+ text = "Peach couldn't verify this app or site. Make sure you trust it before filling a login.",441433 color = peachColors.Breach,442434 fontFamily = PeachType.Body,443435 fontSize = 12.sp,@@ -448,48 +440,35 @@448440 if (!unlocked) {449441 UnlockBody(error, busy, canBiometric, onBiometric, onUnlock)450442 } else {451- // For an unverified caller, this field is a domain confirmation boundary,452- // not a vault-wide search box controlled by attacker-provided origin data.443+ // The search field is a vault-wide substring search. For unverified origins,444+ // the user must type to initiate — empty query shows no entries.453445 var rawQuery by remember { mutableStateOf("") }454446 var debouncedQuery by remember { mutableStateOf("") }455447 LaunchedEffect(rawQuery) {456448 delay(SEARCH_DEBOUNCE_MS)457449 debouncedQuery = rawQuery458450 }459- val confirmedTarget = if (untrustedOrigin) {460- debouncedQuery.trim().takeIf { it.isNotBlank() }?.let(::extractDomain)461- } else {462- null463- }464451 SearchField(465452 value = rawQuery,466453 onValueChange = { rawQuery = it },467454 placeholder = when {468- untrustedOrigin -> "enter site domain, e.g. example.com"469455 matchedEntries.isNotEmpty() -> "search for another entry…"470456 else -> "search vault…"471457 },472458 )473459 474460 val visibleEntries = when {475- untrustedOrigin && confirmedTarget == null -> emptyList()476- untrustedOrigin -> AutofillResponseBuilder477- .matchEntries(allEntries, confirmedTarget)478- .hits461+ untrustedOrigin && debouncedQuery.isBlank() -> emptyList()479462 debouncedQuery.isBlank() -> matchedEntries480463 else -> allEntries.filter { matchesPickerSearch(it, debouncedQuery) }481464 }482- val showSearchEmpty = debouncedQuery.isNotBlank() &&483- visibleEntries.isEmpty() &&484- (!untrustedOrigin || confirmedTarget != null)465+ val showSearchEmpty = debouncedQuery.isNotBlank() && visibleEntries.isEmpty()485466 486467 val emptyMessage = when {487468 untrustedOrigin && debouncedQuery.isBlank() ->488- "Type the site domain to choose a saved login."489- untrustedOrigin && confirmedTarget == null ->490- "Enter a full public domain such as example.com."469+ "Search your vault to choose a login."491470 untrustedOrigin && visibleEntries.isEmpty() ->492- "No saved entries match $confirmedTarget."471+ "No entries match \"${debouncedQuery.trim()}\"."493472 matchedEntries.isEmpty() && debouncedQuery.isBlank() ->494473 "No saved entries match this site."495474 showSearchEmpty ->@@ -509,19 +488,21 @@509488 if (visibleEntries.isNotEmpty()) {510489 LazyColumn {511490 items(visibleEntries, key = { it.id }) { entry ->512- PickerRow(entry, onClick = { onPick(entry, confirmedTarget) })491+ PickerRow(entry, onClick = { onPick(entry) })513492 }514493 }515494 }516495 517- val createOrigin = if (untrustedOrigin) confirmedTarget else origin518- if (!untrustedOrigin || createOrigin != null) {519- CreateNewRow(520- origin = createOrigin,521- typedIdentity = typedIdentity,522- onClick = { onCreateNew(confirmedTarget) },523- )496+ val createOrigin = if (untrustedOrigin) {497+ debouncedQuery.trim().takeIf { it.isNotBlank() }?.let(::extractDomain) ?: origin498+ } else {499+ origin524500 }501+ CreateNewRow(502+ origin = createOrigin,503+ typedIdentity = typedIdentity,504+ onClick = { onCreateNew(debouncedQuery.trim().takeIf { it.isNotBlank() }) },505+ )525506 }526507 }527508 }app/src/test/java/com/peachpasswords/autofill/AutofillPickerSearchTest.kt+18 −0
@@ -109,4 +109,22 @@109109 )110110 assertTrue(autofillPickerEntries(listOf(entry), parsed, limit = 12).isEmpty())111111 }112+113+ @Test fun unverified_origin_user_can_locate_any_entry_via_substring_search() {114+ // For unverified origins, the picker now uses the same substring search115+ // as trusted origins. An unverified-origin user can locate any entry by116+ // typing a substring of title/username/email/url.117+ val entries = listOf(118+ entry("GitHub", username = "june", url = "https://github.com"),119+ entry("Fastmail", email = "[email protected]"),120+ entry("Peach", url = "https://peachpasswords.com"),121+ )122+ assertEquals(listOf("GitHub"), entries.filter { matchesPickerSearch(it, "git") }.map { it.title })123+ assertEquals(listOf("Fastmail"), entries.filter { matchesPickerSearch(it, "fastmail") }.map { it.title })124+ assertEquals(listOf("Peach"), entries.filter { matchesPickerSearch(it, "peach") }.map { it.title })125+ assertEquals(126+ listOf("GitHub", "Fastmail"),127+ entries.filter { matchesPickerSearch(it, "june") }.map { it.title },128+ )129+ }112130 }