Skip to content

Password Generator — Go source

Generate cryptographically-random passwords with a CSPRNG using rejection sampling (no modulo bias). Shows live entropy in bits, a 5-tier strength meter, average offline-GPU crack time, and a Pro mode with the entropy formula, a crack-time-vs-length curve, and a 4-scenario attack table. Everything runs locally - nothing is sent anywhere.

This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Package passwordgenerator is the Go twin of CosmoDev's src/lib/password.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). The charset and entropy logic is pure + deterministic and never
// panics; invalid/empty configurations return zero values exactly as the TS
// functions do.
//
// Password generation uses crypto/rand (a CSPRNG) with rejection sampling — a
// 1:1 mirror of the TS lib's unbiasedIndex + csprngDraw pair. Each character
// index is drawn by taking a uint32 from crypto/rand and rejecting any value
// >= the largest multiple of n ≤ 2^32, which removes the modulo bias of a plain
// draw%n. The table-driven tests in password-generator_test.go share vectors
// with src/lib/password.test.ts so the two implementations are held to one
// contract.
package passwordgenerator

import (
	"crypto/rand"
	"encoding/binary"
	"math"
	"strconv"
	"strings"
)

// Options configures BuildCharset and GeneratePassword.
// Fields are used verbatim — the zero value Options{} selects no character set
// (empty charset, empty password), matching the TS functions which apply no
// implicit defaults. Use DefaultOptions for the standard length-16, all-sets-on
// configuration.
type Options struct {
	Length           int
	Upper            bool
	Lower            bool
	Numbers          bool
	Symbols          bool
	ExcludeAmbiguous bool
}

// DefaultOptions returns the standard password configuration used by the
// CosmoDev UI and the TS test helper on(): length 16 with every character set
// enabled and ambiguous characters kept. It is the Go analogue of the TS
// defaults — Options{} itself remains the all-zero (empty) config.
func DefaultOptions() Options {
	return Options{
		Length:           16,
		Upper:            true,
		Lower:            true,
		Numbers:          true,
		Symbols:          true,
		ExcludeAmbiguous: false,
	}
}

// StrengthVariant is the UI tint for a Tier assessment, mirroring the TS
// 'danger' | 'accent' | 'success' union.
type StrengthVariant string

const (
	VariantDanger  StrengthVariant = "danger"
	VariantAccent  StrengthVariant = "accent"
	VariantSuccess StrengthVariant = "success"
)

// Candidate character sets, identical to SETS in src/lib/password.ts.
var (
	setLower   = "abcdefghijklmnopqrstuvwxyz"
	setUpper   = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	setNumbers = "0123456789"
	setSymbols = "!@#$%^&*()-_=+[]{};:,.<>?/"
)

// ambiguous is the set of visually-confusable characters removed when
// ExcludeAmbiguous is set — mirrors the AMBIGUOUS regex /[O0Il1|]/g in the TS
// lib. '|' is listed for fidelity though no candidate set contains it.
var ambiguous = map[byte]bool{
	'O': true,
	'0': true,
	'I': true,
	'l': true,
	'1': true,
	'|': true,
}

// stripAmbiguous removes every ambiguous byte from cs. cs is always pure ASCII
// (the candidate sets are ASCII), so byte-wise iteration is exact.
func stripAmbiguous(cs string) string {
	var b strings.Builder
	b.Grow(len(cs))
	for i := 0; i < len(cs); i++ {
		if !ambiguous[cs[i]] {
			b.WriteByte(cs[i])
		}
	}
	return b.String()
}

// BuildCharset builds the candidate charset from the selected option flags. It
// is the Go twin of buildCharset() in src/lib/password.ts and must agree with
// it on every shared vector. Set order is lower, upper, numbers, symbols —
// matching the TS concatenation order.
func BuildCharset(o Options) string {
	var b strings.Builder
	if o.Lower {
		b.WriteString(setLower)
	}
	if o.Upper {
		b.WriteString(setUpper)
	}
	if o.Numbers {
		b.WriteString(setNumbers)
	}
	if o.Symbols {
		b.WriteString(setSymbols)
	}
	cs := b.String()
	if o.ExcludeAmbiguous {
		cs = stripAmbiguous(cs)
	}
	return cs
}

// GeneratePassword returns a cryptographically-random, unbiased password of
// Length characters drawn uniformly from the candidate charset via rejection
// sampling over crypto/rand. It mirrors generatePassword + unbiasedIndex in the
// TS lib. It returns "" when the charset is empty or Length < 1.
func GeneratePassword(o Options) string {
	cs := BuildCharset(o)
	if cs == "" || o.Length < 1 {
		return ""
	}
	n := uint64(len(cs))
	out := make([]byte, o.Length)
	for i := 0; i < o.Length; i++ {
		out[i] = cs[unbiasedIndex(n)]
	}
	return string(out)
}

// unbiasedIndex returns a uniform index in [0, n) using crypto/rand with
// rejection sampling. It draws a uint32 and rejects any value >= the largest
// multiple of n ≤ 2^32, eliminating the modulo bias of draw%n. Mirrors the TS
// lib's unbiasedIndex(n, draw). n must be > 0.
func unbiasedIndex(n uint64) int {
	const max = uint64(1) << 32 // 2^32 (Uint32 range, exclusive)
	limit := max - (max % n)
	for {
		var buf [4]byte
		if _, err := rand.Read(buf[:]); err != nil {
			// crypto/rand.Read does not error on modern OSes; treat as fatal.
			panic("passwordgenerator: crypto/rand read failed: " + err.Error())
		}
		r := uint64(binary.LittleEndian.Uint32(buf[:]))
		if r < limit {
			return int(r % n)
		}
	}
}

// Tier is the 5-level entropy assessment returned by StrengthTier, mirroring the
// StrengthTier interface in src/lib/password.ts (label, variant, segments).
type Tier struct {
	Label    string
	Variant  StrengthVariant
	Segments int
}

// EntropyBits returns the theoretical entropy (in bits) of a length-character
// uniform-random password over a charset of the given size. It returns 0 for a
// non-positive length or a charset size <= 1. Mirrors entropyBits in the TS lib.
func EntropyBits(length, charsetSize int) float64 {
	if length <= 0 || charsetSize <= 1 {
		return 0
	}
	return float64(length) * math.Log2(float64(charsetSize))
}

// StrengthTier classifies an entropy value into one of five tiers, 1:1 with the
// five meter segments. Mirrors strengthTier in the TS lib.
func StrengthTier(bits float64) Tier {
	switch {
	case bits >= 100:
		return Tier{Label: "very strong", Variant: VariantSuccess, Segments: 5}
	case bits >= 70:
		return Tier{Label: "strong", Variant: VariantSuccess, Segments: 4}
	case bits >= 45:
		return Tier{Label: "fair", Variant: VariantAccent, Segments: 3}
	case bits >= 28:
		return Tier{Label: "weak", Variant: VariantDanger, Segments: 2}
	default:
		return Tier{Label: "very weak", Variant: VariantDanger, Segments: 1}
	}
}

// Scenario is an attack model's guess rate, mirroring the AttackScenario
// interface in src/lib/password.ts.
type Scenario struct {
	ID               string
	Label            string
	GuessesPerSecond float64
}

// AttackScenarios returns the four documented attack models, mirroring the
// ATTACK_SCENARIOS constant in the TS lib: online-throttled (100/h → 100/3600),
// online (10/s), offline-slow (10^4/s), offline-fast (10^10/s).
func AttackScenarios() []Scenario {
	return []Scenario{
		{ID: "online-throttled", Label: "online, throttled (100/h)", GuessesPerSecond: 100.0 / 3600},
		{ID: "online", Label: "online, no throttle (10/s)", GuessesPerSecond: 10},
		{ID: "offline-slow", Label: "offline, slow hash (10⁴/s)", GuessesPerSecond: 1e4},
		{ID: "offline-fast", Label: "offline, fast GPU (10¹⁰/s)", GuessesPerSecond: 1e10},
	}
}

// CrackTimeSeconds returns the average time to crack (seconds) = 2^(bits-1) /
// guessesPerSecond. The 2^(bits-1) averages over the keyspace (half the space).
// Mirrors crackTimeSeconds in the TS lib.
func CrackTimeSeconds(bits, guessesPerSecond float64) float64 {
	return math.Pow(2, bits-1) / guessesPerSecond
}

// crackUnit is one rung of the formatCrackTime ladder: [factor, unit-name
// AFTER dividing]. Dividing seconds by 60 yields minutes, by 60 again hours,
// then 24→days, 365→years. The name is the bucket landed in AFTER the division.
type crackUnit struct {
	factor int
	name   string
}

// crackUnits mirrors CRACK_UNITS in the TS lib.
var crackUnits = []crackUnit{
	{60, "minute"},
	{60, "hour"},
	{24, "day"},
	{365, "year"},
}

// FormatCrackTime renders a seconds value as a human-readable span. It returns
// "—" for NaN/±Inf/negative input, "< 1 second" for sub-second values, collapses
// to an order-of-magnitude ("10^N years") beyond 10^6 years, and otherwise
// yields "N unit(s)" with singular when the rounded value is 1 (en-US comma
// grouping, matching the TS lib's toLocaleString). Mirrors formatCrackTime.
func FormatCrackTime(seconds float64) string {
	if math.IsNaN(seconds) || math.IsInf(seconds, 0) || seconds < 0 {
		return "—"
	}
	if seconds < 1 {
		return "< 1 second"
	}
	val := seconds
	unit := "second"
	for _, u := range crackUnits {
		if val < float64(u.factor) {
			break
		}
		val /= float64(u.factor)
		unit = u.name
	}
	if unit == "year" && val >= 1e6 {
		return "10^" + strconv.Itoa(int(math.Round(math.Log10(val)))) + " years"
	}
	n := int(math.Round(val))
	return commaInt(n) + " " + unit + pluralSuffix(n)
}

// pluralSuffix returns the empty string for 1 and "s" otherwise, mirroring the
// TS ternary that drops the trailing s only when the rounded value is 1.
func pluralSuffix(n int) string {
	if n == 1 {
		return ""
	}
	return "s"
}

// commaInt formats n with en-US thousands separators (1234567 → "1,234,567"),
// matching JS Number.prototype.toLocaleString in the en-US default locale used
// by formatCrackTime in the TS lib.
func commaInt(n int) string {
	s := strconv.Itoa(n)
	neg := false
	if strings.HasPrefix(s, "-") {
		neg = true
		s = s[1:]
	}
	var b strings.Builder
	for i := 0; i < len(s); i++ {
		if i > 0 && (len(s)-i)%3 == 0 {
			b.WriteByte(',')
		}
		b.WriteByte(s[i])
	}
	if neg {
		return "-" + b.String()
	}
	return b.String()
}

// Point is a single (length, bits) sample on the entropy curve, mirroring the
// return type of entropyCurve in the TS lib.
type Point struct {
	Length int
	Bits   float64
}

// EntropyCurve returns the entropy bits per password length across [from, to]
// (inclusive) with the given step (defaulting to 1 when <= 0). Mirrors
// entropyCurve in the TS lib.
func EntropyCurve(charsetSize, from, to, step int) []Point {
	if step <= 0 {
		step = 1
	}
	out := make([]Point, 0, (to-from)/step+1)
	for l := from; l <= to; l += step {
		out = append(out, Point{Length: l, Bits: EntropyBits(l, charsetSize)})
	}
	return out
}

Also available in 13 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 →