mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-11-22 21:00:51 +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"
|
"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)
|
||||||
}
|
}
|
||||||
sh := h[:5]
|
|
||||||
hreq, err := p.hibp.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", sh),
|
// 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)
|
||||||
|
}
|
||||||
|
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)
|
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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue