Documentation
¶
Overview ¶
Package passwordhash provides OWASP-compliant password hashing and verification using the Argon2id algorithm (RFC 9106), with an optional AES-GCM encryption layer (peppered hashing) for defense in depth.
Problem ¶
Storing passwords securely is one of the most critical and most frequently mishandled tasks in application development. MD5, SHA-1, and even bcrypt are either broken or insufficient against modern GPU-based attacks. Choosing the correct algorithm, tuning its parameters, generating a cryptographically random salt, and encoding everything into a portable, self-describing format — all without introducing subtle timing-attack vulnerabilities — requires deep cryptographic knowledge most teams would rather not reinvent.
Solution ¶
This package encapsulates the full OWASP Password Storage Cheat Sheet (https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html) recommendations into three method pairs on a single Params configuration object:
p := passwordhash.New() // sensible OWASP defaults // Hash a password for storage. hash, err := p.PasswordHash(plaintext) // Verify a login attempt. ok, err := p.PasswordVerify(plaintext, hash) // After a successful verification, detect hashes minted with outdated // parameters so they can be transparently re-hashed and stored again. upgrade, err := p.PasswordNeedsRehash(hash)
For deployments that store a secret pepper outside the database, the encrypted variants add an AES-GCM layer on top of the Argon2id hash:
hash, err := p.EncryptPasswordHash(pepper, plaintext) ok, err := p.EncryptPasswordVerify(pepper, plaintext, hash)
Storage Format ¶
The hashed password is stored as a base64-encoded JSON object that is fully self-describing: it embeds the algorithm name, version, all Argon2id tuning parameters, the random salt, and the derived key. This makes the stored value portable across languages and systems, and allows parameters to be upgraded without invalidating existing hashes.
Example JSON (before base64 encoding):
{
"P": {
"A": "argon2id", // algorithm name (always "argon2id")
"V": 19, // Argon2id version (0x13)
"K": 32, // derived key length in bytes
"S": 16, // salt length in bytes
"T": 3, // time cost (passes over memory)
"M": 65536, // memory cost in KiB
"P": 4 // parallelism (threads)
},
"S": "wQYm4bfktbHq2omIwFu+4Q==", // base64 random salt
"K": "aU8hO900Odq6aKtWiWz3RW9ygn734liJaPtM6ynvkYI=" // base64 Argon2id hash
}
The final stored value is the base64 encoding of the above JSON (~200 bytes).
Alternatively, WithFormat(FormatPHC) selects the PHC string format shared by Argon2 implementations across languages:
$argon2id$v=19$m=65536,t=3,p=4$<base64 salt>$<base64 key>
The JSON format is fully self-contained but is a nurago-specific schema; the PHC format is what external tooling (PHP's password_hash, Python's argon2-cffi and passlib, the Argon2 reference CLI) reads and writes directly. Params.PasswordVerify, Params.PasswordNeedsRehash, and their encrypted counterparts auto-detect which format a stored value uses — a leading '$' marks PHC — so switching formats never invalidates an existing hash. Params.PasswordNeedsRehash also steers format migration: a hash stored in a format outside the configured accepted set (see WithFormat) is reported as needing a rehash, so an existing store converges to the configured format through the ordinary rehash-on-login flow — or stays deliberately mixed when both formats are listed as accepted.
The accepted PHC envelope is deliberately strict: argon2id only (a PHC string minted by an argon2i or argon2d implementation fails with ErrAlgoMismatch), version 19 only, cost parameters in the standard m,t,p order with the optional keyid and data attributes rejected, threads up to 255, canonical unpadded base64 for salt and key (embedded newlines and non-canonical trailing bits are rejected), and the same deserialization bounds that protect the JSON format.
Features ¶
- Argon2id algorithm: resists both side-channel (Argon2i) and GPU-based (Argon2d) attacks — the current OWASP top recommendation (RFC 9106, https://www.rfc-editor.org/info/rfc9106).
- Cryptographically random salt: a fresh salt is generated for every hash, preventing rainbow-table and pre-computation attacks.
- Constant-time comparison: final hash comparison uses crypto/subtle, preventing timing side-channel attacks.
- Self-describing storage format: algorithm, version, and all parameters travel with the hash; no separate migration table is needed when tuning changes.
- Optional pepper encryption: Params.EncryptPasswordHash and Params.EncryptPasswordVerify wrap the Argon2id hash in AES-GCM using a key stored outside the database, so a DB leak alone is insufficient to mount an offline attack.
- Input length guards: the configured minimum length is enforced when hashing (registration policy) and the maximum length is enforced everywhere, rejecting oversized passwords before any CPU-intensive computation; oversized stored hash strings are likewise rejected before any decoding, preventing denial-of-service via extremely long inputs.
- Transparent parameter upgrades: Params.PasswordNeedsRehash and Params.EncryptPasswordNeedsRehash detect hashes minted with outdated parameters so they can be re-hashed on the next successful login.
- Sentinel errors: every failure class is matchable with errors.Is — policy rejection, invalid configuration, corrupt or forged hash data, algorithm/version mismatch, and invalid pepper key.
- Tunable via functional options: WithTime, WithMemory, WithThreads, WithKeyLen, WithSaltLen, WithMinPasswordLength, and WithMaxPasswordLength let each deployment match the OWASP parameter guidance for its hardware profile.
- Portable format: JSON + base64 storage can be decoded by any language with standard libraries, enabling cross-platform password verification.
- PHC interoperability: WithFormat can emit the standard PHC string format ($argon2id$v=19$m=...,t=...,p=...$salt$key), and verification auto-detects and accepts argon2id PHC hashes produced by other implementations (PHP's password_hash, Python's argon2-cffi and passlib, the Argon2 reference CLI), easing migration to and from other ecosystems without bulk re-hashing.
Verification Flow ¶
- Reject the stored string before any decoding if it exceeds the maximum accepted size (16 KiB), bounding the cost of untrusted input.
- Detect the serialization (a leading '$' marks PHC, otherwise base64 JSON) and decode the stored string to recover algorithm, version, parameters, and salt.
- For PHC, reconstruct the salt and key lengths from the decoded byte lengths.
- Validate that the embedded parameters are within accepted bounds and internally consistent (the salt and key byte lengths match their declared sizes), rejecting forged or corrupted blobs before any computation.
- Validate that the stored algorithm and version match the library.
- Re-derive the key from the candidate password using the stored parameters and salt.
- Compare the derived key against the stored key with crypto/subtle.ConstantTimeCompare to prevent timing attacks.
Parameter Tuning ¶
The defaults (T=3, M=64 MiB, P=4) match the second recommended option set of RFC 9106 §4 (https://datatracker.ietf.org/doc/html/rfc9106#section-4). Parallelism is a flat constant, deliberately not derived from runtime.NumCPU(): Argon2 lanes are goroutines, so p=4 is valid on any host, and a machine-independent default keeps the work factor reproducible across heterogeneous fleets — with a per-machine default, hosts with different core counts would mint hashes with different parameters and Params.PasswordNeedsRehash would report an upgrade on every alternating login, re-hashing forever. For production deployments, benchmark Params.PasswordHash on representative hardware and adjust via WithTime, WithMemory, and WithThreads so that hashing takes 0.5–1 s under your expected load.
Benefits ¶
This package delivers a complete, OWASP-compliant password security layer in a single import: correct algorithm, safe defaults, timing-attack-resistant comparison, optional pepper support, and a portable self-describing storage format — letting application code focus on business logic rather than cryptographic plumbing.
Index ¶
- Constants
- Variables
- type Format
- type Hashed
- type Option
- func WithFormat(f Format, accept ...Format) Option
- func WithKeyLen(v uint32) Option
- func WithMaxPasswordLength(v uint32) Option
- func WithMemory(v uint32) Option
- func WithMinPasswordLength(v uint32) Option
- func WithSaltLen(v uint32) Option
- func WithThreads(v uint8) Option
- func WithTime(v uint32) Option
- type Params
- func (ph *Params) EncryptPasswordHash(key []byte, password string) (string, error)
- func (ph *Params) EncryptPasswordNeedsRehash(key []byte, hash string) (bool, error)
- func (ph *Params) EncryptPasswordVerify(key []byte, password, hash string) (bool, error)
- func (ph *Params) PasswordHash(password string) (string, error)
- func (ph *Params) PasswordNeedsRehash(hash string) (bool, error)
- func (ph *Params) PasswordVerify(password, hash string) (bool, error)
Examples ¶
Constants ¶
const ( // DefaultAlgo is the default algorithm used to hash the password. // It corresponds to Type y=2. DefaultAlgo = "argon2id" // DefaultKeyLen is the default derived key length (Tag length) in bytes. // When configuring new hashes the key length is clamped to [16, 1024] bytes; // verification additionally accepts down to 4 bytes for legacy hashes. DefaultKeyLen = 32 // DefaultSaltLen is the default length of the random password salt (Nonce S). // When configuring new hashes the salt length is clamped to [8, 1024] bytes; // verification additionally accepts down to 1 byte for legacy hashes. // The value of 16 bytes is recommended for password hashing. DefaultSaltLen = 16 // DefaultTime (t) is the default number of passes (iterations) over the memory. // It is clamped to [1, 1024] for both hashing and verification. DefaultTime = 3 // DefaultMemory is the default size of the memory in KiB. // It is clamped to [8, 4194304] KiB (up to 4 GiB) and, at hashing time, // rounded down to the nearest multiple of 4*p (with a floor of 8*p). DefaultMemory = 64 * 1024 // DefaultMinPasswordLength is the default minimum length of the input password (Message string P). // It must have a length not greater than 2^(32)-1 bytes. DefaultMinPasswordLength = 8 // DefaultMaxPasswordLength is the default maximum length of the input password (Message string P). // It must have a length not greater than 2^(32)-1 bytes. DefaultMaxPasswordLength = 4096 )
Variables ¶
var ( // ErrPasswordTooShort is returned when a password is shorter than the // configured minimum length. It is enforced only when hashing (registration // policy), never when verifying an existing login. ErrPasswordTooShort = errors.New("the password is too short") // ErrPasswordTooLong is returned when a password exceeds the configured // maximum length. It guards both hashing and verification against // denial-of-service via extremely long inputs. ErrPasswordTooLong = errors.New("the password is too long") // ErrInvalidParams is returned by the hashing methods when the [Params] // configuration is incomplete or out of range (for example a zero-value // struct built without [New], or a field mutated to an invalid value). ErrInvalidParams = errors.New("invalid hashing parameters") // ErrInvalidHashData is returned when a stored hash blob cannot be decoded // (or decrypted), exceeds the maximum accepted size, is missing fields, or // embeds parameters that are out of range or internally inconsistent — all // signs of corruption or forgery. ErrInvalidHashData = errors.New("invalid hash data") // ErrAlgoMismatch is returned when a stored hash was produced by a different // algorithm than the verifying [Params] expects. ErrAlgoMismatch = errors.New("different algorithm type") // ErrVersionMismatch is returned when a stored hash was produced by a // different Argon2 version than the verifying [Params] expects. ErrVersionMismatch = errors.New("different argon2 versions") // ErrInvalidPepperKey is returned by the Encrypt* methods when the pepper key // is not a valid AES key length (16, 24, or 32 bytes). ErrInvalidPepperKey = errors.New("pepper key must be 16, 24, or 32 bytes") )
Sentinel errors returned by the package. Callers can match them with errors.Is to distinguish policy rejections from malformed input, algorithm mismatches (which signal a needed migration), and configuration mistakes.
Functions ¶
This section is empty.
Types ¶
type Format ¶
type Format uint8
Format identifies a serialization of the stored hash string. WithFormat selects which one Params.PasswordHash emits and which ones Params.PasswordNeedsRehash treats as current. Verification auto-detects the format of every stored value, so any Params can read hashes written in either format regardless of configuration.
const ( // FormatJSON is the default self-describing base64-encoded JSON format // documented in the package overview. It is the zero value, so it is what a // [Params] uses unless [WithFormat] selects otherwise. FormatJSON Format = iota // FormatPHC is the PHC string format shared by Argon2 implementations across // languages and runtimes: // // $argon2id$v=19$m=65536,t=3,p=4$<base64 salt>$<base64 key> // // Choose it for interoperability with external tooling that reads or writes // PHC strings (for example PHP's password_hash, Python's argon2-cffi/passlib, // or the Argon2 reference CLI). The salt and key are encoded with the standard // base64 alphabet without padding, as required by the PHC specification. // Only argon2id PHC strings can be verified; the package overview documents // the exact accepted envelope. FormatPHC )
type Hashed ¶
type Hashed struct {
// Params are the Argon2 parameters used to derive Key.
Params *Params `json:"P"`
// Salt is the password salt (Nonce S) of length Params.SaltLen.
// The salt should be unique for each password.
Salt []byte `json:"S"`
// Key is the hashed password (Tag) of length Params.KeyLen.
Key []byte `json:"K"`
}
Hashed stores a derived key together with the parameters needed to verify it.
type Option ¶
type Option func(*Params)
Option applies a configuration change to Params.
func WithFormat ¶
WithFormat selects the serialization emitted by Params.PasswordHash and the set of formats Params.PasswordNeedsRehash treats as current.
f is the format newly minted hashes use: FormatJSON (the default self-describing base64 JSON) or FormatPHC (the cross-language PHC string format). Any other value is normalized to FormatJSON.
accept optionally lists additional formats that PasswordNeedsRehash reports as current; unknown values are ignored rather than aliased to a format the caller did not request. By default only f is accepted, so an existing store converges to f through the ordinary rehash-on-login flow: a stored hash in any other format is reported as needing a rehash even when its Argon2 parameters match the configuration. Listing the other format keeps a deliberately mixed store with no forced convergence:
WithFormat(FormatPHC) // emit PHC, converge existing hashes to PHC WithFormat(FormatPHC, FormatJSON) // emit PHC, keep existing JSON hashes as-is
Verification is unaffected: Params.PasswordVerify auto-detects and accepts every supported format regardless of this option, so no configuration can lock out an existing credential.
It has no effect on the pepper-encrypted methods (Params.EncryptPasswordHash and friends): their output is an opaque AES-GCM ciphertext, so the inner serialization is never read by another system and there is nothing to make interoperable.
Example ¶
package main
import (
"fmt"
"log"
"strings"
"github.com/tecnickcom/nurago/pkg/passwordhash"
)
func main() {
// WithFormat(FormatPHC) emits the cross-language PHC string format instead of
// the default base64 JSON. The same Params verifies it back: the format is
// auto-detected from the stored value.
p := passwordhash.New(
passwordhash.WithFormat(passwordhash.FormatPHC),
passwordhash.WithMemory(16_384),
passwordhash.WithThreads(1),
)
secret := "Example-Password-04"
hash, err := p.PasswordHash(secret)
if err != nil {
log.Fatal(err)
}
fmt.Println(strings.HasPrefix(hash, "$argon2id$v=19$"))
ok, err := p.PasswordVerify(secret, hash)
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
}
Output: true true
func WithKeyLen ¶
WithKeyLen sets the derived key length (Tag length) in bytes. The value is clamped to [16, 1024]: 16 bytes (128 bits) is a safe floor, as shorter keys are trivially brute-forceable offline, and 1024 bytes is the largest length the verification path accepts — a longer key would mint a hash that could never be verified. The default is 32 bytes. (Hashes stored with a shorter key remain verifiable for backward compatibility.)
func WithMaxPasswordLength ¶
WithMaxPasswordLength sets the maximum accepted password length in bytes. Passwords longer than this are rejected before hashing, preventing denial-of-service attacks via extremely long input strings. If the value is lower than the configured minimum length, New raises it to that minimum so the accepted-length window is never empty.
func WithMemory ¶
WithMemory sets Argon2 memory cost in KiB. The value is clamped to [8, 4194304] (up to 4 GiB); 4 GiB is the largest the verification path accepts, so it is the effective ceiling. The actual number of blocks is m', which is m rounded down to the nearest multiple of 4*p.
func WithMinPasswordLength ¶
WithMinPasswordLength sets the minimum accepted password length in bytes. Passwords shorter than this are rejected by Params.PasswordHash before any CPU-intensive computation, enforcing password policy at zero cost. The value is clamped to a minimum of 1 so the length guard can never be silently disabled by passing 0.
func WithSaltLen ¶
WithSaltLen sets random salt length (Nonce S) in bytes. The value is clamped to [8, 1024]: 8 bytes (64 bits) is a conservative floor for rainbow-table and pre-computation resistance, and 1024 bytes is the largest length the verification path accepts. The recommended and default value is 16 bytes. (Hashes stored with a shorter salt remain verifiable for backward compatibility.)
func WithThreads ¶
WithThreads sets Argon2 parallelism (threads/lane count). It controls the degree of parallelism p, which determines how many independent (but synchronizing) computational chains (lanes) can be run. According to RFC 9106, valid values are 1 to 2^(24)-1; this implementation limits the value to 2^(8)-1.
func WithTime ¶
WithTime sets the Argon2id time cost: the number of passes over memory. The value is clamped to [1, 1024]. Higher values increase resistance to brute-force attacks at the cost of hashing latency; 1024 is the largest value the verification path accepts, so it is the effective ceiling. OWASP recommends tuning so that hashing takes 0.5–1 s on target hardware.
type Params ¶
type Params struct {
// Algo is the algorithm used to hash the password.
// It corresponds to Type y=2.
Algo string `json:"A"`
// Version is the algorithm version.
Version uint8 `json:"V"`
// KeyLen is the length of the derived key (Tag length) in bytes.
// New hashes use a value in [16, 1024]; verification accepts [4, 1024].
KeyLen uint32 `json:"K"`
// SaltLen is the length of the random password salt (Nonce S) in bytes.
// New hashes use a value in [8, 1024]; verification accepts [1, 1024].
// 16 bytes is recommended and the salt should be unique for each password.
SaltLen uint32 `json:"S"`
// Time (t) is the number of passes over the memory, a value in [1, 1024].
Time uint32 `json:"T"`
// Memory is the size of the memory in KiB, a value in [8, 4194304] (up to 4 GiB).
// The actual number of blocks is m', which is m rounded down to the nearest multiple of 4*p.
Memory uint32 `json:"M"`
// Threads (p) is the degree of parallelism p that determines how many independent
// (but synchronizing) computational chains (lanes) can be run.
// According to RFC 9106 it must be an integer value from 1 to 2^(24)-1,
// but in this implementation is limited to 2^(8)-1.
Threads uint8 `json:"P"`
// contains filtered or unexported fields
}
Params contains the Argon2id parameters and limits used for password hashing.
A Params value returned by New is safe for concurrent use by multiple goroutines, provided its fields are not modified after construction. The exported fields exist so the configuration can be serialized alongside each hash; mutating them on a shared instance while hashing or verifying is a data race and may also invalidate the parameter invariants enforced by New.
func New ¶
New creates a Params instance with OWASP-recommended Argon2id defaults, then applies any provided Option functions.
Defaults:
- Algorithm: argon2id (RFC 9106)
- KeyLen: 32 bytes
- SaltLen: 16 bytes
- Time: 3 passes
- Memory: 64 MiB
- Threads: 4 lanes (RFC 9106 §4), machine-independent by design
- MinPasswordLen: 8 bytes
- MaxPasswordLen: 4096 bytes
Password lengths are measured in bytes, not Unicode characters: a multi-byte UTF-8 password counts its encoded length. Callers that need a character-based policy, or that must match a password typed on systems using different Unicode normalization forms (NFC vs NFD), should normalize (for example to NFKC) before hashing.
Use WithTime, WithMemory, WithThreads, WithKeyLen, WithSaltLen, WithMinPasswordLength, and WithMaxPasswordLength to tune for your hardware profile before deploying to production.
func (*Params) EncryptPasswordHash ¶
EncryptPasswordHash hashes password with Argon2id and then encrypts the resulting hash object with AES-GCM using key as the pepper.
key is the AES pepper and must be a valid AES key length of 16, 24, or 32 bytes (for AES-128, AES-192, or AES-256 respectively); any other length returns an error before hashing.
Because the pepper is stored separately from the database (e.g. in a secrets manager or HSM), an attacker who obtains the database alone cannot perform an offline dictionary attack — the ciphertext is opaque without the key.
The returned string is a base64-encoded AES-GCM ciphertext. Use Params.EncryptPasswordVerify with the same key to verify.
func (*Params) EncryptPasswordNeedsRehash ¶
EncryptPasswordNeedsRehash is the pepper-encrypted counterpart to Params.PasswordNeedsRehash for hashes produced by Params.EncryptPasswordHash. key is the AES pepper and must be a valid AES key length (16, 24, or 32 bytes).
func (*Params) EncryptPasswordVerify ¶
EncryptPasswordVerify decrypts the hash produced by Params.EncryptPasswordHash using key, then verifies password against the decrypted Argon2id hash.
key is the AES pepper and must be a valid AES key length of 16, 24, or 32 bytes (for AES-128, AES-192, or AES-256 respectively); any other length returns an error before decryption.
As with Params.PasswordVerify, the minimum-length policy is not enforced on verification; the maximum-length guard is.
Returns (true, nil) on a successful match, (false, nil) on a non-match, or (false, err) if password exceeds the configured maximum length, if decryption fails (wrong key, tampered ciphertext), or the inner hash is malformed.
Example ¶
package main
import (
"fmt"
"log"
"github.com/tecnickcom/nurago/pkg/passwordhash"
)
func main() {
opts := []passwordhash.Option{
passwordhash.WithKeyLen(32),
passwordhash.WithSaltLen(16),
passwordhash.WithTime(3),
passwordhash.WithMemory(16_384),
passwordhash.WithThreads(1),
passwordhash.WithMinPasswordLength(16),
passwordhash.WithMaxPasswordLength(128),
}
p := passwordhash.New(opts...)
key := []byte("0123456789012345")
secret := "Example-Password-02"
hash, err := p.EncryptPasswordHash(key, secret)
if err != nil {
log.Fatal(err)
}
ok, err := p.EncryptPasswordVerify(key, secret, hash)
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
ok, err = p.EncryptPasswordVerify(key, "Example-Wrong-Password-02", hash)
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
}
Output: true false
func (*Params) PasswordHash ¶
PasswordHash hashes password using Argon2id and returns a portable, self-describing string suitable for long-term storage.
A cryptographically random salt of length Params.SaltLen is generated for each call, so two hashes of the same password will always differ. The returned string embeds the algorithm, version, all tuning parameters, the salt, and the derived key — everything needed for future verification.
The serialization is base64-encoded JSON by default, or the interoperable PHC string format when WithFormat(FormatPHC) is set. Both are accepted by Params.PasswordVerify, which auto-detects the format, so the choice only affects what other systems read.
Returns an error if the configuration is invalid (ErrInvalidParams, for example a zero-value Params built without New), if password is shorter than MinPasswordLength (ErrPasswordTooShort) or longer than MaxPasswordLength (ErrPasswordTooLong), or if random salt generation fails. Use Params.PasswordVerify to verify stored hashes.
func (*Params) PasswordNeedsRehash ¶
PasswordNeedsRehash reports whether a stored hash — produced by Params.PasswordHash in either serialization format, or imported as an argon2id PHC string from another implementation — was created with parameters that differ from this Params configuration, and so should be re-hashed. Because the storage format is self-describing, parameters can be upgraded (or an algorithm/version changed) without invalidating stored hashes; PasswordNeedsRehash lets callers detect and transparently upgrade a stored hash on the next successful login.
It returns true if the stored algorithm, version, key length, salt length, time, memory, or threads differ from the current configuration, or if the stored value's serialization is not among the accepted formats (see WithFormat) — so a store gradually converges to the configured format through the same rehash-on-login flow that upgrades parameters. It returns an error only if the hash string is oversized or malformed, or embeds out-of-range parameters; a well-formed hash that matches the current parameters and an accepted format returns (false, nil). PasswordNeedsRehash does not verify the password — call it after a successful Params.PasswordVerify.
Example ¶
package main
import (
"fmt"
"log"
"github.com/tecnickcom/nurago/pkg/passwordhash"
)
func main() {
// A hash produced with a weaker cost is detected as needing a rehash by a
// stronger configuration, so it can be transparently upgraded on next login.
weak := passwordhash.New(passwordhash.WithTime(1))
secret := "Example-Password-03"
hash, err := weak.PasswordHash(secret)
if err != nil {
log.Fatal(err)
}
strong := passwordhash.New(passwordhash.WithTime(4))
// The same configuration that produced the hash reports no rehash needed.
weakNeeds, err := weak.PasswordNeedsRehash(hash)
if err != nil {
log.Fatal(err)
}
// The stronger configuration reports that the stored hash should be upgraded.
strongNeeds, err := strong.PasswordNeedsRehash(hash)
if err != nil {
log.Fatal(err)
}
fmt.Println(weakNeeds)
fmt.Println(strongNeeds)
}
Output: false true
func (*Params) PasswordVerify ¶
PasswordVerify checks whether password matches a stored hash produced by Params.PasswordHash in either serialization format, or an argon2id PHC string minted by another implementation (see the package overview for the accepted envelope).
The stored hash is decoded, its algorithm and version are validated, the candidate password is re-hashed with the stored parameters and salt, and the result is compared using crypto/subtle.ConstantTimeCompare to prevent timing attacks.
The minimum-length policy is deliberately NOT enforced here: it is a registration-time policy applied by Params.PasswordHash. Enforcing it on verification would lock out existing users the moment the minimum is raised, returning an error instead of letting them authenticate and be migrated. The maximum-length guard is still applied, as it is the denial-of-service backstop.
The bounds on parameters deserialized from the stored hash are intentionally generous backstops against corrupted or forged blobs: up to 1024 passes, 4 GiB of memory, and 1024-byte keys and salts. They assume stored hashes originate from a trusted store (your own database); a single forged blob at the upper bounds can still cost significant CPU and memory to verify. Hash strings longer than 16 KiB — four times the largest blob those bounds allow — are rejected before any decoding.
Returns (true, nil) on a successful match, (false, nil) on a non-match, or (false, err) if password exceeds the configured maximum length, or if the hash string is oversized or malformed, embeds out-of-range parameters, or uses an incompatible algorithm/version (all matchable with errors.Is).
Example ¶
package main
import (
"fmt"
"log"
"github.com/tecnickcom/nurago/pkg/passwordhash"
)
func main() {
opts := []passwordhash.Option{
passwordhash.WithKeyLen(32),
passwordhash.WithSaltLen(16),
passwordhash.WithTime(3),
passwordhash.WithMemory(16_384),
passwordhash.WithThreads(1),
passwordhash.WithMinPasswordLength(16),
passwordhash.WithMaxPasswordLength(128),
}
p := passwordhash.New(opts...)
secret := "Example-Password-01"
hash, err := p.PasswordHash(secret)
if err != nil {
log.Fatal(err)
}
ok, err := p.PasswordVerify(secret, hash)
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
ok, err = p.PasswordVerify("Example-Wrong-Password-01", hash)
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
}
Output: true false
Example (Phc) ¶
package main
import (
"fmt"
"log"
"github.com/tecnickcom/nurago/pkg/passwordhash"
)
func main() {
// A hash produced by another Argon2 implementation in the standard PHC string
// format is verified transparently, with no configuration change: verification
// detects the format from the stored value's leading '$'.
p := passwordhash.New()
phc := "$argon2id$v=19$m=65536,t=1,p=16$5wnnitUhezr1gnGhyMEU7A$BcbRTU4SCrd14bVS4sqPFbwonv+yiogOnxbV1pQLdV0"
ok, err := p.PasswordVerify("test", phc)
if err != nil {
log.Fatal(err)
}
fmt.Println(ok)
}
Output: true