PasswordCredentialPlanner.kt

2.5 KB · raw

1package com.peachpasswords.credentials23import com.peachpasswords.shared.extractDomain4import com.peachpasswords.vault.VaultEntry56data class PasswordCredentialCandidate(7    val entryId: String,8    val title: String,9    val username: String,10    val lastUsedAt: Long?,11)1213/**14 * Content-based matching for Credential Manager password rows — the same15 * stance as autofill (AutofillResponseBuilder): registrable-domain matching16 * for web callers, app-link-domain / app-title matching for native callers.17 * No caller-trust gate (removed queue #3); the caller package itself is the18 * framework-authenticated identity.19 */20object PasswordCredentialPlanner {2122    fun candidates(23        entries: List<VaultEntry>,24        domains: Set<String>,25        appTitle: String?,26        allowedUserIds: Set<String>? = null,27    ): List<PasswordCredentialCandidate> {28        val eligible = entries.filter { it.fillable() }29        val byDomain = if (domains.isEmpty()) {30            emptyList()31        } else {32            eligible.filter { entry -> domains.any { entry.matchesDomain(it) } }33        }34        val byTitle = if (appTitle.isNullOrBlank()) {35            emptyList()36        } else {37            eligible.filter { it.title.trim().equals(appTitle, ignoreCase = true) }38        }39        return (byDomain + byTitle)40            .distinctBy { it.id }41            .mapNotNull { entry ->42                val username = entry.username ?: entry.email ?: return@mapNotNull null43                if (!allowedUserIds.isNullOrEmpty() && username !in allowedUserIds) {44                    return@mapNotNull null45                }46                PasswordCredentialCandidate(47                    entryId = entry.id,48                    title = entry.title,49                    username = username,50                    lastUsedAt = entry.lastUsedAt,51                )52            }53            .sortedWith(54                compareByDescending<PasswordCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }55                    .thenBy { it.username.lowercase() },56            )57    }5859    /** Entries that can actually back a password credential row. */60    private fun VaultEntry.fillable(): Boolean =61        deletedAt == null && !password.isNullOrBlank()6263    private fun VaultEntry.matchesDomain(domain: String): Boolean =64        sequenceOf(url)65            .plus(additionalUrls.orEmpty().asSequence())66            .filterNotNull()67            .mapNotNull { extractDomain(it) }68            .any { it == domain }69}