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()