Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/passwordhash"
Hashes and verifies passwords with Argon2id, following RFC 9106 and the OWASP Password Storage Cheat Sheet.
p := passwordhash.New() // RFC 9106 §4 defaults
hash, err := p.PasswordHash(plaintext)
ok, err := p.PasswordVerify(plaintext, hash)
upgrade, err := p.PasswordNeedsRehash(hash)
Argon2id, and what the defaults cost
Argon2id is memory-hard by design: it forces an attacker to commit RAM per guess. That removes the advantage of the GPU and ASIC hardware that bcrypt and PBKDF2 no longer hold back well.
The RFC 9106 §4 defaults are not cheap:
| Operation | ns/op | B/op | allocs/op |
|---|---|---|---|
PasswordHash | 38,041,654 | 67,131,116 | 127 |
PasswordVerify | 37,245,518 | 67,132,214 | 106 |
EncryptPasswordHash | 35,796,210 | 67,120,842 | 95 |
PasswordVerify (PHC format) | 36,339,052 | 67,119,322 | 87 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/passwordhash/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
That is 38 milliseconds and 64 MiB per hash. Both figures have operational consequences that a login endpoint has to be designed around:
- Roughly 26 hashes per second per core. A login endpoint is not going to serve thousands of requests per second per instance.
- 64 MiB of live allocation per concurrent hash. Fifty concurrent logins is 3.2 GB. A container with a 512 MiB limit will be killed by the eighth concurrent login attempt.
Bound the concurrency on the login path, and size the memory limit against it. Unauthenticated login attempts are also a resource-exhaustion vector for exactly this reason, so rate limiting belongs in front.
Verification costs the same as hashing, by construction. That is the property doing the security work, and it is not a cost to optimise away.
Rehash detection
PasswordNeedsRehash reports whether a stored hash was produced with parameters that no longer match the current configuration.
Password parameters have to increase over time as hardware improves, and this is the mechanism that lets them. After a successful verification the plaintext is in hand for exactly one moment; that is when a hash can be silently upgraded:
ok, err := p.PasswordVerify(plaintext, stored)
if ok {
if upgrade, _ := p.PasswordNeedsRehash(stored); upgrade {
newHash, _ := p.PasswordHash(plaintext)
_ = store(userID, newHash)
}
}
Without it, raising the parameters only protects users who happen to change their password. The accounts most worth protecting are the ones that have not logged in for years, and they keep their old parameters forever.
The pepper layer
EncryptPasswordHash and EncryptPasswordVerify add AES-GCM encryption over the Argon2id hash, keyed by a secret held outside the database.
This is defence against one specific scenario: the attacker has your database and not your application secrets. A SQL injection, a leaked backup, a compromised replica. Salted hashes are still crackable offline; encrypted hashes are not, without the pepper.
It does not help when the application host is compromised, since the pepper is there. The two halves have to be stored separately, in a secrets manager or an environment the database operator cannot read, or the layer adds nothing. awssecretcache is one way to hold it.
Note the encrypted variant benchmarks slightly faster than the plain one; the AES-GCM layer is microseconds against Argon2id’s milliseconds and disappears into the noise.
Storage format
Hashes are self-describing, carrying the algorithm parameters and salt alongside the digest, so a stored hash can always be verified even after the configuration changes. That is what makes PasswordNeedsRehash possible and what makes the migration path incremental rather than a flag day.
Dependencies
Reaches golang.org/x/crypto for the Argon2id implementation.
Links
- Reference documentation
- Source code
- In depth: The Life of a Password: Argon2id in Production with nurago
- Breach checking: passwordpwned
- Token issuing: jwt
- The security guide: /docs/security/
- All nurago packages