Skip to content

Passphrase Generator — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.

// passphrase-generator — passphrase generation + entropy scoring.
//
// Language: Swift 5.9+ (Foundation only)
// Ported from src/lib/passphrase-generator.ts
// display source — part of CosmoDev's polyglot tool pages
//
// NOTE: the 7,776-word EFF long wordlist (https://www.eff.org/sedol) must be
// loaded separately — e.g. from eff_wordlist.txt on disk / in a bundle — and
// passed into generatePassphrase as a [String]. It is deliberately not
// inlined (it would dwarf the logic ~40:1). Word selection uses a CSPRNG
// (SystemRandomNumberGenerator) with rejection sampling, so every word index
// is uniform over the 7,776-word list — no modulo bias.

import Foundation

// MARK: - Types

enum Separator: String {
    case space
    case dash
    case dot
    case underscore
    case none

    var string: String {
        switch self {
        case .space: return " "
        case .dash: return "-"
        case .dot: return "."
        case .underscore: return "_"
        case .none: return ""
        }
    }
}

struct PassphraseOptions {
    var wordCount: Int? = nil
    var separator: Separator = .space
    var capitalize: Bool = false
    var appendDigit: Bool = false
    /// Attack speed for the crack-time estimate. Default 1 trillion guesses/s.
    var guessesPerSecond: Double = defaultGuessesPerSecond
}

struct CrackTimeEstimate {
    let seconds: Double
    let human: String
}

struct Passphrase {
    let words: [String]
    let passphrase: String
    let entropy: Double
    let crackTime: CrackTimeEstimate
}

// MARK: - Constants

/// Size of the EFF long wordlist (6^5).
let effWordlistLength = 7776

let minWords = 3
let maxWords = 10
let defaultWords = 6
let defaultGuessesPerSecond = 1e12

// Rejection-sampling ceiling for a uniform uint16 over 7,776: the largest
// multiple of 7,776 that fits in [0, 65536). 7776 x 8 = 62208; draws >= 62208
// are rejected so `v % 7776` is uniform (a plain `% 7776` would favor the
// first 65536 % 7776 = 2048 indexes). Computed from the actual list size so
// any injected wordlist stays uniform too.
private func wordLimit(for count: Int) -> Int {
    (65536 / count) * count
}

// MARK: - Random draws (CSPRNG + rejection sampling)

/// Draw one uniformly-random word from the EFF long wordlist.
private func randomWord(_ wordlist: [String]) -> String {
    let n = wordlist.count
    guard n > 0 else { return "" }
    let limit = wordLimit(for: n)
    var rng = SystemRandomNumberGenerator()
    var v: Int
    repeat {
        v = Int(UInt16.random(in: .min ... .max, using: &rng))
    } while v >= limit
    return wordlist[v % n]
}

/// Draw one uniformly-random digit 0-9 (rejection-sampled on uint8).
private func randomDigit() -> String {
    var rng = SystemRandomNumberGenerator()
    var v: Int
    repeat {
        v = Int(UInt8.random(in: .min ... .max, using: &rng))
    } while v >= 250 // largest multiple of 10 in [0, 256)
    return String(v % 10)
}

// MARK: - Entropy + crack time

/// Theoretical word entropy in bits: wordCount x log2(7776).
func entropyBits(_ wordCount: Int) -> Double {
    guard wordCount > 0 else { return 0 }
    return Double(wordCount) * log2(Double(effWordlistLength))
}

let year = 31_557_600.0 // seconds (Julian year)
let scales: [(Double, String)] = [
    (1e12, "trillion"),
    (1e9, "billion"),
    (1e6, "million"),
    (1e3, "thousand"),
]

/**
 * Human-readable crack-time span. Uses centuries/millennia past a year,
 * then collapses to scaled words ("2.4 billion centuries") so the string
 * stays readable at diceware entropies (60-130 bits).
 */
func formatCrackTime(_ seconds: Double) -> String {
    if seconds.isNaN || seconds.isInfinite || seconds < 0 { return "-" }
    if seconds < 1 { return "< 1 second" }
    if seconds < 60 { return span(seconds, "second") }
    if seconds < 3600 { return span(seconds / 60, "minute") }
    if seconds < 86_400 { return span(seconds / 3600, "hour") }
    if seconds < year { return span(seconds / 86_400, "day") }
    let years = seconds / year
    if years < 100 { return span(years, "year") }
    if years < 1000 { return span(years / 100, "century", plural: "centuries") }
    if years < 1e6 { return span(years / 1000, "millennium", plural: "millennia") }
    // years >= 1e6 here, so centuries >= 10,000: collapse to a scaled plural.
    let centuries = years / 100
    let (factor, name) = scales.first { centuries >= $0.0 }!
    return "\(round(centuries / factor).formatted()) \(name) centuries"
}

/// Round + pluralize `v unit`; 1 decimal below 10, whole numbers above.
private func span(_ v: Double, _ singular: String, plural: String? = nil) -> String {
    let n = round(v)
    let unit = n == 1 ? singular : (plural ?? singular + "s")
    return "\(n.formatted()) \(unit)"
}

private func round(_ v: Double) -> Double {
    v >= 10 ? (v).rounded() : (v * 10).rounded() / 10
}

/// Average time to crack (seconds) = 2^(bits-1) / guessesPerSecond.
func crackTimeSeconds(_ bits: Double, _ guessesPerSecond: Double) -> Double {
    return pow(2, bits - 1) / guessesPerSecond
}

// MARK: - Generation

/// Generate a diceware passphrase from the EFF long wordlist.
func generatePassphrase(_ wordlist: [String], options: PassphraseOptions = PassphraseOptions()) -> Passphrase {
    let raw = options.wordCount ?? defaultWords
    let wordCount = min(maxWords, max(minWords, raw))
    let separator = options.separator
    let capitalize = options.capitalize
    let appendDigit = options.appendDigit
    let gps = options.guessesPerSecond

    let words = (0..<wordCount).map { _ in randomWord(wordlist) }
    let shown = capitalize ? words.map(cap) : words
    var passphrase = shown.joined(separator: separator.string)
    if appendDigit { passphrase += randomDigit() }

    let entropy = entropyBits(wordCount)
    let seconds = crackTimeSeconds(entropy, gps)
    return Passphrase(words: words, passphrase: passphrase, entropy: entropy,
                      crackTime: CrackTimeEstimate(seconds: seconds, human: formatCrackTime(seconds)))
}

private func cap(_ w: String) -> String {
    w.prefix(1).uppercased() + w.dropFirst()
}

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 →