Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/passwordpwned"
Package passwordpwned checks whether a password has appeared in a known data breach, using the Have I Been Pwned (HIBP) Pwned Passwords API v3 (https://haveibeenpwned.com/API/v3#PwnedPasswords).
It uses the HIBP k-anonymity model: only the first 5 hex characters of the SHA-1 hash are sent over the network. The API returns all hash suffixes that share that 5-character prefix (typically 800 to 1,000 entries due to Add-Padding). The full hash match is resolved on the client side, so the password and its complete hash never leave the process.
Create a client with New and call [Client.IsPwnedPassword]:
c, err := passwordpwned.New()
if err != nil {
log.Fatal(err)
}
pwned, err := c.IsPwnedPassword(ctx, password)
if err != nil {
log.Fatal(err)
}
if pwned {
return errors.New("password has been compromised in a data breach, please choose another")
}
To enforce a threshold policy instead (e.g. NIST-style “reject only if seen more than N times”), use [Client.PwnedCount]:
count, err := c.PwnedCount(ctx, password)
if err != nil {
log.Fatal(err)
}
if count > maxAllowedBreaches {
return errors.New("password is too common, please choose another")
}
Features
Requests set the Add-Padding header, so every response contains 800 to 1,000
entries regardless of the real match count. Responses are requested as brotli
and decoded per their declared Content-Encoding (brotli, gzip, or identity);
the decoded body is capped (see WithResponseSizeLimit) to guard against
decompression-bomb style memory exhaustion. A 200 response that is not
structurally valid range data (e.g. a captive-portal HTML page) is rejected
with ErrMalformedResponse instead of being read as “not pwned”. A
httpretrier-backed retry policy handles transient network errors for
read-only requests and honors a server-provided Retry-After header.
Client.PwnedCount returns the raw breach count for threshold policies;
Client.IsPwnedPassword returns a boolean. Client.HealthCheck performs a
bounded range request to verify the endpoint is reachable. WithURL,
WithTimeout, WithHTTPClient, WithUserAgent, WithRetryAttempts,
WithRetryDelay, WithResponseSizeLimit, and WithPingTimeout configure the
client. HTTPClient is an interface, so a mock HTTP client can be injected in
tests.
Security Note
The SHA-1 algorithm is used solely because the HIBP API requires it. The password is hashed locally; only the 5-character prefix is sent over TLS. This package does not store, log, or persist the password or its hash.
When To Use
- Registration and password-change flows should reject known-breached passwords.
- A breach-count threshold suits your policy better than a flat reject.
Example
srv := rangeServer()
defer srv.Close()
// Real code omits WithURL and uses the default HIBP endpoint.
client, err := passwordpwned.New(passwordpwned.WithURL(srv.URL))
if err != nil {
fmt.Println(err)
return
}
pwned, err := client.IsPwnedPassword(context.TODO(), "password")
fmt.Println("password:", pwned, err)
pwned, err = client.IsPwnedPassword(context.TODO(), "correct horse battery staple")
fmt.Println("passphrase:", pwned, err)
// Output:
// password: true <nil>
// passphrase: false <nil>
Full source is in example_passwordpwned_test.go. More runnable examples are on pkg.go.dev.
Dependencies
Importing this package pulls 1 external module:
github.com/aperturerobotics/go-brotli-decoder