2019-01-31 22:42:42 +00:00
|
|
|
package securepassword
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
2024-04-06 22:52:39 +00:00
|
|
|
"context"
|
2022-06-27 17:41:47 +00:00
|
|
|
"crypto/sha1" //#nosec: G505 // HIBP uses shortened SHA1 to query hashes of vulnerable passwordss
|
2024-04-06 22:52:39 +00:00
|
|
|
"errors"
|
2019-01-31 22:42:42 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"strings"
|
2024-04-06 22:52:39 +00:00
|
|
|
"time"
|
2019-01-31 22:42:42 +00:00
|
|
|
)
|
|
|
|
|
2024-04-06 22:52:39 +00:00
|
|
|
const hibpTimeout = 2 * time.Second
|
|
|
|
|
2019-01-31 22:42:42 +00:00
|
|
|
// ErrPasswordInBreach signals the password passed was found in any
|
|
|
|
// breach at least once. The password should not be used if this
|
|
|
|
// error is returned.
|
2022-06-27 17:41:47 +00:00
|
|
|
var ErrPasswordInBreach = errors.New("given password is known to HaveIBeenPwned")
|
2019-01-31 22:42:42 +00:00
|
|
|
|
|
|
|
// CheckHIBPPasswordHash accesses the HaveIBeenPwned API with the
|
|
|
|
// first 5 characters of the SHA1 hash of the password and scans the
|
|
|
|
// result for the password hash. If the hash is found the
|
|
|
|
// ErrPasswordInBreach error is thrown. In case of an HTTP error
|
|
|
|
// another error is thrown. The result will be nil when the password
|
|
|
|
// hash was not returned in the API output.
|
|
|
|
//
|
|
|
|
// See more details at https://haveibeenpwned.com/API/v2#PwnedPasswords
|
|
|
|
func CheckHIBPPasswordHash(password string) error {
|
2022-06-27 17:41:47 +00:00
|
|
|
fullHash := fmt.Sprintf("%x", sha1.Sum([]byte(password))) //#nosec: G401 // See crypto/sha1 import
|
2019-01-31 22:42:42 +00:00
|
|
|
checkHash := fullHash[0:5]
|
|
|
|
|
2024-04-06 22:52:39 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.TODO(), hibpTimeout)
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("https://api.pwnedpasswords.com/range/%s", checkHash), nil)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("creating HTTP request: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
2019-01-31 22:42:42 +00:00
|
|
|
if err != nil {
|
2024-04-06 22:52:39 +00:00
|
|
|
return fmt.Errorf("executing HTTP request: %w", err)
|
2019-01-31 22:42:42 +00:00
|
|
|
}
|
2024-04-06 22:52:39 +00:00
|
|
|
defer resp.Body.Close() //nolint:errcheck
|
2019-01-31 22:42:42 +00:00
|
|
|
|
|
|
|
// Response format:
|
|
|
|
// 0018A45C4D1DEF81644B54AB7F969B88D65:1
|
|
|
|
// 00D4F6E8FA6EECAD2A3AA415EEC418D38EC:2
|
|
|
|
|
|
|
|
scanner := bufio.NewScanner(resp.Body)
|
|
|
|
for scanner.Scan() {
|
|
|
|
if strings.HasPrefix(scanner.Text(), fullHash) {
|
|
|
|
// We don't care for the exact number but only for a match
|
|
|
|
return ErrPasswordInBreach
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|