package com.peachpasswords.vault import java.security.SecureRandom /** * Cryptographically random password generator. Port of the browser * shared/generate.ts. Java's SecureRandom.nextInt(bound) is itself unbiased * (rejection sampling internally), so it stands in for the browser's manual * modulo-bias rejection over crypto.getRandomValues. */ object PasswordGenerator { const val MIN_PASSPHRASE_WORDS = 4 const val DEFAULT_PASSPHRASE_WORDS = 5 const val MAX_PASSPHRASE_WORDS = 12 private const val LOWER = "abcdefghijklmnopqrstuvwxyz" private const val UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" private const val NUMBERS = "0123456789" private const val SYMBOLS = "!@#$%^&*" private val rng = SecureRandom() private val passphraseWords = EffShortWordList.words.filterNot { it.contains("-") } data class Options( val length: Int = 20, val includeLowercase: Boolean = true, val includeUppercase: Boolean = true, val includeNumbers: Boolean = true, val includeSymbols: Boolean = true, val forbiddenChars: String = "", ) fun generate(options: Options = Options()): String { val forbidden = options.forbiddenChars.toSet() fun filter(pool: String) = if (forbidden.isEmpty()) pool else pool.filterNot { it in forbidden } var chars = buildString { if (options.includeLowercase) append(filter(LOWER)) if (options.includeUppercase) append(filter(UPPER)) if (options.includeNumbers) append(filter(NUMBERS)) if (options.includeSymbols) append(filter(SYMBOLS)) } if (chars.isEmpty()) chars = filter(LOWER).ifEmpty { LOWER } val length = options.length.coerceAtLeast(1) return buildString(length) { repeat(length) { append(chars[rng.nextInt(chars.length)]) } } } fun generatePassphrase( wordCount: Int = DEFAULT_PASSPHRASE_WORDS, separator: String = "-", ): String { val count = wordCount.coerceIn(MIN_PASSPHRASE_WORDS, MAX_PASSPHRASE_WORDS) return List(count) { passphraseWords[rng.nextInt(passphraseWords.size)] } .joinToString(separator) } }