Passphrase Generator — Kotlin source
Generate memorable, high-entropy passphrases from the EFF diceware wordlist using cryptographic randomness. Each word is drawn with crypto.getRandomValues and rejection sampling (no modulo bias), and every passphrase shows its exact entropy in bits plus the estimated crack time at 1 trillion guesses/sec. Everything runs locally - nothing is sent anywhere.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Passphrase generation + entropy scoring.
//
// Language: Kotlin 1.9+ (JVM), standard library only.
// Ported from src/lib/passphrase-generator.ts — display source, part of
// CosmoDev's polyglot tool pages. Functionally equivalent to the TS
// reference: same inputs -> same derived entropy and crack-time strings
// (word selection is random in both, drawn from a CSPRNG).
//
// NOTE: the 7,776-word EFF long wordlist must be loaded separately (e.g.
// from a resource file) and passed into generatePassphrase as a List<String>.
// It is deliberately not inlined — it would dwarf the logic ~40:1. Word
// indexes use SecureRandom with rejection sampling, so every index is
// uniform over the wordlist — no modulo bias.
import java.security.SecureRandom
import java.util.Locale
import kotlin.math.log2
import kotlin.math.pow
import kotlin.math.roundToLong
/** Separator inserted between the words of the passphrase. */
enum class Separator(val text: String) {
SPACE(" "),
DASH("-"),
DOT("."),
UNDERSCORE("_"),
NONE(""),
}
/** Everything the generator needs; every field has a sensible default. */
data class PassphraseOptions(
val wordCount: Int? = null,
val separator: Separator = Separator.SPACE,
val capitalize: Boolean = false,
val appendDigit: Boolean = false,
/** Attack speed for the crack-time estimate. Default 1 trillion guesses/s. */
val guessesPerSecond: Double = DEFAULT_GUESSES_PER_SECOND,
)
/** Average time to crack, machine- and human-readable. */
data class CrackTimeEstimate(val seconds: Double, val human: String)
/** One generated passphrase plus its entropy analysis. */
data class Passphrase(
val words: List<String>,
val passphrase: String,
val entropy: Double,
val crackTime: CrackTimeEstimate,
)
/** Size of the EFF long wordlist (6^5). */
const val EFF_WORDLIST_LENGTH = 7776
const val MIN_WORDS = 3
const val MAX_WORDS = 10
const val DEFAULT_WORDS = 6
const val DEFAULT_GUESSES_PER_SECOND = 1e12
private val RNG = SecureRandom()
/**
* Draw one uniformly-random index in [0, size). Rejection-sampled over a
* uint32: draws at or above the largest multiple of `size` that fits in
* 2^32 are rejected, so `v % size` is uniform (a plain `% size` would
* favor the first 2^32 % size indexes).
*/
private fun randomIndex(size: Int): Int {
val limit = (1L shl 32) / size * size
while (true) {
val v = RNG.nextInt().toLong() and 0xffffffffL
if (v < limit) return (v % size).toInt()
}
}
/** Draw one uniformly-random word from the wordlist (CSPRNG). */
private fun randomWord(wordlist: List<String>): String = wordlist[randomIndex(wordlist.size)]
/** Draw one uniformly-random digit 0-9 (CSPRNG, rejection-sampled on a byte). */
private fun randomDigit(): Char {
// 250 is the largest multiple of 10 in [0, 256).
while (true) {
val v = RNG.nextInt(256)
if (v < 250) return '0' + (v % 10)
}
}
/** Theoretical word entropy in bits: wordCount x log2(7776). */
fun entropyBits(wordCount: Int): Double =
if (wordCount <= 0) 0.0 else wordCount * log2(EFF_WORDLIST_LENGTH.toDouble())
private const val YEAR = 31_557_600.0 // seconds (Julian year)
private val SCALES = listOf(
1e12 to "trillion",
1e9 to "billion",
1e6 to "million",
1e3 to "thousand",
)
/**
* Human-readable crack-time span. Uses centuries/millennia past a year,
* then collapses to scaled words ("2.4 trillion centuries") so the string
* stays readable at diceware entropies (60-130 bits).
*/
fun formatCrackTime(seconds: Double): String {
if (seconds.isNaN() || seconds == Double.NEGATIVE_INFINITY || seconds < 0) return "-"
if (seconds < 1.0) return "< 1 second"
if (seconds < 60.0) return span(seconds, "second")
if (seconds < 3600.0) return span(seconds / 60, "minute")
if (seconds < 86_400.0) return span(seconds / 3600, "hour")
if (seconds < YEAR) return span(seconds / 86_400, "day")
val years = seconds / YEAR
if (years < 100.0) return span(years, "year")
if (years < 1000.0) return span(years / 100, "century", "centuries")
if (years < 1e6) return span(years / 1000, "millennium", "millennia")
// years >= 1e6 here, so centuries >= 10,000: collapse to a scaled plural.
val centuries = years / 100
val (factor, name) = SCALES.first { centuries >= it.first }
return "${localeNumber(round(centuries / factor))} $name centuries"
}
/** Round + pluralize `v unit`; 1 decimal below 10, whole numbers above. */
private fun span(v: Double, singular: String, plural: String = "${singular}s"): String {
val n = round(v)
return "${localeNumber(n)} ${if (n == 1.0) singular else plural}"
}
private fun round(v: Double): Double =
if (v >= 10.0) v.roundToLong().toDouble() else (v * 10).roundToLong() / 10.0
/** Grouping thousands, at most one decimal — the shape of JS toLocaleString(). */
private fun localeNumber(n: Double): String =
if (n == n.toLong().toDouble()) String.format(Locale.US, "%,d", n.toLong())
else String.format(Locale.US, "%,.1f", n)
/** Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond. */
fun crackTimeSeconds(bits: Double, guessesPerSecond: Double): Double =
2.0.pow(bits - 1) / guessesPerSecond
/** Generate a diceware passphrase from the given EFF long wordlist. */
fun generatePassphrase(wordlist: List<String>, options: PassphraseOptions = PassphraseOptions()): Passphrase {
require(wordlist.size == EFF_WORDLIST_LENGTH) {
"Expected the $EFF_WORDLIST_LENGTH-word EFF long wordlist, got ${wordlist.size}"
}
val wordCount = (options.wordCount ?: DEFAULT_WORDS).coerceIn(MIN_WORDS, MAX_WORDS)
val gps = options.guessesPerSecond
val words = List(wordCount) { randomWord(wordlist) }
val shown = if (options.capitalize) words.map { it.replaceFirstChar(Char::uppercase) } else words
var passphrase = shown.joinToString(options.separator.text)
if (options.appendDigit) passphrase += randomDigit()
val entropy = entropyBits(wordCount)
val seconds = crackTimeSeconds(entropy, gps)
return Passphrase(words, passphrase, entropy, CrackTimeEstimate(seconds, formatCrackTime(seconds)))
}
Also available in 12 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →