forked from gopasspw/gopass
/
hibp.go
194 lines (174 loc) · 5.75 KB
/
hibp.go
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
package action
import (
"bufio"
"context"
"crypto/sha1"
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
"time"
"github.com/fatih/color"
"github.com/justwatchcom/gopass/utils/notify"
"github.com/justwatchcom/gopass/utils/out"
"github.com/justwatchcom/gopass/utils/termio"
"github.com/muesli/goprogressbar"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// HIBP compares all entries from the store against the provided SHA1 sum dumps
func (s *Action) HIBP(ctx context.Context, c *cli.Context) error {
force := c.Bool("force")
fns := strings.Split(os.Getenv("HIBP_DUMPS"), ",")
if len(fns) < 1 || fns[0] == "" {
return errors.Errorf("Please provide the name(s) of the haveibeenpwned.com password dumps in HIBP_DUMPS. See https://haveibeenpwned.com/Passwords for more information")
}
if !force && !termio.AskForConfirmation(ctx, fmt.Sprintf("This command is checking all your secrets against the haveibeenpwned.com hashes in %+v.\nYou will be asked to unlock all your secrets!\nDo you want to continue?", fns)) {
return errors.Errorf("user aborted")
}
// build a map of all secrets sha sums to their names and also build a sorted (!)
// list of this shasums. As the hibp dump is already sorted this allows for
// a very efficient stream compare in O(n)
t, err := s.Store.Tree(ctx)
if err != nil {
return exitError(ctx, ExitList, err, "failed to list store: %s", err)
}
pwList := t.List(0)
// map sha1sum back to secret name for reporting
shaSums := make(map[string]string, len(pwList))
// build list of sha1sums (must be sorted later!) for stream comparison
sortedShaSums := make([]string, 0, len(shaSums))
// display progress bar
bar := &goprogressbar.ProgressBar{
Total: int64(len(pwList)),
Width: 120,
}
if out.IsHidden(ctx) {
old := goprogressbar.Stdout
goprogressbar.Stdout = ioutil.Discard
defer func() {
goprogressbar.Stdout = old
}()
}
out.Print(ctx, "Computing SHA1 hashes of all your secrets ...")
for _, secret := range pwList {
// check for context cancelation
select {
case <-ctx.Done():
return exitError(ctx, ExitAborted, nil, "user aborted")
default:
}
bar.Current++
bar.Text = fmt.Sprintf("%d of %d secrets computed", bar.Current, bar.Total)
bar.LazyPrint()
// only handle secrets / passwords, never the body
// comparing the body is super hard, as every user may choose to use
// the body of a secret differently. In the future we may support
// go templates to extract and compare data from the body
sec, err := s.Store.Get(ctx, secret)
if err != nil {
out.Print(ctx, "\n"+color.YellowString("Failed to retrieve secret '%s': %s", secret, err))
continue
}
// do not check empty passwords, there should be caught by `gopass audit`
// anyway
if len(sec.Password()) < 1 {
continue
}
sum := sha1sum(sec.Password())
shaSums[sum] = secret
sortedShaSums = append(sortedShaSums, sum)
}
out.Print(ctx, "")
// IMPORTANT: sort after all entries have been added. without the sort
// the stream compare will not work
sort.Strings(sortedShaSums)
out.Print(ctx, "Checking pre-computed SHA1 hashes against the blacklists ...")
matches := make(chan string, 1000)
done := make(chan struct{})
// compare the prepared list against all provided files. with a little more
// code this could be parallelized
for _, fn := range fns {
go s.findHIBPMatches(ctx, fn, shaSums, sortedShaSums, matches, done)
}
matchList := make([]string, 0, 100)
go func() {
for match := range matches {
matchList = append(matchList, match)
}
}()
for range fns {
<-done
}
return s.printHIBPMatches(ctx, matchList)
}
func (s *Action) printHIBPMatches(ctx context.Context, matchList []string) error {
if len(matchList) < 1 {
_ = notify.Notify("gopass - audit HIBP", "Good news - No matches found!")
out.Green(ctx, "Good news - No matches found!")
return nil
}
sort.Strings(matchList)
_ = notify.Notify("gopass - audit HIBP", fmt.Sprintf("Oh no - found %d matches", len(matchList)))
out.Red(ctx, "Oh no - Found some matches:")
for _, m := range matchList {
out.Red(ctx, "\t- %s", m)
}
out.Cyan(ctx, "The passwords in the listed secrets were included in public leaks in the past. This means they are likely included in many word-list attacks and provide only very little security. Strongly consider changing those passwords!")
return exitError(ctx, ExitAudit, nil, "weak passwords found")
}
func (s *Action) findHIBPMatches(ctx context.Context, fn string, shaSums map[string]string, sortedShaSums []string, matches chan<- string, done chan<- struct{}) {
defer func() {
done <- struct{}{}
}()
t0 := time.Now()
fh, err := os.Open(fn)
if err != nil {
out.Red(ctx, "Failed to open file %s: %s", fn, err)
return
}
defer func() {
_ = fh.Close()
}()
out.Debug(ctx, "Checking file %s ...\n", fn)
// index in sortedShaSums
i := 0
lineNo := 0
numMatches := 0
scanner := bufio.NewScanner(fh)
SCAN:
for scanner.Scan() {
// check for context cancelation
select {
case <-ctx.Done():
break SCAN
default:
}
line := strings.TrimSpace(scanner.Text())
lineNo++
if i >= len(sortedShaSums) {
break
}
if line == sortedShaSums[i] {
matches <- shaSums[line]
out.Debug(ctx, "MATCH at line %d: %s / %s from %s", lineNo, line, shaSums[line], fn)
numMatches++
// advance to next sha sum from store and next line in file
i++
continue
}
// advance in sha sums from store until we've reached the position in
// the file
for i < len(sortedShaSums) && line > sortedShaSums[i] {
i++
}
}
d0 := time.Since(t0)
out.Debug(ctx, "Found %d matches in %d lines from %s in %.2fs (%.2f lines / s)\n", numMatches, lineNo, fn, d0.Seconds(), float64(lineNo)/d0.Seconds())
}
func sha1sum(data string) string {
h := sha1.New()
_, _ = h.Write([]byte(data))
return strings.ToUpper(fmt.Sprintf("%x", h.Sum(nil)))
}