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
|
||||
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
122
hibp.go
|
@ -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
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