passwordpwned

package
v1.153.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

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.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidURL is returned by [New] when the configured API base URL cannot
	// be parsed, lacks a scheme or host, or contains a query or fragment.
	ErrInvalidURL = errors.New("invalid API base URL")

	// ErrInvalidUserAgent is returned by [New] when the User-Agent is empty or
	// contains control characters: Go suppresses an empty User-Agent header
	// entirely (and the HIBP API rejects requests without one), while control
	// bytes are rejected by the HTTP transport on every request.
	ErrInvalidUserAgent = errors.New("invalid user agent")

	// ErrMalformedResponse is returned when the response body is empty, missing,
	// not structurally valid HIBP range data, or a matched hash suffix is not
	// followed by the expected ":<count>" sequence.
	ErrMalformedResponse = errors.New("malformed response body")

	// ErrUnexpectedStatus is returned when the API responds with a non-200
	// status code; the code is included in the wrapped message.
	ErrUnexpectedStatus = errors.New("unexpected status code")

	// ErrResponseTooLarge is returned when the decoded response exceeds the
	// configured limit (see [WithResponseSizeLimit]), guarding against
	// decompression-bomb style memory exhaustion.
	ErrResponseTooLarge = errors.New("response body exceeds size limit")

	// ErrUnsupportedEncoding is returned when the server responds with a
	// Content-Encoding this client cannot decode.
	ErrUnsupportedEncoding = errors.New("unsupported response content-encoding")
)

Sentinel errors returned by this package. Match them with errors.Is.

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client queries the HIBP Pwned Passwords API using the k-Anonymity range endpoint. Create one with New; the zero value is not usable.

func New

func New(opts ...Option) (*Client, error)

New creates a Client with the given options applied over sensible defaults.

Defaults:

  • API URL: https://api.pwnedpasswords.com
  • Timeout: 30 s
  • Ping timeout: 5 s (HealthCheck probe)
  • User-Agent: nurago.passwordpwned/1
  • Retry attempts: httpretrier.DefaultAttempts
  • Max response: 8 MiB (decoded)

Use WithURL, WithTimeout, WithHTTPClient, WithUserAgent, WithRetryAttempts, WithRetryDelay, WithResponseSizeLimit, or WithPingTimeout to override individual settings.

All configuration is validated here: an invalid URL, an invalid User-Agent, or invalid retry settings return an error, so a successfully constructed Client cannot fail on configuration at call time.

func (*Client) HealthCheck

func (c *Client) HealthCheck(ctx context.Context) error

HealthCheck verifies that the configured Pwned Passwords endpoint is reachable and healthy.

The HIBP range API has no dedicated ping endpoint, so the probe performs a lightweight GET on a fixed range prefix (without the Add-Padding header, to keep the response small) and reports any transport failure or non-200 status. The response body is not decoded: the status code is the readiness signal. The request is bounded by the ping timeout (see WithPingTimeout) and is never retried.

Note: each probe is a live request against the configured endpoint; the HIBP range API is unauthenticated and CDN-cached, so the cost is negligible at normal probe cadences.

func (*Client) IsPwnedPassword

func (c *Client) IsPwnedPassword(ctx context.Context, password string) (bool, error)

IsPwnedPassword reports whether password has appeared in a known data breach.

It is a thin wrapper over Client.PwnedCount: it returns true when the breach count is greater than zero.

Returns (true, nil) if the password is found in the breach database. Returns (false, nil) if the password is not found or its breach count is zero. Returns (false, err) on any network, encoding, or unexpected HTTP error.

func (*Client) PwnedCount

func (c *Client) PwnedCount(ctx context.Context, password string) (int, error)

PwnedCount returns how many times password appears in the HIBP Pwned Passwords breach database, or 0 if it is not present (including padding entries, which always report a count of 0). This lets callers enforce threshold policies (e.g. NIST-style "reject if seen more than N times") rather than a bare boolean.

The lookup uses the HIBP k-anonymity model:

  1. The SHA-1 hash of password is computed locally.
  2. Only the first 5 hex characters of the hash are sent to the HIBP API.
  3. The API returns all matching hash suffixes (padded to 800 to 1,000 entries).
  4. The full hash is matched client-side; the password never leaves the process.

Returns a non-nil error on any network, encoding, size-limit, or unexpected HTTP error; the count is 0 in that case.

type HTTPClient

type HTTPClient interface {
	// Do sends an HTTP request and returns an HTTP response.
	Do(req *http.Request) (*http.Response, error)
}

HTTPClient is the minimal HTTP transport used by Client.

type Option

type Option func(c *Client)

Option customizes passwordpwned client configuration.

func WithHTTPClient

func WithHTTPClient(hc HTTPClient) Option

WithHTTPClient injects a custom HTTP client implementation. The injected client's own timeout applies (WithTimeout is ignored); if it has none, requests are bounded only by the caller's context.

func WithPingTimeout

func WithPingTimeout(timeout time.Duration) Option

WithPingTimeout sets the per-probe timeout of Client.HealthCheck (default: 5 s). Non-positive values are ignored, keeping the default.

func WithResponseSizeLimit

func WithResponseSizeLimit(limit int64) Option

WithResponseSizeLimit caps the decoded response size (in bytes) to guard against decompression-bomb style memory exhaustion. Non-positive values are ignored, keeping the default limit. Useful when routing through a mirror that legitimately returns larger responses.

func WithRetryAttempts

func WithRetryAttempts(attempts uint) Option

WithRetryAttempts sets the maximum number of total attempts, the initial request plus retries (default: httpretrier.DefaultAttempts, 4). Must be at least 1 (New rejects 0); a value of 1 disables retries.

func WithRetryDelay

func WithRetryDelay(value time.Duration) Option

WithRetryDelay sets the base delay between a failed attempt and its retry (default: httpretrier.DefaultDelay, 1 s). Must be positive (New rejects non-positive values).

func WithTimeout

func WithTimeout(timeout time.Duration) Option

WithTimeout sets the HTTP request timeout of the internally constructed HTTP client (default: 30 s). It has no effect when WithHTTPClient is used: configure the injected client's timeout directly. Non-positive values are ignored, keeping the default: net/http would treat them as "no timeout", leaving requests bounded only by the caller's context.

func WithURL

func WithURL(addr string) Option

WithURL overrides the default HIBP API base URL (default: https://api.pwnedpasswords.com). Useful for routing requests through a self-hosted mirror or a test server. A trailing slash is trimmed; a URL with a query or fragment is rejected by New. Hash-suffix matching is case-insensitive: responses from lowercase-hex mirrors are normalized before matching.

func WithUserAgent

func WithUserAgent(s string) Option

WithUserAgent overrides the User-Agent header used by API requests. The value must be non-empty and free of control characters (New rejects invalid values): Go suppresses an empty User-Agent header entirely, the HIBP API rejects requests without one, and the transport rejects control bytes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL