change to golang
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
__pycache__/*
|
||||
29
Dockerfile
29
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"]
|
||||
|
||||
CMD ["/app/passgen"]
|
||||
69
common.py
69
common.py
@@ -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 = '<b>'
|
||||
post = '</b>'
|
||||
print('<tr><th align=right>%s:</th><td><tt>%s%s%s</tt></td></tr>' % (
|
||||
a, pre, b, post ))
|
||||
def table_start(): print('<hr><div align=center><table border=1>')
|
||||
def table_end(): print('</table></div>')
|
||||
|
||||
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)
|
||||
17
docker-compose.yml
Normal file
17
docker-compose.yml
Normal file
@@ -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
|
||||
20
footer.html
20
footer.html
@@ -1,20 +0,0 @@
|
||||
<hr />
|
||||
|
||||
<p>Notes: <ul>
|
||||
|
||||
<li>No data generated by this page is stored on the server at any point.
|
||||
In other words, we aren't recording your passwords.</li>
|
||||
|
||||
<li>The data used to generate the passwords is derived from Linux's
|
||||
/dev/urandom secure data source, and is carefully masked to prevent
|
||||
biasing or truncation.</li>
|
||||
|
||||
<li>Unless this form is accessed via a secure session (ie HTTPS or a
|
||||
tunnel), the password will be susceptible to being sniffed as it is
|
||||
being transmitted. Use the appropriate precautions.</li>
|
||||
|
||||
</ul></p>
|
||||
|
||||
<hr />
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Secure %(type)s Generator</title>
|
||||
</head>
|
||||
<body>
|
||||
<div align=center><h1>Secure %(type)s Generator</h1></div><hr>
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
print("Content-Type: text/html")
|
||||
print("Status: 302 Found")
|
||||
print("Location: /cgi-bin/ppgen.py")
|
||||
print()
|
||||
10
index.html
10
index.html
@@ -1,10 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="refresh" content="0; url=/cgi-bin/ppgen.cgi">
|
||||
<title>Secure Passphrase Generator</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Redirecting to <a href="/cgi-bin/ppgen.cgi">Secure Passphrase Generator</a>…</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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
|
||||
577
main.go
Normal file
577
main.go
Normal file
@@ -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(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Secure Passphrase Generator</title>
|
||||
<style>
|
||||
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 2rem; background: #f6f7fb; }
|
||||
h1 { margin-bottom: 0.5rem; }
|
||||
form { background: #fff; padding: 1rem 1.5rem; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); max-width: 650px; }
|
||||
label { display: inline-block; width: 180px; }
|
||||
input, select { margin-bottom: 0.5rem; padding: 0.25rem 0.4rem; }
|
||||
button { padding: 0.4rem 0.8rem; border-radius: 4px; border: 1px solid #0d6efd; background: #0d6efd; color: #fff; cursor: pointer; }
|
||||
button:hover { background: #0b5ed7; }
|
||||
table { border-collapse: collapse; margin-top: 1rem; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
||||
th, td { padding: 0.5rem 0.75rem; border-bottom: 1px solid #eee; }
|
||||
th { background: #f0f2f8; text-align: left; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tt { font-family: "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
||||
.error { color: #b00020; margin-top: 0.5rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Secure Passphrase Generator</h1>
|
||||
|
||||
<form method="GET" action="/">
|
||||
<div>
|
||||
<label for="wordcount">Word count</label>
|
||||
<input type="number" id="wordcount" name="wordcount" min="1" value="{{.WordCount}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="minlen">Min word length</label>
|
||||
<input type="number" id="minlen" name="minlen" min="1" value="{{.MinLen}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="maxlen">Max word length</label>
|
||||
<input type="number" id="maxlen" name="maxlen" min="1" value="{{.MaxLen}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="numlen">Digits per separator</label>
|
||||
<input type="number" id="numlen" name="numlen" min="0" value="{{.NumLen}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="symcount">Symbols per separator</label>
|
||||
<input type="number" id="symcount" name="symcount" min="0" value="{{.SymbolCount}}">
|
||||
</div>
|
||||
<div>
|
||||
<label for="randcaps">Capitalization</label>
|
||||
<select id="randcaps" name="randcaps">
|
||||
<option value="first" {{if eq .RandCaps "first"}}selected{{end}}>Random first letter</option>
|
||||
<option value="one" {{if eq .RandCaps "one"}}selected{{end}}>Random one letter</option>
|
||||
<option value="all" {{if eq .RandCaps "all"}}selected{{end}}>Randomize all letters</option>
|
||||
<option value="none" {{if eq .RandCaps "none"}}selected{{end}}>None</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style="margin-top: 0.5rem;">
|
||||
<button type="submit">Generate Passphrase</button>
|
||||
</div>
|
||||
{{if .Error}}
|
||||
<div class="error">{{.Error}}</div>
|
||||
{{end}}
|
||||
</form>
|
||||
|
||||
{{if not .Error}}
|
||||
<table>
|
||||
<tr><th colspan="2">Statistics</th></tr>
|
||||
<tr><td>Word count</td><td>{{.TotalWords}}</td></tr>
|
||||
<tr><td>Average word length</td><td>{{printf "%.2f" .AverageWordLength}}</td></tr>
|
||||
<tr><td>Bits per word</td><td>{{printf "%.2f" .BitsPerWord}}</td></tr>
|
||||
<tr><td>Bits per separator</td><td>{{printf "%.2f" .BitsPerSeparator}}</td></tr>
|
||||
<tr><td>Bits for capitalization</td><td>{{printf "%.2f" .BitsForCapsTotal}}</td></tr>
|
||||
<tr><td>Effective passphrase bits</td><td>{{.EffectiveBits}}</td></tr>
|
||||
<tr><td>Total possible combinations</td><td>{{.TotalCombinations}}</td></tr>
|
||||
</table>
|
||||
|
||||
<table>
|
||||
<tr><th>Passphrase</th></tr>
|
||||
{{range .GeneratedPhrases}}
|
||||
<tr><td><tt>{{.}}</tt></td></tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
</body>
|
||||
</html>
|
||||
`))
|
||||
|
||||
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, ",")
|
||||
}
|
||||
16
makedict
16
makedict
@@ -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)
|
||||
33
ppform.html
33
ppform.html
@@ -1,33 +0,0 @@
|
||||
<form method=get action="ppgen.cgi">
|
||||
<div align=center>
|
||||
<table border=0>
|
||||
<tr>
|
||||
<td>Number of words:</td>
|
||||
<td><input name="wordcount" value="%(wordcount)s" size="2" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Minimum word length:</td>
|
||||
<td><input name="minlen" value="%(minlen)s" size="2" /> characters</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Maximum word length:</td>
|
||||
<td><input name="maxlen" value="%(maxlen)s" size="2" /> characters</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Random capitalization:</td>
|
||||
<td><select name="randcaps">
|
||||
<option value="none" %(randcaps=='none' and 'selected' or '')s>No capitalization</option>
|
||||
<option value="first" %(randcaps=='first' and 'selected' or '')s>Randomly capitalize the first letter</option>
|
||||
<option value="one" %(randcaps=='one' and 'selected' or '')s>Randomly capitalize any one letter</option>
|
||||
<option value="all" %(randcaps=='all' and 'selected' or '')s>Randomly capitalize all letters</option>
|
||||
</select></td>
|
||||
<tr>
|
||||
<td>Length of numbers between words:</td>
|
||||
<td><input name="numlen" value="%(numlen)s" size="2" /> digits</td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type=submit name=submit value="Generate Passphrase"><input type=reset>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p align="center">[ <a href="pwgen.cgi">Click here for the password generator</a> ]</p>
|
||||
12
ppgen.cgi
12
ppgen.cgi
@@ -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__")
|
||||
102
ppgen.py
102
ppgen.py
@@ -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("<tr><th>Passphrase</th></tr>")
|
||||
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("<tr><td><tt>%s</tt></td></tr>" % escape(passphrase))
|
||||
|
||||
table_end()
|
||||
dumpfile('footer.html')
|
||||
25
pwform.html
25
pwform.html
@@ -1,25 +0,0 @@
|
||||
<form method=get action="pwgen.cgi">
|
||||
<div align=center>
|
||||
<table border=0>
|
||||
<tr>
|
||||
<td>Password Length:</td>
|
||||
<td><input name=passlen value="%(passlen)s" size="2" /><select name=length_is_bits>
|
||||
<option value=0 %(length_is_bits==0 and 'selected' or '')s>Characters
|
||||
<option value=1 %(length_is_bits!=0 and 'selected' or '')s>Bits
|
||||
</select></td>
|
||||
</tr>
|
||||
<tr><td colspan=2><input type=checkbox name=use_ucase value=1 %(use_ucase and 'checked' or '')s>Use upper-case Letters</td></tr>
|
||||
<tr><td colspan=2><input type=checkbox name=use_lcase value=1 %(use_lcase and 'checked' or '')s>Use lower-case Letters</td></tr>
|
||||
<tr><td colspan=2><input type=checkbox name=use_digits value=1 %(use_digits and 'checked' or '')s>Use digits</td></tr>
|
||||
<tr><td colspan=2><input type=checkbox name=use_punct value=1 %(use_punct and 'checked' or '')s>Use all punctuation</td></tr>
|
||||
<tr><td colspan=2><input type=checkbox name=excludes value=1 %(excludes and 'checked' or '')s>Exclude 1/l/I and 0/O</td></tr>
|
||||
<tr>
|
||||
<td>Extra Symbols:</td>
|
||||
<td><input name=extra_symbols value="%(extra_symbols)s"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<input type=submit name=submit value="Generate Password"><input type=reset>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p align="center">[ <a href="ppgen.cgi">Click here for the passphrase generator</a> ]</p>
|
||||
12
pwgen.cgi
12
pwgen.cgi
@@ -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__")
|
||||
74
pwgen.py
74
pwgen.py
@@ -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("<tr><th>Password</th></tr>")
|
||||
for i in range(10):
|
||||
password = ''.join([ chars[randval.get(len(chars))]
|
||||
for j in range(passlen) ])
|
||||
|
||||
print("<tr><td><tt>%s</tt></td></tr>" % escape(password))
|
||||
|
||||
table_end()
|
||||
dumpfile('footer.html')
|
||||
37
worddict.py
37
worddict.py
@@ -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()
|
||||
Reference in New Issue
Block a user