-
-
Notifications
You must be signed in to change notification settings - Fork 2
/
hibp.go
266 lines (220 loc) Β· 7.04 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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
// Package hibp provides Go binding to all 3 APIs of the "Have I Been Pwned" by Troy Hunt
package hibp
import (
"bytes"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
)
// Version represents the version of this package
const Version = "1.0.5"
// BaseURL is the base URL for the majority of API endpoints
const BaseURL = "https://haveibeenpwned.com/api/v3"
// PasswdBaseURL is the base URL for the pwned passwords API endpoints
const PasswdBaseURL = "https://api.pwnedpasswords.com"
// DefaultUserAgent defines the default UA string for the HTTP client
// Currently the URL in the UA string is comment out, as there is a bug in the HIBP API
// not allowing multiple slashes
const DefaultUserAgent = `go-hibp/` + Version + ` (+https://github.com/wneessen/go-hibp)`
// DefaultTimeout is the default timeout value for the HTTP client
const DefaultTimeout = time.Second * 5
// List of common errors
var (
// ErrNoAccountID is returned if no account ID is given to the corresponding API method
ErrNoAccountID = errors.New("no account ID given")
// ErrNoName is returned if no name is given to the corresponding API method
ErrNoName = errors.New("no name given")
// ErrNonPositiveResponse should be returned if a HTTP request failed with a non HTTP-200 status
ErrNonPositiveResponse = errors.New("non HTTP-200 response for HTTP request")
// ErrPrefixLengthMismatch should be used if a given prefix does not match the
// expected length
ErrPrefixLengthMismatch = errors.New("password hash prefix must be 5 characters long")
// ErrSHA1LengthMismatch should be used if a given SHA1 checksum does not match the
// expected length
ErrSHA1LengthMismatch = errors.New("SHA1 hash size needs to be 160 bits")
// ErrNTLMLengthMismatch should be used if a given NTLM hash does not match the
// expected length
ErrNTLMLengthMismatch = errors.New("NTLM hash size needs to be 128 bits")
// ErrSHA1Invalid should be used if a given string does not represent a valid SHA1 hash
ErrSHA1Invalid = errors.New("not a valid SHA1 hash")
// ErrNTLMInvalid should be used if a given string does not represent a valid NTLM hash
ErrNTLMInvalid = errors.New("not a valid NTLM hash")
// ErrUnsupportedHashMode should be used if a given hash mode is not supported
ErrUnsupportedHashMode = errors.New("hash mode not supported")
)
// Client is the HIBP client object
type Client struct {
hc *http.Client // HTTP client to perform the API requests
to time.Duration // HTTP client timeout
ak string // HIBP API key
ua string // User agent string for the HTTP client
// If set to true, the HTTP client will sleep instead of failing in case the HTTP 429
// rate limit hits a request
rlSleep bool
PwnedPassAPI *PwnedPassAPI // Reference to the PwnedPassAPI API
PwnedPassAPIOpts *PwnedPasswordOptions // Additional options for the PwnedPassAPI API
BreachAPI *BreachAPI // Reference to the BreachAPI
PasteAPI *PasteAPI // Reference to the PasteAPI
}
// Option is a function that is used for grouping of Client options.
type Option func(*Client)
// New creates and returns a new HIBP client object
func New(options ...Option) Client {
c := Client{}
// Set defaults
c.to = DefaultTimeout
c.PwnedPassAPIOpts = &PwnedPasswordOptions{
HashMode: HashModeSHA1,
WithPadding: false,
}
c.ua = DefaultUserAgent
// Set additional options
for _, opt := range options {
if opt == nil {
continue
}
opt(&c)
}
// Add a http client to the Client object
c.hc = httpClient(c.to)
// Associate the different HIBP service APIs with the Client
c.PwnedPassAPI = &PwnedPassAPI{
hibp: &c,
ParamMap: make(map[string]string),
}
c.BreachAPI = &BreachAPI{hibp: &c}
c.PasteAPI = &PasteAPI{hibp: &c}
return c
}
// WithHTTPTimeout overrides the default http client timeout
func WithHTTPTimeout(t time.Duration) Option {
return func(c *Client) {
c.to = t
}
}
// WithAPIKey set the optional API key to the Client object
func WithAPIKey(k string) Option {
return func(c *Client) {
c.ak = k
}
}
// WithPwnedPadding enables padding-mode for the PwnedPasswords API client
func WithPwnedPadding() Option {
return func(c *Client) {
c.PwnedPassAPIOpts.WithPadding = true
}
}
// WithUserAgent sets a custom user agent string for the HTTP client
func WithUserAgent(a string) Option {
if a == "" {
return nil
}
return func(c *Client) {
c.ua = a
}
}
// WithRateLimitSleep let's the HTTP client sleep in case the API rate limiting hits (Defaults to fail)
func WithRateLimitSleep() Option {
return func(c *Client) {
c.rlSleep = true
}
}
// WithPwnedNTLMHash sets the hash mode for the PwnedPasswords API to NTLM hashes
//
// Note: This option only affects the generic methods like PwnedPassAPI.CheckPassword
// or PwnedPassAPI.ListHashesPassword. For any specifc method with the hash type in
// the method name, this option is ignored and the hash type of the function is
// forced
func WithPwnedNTLMHash() Option {
return func(c *Client) {
c.PwnedPassAPIOpts.HashMode = HashModeNTLM
}
}
// HTTPReq performs an HTTP request to the corresponding API
func (c *Client) HTTPReq(m, p string, q map[string]string) (*http.Request, error) {
u, err := url.Parse(p)
if err != nil {
return nil, err
}
if m == http.MethodGet {
uq := u.Query()
for k, v := range q {
uq.Add(k, v)
}
u.RawQuery = uq.Encode()
}
hr, err := http.NewRequest(m, u.String(), nil)
if err != nil {
return nil, err
}
if m == http.MethodPost {
pd := url.Values{}
for k, v := range q {
pd.Add(k, v)
}
rb := io.NopCloser(bytes.NewBufferString(pd.Encode()))
hr.Body = rb
}
hr.Header.Set("Accept", "application/json")
hr.Header.Set("user-agent", c.ua)
if c.ak != "" {
hr.Header.Set("hibp-api-key", c.ak)
}
if c.PwnedPassAPIOpts.WithPadding {
hr.Header.Set("Add-Padding", "true")
}
return hr, nil
}
// HTTPResBody performs the API call to the given path and returns the response body as byte array
func (c *Client) HTTPResBody(m string, p string, q map[string]string) ([]byte, *http.Response, error) {
hreq, err := c.HTTPReq(m, p, q)
if err != nil {
return nil, nil, err
}
hr, err := c.hc.Do(hreq)
if err != nil {
return nil, hr, err
}
defer func() {
_ = hr.Body.Close()
}()
hb, err := io.ReadAll(hr.Body)
if err != nil {
return nil, hr, err
}
if hr.StatusCode == 429 && c.rlSleep {
headerDelay := hr.Header.Get("Retry-After")
delayTime, err := time.ParseDuration(headerDelay + "s")
if err != nil {
return nil, hr, err
}
log.Printf("API rate limit hit. Retrying request in %s", delayTime.String())
time.Sleep(delayTime)
return c.HTTPResBody(m, p, q)
}
if hr.StatusCode != 200 {
return nil, hr, fmt.Errorf("HTTP %s: %w", hr.Status, ErrNonPositiveResponse)
}
return hb, hr, nil
}
// httpClient returns a custom http client for the HIBP Client object
func httpClient(to time.Duration) *http.Client {
tc := &tls.Config{
MaxVersion: tls.VersionTLS13,
MinVersion: tls.VersionTLS12,
}
ht := &http.Transport{TLSClientConfig: tc}
hc := &http.Client{
Transport: ht,
Timeout: DefaultTimeout,
}
if to.Nanoseconds() > 0 {
hc.Timeout = to
}
return hc
}