mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-12-22 18:20:38 +01:00
#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:
parent
1642ee7255
commit
05ea767ee1
2 changed files with 129 additions and 27 deletions
59
password.go
59
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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue