crypto

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
const DefaultBcryptCost = bcrypt.DefaultCost

DefaultBcryptCost matches bcrypt's own default and is what HashPassword uses unless overridden via HashPasswordWithCost.

Variables

View Source
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).

View Source
var ErrUnknownHashFormat = errors.New("authit/crypto: unrecognised password hash format")

ErrUnknownHashFormat is returned when a stored hash matches no algorithm this package understands.

View Source
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

func CheckPassword(password, hash string) bool

CheckPassword reports whether password matches the given bcrypt hash.

func DecryptSecret

func DecryptSecret(key []byte, ciphertext []byte) (string, error)

DecryptSecret reverses EncryptSecret.

func EncryptSecret

func EncryptSecret(key []byte, plaintext string) ([]byte, error)

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

func GenerateBackupCodes(n int) ([]string, error)

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

func GenerateNumericCode(n int) (string, error)

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

func GenerateOpaqueToken() (raw string, hash string, err error)

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

func GenerateStateToken() (string, error)

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

func GenerateTOTPSecret(issuer, accountName string) (*otp.Key, error)

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

func GenerateUserCode() (string, error)

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

func HashBackupCode(code string) string

HashBackupCode hashes a backup code for storage/comparison, using the same opaque-token hash as everywhere else in authit.

func HashPassword

func HashPassword(password string) (string, error)

HashPassword hashes a plaintext password for storage.

func HashPasswordWithCost

func HashPasswordWithCost(password string, cost int) (string, error)

HashPasswordWithCost hashes a plaintext password using an explicit bcrypt cost, e.g. a lower cost in tests to keep them fast.

func HashToken

func HashToken(raw string) string

HashToken hashes a raw opaque token for lookup/storage.

func NewID

func NewID() (string, error)

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

func ValidateTOTPCode(secret, code string) bool

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

type PasswordValidator func(ctx context.Context, email, password string) error

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.

Jump to

Keyboard shortcuts

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