v0.1.1: Complete refactor

This commit is contained in:
Winni Neessen 2021-09-21 11:21:04 +02:00
parent f2941917d0
commit 44451d4f76
Signed by: wneessen
GPG key ID: 385AC9889632126E
5 changed files with 290 additions and 36 deletions

View file

@ -1,2 +1,26 @@
# go-hibp # go-hibp - Simple go client for the HIBP API
Simple Go implementation of the HIBP Passwords API
[![Go Reference](https://pkg.go.dev/badge/github.com/wneessen/go-hibp.svg)](https://pkg.go.dev/github.com/wneessen/go-hibp) [![Go Report Card](https://goreportcard.com/badge/github.com/wneessen/go-hibp)](https://goreportcard.com/report/github.com/wneessen/go-hibp) [![Build Status](https://api.cirrus-ci.com/github/wneessen/go-hibp.svg)](https://cirrus-ci.com/github/wneessen/go-hibp)
## Usage
### Pwned Passwords API
```go
package main
import (
"fmt"
"github.com/wneessen/go-hibp"
)
func main() {
hc := New()
m, _, err := hc.PwnedPassword.CheckPassword("test123")
if err != nil {
panic(err)
}
if m != nil && m.Count != 0 {
fmt.Println("Your password was found in the pwned passwords DB")
}
}
```

122
hibp.go
View file

@ -1,44 +1,98 @@
package go_hibp package hibp
import ( import (
"bufio" "crypto/tls"
"crypto/sha1"
"fmt" "fmt"
"net/http" "net/http"
"strings" "net/url"
"time" "time"
) )
// HIBPUrl represents the main API url for the HIBP password API // Version represents the version of this package
const HIBPUrl = "https://api.pwnedpasswords.com/range/" const Version = "0.1.1"
// Check queries the HIBP database and checks if a given string is was found // Client is the HIBP client object
func Check(p string) (ip bool, err error) { type Client struct {
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(p))) hc *http.Client // HTTP client to perform the API requests
fp := shaSum[0:5] to time.Duration // HTTP client timeout
sp := shaSum[5:] ak string // HIBP API key
ip = false
httpClient := &http.Client{Timeout: time.Second * 2} PwnedPassword *PwnedPassword // Reference to the PwnedPassword API
httpRes, err := httpClient.Get(HIBPUrl + fp) }
if err != nil {
return false, err // Option is a function that is used for grouping of Client options.
} type Option func(*Client)
defer func() {
err = httpRes.Body.Close() // New creates and returns a new HIBP client object
}() func New(options ...Option) *Client {
c := &Client{}
scanObj := bufio.NewScanner(httpRes.Body)
for scanObj.Scan() { // Set defaults
scanLine := strings.SplitN(scanObj.Text(), ":", 2) c.to = time.Second * 5
if strings.ToLower(scanLine[0]) == sp {
ip = true // Set additional options
break for _, opt := range options {
} opt(c)
} }
if err := scanObj.Err(); err != nil {
return ip, err // Add a http client to the Client object
} c.hc = httpClient(c.to)
return ip, nil // Associate the different HIBP service APIs with the Client
c.PwnedPassword = &PwnedPassword{hc: c}
return c
}
// WithHttpTimeout overrides the default http client timeout
func WithHttpTimeout(t time.Duration) Option {
return func(c *Client) {
c.to = t
}
}
// WithApiKey set the optional API key to the Client object
func WithApiKey(k string) Option {
return func(c *Client) {
c.ak = k
}
}
// HttpReq performs an HTTP request to the corresponding API
func (c *Client) HttpReq(m, p string) (*http.Request, error) {
u, err := url.Parse(p)
if err != nil {
return nil, err
}
hr, err := http.NewRequest(m, u.String(), nil)
if err != nil {
return nil, err
}
hr.Header.Set("Accept", "application/json")
hr.Header.Set("User-Agent", fmt.Sprintf("go-hibp v%s - https://github.com/wneessen/go-hibp", Version))
if c.ak != "" {
hr.Header["hibp-api-key"] = []string{c.ak}
}
return hr, nil
}
// httpClient returns a custom http client for the HIBP Client object
func httpClient(to time.Duration) *http.Client {
tlsConfig := &tls.Config{
MaxVersion: tls.VersionTLS13,
MinVersion: tls.VersionTLS12,
}
httpTransport := &http.Transport{TLSClientConfig: tlsConfig}
httpClient := &http.Client{
Transport: httpTransport,
Timeout: 5 * time.Second,
}
if to.Nanoseconds() > 0 {
httpClient.Timeout = to
}
return httpClient
} }

26
hibp_test.go Normal file
View file

@ -0,0 +1,26 @@
package hibp
import (
"testing"
"time"
)
// TestNew tests the New() function
func TestNew(t *testing.T) {
hc := New()
if hc == nil {
t.Errorf("hibp client creation failed")
}
}
// TestNewWithHttpTimeout tests the New() function
func TestNewWithHttpTimeout(t *testing.T) {
hc := New(WithHttpTimeout(time.Second * 10))
if hc == nil {
t.Errorf("hibp client creation failed")
}
if hc.to != time.Second*10 {
t.Errorf("hibp client timeout option was not set properly. Expected %d, got: %d",
time.Second*10, hc.to)
}
}

84
password.go Normal file
View file

@ -0,0 +1,84 @@
package hibp
import (
"bufio"
"crypto/sha1"
"fmt"
"net/http"
"strconv"
"strings"
)
// PwnedPassword is a HIBP Pwned Passwords API client
type PwnedPassword struct {
hc *Client
}
// Match represents a match in the Pwned Passwords API
type Match struct {
Hash string
Count int64
}
// CheckPassword checks the Pwned Passwords database against a given password string
func (p *PwnedPassword) CheckPassword(pw string) (*Match, *http.Response, error) {
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(pw)))
return p.CheckSHA1(shaSum)
}
// CheckSHA1 checks the Pwned Passwords database against a given SHA1 checksum of a password
func (p *PwnedPassword) CheckSHA1(h string) (*Match, *http.Response, error) {
pwMatches, hr, err := p.apiCall(h)
if err != nil {
return &Match{}, hr, err
}
for _, m := range pwMatches {
if m.Hash == h {
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 *PwnedPassword) apiCall(h string) ([]Match, *http.Response, error) {
sh := h[:5]
hreq, err := p.hc.HttpReq(http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", sh))
if err != nil {
return nil, nil, err
}
hr, err := p.hc.hc.Do(hreq)
if err != nil {
return nil, nil, err
}
if hr.StatusCode != 200 {
return nil, hr, fmt.Errorf("API responded with non HTTP-200: %s", hr.Status)
}
defer func() {
_ = hr.Body.Close()
}()
var pwMatches []Match
scanObj := bufio.NewScanner(hr.Body)
for scanObj.Scan() {
hp := strings.SplitN(scanObj.Text(), ":", 2)
fh := fmt.Sprintf("%s%s", sh, strings.ToLower(hp[0]))
hc, err := strconv.ParseInt(hp[1], 10, 64)
if err != nil {
continue
}
pwMatches = append(pwMatches, Match{
Hash: fh,
Count: hc,
})
}
if err := scanObj.Err(); err != nil {
return nil, nil, err
}
return pwMatches, hr, nil
}

66
password_test.go Normal file
View file

@ -0,0 +1,66 @@
package hibp
import (
"testing"
)
// TestPwnedPasswordString verifies the Pwned Passwords API with the CheckPassword method
func TestPwnedPasswordString(t *testing.T) {
testTable := []struct {
testName string
pwString string
isLeaked bool
}{
{"weak password 'test123' is expected to be leaked", "test123", true},
{"strong, unknown password is expected to be not leaked",
"F/0Ws#.%{Z/NVax=OU8Ajf1qTRLNS12p/?s/adX", false},
}
hc := New()
for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) {
m, _, err := hc.PwnedPassword.CheckPassword(tc.pwString)
if err != nil {
t.Error(err)
}
if m == nil && tc.isLeaked {
t.Errorf("password is expected to be leaked but 0 leaks were returned in Pwned Passwords DB")
}
if m != nil && m.Count > 0 && !tc.isLeaked {
t.Errorf("password is not expected to be leaked but %d leaks were found in Pwned Passwords DB",
m.Count)
}
})
}
}
// TestPwnedPasswordHash verifies the Pwned Passwords API with the CheckSHA1 method
func TestPwnedPasswordHash(t *testing.T) {
testTable := []struct {
testName string
pwHash string
isLeaked bool
}{
{"weak password 'test123' is expected to be leaked",
"7288edd0fc3ffcbe93a0cf06e3568e28521687bc", true},
{"strong, unknown password is expected to be not leaked",
"90efc095c82eab44e882fda507cfab1a2cd31fc0", false},
}
hc := New()
for _, tc := range testTable {
t.Run(tc.testName, func(t *testing.T) {
m, _, err := hc.PwnedPassword.CheckSHA1(tc.pwHash)
if err != nil {
t.Error(err)
}
if m == nil && tc.isLeaked {
t.Errorf("password is expected to be leaked but 0 leaks were returned in Pwned Passwords DB")
}
if m != nil && m.Count > 0 && !tc.isLeaked {
t.Errorf("password is not expected to be leaked but %d leaks were found in Pwned Passwords DB",
m.Count)
}
})
}
}