PasswordCredentialPlanner.kt

2.6 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, verified app-link domains for native callers, and an exact17 * package-name association for entries the user saved from this app. A native18 * app's display label is never a match key (PAI-2026-001).19 * No caller-trust gate (removed queue #3); the caller package itself is the20 * framework-authenticated identity.21 */22object PasswordCredentialPlanner {2324    fun candidates(25        entries: List<VaultEntry>,26        domains: Set<String>,27        packageName: String? = null,28        allowedUserIds: Set<String>? = null,29    ): List<PasswordCredentialCandidate> {30        val eligible = entries.filter { it.fillable() }31        val byDomain = if (domains.isEmpty()) {32            emptyList()33        } else {34            eligible.filter { entry -> domains.any { entry.matchesDomain(it) } }35        }36        val byPackage = if (packageName.isNullOrBlank()) {37            emptyList()38        } else {39            eligible.filter { it.url == packageName || it.additionalUrls.orEmpty().any { url -> url == packageName } }40        }41        return (byDomain + byPackage)42            .distinctBy { it.id }43            .mapNotNull { entry ->44                val username = entry.username ?: entry.email ?: return@mapNotNull null45                if (!allowedUserIds.isNullOrEmpty() && username !in allowedUserIds) {46                    return@mapNotNull null47                }48                PasswordCredentialCandidate(49                    entryId = entry.id,50                    title = entry.title,51                    username = username,52                    lastUsedAt = entry.lastUsedAt,53                )54            }55            .sortedWith(56                compareByDescending<PasswordCredentialCandidate> { it.lastUsedAt ?: Long.MIN_VALUE }57                    .thenBy { it.username.lowercase() },58            )59    }6061    /** Entries that can actually back a password credential row. */62    private fun VaultEntry.fillable(): Boolean =63        deletedAt == null && !password.isNullOrBlank()6465    private fun VaultEntry.matchesDomain(domain: String): Boolean =66        sequenceOf(url)67            .plus(additionalUrls.orEmpty().asSequence())68            .filterNotNull()69            .mapNotNull { extractDomain(it) }70            .any { it == domain }71}