Merge pull request #15 from wneessen/14-prefix

#14: Add `ListHashes*()` methods to get access to all returned hashes
This commit is contained in:
Winni Neessen 2022-06-08 17:29:17 +02:00 committed by GitHub
commit 3ab68b9110
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 129 additions and 27 deletions

View file

@ -9,6 +9,16 @@ import (
"strings" "strings"
) )
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"
)
// PwnedPassApi is a HIBP Pwned Passwords API client // PwnedPassApi is a HIBP Pwned Passwords API client
type PwnedPassApi struct { type PwnedPassApi struct {
hibp *Client // References back to the parent HIBP client hibp *Client // References back to the parent HIBP client
@ -33,9 +43,13 @@ func (p *PwnedPassApi) CheckPassword(pw string) (*Match, *http.Response, error)
return p.CheckSHA1(shaSum) return p.CheckSHA1(shaSum)
} }
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password // CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password string
func (p *PwnedPassApi) CheckSHA1(h string) (*Match, *http.Response, error) { func (p *PwnedPassApi) CheckSHA1(h string) (*Match, *http.Response, error) {
pwMatches, hr, err := p.apiCall(h) if len(h) != 40 {
return nil, nil, fmt.Errorf(ErrSHA1LengthMismatch)
}
pwMatches, hr, err := p.ListHashesPrefix(h[:5])
if err != nil { if err != nil {
return &Match{}, hr, err return &Match{}, hr, err
} }
@ -45,18 +59,41 @@ func (p *PwnedPassApi) CheckSHA1(h string) (*Match, *http.Response, error) {
return &m, hr, nil return &m, hr, nil
} }
} }
return nil, hr, nil return nil, hr, nil
} }
// apiCall performs the API call to the Pwned Password API endpoint and returns // ListHashesPassword checks the Pwned Password API endpoint for all hashes based on a given
// the http.Response // password string and returns the a slice of Match as well as the http.Response
func (p *PwnedPassApi) apiCall(h string) ([]Match, *http.Response, error) { //
if len(h) < 5 { // NOTE: If the `WithPwnedPadding` option is set to true, the returned list will be padded and might
return nil, nil, fmt.Errorf("password hash cannot be shorter than 5 characters") // contain junk data
func (p *PwnedPassApi) ListHashesPassword(pw string) ([]Match, *http.Response, error) {
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
func (p *PwnedPassApi) ListHashesSHA1(h string) ([]Match, *http.Response, error) {
if len(h) != 40 {
return nil, nil, fmt.Errorf(ErrSHA1LengthMismatch)
} }
sh := h[:5] return p.ListHashesPrefix(h[:5])
hreq, err := p.hibp.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", sh), }
// 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
func (p *PwnedPassApi) ListHashesPrefix(pf string) ([]Match, *http.Response, error) {
if len(pf) != 5 {
return nil, nil, fmt.Errorf(ErrPrefixLengthMismatch)
}
hreq, err := p.hibp.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", pf),
nil) nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@ -76,7 +113,7 @@ func (p *PwnedPassApi) apiCall(h string) ([]Match, *http.Response, error) {
scanObj := bufio.NewScanner(hr.Body) scanObj := bufio.NewScanner(hr.Body)
for scanObj.Scan() { for scanObj.Scan() {
hp := strings.SplitN(scanObj.Text(), ":", 2) hp := strings.SplitN(scanObj.Text(), ":", 2)
fh := fmt.Sprintf("%s%s", sh, strings.ToLower(hp[0])) fh := fmt.Sprintf("%s%s", strings.ToLower(pf), strings.ToLower(hp[0]))
hc, err := strconv.ParseInt(hp[1], 10, 64) hc, err := strconv.ParseInt(hp[1], 10, 64)
if err != nil { if err != nil {
continue continue

View file

@ -5,16 +5,32 @@ import (
"testing" "testing"
) )
// TestPwnedPasswordString verifies the Pwned Passwords API with the CheckPassword method const (
func TestPwnedPasswordString(t *testing.T) { // PwStringInsecure is the string representation of an insecure password
PwStringInsecure = "test"
// PwStringSecure is the string representation of an insecure password
PwStringSecure = "F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX"
// PwHashInsecure is the SHA1 checksum of an insecure password
// Represents the string: test
PwHashInsecure = "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"
// PwHashSecure is the SHA1 checksum of a secure password
// Represents the string: F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX
PwHashSecure = "90efc095c82eab44e882fda507cfab1a2cd31fc0"
)
// TestPwnedPassApi_CheckPassword verifies the Pwned Passwords API with the CheckPassword method
func TestPwnedPassApi_CheckPassword(t *testing.T) {
testTable := []struct { testTable := []struct {
testName string testName string
pwString string pwString string
isLeaked bool isLeaked bool
}{ }{
{"weak password 'test123' is expected to be leaked", "test123", true}, {"weak password 'test123' is expected to be leaked", PwStringInsecure, true},
{"strong, unknown password is expected to be not leaked", {"strong, unknown password is expected to be not leaked",
"F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX", false}, PwStringSecure, false},
} }
hc := New() hc := New()
for _, tc := range testTable { for _, tc := range testTable {
@ -34,18 +50,18 @@ func TestPwnedPasswordString(t *testing.T) {
} }
} }
// TestPwnedPasswordHash verifies the Pwned Passwords API with the CheckSHA1 method // TestPwnedPassApi_CheckSHA1 verifies the Pwned Passwords API with the CheckSHA1 method
func TestPwnedPasswordHash(t *testing.T) { func TestPwnedPassApi_CheckSHA1(t *testing.T) {
testTable := []struct { testTable := []struct {
testName string testName string
pwHash string pwHash string
isLeaked bool isLeaked bool
shouldFail bool shouldFail bool
}{ }{
{"weak password 'test123' is expected to be leaked", {"weak password 'test' is expected to be leaked",
"7288edd0fc3ffcbe93a0cf06e3568e28521687bc", true, false}, PwHashInsecure, true, false},
{"strong, unknown password is expected to be not leaked", {"strong, unknown password is expected to be not leaked",
"90efc095c82eab44e882fda507cfab1a2cd31fc0", false, false}, PwHashSecure, false, false},
{"empty string should fail", {"empty string should fail",
"", false, true}, "", false, true},
} }
@ -68,21 +84,70 @@ func TestPwnedPasswordHash(t *testing.T) {
} }
} }
// TestPwnedPassApi_apiCall tests the non-public apiCall method (especially for failures that are not // TestPwnedPassApi_ListHashesPrefix tests the ListHashesPrefix method (especially for failures that are not
// tested by the other tests already) // tested by the other tests already)
func TestPwnedPassApi_apiCall(t *testing.T) { func TestPwnedPassApi_ListHashesPrefix(t *testing.T) {
hc := New() hc := New()
// Should return a 404 // Should return at least 1 restults
_, _, err := hc.PwnedPassApi.apiCall("ZZZZZZZZZZZZZZ") l, _, err := hc.PwnedPassApi.ListHashesPrefix("a94a8")
if err != nil {
t.Errorf("ListHashesPrefix was not supposed to fail, but did: %s", err)
}
if len(l) <= 0 {
t.Errorf("ListHashesPrefix was supposed to return a list longer than 0")
}
// Prefix has wrong size
_, _, err = hc.PwnedPassApi.ListHashesPrefix("ZZZZZZZZZZZZZZ")
if err == nil { if err == nil {
t.Errorf("apiCall was supposed to fail, but didn't") t.Errorf("ListHashesPrefix was supposed to fail, but didn't")
} }
// Non allowed characters // Non allowed characters
_, _, err = hc.PwnedPassApi.apiCall(string([]byte{0})) _, _, err = hc.PwnedPassApi.ListHashesPrefix(string([]byte{0, 0, 0, 0, 0}))
if err == nil { if err == nil {
t.Errorf("apiCall was supposed to fail, but didn't") t.Errorf("ListHashesPrefix was supposed to fail, but didn't")
}
}
// TestPwnedPassApi_ListHashesSHA1 tests the PwnedPassApi.ListHashesSHA1 metethod
func TestPwnedPassApi_ListHashesSHA1(t *testing.T) {
hc := New()
// List length should be >0
l, _, err := hc.PwnedPassApi.ListHashesSHA1(PwHashInsecure)
if err != nil {
t.Errorf("ListHashesSHA1 was not supposed to fail, but did: %s", err)
}
if len(l) <= 0 {
t.Errorf("ListHashesSHA1 was supposed to return a list longer than 0")
}
// Hash has wrong size
_, _, err = hc.PwnedPassApi.ListHashesSHA1(PwStringInsecure)
if err == nil {
t.Errorf("ListHashesSHA1 was supposed to fail, but didn't")
}
}
// TestPwnedPassApi_ListHashesPassword tests the PwnedPassApi.ListHashesPassword metethod
func TestPwnedPassApi_ListHashesPassword(t *testing.T) {
hc := New()
// List length should be >0
l, _, err := hc.PwnedPassApi.ListHashesPassword(PwStringInsecure)
if err != nil {
t.Errorf("ListHashesPassword was not supposed to fail, but did: %s", err)
}
if len(l) <= 0 {
t.Errorf("ListHashesPassword was supposed to return a list longer than 0")
}
// Empty string has no checksum
_, _, err = hc.PwnedPassApi.ListHashesSHA1("")
if err == nil {
t.Errorf("ListHashesPassword was supposed to fail, but didn't")
} }
} }