2021-09-21 11:21:04 +02:00
|
|
|
package hibp
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"crypto/sha1"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
2022-06-08 17:26:41 +02:00
|
|
|
const (
|
|
|
|
// ErrPrefixLengthMismatch should be used if a given prefix does not match the
|
|
|
|
// expected length
|
|
|
|
ErrPrefixLengthMismatch = "password hash prefix must be 5 characters long"
|
|
|
|
|
|
|
|
// ErrSHA1LengthMismatch should be used if a given SHA1 checksum does not match the
|
|
|
|
// expected length
|
|
|
|
ErrSHA1LengthMismatch = "SHA1 hash size needs to be 160 bits"
|
|
|
|
)
|
|
|
|
|
2022-10-29 15:32:12 +02:00
|
|
|
// PwnedPassAPI is a HIBP Pwned Passwords API client
|
|
|
|
type PwnedPassAPI struct {
|
2021-09-21 19:46:48 +02:00
|
|
|
hibp *Client // References back to the parent HIBP client
|
2021-09-21 11:21:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Match represents a match in the Pwned Passwords API
|
|
|
|
type Match struct {
|
2021-09-21 19:46:48 +02:00
|
|
|
Hash string // SHA1 hash of the matching password
|
|
|
|
Count int64 // Represents the number of leaked accounts that hold/held this password
|
2021-09-21 11:21:04 +02:00
|
|
|
}
|
|
|
|
|
2021-09-21 18:21:23 +02:00
|
|
|
// PwnedPasswordOptions is a struct of additional options for the PP API
|
|
|
|
type PwnedPasswordOptions struct {
|
2021-09-21 19:46:48 +02:00
|
|
|
// WithPadding controls if the PwnedPassword API returns with padding or not
|
|
|
|
// See: https://haveibeenpwned.com/API/v3#PwnedPasswordsPadding
|
2021-09-21 18:21:23 +02:00
|
|
|
WithPadding bool
|
|
|
|
}
|
|
|
|
|
2021-09-21 11:21:04 +02:00
|
|
|
// CheckPassword checks the Pwned Passwords database against a given password string
|
2022-10-29 15:32:12 +02:00
|
|
|
func (p *PwnedPassAPI) CheckPassword(pw string) (*Match, *http.Response, error) {
|
2021-09-21 11:21:04 +02:00
|
|
|
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
|
|
|
return p.CheckSHA1(shaSum)
|
|
|
|
}
|
|
|
|
|
2022-06-08 17:26:41 +02:00
|
|
|
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password string
|
2022-10-29 15:32:12 +02:00
|
|
|
func (p *PwnedPassAPI) CheckSHA1(h string) (*Match, *http.Response, error) {
|
2022-06-08 17:26:41 +02:00
|
|
|
if len(h) != 40 {
|
|
|
|
return nil, nil, fmt.Errorf(ErrSHA1LengthMismatch)
|
|
|
|
}
|
|
|
|
|
|
|
|
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
|
2021-09-21 11:21:04 +02:00
|
|
|
if err != nil {
|
|
|
|
return &Match{}, hr, err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, m := range pwMatches {
|
|
|
|
if m.Hash == h {
|
|
|
|
return &m, hr, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil, hr, nil
|
|
|
|
}
|
|
|
|
|
2022-06-08 17:26:41 +02:00
|
|
|
// ListHashesPassword checks the Pwned Password API endpoint for all hashes based on a given
|
|
|
|
// password string and returns the a slice of Match as well as the http.Response
|
|
|
|
//
|
|
|
|
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
|
|
|
// contain junk data
|
2022-10-29 15:32:12 +02:00
|
|
|
func (p *PwnedPassAPI) ListHashesPassword(pw string) ([]Match, *http.Response, error) {
|
2022-06-08 17:26:41 +02:00
|
|
|
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
|
|
|
|
return p.ListHashesSHA1(shaSum)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListHashesSHA1 checks the Pwned Password API endpoint for all hashes based on a given
|
|
|
|
// SHA1 checksum and returns the a slice of Match as well as the http.Response
|
|
|
|
//
|
|
|
|
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
|
|
|
// contain junk data
|
2022-10-29 15:32:12 +02:00
|
|
|
func (p *PwnedPassAPI) ListHashesSHA1(h string) ([]Match, *http.Response, error) {
|
2022-06-08 17:26:41 +02:00
|
|
|
if len(h) != 40 {
|
|
|
|
return nil, nil, fmt.Errorf(ErrSHA1LengthMismatch)
|
|
|
|
}
|
|
|
|
return p.ListHashesPrefix(h[:5])
|
|
|
|
}
|
|
|
|
|
|
|
|
// ListHashesPrefix checks the Pwned Password API endpoint for all hashes based on a given
|
|
|
|
// SHA1 checksum prefix and returns the a slice of Match as well as the http.Response
|
|
|
|
//
|
|
|
|
// NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
|
|
|
|
// contain junk data
|
2022-10-29 15:32:12 +02:00
|
|
|
func (p *PwnedPassAPI) ListHashesPrefix(pf string) ([]Match, *http.Response, error) {
|
2022-06-08 17:26:41 +02:00
|
|
|
if len(pf) != 5 {
|
|
|
|
return nil, nil, fmt.Errorf(ErrPrefixLengthMismatch)
|
2022-05-08 12:44:20 +02:00
|
|
|
}
|
2022-10-29 15:32:12 +02:00
|
|
|
hreq, err := p.hibp.HTTPReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", pf),
|
2021-09-21 19:46:48 +02:00
|
|
|
nil)
|
2021-09-21 11:21:04 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
2021-09-21 18:21:23 +02:00
|
|
|
hr, err := p.hibp.hc.Do(hreq)
|
2021-09-21 11:21:04 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
if hr.StatusCode != 200 {
|
|
|
|
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
|
|
|
|
}
|
|
|
|
defer func() {
|
|
|
|
_ = hr.Body.Close()
|
|
|
|
}()
|
|
|
|
|
|
|
|
var pwMatches []Match
|
|
|
|
scanObj := bufio.NewScanner(hr.Body)
|
|
|
|
for scanObj.Scan() {
|
|
|
|
hp := strings.SplitN(scanObj.Text(), ":", 2)
|
2022-06-08 17:26:41 +02:00
|
|
|
fh := fmt.Sprintf("%s%s", strings.ToLower(pf), strings.ToLower(hp[0]))
|
2021-09-21 11:21:04 +02:00
|
|
|
hc, err := strconv.ParseInt(hp[1], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
continue
|
|
|
|
}
|
2022-06-09 16:21:10 +02:00
|
|
|
if hc == 0 {
|
|
|
|
continue
|
|
|
|
}
|
2021-09-21 11:21:04 +02:00
|
|
|
pwMatches = append(pwMatches, Match{
|
|
|
|
Hash: fh,
|
|
|
|
Count: hc,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := scanObj.Err(); err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return pwMatches, hr, nil
|
|
|
|
}
|