package main import ( "bufio" "crypto/rand" "fmt" "html/template" "log" "math" "math/big" "net/http" "os" "strconv" "strings" "time" "unicode" ) type WordData struct { ByLen map[int][]string } var words WordData // Fixed symbol alphabet const symbolAlphabet = "!@#$%^&*()-+',?" type PageData struct { // Form values WordCount int MinLen int MaxLen int NumLen int SymbolCount int // symbols per separator RandCaps string // Stats TotalWords int AverageWordLength float64 BitsPerWord float64 BitsPerSeparator float64 BitsForCapsTotal float64 EffectiveBits int TotalCombinations string Error string GeneratedPhrases []string } // commonLogMiddleware logs requests in Apache/Nginx "Common Log Format" func commonLogMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Wrap ResponseWriter so we can capture status/size lrw := &loggingResponseWriter{ResponseWriter: w, status: 200} start := time.Now() next.ServeHTTP(lrw, r) duration := time.Since(start) // Determine client IP ip := r.RemoteAddr if xf := r.Header.Get("X-Forwarded-For"); xf != "" { ip = xf } // Common Log Format: // 127.0.0.1 - - [10/Oct/2000:13:55:36 -0700] "GET /path HTTP/1.1" 200 2326 log.Printf(`%s - - [%s] "%s %s %s" %d %d "%s" "%s" %v`, ip, time.Now().Format("02/Jan/2006:15:04:05 -0700"), r.Method, r.RequestURI, r.Proto, lrw.status, lrw.bytes, r.Referer(), r.UserAgent(), duration, ) }) } // loggingResponseWriter allows us to capture status code & bytes written type loggingResponseWriter struct { http.ResponseWriter status int bytes int } func (lrw *loggingResponseWriter) WriteHeader(code int) { lrw.status = code lrw.ResponseWriter.WriteHeader(code) } func (lrw *loggingResponseWriter) Write(b []byte) (int, error) { n, err := lrw.ResponseWriter.Write(b) lrw.bytes += n return n, err } // Single-file HTML template var pageTmpl = template.Must(template.New("page").Parse(` Secure Passphrase Generator

Secure Passphrase Generator

{{if .Error}}
{{.Error}}
{{end}}
{{if not .Error}}
Statistics
Word count{{.TotalWords}}
Average word length{{printf "%.2f" .AverageWordLength}}
Bits per word{{printf "%.2f" .BitsPerWord}}
Bits per separator{{printf "%.2f" .BitsPerSeparator}}
Bits for capitalization{{printf "%.2f" .BitsForCapsTotal}}
Effective passphrase bits{{.EffectiveBits}}
Total possible combinations{{.TotalCombinations}}
{{range .GeneratedPhrases}} {{end}}
Passphrase
{{.}}

Notes

{{end}} `)) func main() { wordlistPath := os.Getenv("WORDLIST") if wordlistPath == "" { wordlistPath = "words.txt" } log.Printf("Loading wordlist from %s", wordlistPath) wd, err := loadWordlist(wordlistPath) if err != nil { log.Fatalf("failed to load wordlist: %v", err) } words = wd log.Printf("Loaded %d distinct word lengths", len(words.ByLen)) http.Handle("/", commonLogMiddleware(http.HandlerFunc(handlePassphrase))) port := os.Getenv("PORT") if port == "" { port = "8000" } addr := ":" + port log.Printf("Listening on %s", addr) log.Fatal(http.ListenAndServe(addr, nil)) } func loadWordlist(path string) (WordData, error) { f, err := os.Open(path) if err != nil { return WordData{}, err } defer f.Close() byLen := make(map[int][]string) scanner := bufio.NewScanner(f) for scanner.Scan() { w := strings.TrimSpace(scanner.Text()) if w == "" { continue } // Keep only alphabetic words ok := true for _, r := range w { if !unicode.IsLetter(r) { ok = false break } } if !ok { continue } w = strings.ToLower(w) byLen[len(w)] = append(byLen[len(w)], w) } if err := scanner.Err(); err != nil { return WordData{}, err } return WordData{ByLen: byLen}, nil } func handlePassphrase(w http.ResponseWriter, r *http.Request) { wordcount := parseIntQuery(r, "wordcount", 3) minlen := parseIntQuery(r, "minlen", 5) maxlen := parseIntQuery(r, "maxlen", 8) numlen := parseIntQuery(r, "numlen", 1) symbolCount := parseIntQuery(r, "symcount", 1) randcaps := r.URL.Query().Get("randcaps") if randcaps == "" { randcaps = "first" } if minlen < 1 { minlen = 1 } if maxlen < minlen { maxlen = minlen } if wordcount < 1 { wordcount = 1 } if numlen < 0 { numlen = 0 } if symbolCount < 0 { symbolCount = 0 } data := PageData{ WordCount: wordcount, MinLen: minlen, MaxLen: maxlen, NumLen: numlen, SymbolCount: symbolCount, RandCaps: randcaps, } usewords, totalLen := collectWordsInRange(words.ByLen, minlen, maxlen) if len(usewords) == 0 { data.Error = "No words available for the specified length range." _ = pageTmpl.Execute(w, data) return } totalWords := len(usewords) avgLen := float64(totalLen) / float64(totalWords) wordBits := math.Log2(float64(totalWords)) // Caps var capBitsPerWord float64 var capMult float64 switch randcaps { case "first": capBitsPerWord = 1 capMult = 2 case "one": capBitsPerWord = math.Log2(avgLen + 1) capMult = avgLen + 1 case "all": capBitsPerWord = avgLen capMult = math.Pow(2, avgLen) default: capBitsPerWord = 0 capMult = 1 } // Separators: after every word if any digits or symbols are enabled segmentsEnabled := (numlen > 0 || symbolCount > 0) segments := 0 if segmentsEnabled { segments = wordcount } // Bits per separator segment separatorBitsPerSegment := 0.0 if segmentsEnabled { separatorBitsPerSegment = separatorBits(numlen, symbolCount, len(symbolAlphabet)) } // Total bits passBits := (wordBits+capBitsPerWord)*float64(wordcount) + separatorBitsPerSegment*float64(segments) // Total combinations ≈ combosPerWord := math.Pow(float64(totalWords), float64(wordcount)) combosCaps := math.Pow(capMult, float64(wordcount)) segmentCombos := 1.0 if segmentsEnabled { segmentCombos = math.Pow(separatorCombos(numlen, symbolCount, len(symbolAlphabet)), float64(segments)) } combos := combosPerWord * combosCaps * segmentCombos combosStr := fmt.Sprintf("%.3e", combos) if !math.IsInf(combos, 0) && combos < 9.999e15 { combosStr = commafyInt(int64(combos + 0.5)) } data.TotalWords = totalWords data.AverageWordLength = avgLen data.BitsPerWord = wordBits data.BitsPerSeparator = separatorBitsPerSegment data.BitsForCapsTotal = capBitsPerWord * float64(wordcount) data.EffectiveBits = int(passBits + 0.5) data.TotalCombinations = combosStr data.GeneratedPhrases = generatePassphrases(usewords, wordcount, numlen, symbolCount, randcaps) w.Header().Set("Content-Type", "text/html; charset=utf-8") if err := pageTmpl.Execute(w, data); err != nil { log.Printf("template error: %v", err) } } func parseIntQuery(r *http.Request, name string, def int) int { s := r.URL.Query().Get(name) if s == "" { return def } n, err := strconv.Atoi(s) if err != nil { return def } return n } // collectWordsInRange flattens words[length] into a single slice for [minLen, maxLen]. func collectWordsInRange(byLen map[int][]string, minLen, maxLen int) ([]string, int) { var all []string totalLen := 0 for l := minLen; l <= maxLen; l++ { ws := byLen[l] if len(ws) == 0 { continue } totalLen += len(ws) * l all = append(all, ws...) } return all, totalLen } // separatorCombos returns the number of possible distinct separator segments. func separatorCombos(numlen, symCountPerSeg, symAlphabetSize int) float64 { // No separators if numlen <= 0 && symCountPerSeg <= 0 { return 1 } // Only symbols if numlen <= 0 && symCountPerSeg > 0 { return math.Pow(float64(symAlphabetSize), float64(symCountPerSeg)) } // Only digits if numlen > 0 && symCountPerSeg <= 0 { return math.Pow(10, float64(numlen)) } // Mixed: exactly symCountPerSeg symbols and numlen digits totalLen := numlen + symCountPerSeg return comb(totalLen, symCountPerSeg) * math.Pow(float64(symAlphabetSize), float64(symCountPerSeg)) * math.Pow(10, float64(numlen)) } // separatorBits = log2(combos) for a single segment func separatorBits(numlen, symCountPerSeg, symAlphabetSize int) float64 { combos := separatorCombos(numlen, symCountPerSeg, symAlphabetSize) if combos <= 0 { return 0 } return math.Log2(combos) } // generatePassphrases builds N phrases with separators after each word when enabled. func generatePassphrases(usewords []string, wordcount, numlen, symCountPerSeg int, randcaps string) []string { const numPhrases = 10 if len(usewords) == 0 { return nil } segmentsEnabled := (numlen > 0 || symCountPerSeg > 0) symRunes := []rune(symbolAlphabet) phrases := make([]string, 0, numPhrases) for i := 0; i < numPhrases; i++ { var b strings.Builder for j := 0; j < wordcount; j++ { word := usewords[secureRandInt(len(usewords))] word = applyRandCaps(word, randcaps) b.WriteString(word) if segmentsEnabled { segment := makeSeparatorSegment(numlen, symCountPerSeg, symRunes) b.WriteString(segment) } } phrases = append(phrases, b.String()) } return phrases } // makeSeparatorSegment creates one separator segment. // Cases: // - numlen <= 0, symCountPerSeg <= 0: empty // - numlen <= 0, symCountPerSeg > 0: symbols only // - numlen > 0, symCountPerSeg <= 0: digits only // - numlen > 0, symCountPerSeg > 0: mixed with exactly symCountPerSeg symbols. func makeSeparatorSegment(numlen, symCountPerSeg int, symRunes []rune) string { digits := "0123456789" symAlphabetSize := len(symRunes) if numlen <= 0 && symCountPerSeg <= 0 { return "" } if numlen <= 0 && symCountPerSeg > 0 { // Only symbols runes := make([]rune, symCountPerSeg) for i := 0; i < symCountPerSeg; i++ { runes[i] = symRunes[secureRandInt(symAlphabetSize)] } return string(runes) } if numlen > 0 && symCountPerSeg <= 0 { // Only digits var b strings.Builder for i := 0; i < numlen; i++ { b.WriteByte(digits[secureRandInt(10)]) } return b.String() } // Mixed: exactly symCountPerSeg symbols and numlen digits totalLen := numlen + symCountPerSeg runes := make([]rune, 0, totalLen) // Add required number of symbols for i := 0; i < symCountPerSeg; i++ { runes = append(runes, symRunes[secureRandInt(symAlphabetSize)]) } // Add required number of digits for i := 0; i < numlen; i++ { runes = append(runes, rune(digits[secureRandInt(10)])) } // Shuffle runes shuffleRunes(runes) return string(runes) } func secureRandInt(n int) int { if n <= 0 { return 0 } max := big.NewInt(int64(n)) v, err := rand.Int(rand.Reader, max) if err != nil { return 0 } return int(v.Int64()) } func shuffleRunes(r []rune) { for i := len(r) - 1; i > 0; i-- { j := secureRandInt(i + 1) r[i], r[j] = r[j], r[i] } } func applyRandCaps(word, mode string) string { if word == "" { return word } switch mode { case "first": if secureRandInt(2) == 1 { return strings.ToUpper(word[:1]) + word[1:] } case "one": k := secureRandInt(len(word) + 1) if k < len(word) { return word[:k] + strings.ToUpper(word[k:k+1]) + word[k+1:] } case "all": var out []rune for _, r := range word { if secureRandInt(2) == 1 { out = append(out, unicode.ToUpper(r)) } else { out = append(out, unicode.ToLower(r)) } } return string(out) } return word } // comb computes "n choose k" as float64 (safe for small n). func comb(n, k int) float64 { if k < 0 || k > n { return 0 } if k == 0 || k == n { return 1 } if k > n-k { k = n - k } result := 1.0 for i := 1; i <= k; i++ { result *= float64(n - k + i) result /= float64(i) } return result } func commafyInt(n int64) string { sign := "" if n < 0 { sign = "-" n = -n } s := strconv.FormatInt(n, 10) if len(s) <= 3 { return sign + s } var parts []string for len(s) > 3 { parts = append([]string{s[len(s)-3:]}, parts...) s = s[:len(s)-3] } if len(s) > 0 { parts = append([]string{s}, parts...) } return sign + strings.Join(parts, ",") }