forked from gopasspw/gopass
-
Notifications
You must be signed in to change notification settings - Fork 0
/
hibp.go
166 lines (153 loc) · 4.9 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
package action
import (
"bufio"
"crypto/sha1"
"fmt"
"os"
"sort"
"strings"
"time"
"github.com/fatih/color"
"github.com/muesli/goprogressbar"
"github.com/urfave/cli"
)
// HIBP compares all entries from the store against the provided SHA1 sum dumps
func (s *Action) HIBP(c *cli.Context) error {
force := c.Bool("force")
fns := strings.Split(os.Getenv("HIBP_DUMPS"), ",")
if len(fns) < 1 || fns[0] == "" {
return fmt.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 && !s.askForConfirmation(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 fmt.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()
if err != nil {
return 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,
}
fmt.Println("Computing SHA1 hashes of all your secrets ...")
for _, secret := range pwList {
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
content, err := s.Store.GetFirstLine(secret)
if err != nil {
fmt.Println("\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(content) < 1 {
continue
}
sum := sha1sum(content)
shaSums[sum] = secret
sortedShaSums = append(sortedShaSums, sum)
}
fmt.Println("")
// IMPORTANT: sort after all entries have been added. without the sort
// the stream compare will not work
sort.Strings(sortedShaSums)
fmt.Println("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 findHIBPMatches(fn, shaSums, sortedShaSums, matches, done)
}
matchList := make([]string, 0, 100)
go func() {
for match := range matches {
matchList = append(matchList, match)
}
}()
for range fns {
<-done
}
if len(matchList) < 0 {
fmt.Println(color.GreenString("Good news - No matches found!"))
return nil
}
sort.Strings(matchList)
fmt.Println(color.RedString("Oh no - Found some matches:"))
for _, m := range matchList {
fmt.Println(color.RedString("\t- %s", m))
}
fmt.Println(color.CyanString("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 fmt.Errorf("Some matches found")
}
func findHIBPMatches(fn string, shaSums map[string]string, sortedShaSums []string, matches chan<- string, done chan<- struct{}) {
defer func() {
done <- struct{}{}
}()
t0 := time.Now()
var debug bool
if gdb := os.Getenv("GOPASS_DEBUG"); gdb != "" {
debug = true
}
fh, err := os.Open(fn)
if err != nil {
fmt.Println(color.RedString("Failed to open file %s: %s", fn, err))
return
}
defer func() {
_ = fh.Close()
}()
if debug {
fmt.Printf("Checking file %s ...\n", fn)
}
// index in sortedShaSums
i := 0
lineNo := 0
numMatches := 0
scanner := bufio.NewScanner(fh)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
lineNo++
if i >= len(sortedShaSums) {
break
}
if line == sortedShaSums[i] {
matches <- shaSums[line]
if debug {
fmt.Printf("MATCH at line %d: %s / %s from %s\n", 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++
}
}
if debug {
d0 := time.Since(t0)
fmt.Printf("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 []byte) string {
h := sha1.New()
_, _ = h.Write(data)
return strings.ToUpper(fmt.Sprintf("%x", h.Sum(nil)))
}