-
Notifications
You must be signed in to change notification settings - Fork 4
/
hibp.go
170 lines (140 loc) · 5.19 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
package main
import (
"context"
"crypto/sha1"
"fmt"
"sort"
"github.com/fatih/color"
hibpapi "github.com/gopasspw/gopass-hibp/pkg/hibp/api"
hibpdump "github.com/gopasspw/gopass-hibp/pkg/hibp/dump"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/debug"
"github.com/gopasspw/gopass/pkg/gopass"
"github.com/gopasspw/gopass/pkg/termio"
)
type hibp struct {
gp gopass.Store
}
// CheckAPI checks your secrets against the HIBPv2 API.
func (s *hibp) CheckAPI(ctx context.Context, force bool) error {
if !force && !termio.AskForConfirmation(ctx, "This command is checking all your secrets against the haveibeenpwned.com API.\n\nThis will send five bytes of each passwords SHA1 hash to an untrusted server!\n\nYou will be asked to unlock all your secrets!\nDo you want to continue?") {
return fmt.Errorf("user aborted")
}
shaSums, sortedShaSums, err := s.precomputeHashes(ctx)
if err != nil {
return err
}
fmt.Println("Checking pre-computed SHA1 hashes against the HIBP API ...")
// compare the prepared list against all provided files
matchList := make([]string, 0, len(sortedShaSums))
for _, shaSum := range sortedShaSums {
freq, err := hibpapi.Lookup(shaSum)
if err != nil {
fmt.Printf("Failed to check HIBP API: %s\n", err)
continue
}
if freq < 1 {
continue
}
if pw, found := shaSums[shaSum]; found {
matchList = append(matchList, pw)
}
}
return s.printMatches(matchList)
}
// CheckDump checks your secrets against the provided HIBPv2 Dumps.
func (s *hibp) CheckDump(ctx context.Context, force bool, dumps []string) error {
fmt.Println("Using the HIBPv2 dumps is very expensive. If you can condone leaking a few bits of entropy per secret you should probably use the '--api' flag.")
if len(dumps) < 1 {
return fmt.Errorf("need a least one dump file")
}
// New also checks if there is at least one valid dump file given
scanner, err := hibpdump.New(dumps...)
if err != nil {
return fmt.Errorf("failed to create new HIBP Dump scanner: %w", err)
}
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?", dumps)) {
return fmt.Errorf("user aborted")
}
shaSums, sortedShaSums, err := s.precomputeHashes(ctx)
if err != nil {
return err
}
fmt.Println("Checking hashes against the provided dumps. This will take a while.")
matchedSums := scanner.LookupBatch(ctx, sortedShaSums)
debug.Log("In: %+v - Out: %+v", sortedShaSums, matchedSums)
matchList := make([]string, 0, len(matchedSums))
for _, matchedSum := range matchedSums {
if pw, found := shaSums[matchedSum]; found {
matchList = append(matchList, pw)
}
}
return s.printMatches(matchList)
}
func (s *hibp) precomputeHashes(ctx context.Context) (map[string]string, []string, error) {
// 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)
pwList, err := s.gp.List(ctx)
if err != nil {
return nil, nil, err
}
// 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 := termio.NewProgressBar(int64(len(pwList)))
bar.Hidden = ctxutil.IsHidden(ctx)
fmt.Println("Computing SHA1 hashes of all your secrets ...")
for _, secret := range pwList {
// check for context cancelation
select {
case <-ctx.Done():
return nil, nil, fmt.Errorf("user aborted")
default:
}
bar.Inc()
// 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.gp.Get(ctx, secret, "latest")
if err != nil {
fmt.Printf("\n" + color.YellowString("Failed to retrieve secret '%s': %s\n", secret, err))
continue
}
pw := sec.Password()
// do not check empty passwords, there should be caught by `gopass audit`
// anyway
if len(pw) < 1 {
continue
}
sum := sha1hex(pw)
shaSums[sum] = secret
sortedShaSums = append(sortedShaSums, sum)
}
bar.Done()
// IMPORTANT: sort after all entries have been added. without the sort
// the stream compare will not work
sort.Strings(sortedShaSums)
return shaSums, sortedShaSums, nil
}
func (s *hibp) printMatches(matchList []string) error {
if len(matchList) < 1 {
fmt.Println("Good news - No matches found!")
return nil
}
sort.Strings(matchList)
fmt.Println("Oh no - Found some matches:")
for _, m := range matchList {
fmt.Printf("\t- %s\n", m)
}
fmt.Println("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("weak passwords found")
}
func sha1hex(data string) string {
h := sha1.New()
_, _ = h.Write([]byte(data))
return fmt.Sprintf("%X", h.Sum(nil))
}