This commit is contained in:
2025-10-06 06:51:38 +00:00
parent 7fb810bd42
commit 1785335b1f
2 changed files with 593 additions and 0 deletions

3
go.mod Normal file
View File

@@ -0,0 +1,3 @@
module git.send.nrw/sendnrw/passwordsuggester
go 1.25.1

590
main.go Normal file
View File

@@ -0,0 +1,590 @@
// 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"]
}
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 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", ":8081")
specials := strEnv("SPECIALS", "!@#$%^&*()-_=+[]{};:,.?/")
leet := parseLeetMap(strEnv("LEET_MAP", "a:4,e:3,o:0,t:7"))
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,
}
}
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))
}
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) string {
var b strings.Builder
for _, r := range s {
lr := unicode.ToLower(r)
if rep, ok := leet[lr]; ok {
b.WriteString(rep)
} 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 {
// 34 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 + 35 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) + randFrom(cfg.Specials) + applyLeet(w2, cfg.LeetMap)
// streue zweite Sonderzeichen
base = insertAt(base, randPos(base), randFrom(cfg.Specials))
// 35 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("<!doctype html>\n")
b.WriteString("<html lang='de'>\n<head>\n<meta charset='utf-8'>\n<meta name='viewport' content='width=device-width,initial-scale=1'>\n")
b.WriteString("<title>Password Forge (Go)</title>\n<style>\n")
b.WriteString(":root{--bg:#0b0f14;--card:#111827;--ink:#e5e7eb;--muted:#9ca3af;--accent:#60a5fa;--ok:#34d399;--bad:#f87171}\n")
b.WriteString("*{box-sizing:border-box}\n")
b.WriteString("body{margin:0;background:var(--bg);color:var(--ink);font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif}\n")
b.WriteString(".container{max-width:880px;margin:32px auto;padding:0 16px}\n")
b.WriteString(".card{background:var(--card);border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.35);padding:20px}\n")
b.WriteString("h1{margin:0 0 8px;font-size:24px}\n")
b.WriteString("p.sub{margin:0 0 18px;color:var(--muted)}\n")
b.WriteString("label{display:block;font-size:14px;color:var(--muted);margin-bottom:6px}\n")
b.WriteString("textarea{width:100%;min-height:90px;border:1px solid #1f2937;background:#0f1622;color:var(--ink);border-radius:12px;padding:12px;resize:vertical}\n")
b.WriteString("button{cursor:pointer;border:0;border-radius:12px;padding:10px 14px;background:var(--accent);color:#0b0f14;font-weight:600}\n")
b.WriteString(".row{display:flex;gap:12px;align-items:flex-end;flex-wrap:wrap}\n")
b.WriteString(".output{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;background:#0f1622;color:#e2e8f0;border-radius:12px;padding:12px;word-break:break-all}\n")
b.WriteString(".grid{display:grid;grid-template-columns:1fr;gap:12px}\n")
b.WriteString("@media(min-width:800px){.grid{grid-template-columns:2fr 1fr}}\n")
b.WriteString(".badge{display:inline-flex;align-items:center;gap:6px;border-radius:999px;background:#0f1622;padding:6px 10px;font-size:12px;color:var(--muted)}\n")
b.WriteString(".badge.ok{color:var(--ok)}\n")
b.WriteString(".badge.bad{color:var(--bad)}\n")
b.WriteString("small.kv{color:var(--muted);display:block}\n")
b.WriteString(".suggestion{padding:10px;border:1px dashed #283243;border-radius:10px;background:#0f1622;word-break:break-all}\n")
b.WriteString("kbd{background:#111827;border:1px solid #223046;border-radius:6px;padding:3px 6px}\n")
b.WriteString("footer{opacity:.7;margin-top:20px;font-size:12px}\n")
b.WriteString("</style>\n</head>\n<body>\n")
b.WriteString("<div class='container'>\n <div class='card'>\n <h1>🔐 Password Forge</h1>\n")
b.WriteString(" <p class='sub'>Aus einem Satz ein starkes Kennwort erzeugen mit konfigurierbaren Regeln (alles lokal).</p>\n")
b.WriteString(" <div class='grid'>\n <div>\n <label for='sentence'>Eingabesatz</label>\n")
b.WriteString(" <textarea id='sentence' placeholder='Z.B. “Mein erster Urlaub war 2012 in Porto!”'></textarea>\n")
b.WriteString(" <div class='row' style='margin-top:10px'>\n")
b.WriteString(" <button id='btnGen'>Kennwort erzeugen</button>\n")
b.WriteString(" <span id='status' class='badge'>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("</span>\n")
b.WriteString(" </div>\n <div style='margin-top:12px'>\n <label>Ergebnis</label>\n")
b.WriteString(" <div id='out' class='output'></div>\n <small id='cplx' class='kv'></small>\n")
b.WriteString(" </div>\n </div>\n <div>\n")
b.WriteString(" <div style='display:flex;justify-content:space-between;align-items:center'>\n")
b.WriteString(" <label>Zufällige Vorschläge</label>\n")
b.WriteString(" <button id='btnRefresh' style='background:#38bdf8'>Neu</button>\n")
b.WriteString(" </div>\n")
b.WriteString(" <div id='sugs' style='display:grid;gap:8px'></div>\n")
b.WriteString(" </div>\n </div>\n <footer>\n Running on <kbd>")
b.WriteString(html.EscapeString(cfg.Addr))
b.WriteString("</kbd> • ENV: <kbd>MIN_LENGTH=")
b.WriteString(strconv.Itoa(cfg.MinLength))
b.WriteString("</kbd> <kbd>MIN_LOWER=")
b.WriteString(strconv.Itoa(cfg.MinLower))
b.WriteString("</kbd> <kbd>MIN_UPPER=")
b.WriteString(strconv.Itoa(cfg.MinUpper))
b.WriteString("</kbd> <kbd>MIN_DIGITS=")
b.WriteString(strconv.Itoa(cfg.MinDigits))
b.WriteString("</kbd> <kbd>MIN_SPECIAL=")
b.WriteString(strconv.Itoa(cfg.MinSpecial))
b.WriteString("</kbd>\n")
b.WriteString(" </footer>\n </div>\n</div>\n<script>\n")
b.WriteString("const $ = function(sel){ return document.querySelector(sel); };\n")
b.WriteString("function renderComplexity(c, ok){\n")
b.WriteString(" const badge = ok ? 'badge ok' : 'badge bad';\n")
b.WriteString(" return '<span class=\"' + badge + '\">L:' + c.length + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">a:' + c.lower + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">A:' + c.upper + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">0:' + c.digits + '</span> '\n")
b.WriteString(" + '<span class=\"' + badge + '\">#:' + c.special + '</span>'; }\n")
b.WriteString("async function postJSON(url, data){\n")
b.WriteString(" const res = await fetch(url,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});\n")
b.WriteString(" return await res.json(); }\n")
b.WriteString("async function getJSON(url){ const res = await fetch(url); return await res.json(); }\n")
b.WriteString("async function generate(){\n")
b.WriteString(" const sentence = $('#sentence').value;\n")
b.WriteString(" const res = await postJSON('/api/generate', { sentence });\n")
b.WriteString(" if(res && res.password){ $('#out').textContent = res.password; $('#cplx').innerHTML = renderComplexity(res.complexity, res.ok); }\n")
b.WriteString(" else { $('#out').textContent = res.message || 'Fehler'; $('#cplx').textContent = ''; } }\n")
b.WriteString("async function loadSugs(){\n")
b.WriteString(" const data = await getJSON('/api/suggest');\n")
b.WriteString(" const suggestions = (data && data.suggestions) ? data.suggestions : [];\n")
b.WriteString(" const wrap = $('#sugs'); wrap.innerHTML = '';\n")
b.WriteString(" suggestions.forEach(function(s){ const d = document.createElement('div'); d.className='suggestion'; d.textContent = s; wrap.appendChild(d); }); }\n")
b.WriteString("$('#btnGen').addEventListener('click', generate);\n")
b.WriteString("$('#btnRefresh').addEventListener('click', loadSugs);\n")
b.WriteString("loadSugs();\n")
b.WriteString("</script>\n</body>\n</html>\n")
return b.String()
}
/* -------------------- Small built-in word list -------------------- */
var wordList = []string{
"apfel","atlas","anker","balou","blitz","brise","cobalt","cobra","dialog","dorado","dampf","edel","eule",
"falke","fjord","funk","galaxy","garnet","geist","harbor","herz","honig","indigo","ion","juno","jade",
"kiesel","komet","kobalt","lotus","lava","lynx","meteor","moos","nimbus","neon","novum","opal","orbit",
"pixel","polaris","quarz","quell","raven","regen","rythm","saphir","sigma","silber","tau","titan","topas",
"umbra","union","vektor","violett","vulkan","wolke","wogen","xenon","yukon","zenit","zeder",
"amber","boreal","cedar","delta","ember","fable","gamma","helios","iris","jelly","kappa","lemur","magma",
"nova","onyx","plume","quince","ridge","spruce","terra","ultra","vivid","willow","xerox","yodel","zesty",
}
/* -------------------- END -------------------- */