From 64f9da154916d8402ea65fff78519ff3069aeff8 Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Wed, 10 Dec 2025 14:27:04 +1100 Subject: [PATCH] change to golang --- .gitignore | 2 + Dockerfile | 29 ++- common.py | 69 ------ docker-compose.yml | 17 ++ footer.html | 20 -- header.html | 6 - index.cgi | 5 - index.html | 10 - longrandom.py | 21 -- main.go | 577 +++++++++++++++++++++++++++++++++++++++++++++ makedict | 16 -- ppform.html | 33 --- ppgen.cgi | 12 - ppgen.py | 102 -------- pwform.html | 25 -- pwgen.cgi | 12 - pwgen.py | 74 ------ worddict.py | 37 --- 18 files changed, 612 insertions(+), 455 deletions(-) create mode 100644 .gitignore delete mode 100644 common.py create mode 100644 docker-compose.yml delete mode 100644 footer.html delete mode 100644 header.html delete mode 100755 index.cgi delete mode 100644 index.html delete mode 100644 longrandom.py create mode 100644 main.go delete mode 100755 makedict delete mode 100644 ppform.html delete mode 100755 ppgen.cgi delete mode 100755 ppgen.py delete mode 100644 pwform.html delete mode 100755 pwgen.cgi delete mode 100755 pwgen.py delete mode 100644 worddict.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a04718 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +__pycache__/* diff --git a/Dockerfile b/Dockerfile index 94a8f6a..e7233fc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,24 @@ -FROM python:3.12-slim +# Build stage +FROM golang:1.25-bookworm AS build -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* +WORKDIR /src +COPY main.go . -RUN ln -sf /usr/local/bin/python3 /usr/bin/python3 +# Build a static-ish binary +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o passgen main.go + +# Runtime stage: minimal image with just the binary + wordlist +FROM debian:13-slim WORKDIR /app -COPY . /app/ -RUN mkdir -p /app/cgi-bin \ - && cp /app/*.py /app/*.cgi /app/cgi-bin/ \ - && cp /app/words.txt /app/cgi-bin/ \ - && cp /app/*.html /app/cgi-bin/ +# Copy binary and wordlist +COPY --from=build /src/passgen /app/passgen +COPY words.txt /app/words.txt -RUN chmod +x /app/index.cgi -RUN chmod +x /app/cgi-bin/*.py /app/cgi-bin/*.cgi || true +ENV WORDLIST=/app/words.txt +ENV PORT=8000 EXPOSE 8000 -CMD ["python", "-m", "http.server", "8000", "--cgi"] \ No newline at end of file + +CMD ["/app/passgen"] \ No newline at end of file diff --git a/common.py b/common.py deleted file mode 100644 index 4d29941..0000000 --- a/common.py +++ /dev/null @@ -1,69 +0,0 @@ -import cgitb -cgitb.enable() - -import cgi -import os -import sys - -def bool(s): - try: return int(s) - except ValueError: pass - return s == 'on' - -def commafy(s): - s = str(int(s)) - o = '' - while len(s) > 3: - o = ',' + s[-3:] + o - s = s[:-3] - return s + o - -def escape(s): - return str(s).replace('&', '&').replace('<', '<').replace('>', '>') - -def form_get(form, name, default, conv=str): - try: - item = form[name] - except KeyError: - return default - if item.file: - print("Uploads are not allowed!") - sys.exit(1) - if type(item.value) is not type(''): - print("Multiple values are not allowed!") - sys.exit(1) - try: - value = conv(item.value) - except: - print("'%s' failed to convert" % name) - sys.exit(1) - return value - -class EvalDict: - def __init__(self, values): - self.values = values - def __getitem__(self, key): - return escape(eval(key, self.values)) - -def row(a,b,bold=0): - pre = post = '' - if bold: - pre = '' - post = '' - print('%s:%s%s%s' % ( - a, pre, b, post )) -def table_start(): print('
') -def table_end(): print('
') - -def dumpfile(filename, dict=None): - # Always resolve relative to this file's directory - here = os.path.dirname(__file__) - path = os.path.join(here, filename) - - with open(path, encoding="utf-8") as f: - data = f.read() - - if dict is not None: - data = data % dict - - sys.stdout.write(data) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b6f1c94 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3.9" + +services: + passgen: + build: + context: . + dockerfile: Dockerfile + container_name: passphrase-generator + environment: + # Path to the wordlist inside the container + WORDLIST: /app/words.txt + # Port the Go server listens on inside the container + PORT: "8000" + ports: + # host:container + - "8000:8000" + restart: unless-stopped diff --git a/footer.html b/footer.html deleted file mode 100644 index 545302b..0000000 --- a/footer.html +++ /dev/null @@ -1,20 +0,0 @@ -
- -

Notes:

- -
- - diff --git a/header.html b/header.html deleted file mode 100644 index b972f1b..0000000 --- a/header.html +++ /dev/null @@ -1,6 +0,0 @@ - - -Secure %(type)s Generator - - -

Secure %(type)s Generator


diff --git a/index.cgi b/index.cgi deleted file mode 100755 index c62ea73..0000000 --- a/index.cgi +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env python3 -print("Content-Type: text/html") -print("Status: 302 Found") -print("Location: /cgi-bin/ppgen.py") -print() \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100644 index 6589ade..0000000 --- a/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Secure Passphrase Generator - - -

Redirecting to Secure Passphrase Generator

- - diff --git a/longrandom.py b/longrandom.py deleted file mode 100644 index 80ebae5..0000000 --- a/longrandom.py +++ /dev/null @@ -1,21 +0,0 @@ -import math, os -from functools import reduce - -log256 = math.log(256) - -class LongRandom: - def getint(self, bytes): - value = list(os.urandom(bytes)) - return reduce(lambda x,y: (x<<8)|y, value) - def get(self, modulus): - # Find the largest power-of-2 that modulus fits into - bytes = int(math.ceil(math.log(modulus) / log256)) - maxval = 256 ** bytes - # maxmod is the largest multiple of modulus not greater than maxval - maxmult = maxval - maxval % modulus - while True: - value = self.getint(bytes) - # Stop generating when the value would not cause bias - if value <= maxmult: - break - return value % modulus diff --git a/main.go b/main.go new file mode 100644 index 0000000..8b03cf6 --- /dev/null +++ b/main.go @@ -0,0 +1,577 @@ +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
{{.}}
+ {{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, ",") +} diff --git a/makedict b/makedict deleted file mode 100755 index 576e082..0000000 --- a/makedict +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/python3 -import sys - -words = [ ] -for filename in sys.argv[1:]: - for line in open(filename): - line = line.strip().lower() - try: - words[len(line)][line] = None - except IndexError: - while len(words) <= len(line): - words.append({}) -for i in range(len(words)): - words[i] = sorted(words[i].keys()) - -print('words =', words) diff --git a/ppform.html b/ppform.html deleted file mode 100644 index e1db38c..0000000 --- a/ppform.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
- - - - - - - - - - - - - - - - - - - - -
Number of words:
Minimum word length: characters
Maximum word length: characters
Random capitalization:
Length of numbers between words: digits
- -
-
- -

[ Click here for the password generator ]

diff --git a/ppgen.cgi b/ppgen.cgi deleted file mode 100755 index 6ecbd1f..0000000 --- a/ppgen.cgi +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import os -import runpy - -# Directory this CGI script lives in -here = os.path.dirname(__file__) - -# Path to the real script -script = os.path.join(here, "ppgen.py") - -# Execute ppgen.py as if it were the main script -runpy.run_path(script, run_name="__main__") \ No newline at end of file diff --git a/ppgen.py b/ppgen.py deleted file mode 100755 index b6648fb..0000000 --- a/ppgen.py +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/python3 -from math import * -import random -import string -import sys -import os - -from common import * -from longrandom import LongRandom -from worddict import words - -print("Content-Type: text/html") -print() - -qs = os.environ.get('QUERY_STRING', None) -if not qs: - minlen = 1 - maxlen = 8 - numlen = 2 - wordcount = 3 - extra_symbols = '' - randcaps = 'first' -else: - form = cgi.FieldStorage() - wordcount = form_get(form, 'wordcount', 3, int) - minlen = form_get(form, 'minlen', 2, int) - maxlen = form_get(form, 'maxlen', 8, int) - numlen = form_get(form, 'numlen', 2, int) - randcaps = form_get(form, 'randcaps', 'first', str) - -dumpfile('header.html', {'type':'Passphrase'}) -dict = EvalDict(vars()) -here = os.path.dirname(__file__) -form_path = os.path.join(here, 'ppform.html') -with open(form_path, encoding="utf-8") as f: - sys.stdout.write(f.read() % dict) - -usewords = [ ] -totalen = 0 -for i in range(minlen, maxlen+1): - totalen += len(words[i]) * i - usewords.extend(words[i]) - -log_2 = log(2) -wordbits = log(len(usewords)) / log_2 -medlen = len(usewords[len(usewords)//2]) -avglen = totalen / len(usewords) -if randcaps == 'first': - capbits = 1 - capmult = 2 -elif randcaps == 'one': - # Use estimate based on average word length - capbits = log(avglen+1) / log_2 - capmult = avglen+1 -elif randcaps == 'all': - # One bit per average word length - capbits = avglen - capmult = 2**avglen -else: - capbits = 0 - capmult = 1 -numbits = log(10) * numlen / log_2 -numfmt = '{{:0{}}}'.format(numlen) -passbits = (wordbits + capbits) * wordcount + numbits * ((wordcount - 1) or 1) - -table_start() -row('Word count', commafy(len(usewords))) -row('Average word length', '%.2f' % avglen) -row('Bits per word', '%.2f' % wordbits) -row('Bits per number', '%.2f' % numbits) -row('Bits for capitalization', '%.2f' % (capbits * wordcount)) -row('Effective passphrase bits', int(passbits)) -row('Total possible combinations', - commafy(len(usewords)**wordcount * (10**numlen)**((wordcount-1) or 1) * capmult**wordcount)) -table_end() - -randval = LongRandom() - -table_start() -print("Passphrase") -for i in range(10): - passphrase = '' - for j in range(wordcount): - word = usewords[randval.get(len(usewords))] - if randcaps == 'first': - if randval.get(2): - word = word[0].upper() + word[1:] - elif randcaps == 'one': - k = randval.get(len(word)+1) - if k < len(word): - word = word[:k] + word[k].upper() + word[k+1:] - elif randcaps == 'all': - word = ''.join([ randval.get(2) and ch.upper() or ch.lower() - for ch in word ]) - passphrase += word - if numlen > 0 and (j < wordcount-1 or wordcount == 1): - passphrase += numfmt.format(randval.get(10 ** numlen)) - - print("%s" % escape(passphrase)) - -table_end() -dumpfile('footer.html') diff --git a/pwform.html b/pwform.html deleted file mode 100644 index b0c94a4..0000000 --- a/pwform.html +++ /dev/null @@ -1,25 +0,0 @@ -
-
- - - - - - - - - - - - - - -
Password Length:
Use upper-case Letters
Use lower-case Letters
Use digits
Use all punctuation
Exclude 1/l/I and 0/O
Extra Symbols:
- -
-
- -

[ Click here for the passphrase generator ]

diff --git a/pwgen.cgi b/pwgen.cgi deleted file mode 100755 index 80c7e00..0000000 --- a/pwgen.cgi +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env python3 -import os -import runpy - -# Directory this CGI script lives in -here = os.path.dirname(__file__) - -# Path to the real script -script = os.path.join(here, "pwgen.py") - -# Execute ppgen.py as if it were the main script -runpy.run_path(script, run_name="__main__") \ No newline at end of file diff --git a/pwgen.py b/pwgen.py deleted file mode 100755 index d396ade..0000000 --- a/pwgen.py +++ /dev/null @@ -1,74 +0,0 @@ -#!/usr/bin/python3 -from math import * -import random -import string -import sys -import os - -from common import * -from longrandom import LongRandom - -print("Content-Type: text/html") -print() - -qs = os.environ.get('QUERY_STRING', None) -if not qs: - use_lcase = use_digits = 1 - use_punct = use_ucase = 0 - passlen = 64 - length_is_bits = 1 - extra_symbols = '' - excludes = True -else: - form = cgi.FieldStorage() - passlen = form_get(form, 'passlen', 12, int) - length_is_bits = form_get(form, 'length_is_bits', 0, int) - use_ucase = form_get(form, 'use_ucase', 0, int) - use_lcase = form_get(form, 'use_lcase', 0, int) - use_digits = form_get(form, 'use_digits', 0, int) - use_punct = form_get(form, 'use_punct', 0, int) - extra_symbols = form_get(form, 'extra_symbols', '', str) - excludes = form_get(form, 'excludes', 0, int) - -dumpfile('header.html', {'type': 'Password'}) -dict = EvalDict(vars()) -here = os.path.dirname(__file__) -form_path = os.path.join(here, 'pwform.html') -with open(form_path, encoding="utf-8") as f: - sys.stdout.write(f.read() % dict) - - -chars = extra_symbols -if use_lcase: chars += 'abcdefghijklmnopqrstuvwxyz' -if use_ucase: chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' -if use_digits: chars += '0123456789' -if use_punct: chars += '~`!@#$%^&*()-_=+[]{}\\|;:,./<>?\'"' -if excludes: - for char in '1lI0O': - chars = chars.replace(char, '') - -charbits = log(len(chars)) / log(2) -if length_is_bits: - passlen = int(passlen // charbits + 1) -passbits = passlen * charbits - -table_start() -row('Password Length', passlen) -row('Bits per character', '%.2f'%charbits) -row('Effective password bits', int(passbits)) -row('Total possible combinations', - commafy(len(chars) ** passlen)) -table_end() - -randval = LongRandom() - -table_start() -print("Password") -for i in range(10): - password = ''.join([ chars[randval.get(len(chars))] - for j in range(passlen) ]) - - print("%s" % escape(password)) - -table_end() -dumpfile('footer.html') diff --git a/worddict.py b/worddict.py deleted file mode 100644 index da4b9d9..0000000 --- a/worddict.py +++ /dev/null @@ -1,37 +0,0 @@ -# worddict.py -# -# Builds: -# words[length] = [word1, word2, ...] -# -# Designed for ppgen.py and the dwyl words_alpha.txt word list. - -import os -from collections import defaultdict - -WORDLIST_PATH = os.path.join(os.path.dirname(__file__), "words.txt") - -words = defaultdict(list) - -def load_wordlist(): - if not os.path.exists(WORDLIST_PATH): - raise FileNotFoundError( - f"Missing wordlist file: {WORDLIST_PATH}\n" - "Download https://github.com/dwyl/english-words/blob/master/words_alpha.txt " - "and save it as words.txt" - ) - - with open(WORDLIST_PATH, "r", encoding="utf-8") as f: - for line in f: - w = line.strip() - if not w: - continue - - # dwyl list is already lowercase and alphabetical — - # but we'll enforce that anyway: - if not w.isalpha(): - continue - - words[len(w)].append(w) - -# Load at import time -load_wordlist()