PasswordPolicy.kt

3.6 KB · raw

1package com.peachpasswords.autofill23/**4 * Site password constraints detected from a password field's HTML attributes5 * (exposed by [android.app.assist.AssistStructure.ViewNode.getHtmlInfo]).6 *7 * Android parity for the browser's `content/policy.ts` + `popup/generator-policy.ts`,8 * scoped to what the platform actually surfaces: AssistStructure carries a web9 * field's `minlength` / `maxlength` / `pattern` attributes, but NOT the surrounding10 * page prose the browser also scrapes ("8-20 characters, must contain a number").11 * Used to size a generated password so the site will accept it.12 */13data class PasswordPolicy(14    val minLength: Int? = null,15    val maxLength: Int? = null,16) {17    /**18     * Length to generate at: the strongest password the site accepts. Mirrors19     * `generator-policy.ts` — prefer [maxLength] (capped at [hardMax]); otherwise20     * a strong [default]; then clamp into the site's bounds.21     */22    fun generationLength(default: Int = DEFAULT_LENGTH, hardMax: Int = HARD_MAX): Int {23        val target = (maxLength ?: default).coerceAtMost(hardMax)24        val raised = minLength?.let { target.coerceAtLeast(it) } ?: target25        val capped = maxLength?.let { raised.coerceAtMost(it) } ?: raised26        return capped.coerceAtLeast(MIN_GENERATION_LENGTH)27    }2829    /** True when the site's maximum is below the minimum strong-password length. */30    val isWeaklyConstrained: Boolean31        get() = maxLength != null && maxLength < MIN_GENERATION_LENGTH3233    /** Short human note about the active constraint, or empty when unconstrained. */34    fun constraintNote(length: Int): String = when {35        isWeaklyConstrained -> "This site limits passwords to $maxLength characters (weak)"36        maxLength != null && length >= maxLength -> "This site allows max $maxLength characters"37        minLength != null && length <= minLength -> "This site requires at least $minLength characters"38        else -> ""39    }404142    companion object {43        const val DEFAULT_LENGTH = 2044        const val MIN_GENERATION_LENGTH = 1245        const val HARD_MAX = 2564647        /**48         * Build a policy from a password field's HTML attributes (keys lower-cased).49         * Reads `minlength`/`maxlength` directly, then falls back to `pattern`50         * quantifiers — `.{N,M}` (range), `.{N,}` (min only), `.{N}` (exact) — the51         * same precedence policy.ts uses. Attribute values win over pattern.52         */53        fun fromHtmlAttributes(attrs: Map<String, String>): PasswordPolicy {54            var min = attrs["minlength"]?.toIntOrNull()?.takeIf { it > 0 }55            var max = attrs["maxlength"]?.toIntOrNull()?.takeIf { it > 0 }56            val pattern = attrs["pattern"]57            if (pattern != null && (min == null || max == null)) {58                val range = Regex("\\{(\\d+),(\\d+)\\}").find(pattern)59                val minOnly = Regex("\\{(\\d+),\\}").find(pattern)60                val exact = Regex("\\{(\\d+)\\}").find(pattern)61                when {62                    range != null -> {63                        if (min == null) min = range.groupValues[1].toIntOrNull()64                        if (max == null) max = range.groupValues[2].toIntOrNull()65                    }66                    minOnly != null -> if (min == null) min = minOnly.groupValues[1].toIntOrNull()67                    exact != null -> {68                        val n = exact.groupValues[1].toIntOrNull()69                        if (min == null) min = n70                        if (max == null) max = n71                    }72                }73            }74            return PasswordPolicy(minLength = min, maxLength = max)75        }76    }77}