PasswordGenerator.kt

2.2 KB · raw

1package com.peachpasswords.vault23import java.security.SecureRandom45/**6 * Cryptographically random password generator. Port of the browser7 * shared/generate.ts. Java's SecureRandom.nextInt(bound) is itself unbiased8 * (rejection sampling internally), so it stands in for the browser's manual9 * modulo-bias rejection over crypto.getRandomValues.10 */11object PasswordGenerator {12    const val MIN_PASSPHRASE_WORDS = 413    const val DEFAULT_PASSPHRASE_WORDS = 514    const val MAX_PASSPHRASE_WORDS = 121516    private const val LOWER = "abcdefghijklmnopqrstuvwxyz"17    private const val UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"18    private const val NUMBERS = "0123456789"19    private const val SYMBOLS = "!@#$%^&*"2021    private val rng = SecureRandom()22    private val passphraseWords = EffShortWordList.words.filterNot { it.contains("-") }2324    data class Options(25        val length: Int = 20,26        val includeLowercase: Boolean = true,27        val includeUppercase: Boolean = true,28        val includeNumbers: Boolean = true,29        val includeSymbols: Boolean = true,30        val forbiddenChars: String = "",31    )3233    fun generate(options: Options = Options()): String {34        val forbidden = options.forbiddenChars.toSet()35        fun filter(pool: String) = if (forbidden.isEmpty()) pool else pool.filterNot { it in forbidden }3637        var chars = buildString {38            if (options.includeLowercase) append(filter(LOWER))39            if (options.includeUppercase) append(filter(UPPER))40            if (options.includeNumbers) append(filter(NUMBERS))41            if (options.includeSymbols) append(filter(SYMBOLS))42        }43        if (chars.isEmpty()) chars = filter(LOWER).ifEmpty { LOWER }4445        val length = options.length.coerceAtLeast(1)46        return buildString(length) {47            repeat(length) { append(chars[rng.nextInt(chars.length)]) }48        }49    }5051    fun generatePassphrase(52        wordCount: Int = DEFAULT_PASSPHRASE_WORDS,53        separator: String = "-",54    ): String {55        val count = wordCount.coerceIn(MIN_PASSPHRASE_WORDS, MAX_PASSPHRASE_WORDS)56        return List(count) { passphraseWords[rng.nextInt(passphraseWords.size)] }57            .joinToString(separator)58    }59}