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 →