From 05ea767ee1057070d3834a291f6329f6b6824adb Mon Sep 17 00:00:00 2001 From: Winni Neessen Date: Wed, 8 Jun 2022 17:26:41 +0200 Subject: [PATCH] #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 --- password.go | 59 +++++++++++++++++++++++------ password_test.go | 97 ++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 129 insertions(+), 27 deletions(-) diff --git a/password.go b/password.go index da02614..8b37a2e 100644 --- a/password.go +++ b/password.go @@ -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 diff --git a/password_test.go b/password_test.go index 2d3c0ef..65dd035 100644 --- a/password_test.go +++ b/password_test.go @@ -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") } }