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
Simple Go implementation of the HIBP Passwords API
# go-hibp - Simple go client for the HIBP 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 (
"bufio"
"crypto/sha1"
"crypto/tls"
"fmt"
"net/http"
"strings"
"net/url"
"time"
)
// HIBPUrl represents the main API url for the HIBP password API
const HIBPUrl = "https://api.pwnedpasswords.com/range/"
// Version represents the version of this package
const Version = "0.1.1"
// Check queries the HIBP database and checks if a given string is was found
func Check(p string) (ip bool, err error) {
shaSum := fmt.Sprintf("%x", sha1.Sum([]byte(p)))
fp := shaSum[0:5]
sp := shaSum[5:]
ip = false
// Client is the HIBP client object
type Client struct {
hc *http.Client // HTTP client to perform the API requests
to time.Duration // HTTP client timeout
ak string // HIBP API key
httpClient := &http.Client{Timeout: time.Second * 2}
httpRes, err := httpClient.Get(HIBPUrl + fp)
if err != nil {
return false, err
}
defer func() {
err = httpRes.Body.Close()
}()
scanObj := bufio.NewScanner(httpRes.Body)
for scanObj.Scan() {
scanLine := strings.SplitN(scanObj.Text(), ":", 2)
if strings.ToLower(scanLine[0]) == sp {
ip = true
break
}
}
if err := scanObj.Err(); err != nil {
return ip, err
}
return ip, nil
PwnedPassword *PwnedPassword // Reference to the PwnedPassword API
}
// Option is a function that is used for grouping of Client options.
type Option func(*Client)
// New creates and returns a new HIBP client object
func New(options ...Option) *Client {
c := &Client{}
// Set defaults
c.to = time.Second * 5
// Set additional options
for _, opt := range options {
opt(c)
}
// Add a http client to the Client object
c.hc = httpClient(c.to)
// 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)
}
})
}
}