mirror of
https://github.com/wneessen/go-hibp.git
synced 2024-11-22 21:00:51 +01:00
v0.1.1: Complete refactor
This commit is contained in:
parent
f2941917d0
commit
44451d4f76
5 changed files with 290 additions and 36 deletions
28
README.md
28
README.md
|
@ -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
122
hibp.go
|
@ -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
26
hibp_test.go
Normal 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
84
password.go
Normal 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
66
password_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue