mirror of
https://github.com/kopia/kopia.git
synced 2026-01-26 15:28:06 -05:00
168 lines
4.9 KiB
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
|
|
}
|