Documentation
¶
Overview ¶
Package crypto provides the low-level primitives authit's service packages build on: password hashing, opaque token generation/hashing, TOTP enrollment/validation, and secret-at-rest encryption. Nothing here touches storage or transport.
Index ¶
- Constants
- Variables
- func CheckPassword(password, hash string) bool
- func DecryptSecret(key []byte, ciphertext []byte) (string, error)
- func EncryptSecret(key []byte, plaintext string) ([]byte, error)
- func GenerateBackupCodes(n int) ([]string, error)
- func GenerateNumericCode(n int) (string, error)
- func GenerateOpaqueToken() (raw string, hash string, err error)
- func GenerateStateToken() (string, error)
- func GenerateTOTPSecret(issuer, accountName string) (*otp.Key, error)
- func GenerateUserCode() (string, error)
- func HashBackupCode(code string) string
- func HashPassword(password string) (string, error)
- func HashPasswordWithCost(password string, cost int) (string, error)
- func HashToken(raw string) string
- func NewID() (string, error)
- func ValidateTOTPCode(secret, code string) bool
- type Argon2idHasher
- type BcryptHasher
- type Hasher
- type PasswordValidator
Constants ¶
const ( DefaultArgon2Memory uint32 = 19 * 1024 // KiB DefaultArgon2Time uint32 = 2 DefaultArgon2Threads uint8 = 1 DefaultArgon2KeyLen uint32 = 32 DefaultArgon2SaltLen uint32 = 16 )
Argon2id parameter defaults, from OWASP's Password Storage Cheat Sheet minimum for Argon2id: 19 MiB of memory, 2 iterations, 1 degree of parallelism.
Memory is per concurrent hash, which is the parameter to think about before raising: at the default, a burst of 100 simultaneous logins wants roughly 1.9 GiB. Raise DefaultArgon2Memory only alongside a limit on concurrent authentication.
const ( DefaultMinPasswordLength = 12 DefaultMaxPasswordLength = 1024 )
Password length bounds used by DefaultPasswordPolicy.
The minimum is OWASP's current floor. The maximum exists because a password is attacker-controlled input to a deliberately expensive function: without a ceiling, a multi-megabyte password is a cheap way to make the server do expensive work. It is set far above any real password so it never rejects a passphrase or a password-manager string.
const BackupCodeBytes = 8
BackupCodeBytes is the entropy behind each generated recovery code. A recovery code is a complete bypass of the second factor, so it is sized as a standalone credential rather than as something a human is expected to memorise: 8 bytes is 64 bits, rendered as 16 hex characters.
It was 4 bytes (32 bits) before, which is brute-forceable given enough attempts. Codes issued under the old size keep working -- they are stored hashed and verification does not check length -- but any account enrolled before this change should be prompted to regenerate.
const DefaultBcryptCost = bcrypt.DefaultCost
DefaultBcryptCost matches bcrypt's own default and is what HashPassword uses unless overridden via HashPasswordWithCost.
Variables ¶
var ErrInvalidKeySize = errors.New("authit/crypto: key must be 32 bytes")
ErrInvalidKeySize is returned by EncryptSecret/DecryptSecret when the key is not 32 bytes (AES-256).
var ErrUnknownHashFormat = errors.New("authit/crypto: unrecognised password hash format")
ErrUnknownHashFormat is returned when a stored hash matches no algorithm this package understands.
var ErrWeakPassword = errors.New("authit/crypto: password does not meet policy")
ErrWeakPassword is the sentinel behind every rejection from the built-in policies. Custom validators are free to return their own errors; the service packages pass whatever comes back through unchanged, so a host can surface a specific reason to the user.
Functions ¶
func CheckPassword ¶
CheckPassword reports whether password matches the given bcrypt hash.
func DecryptSecret ¶
DecryptSecret reverses EncryptSecret.
func EncryptSecret ¶
EncryptSecret encrypts plaintext (e.g. a TOTP secret) with AES-256-GCM under key, so it can be stored at rest without exposing it to anyone with read access to the database alone.
func GenerateBackupCodes ¶
GenerateBackupCodes returns n single-use recovery codes in plaintext. The caller is responsible for showing them to the user exactly once and persisting only their hashes via HashBackupCode.
func GenerateNumericCode ¶ added in v0.4.0
GenerateNumericCode returns a cryptographically random decimal string of exactly n digits, leading zeros included.
It draws uniformly. The obvious implementation -- read a byte and take it modulo 10 -- is biased: 256 is not a multiple of 10, so 0 through 5 come up slightly more often than 6 through 9. Over a six-digit code that is not a break on its own, but it shrinks the search an attacker has to do, and there is no reason to accept it when rejection sampling costs a loop.
A code this short is only safe behind a strict attempt limit; see emaillogin.Config.MaxCodeAttempts for what that has to look like.
func GenerateOpaqueToken ¶
GenerateOpaqueToken returns a fresh cryptographically random token (used for refresh tokens, password reset links, email verification links, and pending-2FA sessions) together with its hash. Only the hash should ever be persisted; the raw value is returned to the caller exactly once.
func GenerateStateToken ¶ added in v0.4.0
GenerateStateToken returns a random, URL-safe value for an OAuth 2.0 `state` parameter.
Unlike the tokens above there is no hash to store: state is compared against a copy the host kept for the duration of one redirect, not looked up in a database, so there is nothing at rest to protect. It is separate from GenerateOpaqueToken only so that call sites say what they mean.
func GenerateTOTPSecret ¶
GenerateTOTPSecret creates a new base32 TOTP secret for the given account (typically the user's email) under issuer (typically the host application's name).
func GenerateUserCode ¶
GenerateUserCode returns an 8-character device-flow user code formatted as "XXXX-XXXX" (RFC 8628 §6.1's recommended shape), e.g. "WDJB-MJHT". At ~34.5 bits of entropy it is deliberately low relative to the device_code — the security property comes from rate-limiting guesses, not from the code's entropy alone (RFC 8628 §5.2).
authit supplies half of that limit itself: device.Config.RateLimiter bounds failed user-code lookups, which is the part a host cannot implement from outside because the lookup happens inside the device package. Set it. The other half — per-IP limiting on whatever route you expose the verification form at — is still yours, and this comment used to name a requirement without saying where either half came from.
func HashBackupCode ¶
HashBackupCode hashes a backup code for storage/comparison, using the same opaque-token hash as everywhere else in authit.
func HashPassword ¶
HashPassword hashes a plaintext password for storage.
func HashPasswordWithCost ¶
HashPasswordWithCost hashes a plaintext password using an explicit bcrypt cost, e.g. a lower cost in tests to keep them fast.
func NewID ¶
NewID returns a random UUIDv4 string. authit's service packages use this to assign IDs before handing records to a Store, so stores never need to generate identifiers themselves.
func ValidateTOTPCode ¶
ValidateTOTPCode reports whether code is a currently-valid TOTP code for secret.
Types ¶
type Argon2idHasher ¶ added in v0.4.0
type Argon2idHasher struct {
Memory uint32 // KiB
Time uint32
Threads uint8
KeyLen uint32
SaltLen uint32
}
Argon2idHasher hashes with Argon2id and encodes the result in the PHC string format used by the reference implementation:
$argon2id$v=19$m=19456,t=2,p=1$<salt>$<key>
The parameters travel with the hash, so changing them does not invalidate existing rows -- Verify reads each hash's own parameters, and NeedsRehash flags rows that are weaker than the current settings.
func NewArgon2idHasher ¶ added in v0.4.0
func NewArgon2idHasher() Argon2idHasher
NewArgon2idHasher returns an Argon2idHasher with the default parameters.
func (Argon2idHasher) Hash ¶ added in v0.4.0
func (h Argon2idHasher) Hash(password string) (string, error)
func (Argon2idHasher) NeedsRehash ¶ added in v0.4.0
func (h Argon2idHasher) NeedsRehash(hash string) bool
NeedsRehash reports true for any hash that is not argon2id -- notably every bcrypt hash, which is how an existing corpus migrates -- and for argon2id hashes whose stored parameters are weaker than this hasher's.
func (Argon2idHasher) Verify ¶ added in v0.4.0
func (h Argon2idHasher) Verify(password, hash string) bool
type BcryptHasher ¶ added in v0.4.0
type BcryptHasher struct {
// Cost is the bcrypt work factor. Zero means DefaultBcryptCost.
Cost int
}
BcryptHasher hashes with bcrypt. It is no longer the default: bcrypt is not memory-hard, and it silently ignores everything past the 72nd byte of a password (x/crypto returns bcrypt.ErrPasswordTooLong rather than truncating, so Hash surfaces that as an error). It remains available for applications that must stay compatible with an existing corpus of bcrypt hashes they cannot re-hash.
func (BcryptHasher) Hash ¶ added in v0.4.0
func (h BcryptHasher) Hash(password string) (string, error)
func (BcryptHasher) NeedsRehash ¶ added in v0.4.0
func (h BcryptHasher) NeedsRehash(hash string) bool
NeedsRehash reports true for any hash that is not bcrypt, and for bcrypt hashes stored at a lower cost than this hasher would use now.
func (BcryptHasher) Verify ¶ added in v0.4.0
func (h BcryptHasher) Verify(password, hash string) bool
type Hasher ¶ added in v0.4.0
type Hasher interface {
// Hash returns a self-describing hash of password.
Hash(password string) (string, error)
// Verify reports whether password produced hash. It must return false,
// not an error, for any malformed or unrecognised hash.
Verify(password, hash string) bool
// NeedsRehash reports whether hash was produced by a weaker algorithm
// or weaker parameters than this Hasher would use now.
NeedsRehash(hash string) bool
}
Hasher turns a plaintext password into a string safe to store, and checks a candidate against one. A host application selects an implementation via user.Config.PasswordHasher / superuser.Config.PasswordHasher; the default is Argon2id with OWASP-recommended parameters.
Implementations MUST be able to Verify any hash format authit has ever written, not only their own -- otherwise switching hashers logs every existing user out permanently. Both implementations here dispatch on the hash's self-describing prefix, so an application can move from bcrypt to argon2id (or back) with no migration: NeedsRehash reports which stored hashes are not in the current hasher's preferred form, and the service packages re-hash them on the next successful login.
func DefaultHasher ¶ added in v0.4.0
func DefaultHasher() Hasher
DefaultHasher returns the Hasher used when a Config leaves PasswordHasher nil: Argon2id at OWASP's recommended minimum parameters.
type PasswordValidator ¶ added in v0.4.0
PasswordValidator reports whether a password is acceptable, returning a non-nil error to reject it. The email address of the account the password is being set on is supplied so a policy can reject passwords derived from it; it is the address the caller passed in, and may be empty on flows where it is not known.
A validator is called before the password is hashed, on registration, password change and password reset. It is not called on login: an existing password that no longer meets a tightened policy must still let its owner in, or raising the policy locks out the users it was meant to protect.
func AllPolicies ¶ added in v0.4.0
func AllPolicies(validators ...PasswordValidator) PasswordValidator
AllPolicies runs each validator in order and returns the first rejection. Nil validators are skipped, so it is safe to build a list conditionally.
func DefaultPasswordPolicy ¶ added in v0.4.0
func DefaultPasswordPolicy() PasswordValidator
DefaultPasswordPolicy is what a Config with no PasswordValidator gets. It enforces length only, deliberately: composition rules ("one uppercase, one digit") measurably reduce entropy by steering users toward predictable substitutions, and blocklist checks need a corpus authit has no business shipping. Compose your own for anything stricter -- a breached-password check is the highest-value addition.
func LengthPolicy ¶ added in v0.4.0
func LengthPolicy(min, max int) PasswordValidator
LengthPolicy rejects passwords shorter than min or longer than max runes. A max of 0 means DefaultMaxPasswordLength.
func NotEmailPolicy ¶ added in v0.4.0
func NotEmailPolicy() PasswordValidator
NotEmailPolicy rejects a password that contains, or is contained by, the local part of the account's email address. Compose it with LengthPolicy via AllPolicies.