#14: Add ListHashes*() methods to get access to all returned hashes

- This method replaces the previously private apiCall() method
- Added `ListHashesSHA1()` as well as `ListHashesPassword()` to keep consistency in the naming schema
- Added length checks for SHA1() methods
- Added length check for Prefix() method
This commit is contained in:
Winni Neessen 2022-06-08 17:26:41 +02:00
parent 1642ee7255
commit 05ea767ee1
Signed by: wneessen
GPG key ID: 385AC9889632126E
2 changed files with 129 additions and 27 deletions

View file

@ -9,6 +9,16 @@ import (
"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
type PwnedPassApi struct {
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)
}
// 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) {
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 {
return &Match{}, hr, err
}
@ -45,18 +59,41 @@ func (p *PwnedPassApi) CheckSHA1(h string) (*Match, *http.Response, error) {
return &m, hr, nil
}
}
return nil, hr, nil
}
// apiCall performs the API call to the Pwned Password API endpoint and returns
// the http.Response
func (p *PwnedPassApi) apiCall(h string) ([]Match, *http.Response, error) {
if len(h) < 5 {
return nil, nil, fmt.Errorf("password hash cannot be shorter than 5 characters")
// 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
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]
hreq, err := p.hibp.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", sh),
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
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)
if err != nil {
return nil, nil, err
@ -76,7 +113,7 @@ func (p *PwnedPassApi) apiCall(h string) ([]Match, *http.Response, error) {
scanObj := bufio.NewScanner(hr.Body)
for scanObj.Scan() {
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)
if err != nil {
continue

View file

@ -5,16 +5,32 @@ import (
"testing"
)
// TestPwnedPasswordString verifies the Pwned Passwords API with the CheckPassword method
func TestPwnedPasswordString(t *testing.T) {
const (
// 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 {
testName string
pwString string
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",
"F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX", false},
PwStringSecure, false},
}
hc := New()
for _, tc := range testTable {
@ -34,18 +50,18 @@ func TestPwnedPasswordString(t *testing.T) {
}
}
// TestPwnedPasswordHash verifies the Pwned Passwords API with the CheckSHA1 method
func TestPwnedPasswordHash(t *testing.T) {
// TestPwnedPassApi_CheckSHA1 verifies the Pwned Passwords API with the CheckSHA1 method
func TestPwnedPassApi_CheckSHA1(t *testing.T) {
testTable := []struct {
testName string
pwHash string
isLeaked bool
shouldFail bool
}{
{"weak password 'test123' is expected to be leaked",
"7288edd0fc3ffcbe93a0cf06e3568e28521687bc", true, false},
{"weak password 'test' is expected to be leaked",
PwHashInsecure, true, false},
{"strong, unknown password is expected to be not leaked",
"90efc095c82eab44e882fda507cfab1a2cd31fc0", false, false},
PwHashSecure, false, false},
{"empty string should fail",
"", 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)
func TestPwnedPassApi_apiCall(t *testing.T) {
func TestPwnedPassApi_ListHashesPrefix(t *testing.T) {
hc := New()
// Should return a 404
_, _, err := hc.PwnedPassApi.apiCall("ZZZZZZZZZZZZZZ")
// Should return at least 1 restults
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 {
t.Errorf("apiCall was supposed to fail, but didn't")
t.Errorf("ListHashesPrefix was supposed to fail, but didn't")
}
// Non allowed characters
_, _, err = hc.PwnedPassApi.apiCall(string([]byte{0}))
_, _, err = hc.PwnedPassApi.ListHashesPrefix(string([]byte{0, 0, 0, 0, 0}))
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")
}
}