password

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Package password hashes and verifies passwords with argon2id.

Three decisions are worth knowing about before reading further.

The stored value is a PHC string — the same format argon2's reference tools print — so a hash carries its own salt and cost and can be verified by anything that speaks the format, including a future version of this package that has raised the cost.

The cost is stored alongside it in its own columns, which looks redundant and is not: "which accounts still have a hash below the current cost" has to be a query rather than a scan through a parser, or nobody ever runs it.

And verification reports whether the stored hash is now too cheap, so login can quietly upgrade it. A cost you can raise but never apply to existing accounts is a cost you have not raised.

Index

Constants

View Source
const Algorithm = "argon2id"

Algorithm is the only one this package writes. It is stored explicitly so that adding a second one later is a migration rather than an archaeology project.

Variables

View Source
var ErrMalformed = errors.New("password: the stored hash is not a valid argon2id string")

ErrMalformed means a stored hash could not be read.

It is distinct from a wrong password on purpose: one is somebody typing, the other is a corrupted row, and treating the second as the first would leave an account permanently unable to log in with no sign of why.

Functions

This section is empty.

Types

type BreachChecker

type BreachChecker interface {
	// Count returns the number of times the password appears in breach
	// corpora, or zero if it does not appear.
	//
	// An implementation that cannot reach its source returns an error rather
	// than zero. The caller decides what to do about it; a checker that
	// silently answers "not breached" when it is offline is worse than none,
	// because it looks like it is working.
	Count(ctx context.Context, plain string) (int, error)
}

BreachChecker reports how many times a password has appeared in a known breach.

type Credential

type Credential struct {
	// Encoded is the PHC string. It is the whole truth: salt, cost, and hash.
	Encoded string
	// Algorithm and Params duplicate what Encoded already says, so that
	// finding every credential due for a rehash is an indexed query rather
	// than a scan.
	Algorithm string
	Params    Params
}

Credential is what a row stores.

type HIBP

type HIBP struct {
	// Client defaults to one with a short timeout. A password check is on the
	// sign-up path, and a hung request there is worse than a skipped check.
	Client *http.Client
	// BaseURL defaults to the public API. It is settable so a test can serve
	// its own answers and an air-gapped deployment can host a mirror.
	BaseURL string
}

HIBP checks a password against Have I Been Pwned's range API.

The password never leaves the process. Only the first five hex digits of its SHA-1 are sent; the service answers with every suffix it holds under that prefix — several hundred of them — and the match happens here. That is what makes asking a third party about a password defensible: the third party learns that somebody, somewhere, has a password whose hash starts with those five characters, which is true of millions of people.

SHA-1 is not a choice; it is the format the range API speaks. It is being used as a lookup key against a public corpus, not to protect anything.

func NewHIBP

func NewHIBP() *HIBP

NewHIBP builds a checker against the public API.

func (*HIBP) Count

func (h *HIBP) Count(ctx context.Context, plain string) (int, error)

Count implements BreachChecker.

type Hasher

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

Hasher hashes at a fixed cost.

func New

func New(p Params) *Hasher

New builds a hasher. Zero-valued parameters fall back to the defaults, so a caller who only wants to raise the memory can say just that.

func (*Hasher) Dummy

func (h *Hasher) Dummy() string

Dummy returns a hash to verify against when there is no such account.

Verifying it is how a login for an unknown address spends the same time as one for a known address. Skipping the work turns response time into a membership oracle, and an attacker who can enumerate your users has done most of the work already.

It is derived once, at this hasher's cost, so it stays a fair stand-in when the cost is raised.

func (*Hasher) Hash

func (h *Hasher) Hash(plain string) (Credential, error)

Hash derives a credential from a plaintext password.

func (*Hasher) Params

func (h *Hasher) Params() Params

Params returns the cost this hasher writes.

func (*Hasher) Verify

func (h *Hasher) Verify(encoded, plain string) (ok bool, needsRehash bool, err error)

Verify checks a password against a stored hash.

The second return says the stored hash is cheaper than this hasher's cost, so a caller that has just been handed the plaintext should rehash and store it. That is the only moment it can: the plaintext is never available again.

type Params

type Params struct {
	// Memory is the working set in KiB. It is the parameter that actually
	// costs an attacker something: iterations can be parallelized across
	// cheap cores, memory cannot.
	Memory uint32 `json:"memory"`
	// Iterations is how many passes over that memory.
	Iterations uint32 `json:"iterations"`
	// Parallelism is how many lanes. It should not exceed the cores a request
	// may reasonably occupy.
	Parallelism uint8 `json:"parallelism"`
	// SaltLength and KeyLength are in bytes.
	SaltLength uint32 `json:"salt_length"`
	KeyLength  uint32 `json:"key_length"`
}

Params are argon2id's cost parameters.

func DefaultParams

func DefaultParams() Params

DefaultParams follow the OWASP recommendation for argon2id: 64 MiB, three passes, two lanes.

They cost roughly 50ms on a current server, which is the point — it is slow enough to make an offline guess expensive and fast enough that a login does not feel like one. Raise Memory first if you raise anything.

func (Params) AtLeast

func (p Params) AtLeast(want Params) bool

AtLeast reports whether p is at least as expensive as want.

It is what decides a rehash, so it is deliberately conservative: a hash is only out of date when it is cheaper on a dimension that matters, not when it merely differs.

type Policy

type Policy struct {
	// MinLength is in characters, not bytes, so a passphrase in a language
	// that needs more than one byte per character is not penalized for it.
	MinLength int
	// MaxLength bounds the work. It is not about password strength: argon2id
	// over a ten-megabyte "password" is a denial of service anyone can send,
	// and the limit is what stops it.
	MaxLength int
	// Breached, when set, rejects passwords known to have leaked. See [HIBP].
	Breached BreachChecker
}

Policy is what a new password must satisfy.

There are no composition rules, and that is the recommendation rather than an omission: demanding an uppercase letter, a digit, and a symbol pushes people toward Password1! and a sticky note, which is worse on both counts than a longer passphrase. Length and a breach check are what actually help.

func DefaultPolicy

func DefaultPolicy() Policy

DefaultPolicy is twelve characters, no composition rules, no breach check.

The breach check is off by default because it talks to a third party, and that is a decision an application makes rather than one a library makes for it.

func (Policy) Check

func (p Policy) Check(ctx context.Context, plain string) error

Check validates a password against the policy.

A breach checker that fails is not a failed check. The service being down is not the person's fault, and refusing to let anybody set a password because a third party is having an outage trades a small risk for a total one.

Jump to

Keyboard shortcuts

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