// main.go package main import ( "crypto/rand" "encoding/json" "fmt" "html" "log" "math/big" "net/http" "os" "strconv" "strings" "unicode" ) type Config struct { Addr string MinLength int MinLower int MinUpper int MinDigits int MinSpecial int Specials string LeetMap map[rune]string Suggestions int RuleSets []string // e.g. ["random","passphrase","hybrid"] LeetProb float64 } type genRequest struct { Sentence string `json:"sentence"` } type complexity struct { Lower int `json:"lower"` Upper int `json:"upper"` Digits int `json:"digits"` Special int `json:"special"` Length int `json:"length"` } type genResponse struct { Password string `json:"password"` Complexity complexity `json:"complexity"` Ok bool `json:"ok"` Message string `json:"message,omitempty"` } func main() { cfg := loadConfig() http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprint(w, indexHTML(cfg)) }) http.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req genRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSON(w, genResponse{Ok: false, Message: "Ungültige Eingabe"}) return } pw, cplx := generateFromSentence(strings.TrimSpace(req.Sentence), cfg) ok := meets(cfg, cplx) writeJSON(w, genResponse{Ok: ok, Password: pw, Complexity: cplx}) }) http.HandleFunc("/api/suggest", func(w http.ResponseWriter, r *http.Request) { // Gibt drei+ Vorschläge aus mindestens 3 RuleSets zurück out := struct { Suggestions []string `json:"suggestions"` }{Suggestions: suggestions(cfg)} writeJSON(w, out) }) log.Printf("Listening on %s ...", cfg.Addr) if err := http.ListenAndServe(cfg.Addr, nil); err != nil { log.Fatal(err) } } /* -------------------- Config & ENV -------------------- */ func floatEnv(k string, def float64) float64 { if v := os.Getenv(k); v != "" { v = strings.ReplaceAll(v, ",", ".") // deutsche Kommas erlauben if f, err := strconv.ParseFloat(v, 64); err == nil && f >= 0 && f <= 1 { return f } } return def } func randChance(p float64) bool { if p <= 0 { return false } if p >= 1 { return true } // 1e6 Auflösung reicht; kryptografisch sicher über mustCryptoInt return mustCryptoInt(1_000_000) < int(p*1_000_000) } func loadConfig() Config { minLen := intEnv("MIN_LENGTH", 12) minLower := intEnv("MIN_LOWER", 2) minUpper := intEnv("MIN_UPPER", 2) minDigits := intEnv("MIN_DIGITS", 2) minSpecial := intEnv("MIN_SPECIAL", 2) addr := strEnv("SERVER_ADDR", ":8080") specials := strEnv("SPECIALS", "!@#$%&*()-=+:?") leet := parseLeetMap(strEnv("LEET_MAP", "a:4,e:3,o:0,t:7")) leetProb := floatEnv("LEET_PROB", 0.35) sug := intEnv("SUGGESTION_COUNT", 5) rules := strEnv("RULESETS", "random,passphrase,hybrid") rs := []string{} for _, p := range strings.Split(rules, ",") { p = strings.TrimSpace(p) if p != "" { rs = append(rs, p) } } if len(rs) == 0 { rs = []string{"random", "passphrase", "hybrid"} } return Config{ Addr: addr, MinLength: minLen, MinLower: minLower, MinUpper: minUpper, MinDigits: minDigits, MinSpecial: minSpecial, Specials: specials, LeetMap: leet, Suggestions: sug, RuleSets: rs, LeetProb: leetProb, } } func strEnv(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func intEnv(k string, def int) int { if v := os.Getenv(k); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } func parseLeetMap(s string) map[rune]string { // Format: "a:4,e:3,o:0,t:7" out := map[rune]string{} for _, pair := range strings.Split(s, ",") { p := strings.Split(strings.TrimSpace(pair), ":") if len(p) == 2 && len(p[0]) == 1 { k := []rune(strings.ToLower(p[0]))[0] out[k] = p[1] } } return out } /* -------------------- Core Generation -------------------- */ func generateFromSentence(sentence string, cfg Config) (string, complexity) { if sentence == "" { // Leerer Input -> generiere einen Vorschlag nach "hybrid" pw := genHybrid(cfg) return ensureAll(pw, cfg) } // 1) Normalisieren & Leet words := strings.Fields(sentence) if len(words) == 0 { pw := genHybrid(cfg) return ensureAll(pw, cfg) } // ersetze Leerzeichen durch zufällige Sonderzeichen var b strings.Builder for i, w := range words { if i > 0 { b.WriteString(randFrom(cfg.Specials)) } b.WriteString(applyLeet(w, cfg.LeetMap, cfg.LeetProb)) } pw := b.String() // 2) Zufällige Groß-/Kleinschreibung einstreuen pw = sprinkleCase(pw) // 3) Komplexität & Mindestlänge sicherstellen return ensureAll(pw, cfg) } func applyLeet(s string, leet map[rune]string, p float64) string { var b strings.Builder for _, r := range s { lr := unicode.ToLower(r) if rep, ok := leet[lr]; ok && randChance(p) { b.WriteString(rep) // Ersetze nur mit Chance p } else { b.WriteRune(r) } } return b.String() } func sprinkleCase(s string) string { rs := []rune(s) for i, r := range rs { if unicode.IsLetter(r) { // ~50% Chance flip case if coin() { if unicode.IsUpper(r) { rs[i] = unicode.ToLower(r) } else { rs[i] = unicode.ToUpper(r) } } } } return string(rs) } func ensureAll(pw string, cfg Config) (string, complexity) { // Schrittweise Komplexität erzwingen, dann Mindestlänge pw, c := ensureComplexity(pw, cfg) if runeLen(pw) < cfg.MinLength { need := cfg.MinLength - runeLen(pw) allSet := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cfg.Specials for i := 0; i < need; i++ { pw = insertAt(pw, randPos(pw), randFrom(allSet)) } c = countComplexity(pw, cfg) } return pw, c } func ensureComplexity(s string, cfg Config) (string, complexity) { pw := s c := countComplexity(pw, cfg) // Ziffern for c.Digits < cfg.MinDigits { pw = insertAt(pw, randPos(pw), randFrom("0123456789")) c = countComplexity(pw, cfg) } // Sonderzeichen for c.Special < cfg.MinSpecial { pw = insertAt(pw, randPos(pw), randFrom(cfg.Specials)) c = countComplexity(pw, cfg) } // Großbuchstaben for c.Upper < cfg.MinUpper { if i := findIndexLetter(pw, false); i >= 0 { // irgendein Buchstabe pw = setCaseAt(pw, i, true) } else { pw = insertAt(pw, randPos(pw), randFrom("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) } c = countComplexity(pw, cfg) } // Kleinbuchstaben for c.Lower < cfg.MinLower { if i := findIndexLetter(pw, false); i >= 0 { pw = setCaseAt(pw, i, false) } else { pw = insertAt(pw, randPos(pw), randFrom("abcdefghijklmnopqrstuvwxyz")) } c = countComplexity(pw, cfg) } return pw, c } func countComplexity(s string, cfg Config) complexity { var c complexity for _, r := range s { switch { case unicode.IsLower(r): c.Lower++ case unicode.IsUpper(r): c.Upper++ case unicode.IsDigit(r): c.Digits++ default: if strings.ContainsRune(cfg.Specials, r) { c.Special++ } } } c.Length = runeLen(s) return c } func meets(cfg Config, c complexity) bool { return c.Length >= cfg.MinLength && c.Lower >= cfg.MinLower && c.Upper >= cfg.MinUpper && c.Digits >= cfg.MinDigits && c.Special >= cfg.MinSpecial } /* -------------------- Suggestions (3+ RuleSets) -------------------- */ func suggestions(cfg Config) []string { rs := []string{} // Mindestens eins je RuleSet for _, set := range cfg.RuleSets { switch set { case "random": rs = append(rs, genRandom(cfg)) case "passphrase": rs = append(rs, genPassphrase(cfg)) case "hybrid": rs = append(rs, genHybrid(cfg)) } } // Falls mehr gewünscht, fülle gemischt auf for len(rs) < cfg.Suggestions { switch randPick([]string{"random", "passphrase", "hybrid"}) { case "random": rs = append(rs, genRandom(cfg)) case "passphrase": rs = append(rs, genPassphrase(cfg)) default: rs = append(rs, genHybrid(cfg)) } } return rs } func genRandom(cfg Config) string { all := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + cfg.Specials // Länge: mindestens MinLength bis MinLength+6 L := cfg.MinLength + mustCryptoInt(5) + 2 // +2..+6 var b strings.Builder for i := 0; i < L; i++ { b.WriteString(randFrom(all)) } pw, _ := ensureAll(b.String(), cfg) return pw } func genPassphrase(cfg Config) string { // 3–4 Wörter, zufällige Trenner, zufällige Großschreibung & Leet-sprinkles n := 3 + mustCryptoInt(2) // 3..4 parts := make([]string, 0, n) for i := 0; i < n; i++ { w := wordList[mustCryptoInt(len(wordList))] if coin() { w = strings.ToUpper(w[:1]) + w[1:] } // gelegentlich Leet auf einzelne Buchstaben if coin() && len(w) > 1 { idx := mustCryptoInt(len(w)) r := []rune(w) lr := unicode.ToLower(r[idx]) if rep, ok := cfg.LeetMap[lr]; ok { r[idx] = []rune(rep)[0] w = string(r) } } parts = append(parts, w) } sep := randFrom(cfg.Specials) pw := strings.Join(parts, sep) // Zahlen zufällig einsprenkeln if coin() { pw = insertAt(pw, randPos(pw), strconv.Itoa(10+mustCryptoInt(89))) } pw, _ = ensureAll(pw, cfg) return pw } func genHybrid(cfg Config) string { // 2 Wörter + 2 Sonderzeichen + 3–5 Ziffern gemischt w1 := wordList[mustCryptoInt(len(wordList))] w2 := wordList[mustCryptoInt(len(wordList))] if coin() { w1 = strings.ToUpper(w1[:1]) + w1[1:] } if coin() { w2 = strings.ToUpper(w2[:1]) + w2[1:] } base := applyLeet(w1, cfg.LeetMap, cfg.LeetProb) + randFrom(cfg.Specials) + applyLeet(w2, cfg.LeetMap, cfg.LeetProb) // streue zweite Sonderzeichen base = insertAt(base, randPos(base), randFrom(cfg.Specials)) // 3–5 Ziffern an zufälligen Stellen numN := 3 + mustCryptoInt(3) for i := 0; i < numN; i++ { base = insertAt(base, randPos(base), randFrom("0123456789")) } pw, _ := ensureAll(base, cfg) return pw } /* -------------------- Utilities -------------------- */ func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") _ = json.NewEncoder(w).Encode(v) } func runeLen(s string) int { return len([]rune(s)) } func randFrom(pool string) string { i := mustCryptoInt(len(pool)) return string(pool[i]) } func randPos(s string) int { L := runeLen(s) if L == 0 { return 0 } return mustCryptoInt(L + 1) // erlauben "ans Ende" } func insertAt(s string, pos int, piece string) string { rs := []rune(s) if pos <= 0 { return piece + s } if pos >= len(rs) { return s + piece } var b strings.Builder b.WriteString(string(rs[:pos])) b.WriteString(piece) b.WriteString(string(rs[pos:])) return b.String() } func findIndexLetter(s string, onlyLower bool) int { rs := []rune(s) // Zufällige Startposition, um Muster zu vermeiden start := mustCryptoInt(len(rs) + 1) for i := 0; i < len(rs); i++ { idx := (start + i) % len(rs) r := rs[idx] if unicode.IsLetter(r) { if onlyLower { if unicode.IsLower(r) { return idx } } else { return idx } } } return -1 } func setCaseAt(s string, idx int, upper bool) string { rs := []rune(s) if idx < 0 || idx >= len(rs) { return s } if !unicode.IsLetter(rs[idx]) { // wenn keine Letter -> füge stattdessen einen passenden Buchstaben ein if upper { return insertAt(s, idx, randFrom("ABCDEFGHIJKLMNOPQRSTUVWXYZ")) } return insertAt(s, idx, randFrom("abcdefghijklmnopqrstuvwxyz")) } if upper { rs[idx] = unicode.ToUpper(rs[idx]) } else { rs[idx] = unicode.ToLower(rs[idx]) } return string(rs) } func coin() bool { return mustCryptoInt(2) == 0 } func mustCryptoInt(max int) int { if max <= 1 { return 0 } nBig, err := rand.Int(rand.Reader, big.NewInt(int64(max))) if err != nil { panic(err) } return int(nBig.Int64()) } func randPick(options []string) string { return options[mustCryptoInt(len(options))] } /* -------------------- Minimal UI (alles lokal) -------------------- */ func indexHTML(cfg Config) string { var b strings.Builder b.WriteString("\n") b.WriteString("\n\n\n\n") b.WriteString("Password Forge (Go)\n\n\n\n") b.WriteString("
\n
\n

🔐 Password Forge

\n") b.WriteString("

Aus einem Satz ein starkes Kennwort erzeugen - alternativ geben Sie einzelne Wörter ein.

\n") b.WriteString("
\n
\n \n") b.WriteString(" \n") b.WriteString("
\n") b.WriteString(" \n") b.WriteString(" Mindestlänge: ") b.WriteString(strconv.Itoa(cfg.MinLength)) b.WriteString(" • lower≥") b.WriteString(strconv.Itoa(cfg.MinLower)) b.WriteString(" upper≥") b.WriteString(strconv.Itoa(cfg.MinUpper)) b.WriteString(" digits≥") b.WriteString(strconv.Itoa(cfg.MinDigits)) b.WriteString(" special≥") b.WriteString(strconv.Itoa(cfg.MinSpecial)) b.WriteString("\n") b.WriteString("
\n
\n \n") b.WriteString("
\n \n") b.WriteString("
\n
\n
\n") b.WriteString("
\n") b.WriteString(" \n") b.WriteString(" \n") b.WriteString("
\n") b.WriteString("
\n") b.WriteString("
\n
\n \n
\n
\n\n\n\n") return b.String() } /* -------------------- Small built-in word list -------------------- */ var wordList = []string{ "amt","behörde","rathaus","kreisamt","meldeamt","bauamt","forstamt","jagdamt","kasse","haushalt", "doppik","kameral","konto","buchung","beleg","rechnung","revision","vergabe","gesetz","satzung", "erlass","bescheid","antrag","auflage","ermessen","frist","recht","klage","einwand","widerruf", "freigabe","lizenz","akte","akten","postfach","stempel","vorlage","signatur","eakte","dms", "vorgang","ablage","archiv","ozg","eid","ifg","dsgvo","bsi","egov","portal", "leitweg","peppol","vpn","backup","rollen","rechte","audit","token","tls","rat", "stadtrat","kreistag","landtag","senat","wahl","wahlamt","mandat","sitzung","gebühr","kosten", "ordnung","bußgeld","einsatz","streife","polizei","zoll","rettung","lage","sirene","nina", "thw","verkehr","öpnv","fahrplan","tarif","ticket","fahrzeug","zeugnis","bauplan","planung", "abnahme","denkmal","forst","wasser","abfall","müll","klärwerk","luftplan","lärm","energie", "steuer","einkauf","umsatz","mahnung","sperre","vermerk","mittel","zuschuss","projekt","bericht", "kennzahl","ziel","qualität","schulung","barriere","feedback","hotline","service","presse","bekannt", "zustell","zwang","treuhand","amtsweg","hauspost","intranet","aufsicht","referat","dezernat","bereich", "stelle","termin","formular","zahlung","automat","opendata","abfrage","meldung","gewerbe","parken", "ausweis","urkunde","pass","schein","bafög","wohngeld","schule","kita","kiga","jugend", "hilfe","pflege","rente","familie","arbeit","agentur","asyl","visum","titel","status", } /* -------------------- END -------------------- */