-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdiceware.go
More file actions
154 lines (134 loc) · 3.43 KB
/
Copy pathdiceware.go
File metadata and controls
154 lines (134 loc) · 3.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// Package diceware turns a diceware wordlist into passphrases.
package diceware
import (
"bufio"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"math"
"os"
"strings"
)
// LoadWords reads a wordlist from the file at path.
func LoadWords(path string) ([]string, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open wordlist %q: %w", path, err)
}
defer f.Close()
return ReadWords(f, fmt.Sprintf("%q", path))
}
// ReadWords reads a wordlist from r, accepting both "DDDDD<TAB>word" and bare
// word lines. name identifies the source in error messages.
func ReadWords(r io.Reader, name string) ([]string, error) {
words := make([]string, 0, 8192)
s := bufio.NewScanner(r)
lineNo := 0
for s.Scan() {
lineNo++
word, err := parseWord(s.Text(), lineNo)
if err != nil {
return nil, fmt.Errorf("parse wordlist %s: %w", name, err)
}
if word == "" {
continue
}
words = append(words, word)
}
if err := s.Err(); err != nil {
return nil, fmt.Errorf("scan wordlist %s: %w", name, err)
}
if len(words) == 0 {
return nil, fmt.Errorf("wordlist %s is empty", name)
}
return words, nil
}
func parseWord(line string, lineNo int) (string, error) {
trimmed := strings.TrimSpace(line)
if trimmed == "" {
return "", nil
}
// Cut before trimming: trimming first turns "11111\t" into the word "11111".
if code, wordPart, ok := strings.Cut(line, "\t"); ok {
if strings.ContainsRune(wordPart, '\t') {
return "", fmt.Errorf("line %d has invalid tab format", lineNo)
}
if !isDiceCode(strings.TrimSpace(code)) {
return "", fmt.Errorf("line %d has invalid dice code", lineNo)
}
word := strings.TrimSpace(wordPart)
if word == "" {
return "", fmt.Errorf("line %d has empty word", lineNo)
}
return word, nil
}
if strings.ContainsRune(trimmed, ' ') {
return "", fmt.Errorf("line %d is not a supported format", lineNo)
}
return trimmed, nil
}
func isDiceCode(s string) bool {
if len(s) != 5 {
return false
}
for _, r := range s {
if r < '1' || r > '6' {
return false
}
}
return true
}
// GenerateList returns passphraseCount passphrases, each one wordCount words
// joined by separator.
func GenerateList(words []string, passphraseCount int, wordCount int, separator string) ([]string, error) {
if passphraseCount <= 0 {
return nil, errors.New("passphrase count must be > 0")
}
out := make([]string, passphraseCount)
for i := range out {
phrase, err := Generate(words, wordCount)
if err != nil {
return nil, err
}
out[i] = strings.Join(phrase, separator)
}
return out, nil
}
// Generate returns wordCount words drawn from words uniformly at random, with
// replacement, using crypto/rand.
func Generate(words []string, wordCount int) ([]string, error) {
if len(words) == 0 {
return nil, errors.New("no words available")
}
if wordCount <= 0 {
return nil, fmt.Errorf("invalid word count %d", wordCount)
}
out := make([]string, wordCount)
for i := range out {
n, err := secureRandomIndex(len(words))
if err != nil {
return nil, fmt.Errorf("secure random index: %w", err)
}
out[i] = words[n]
}
return out, nil
}
func secureRandomIndex(limit int) (int, error) {
if limit <= 0 {
return 0, errors.New("limit must be > 0")
}
n := uint64(limit)
bound := (math.MaxUint64 / n) * n
var buf [8]byte
for {
if _, err := rand.Read(buf[:]); err != nil {
return 0, err
}
v := binary.LittleEndian.Uint64(buf[:])
if v < bound {
return int(v % n), nil
}
}
}