Files
kopia/cli/command_genpasswd.go

168 lines
4.9 KiB
Go

package cli
import (
"bytes"
"crypto/rand"
"fmt"
"io"
"io/ioutil"
"math"
"net/http"
"strings"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
genPasswordCommand = app.Command("genpasswd", "Generate memorable password - inspired by http://xkcd.com/936/")
genPasswordCount = genPasswordCommand.Flag("num-passwords", "Number of passwords to generate.").Short('n').Default("20").Int()
genPasswordWordsPerPassword = genPasswordCommand.Flag("words-per-password", "Number of words per password.").Short('w').Default("6").Int()
genPasswordWordsSource = genPasswordCommand.Flag("word-list", "Path or URL to the word list.").Default("http://kopia.github.io/words/en.txt").String()
genPasswordWordSeparator = genPasswordCommand.Flag("words-separator", "Word separator.").Default("-").Short('s').String()
genPasswordUppercase = genPasswordCommand.Flag("uppercase", "Use upper-case versions of words.").Short('u').Bool()
genPasswordCapitalize = genPasswordCommand.Flag("capitalize", "Use capitalized versions of words.").Short('c').Bool()
genPasswordL33t = genPasswordCommand.Flag("l33t", "Use common substitutions o->0, s->$, etc.").Short('l').Bool()
genPasswordMinWordLength = genPasswordCommand.Flag("min-word-length", "Minimum dictionary word length.").Default("4").Int()
genPasswordMaxWordLength = genPasswordCommand.Flag("max-word-length", "Maximum dictionary word length.").Default("8").Int()
l33tSubstTable = map[string]string{
"a": "4",
"e": "3",
"l": "1",
"s": "$",
"o": "0",
}
)
func init() {
genPasswordCommand.Action(runGenPassword)
}
func genleet(result *[]string, prefix string, suffix string) {
if len(suffix) == 0 {
return
}
*result = append(*result, prefix+suffix)
ch := suffix[0:1]
genleet(result, prefix+ch, suffix[1:])
if subst, ok := l33tSubstTable[ch]; ok {
genleet(result, prefix+subst, suffix[1:])
}
}
func l33t(w string) []string {
var result []string
genleet(&result, "", w)
return result
}
func getWordList() ([]string, error) {
var allWords []byte
var err error
// Read the words file, that is typically 2-5MB, not a big deal.
if strings.HasPrefix(*genPasswordWordsSource, "http") {
// File not found, try URL.
fmt.Printf("Downloading word list from %v ...\n", *genPasswordWordsSource)
resp, err := http.Get(*genPasswordWordsSource)
if err != nil {
return nil, err
}
defer resp.Body.Close()
allWords, err = ioutil.ReadAll(resp.Body)
} else {
fmt.Printf("Using word list from file: %v\n", *genPasswordWordsSource)
allWords, err = ioutil.ReadFile(*genPasswordWordsSource)
}
if err != nil {
return nil, err
}
words := bytes.Split(allWords, []byte("\n"))
var usableWords []string
for _, w := range words {
word := strings.TrimSpace(string(w))
if len(word) >= *genPasswordMinWordLength && len(word) <= *genPasswordMaxWordLength {
usableWords = append(usableWords, word)
if *genPasswordUppercase {
usableWords = append(usableWords, strings.ToUpper(word))
}
if *genPasswordCapitalize {
usableWords = append(usableWords, strings.ToUpper(word[0:1])+word[1:])
}
if *genPasswordL33t {
usableWords = append(usableWords, l33t(word)...)
}
}
}
if len(usableWords) < 500 {
return nil, fmt.Errorf("word list too short: %v entries", len(usableWords))
}
fmt.Printf("Got %v usable words.\n", len(usableWords))
fmt.Printf("\n")
return usableWords, nil
}
func runGenPassword(context *kingpin.ParseContext) error {
if *genPasswordWordsPerPassword < 1 || *genPasswordWordsPerPassword > 8 {
return fmt.Errorf("--words-per-password must be between 1 and 8")
}
usableWords, err := getWordList()
if err != nil {
return fmt.Errorf("unable to read word list: %v", err)
}
randomBitsPerWord := math.Log2(float64(len(usableWords)))
randomBitsPerSeparator := math.Log2(float64(len(*genPasswordWordSeparator)))
wordCharSetSize := 26
if *genPasswordUppercase || *genPasswordCapitalize {
wordCharSetSize *= 2
}
fmt.Printf("Memorable passwords, inspired by http://xkcd.com/936/ :\n")
fmt.Printf("\n")
for i := 0; i < *genPasswordCount; i++ {
pass := generatePasswordFromWords(usableWords)
fmt.Printf("%2d. %-60v blind entropy %.2f bits\n", i+1, pass, math.Log2(math.Pow(float64(wordCharSetSize), float64(len(pass)))))
}
fmt.Printf("\n")
fmt.Printf("Password entropy with full knowledge of the algorithm: %.2f bits.\n",
randomBitsPerWord*float64(*genPasswordWordsPerPassword)+
randomBitsPerSeparator*float64(len(*genPasswordWordSeparator)))
return nil
}
func secureRandomInt() int {
var b [4]byte
io.ReadFull(rand.Reader, b[:])
return ((int(b[0]) & 0x7F) << 24) | (int(b[1]) << 16) | (int(b[2]) << 8) | int(b[3])
}
func generatePasswordFromWords(words []string) string {
var result string
separators := *genPasswordWordSeparator
for i := 0; i < *genPasswordWordsPerPassword; i++ {
if i > 0 {
x := secureRandomInt() % len(separators)
result += separators[x : x+1]
}
result += words[secureRandomInt()%len(words)]
}
return result
}