Documentation
¶
Overview ¶
Package auth holds the server's credentials: how a password is hashed, how a random token is minted, and how any of it is compared.
It is a leaf package on purpose. Every comparison in here is constant time, every random value comes from crypto/rand, and no secret is ever stored in the form it was presented in — a password becomes an argon2id digest, a token becomes its SHA-256. A database dump therefore contains nothing that can be replayed, which is the property the whole design is for.
Two kinds of credential live here and they are not interchangeable:
- An administrator signs in to the admin panel with an email and a password, and gets a session cookie.
- A driver's machine never has a password. It is paired once and holds a 256-bit device token.
Index ¶
- Constants
- Variables
- func CheckPasswordStrength(password string) error
- func EqualBytes(a, b []byte) bool
- func EqualSetupToken(want, got string) bool
- func EqualString(a, b string) bool
- func HashPassword(password string) (string, error)
- func HashPasswordWith(p Params, password string) (string, error)
- func HashToken(plain string) []byte
- func MaxConcurrentHashes() int
- func NeedsRehash(encoded string) bool
- func NewCSRFToken() (string, error)
- func NewSessionToken() (plain string, sum []byte, err error)
- func NewSetupToken() (string, error)
- func NewUserCode() (string, error)
- func NormaliseSetupToken(s string) string
- func NormaliseUserCode(s string) string
- func SplitDeviceToken(plain string) (sum []byte, prefix string, err error)
- func VerifyPassword(encoded, password string) (bool, error)
- type DeviceToken
- type Keyring
- type PairingCodes
- type Params
- type SecretKey
Constants ¶
const CSRFTokenBytes = 32
CSRFTokenBytes is the entropy of a cross-site request forgery token.
const DeviceCodeBytes = 32
DeviceCodeBytes is the entropy of the device code a client polls with: 256 bits, the same as the token it is exchanged for. It is a secret, so it is stored the way every secret here is — as its SHA-256.
const DevicePrefixLen = 8
DevicePrefixLen is how much of a device token is kept in clear text, purely so a lookup is an index seek rather than a scan of every row. Eight characters of base64 is 48 bits, which narrows a lookup to one row in practice and is far too little to reconstruct anything.
const DeviceTokenBytes = 32
DeviceTokenBytes is the entropy of a device token: 256 bits. It is never guessed, only stolen, so the length is chosen to end the conversation about brute force rather than to be convenient.
const MaxPasswordLen = 1024
MaxPasswordLen caps what is hashed, because argon2id will happily chew through a megabyte of input and an unauthenticated form must not be able to ask it to.
const MinPasswordLen = 12
MinPasswordLen is the shortest password the server accepts. Length is the only rule: composition rules push people towards "Passw0rd!" and buy nothing.
const SecretKeyBytes = 32
SecretKeyBytes is the length of the data key that encrypts the operator's own API keys at rest: AES-256.
const SessionTokenBytes = 32
SessionTokenBytes is the entropy of an admin session cookie.
const SetupTokenBytes = 20
SetupTokenBytes is the entropy of the one-time setup token: 160 bits, which encodes as exactly 32 characters of the alphabet above with nothing left over.
const SetupTokenGroup = 4
SetupTokenGroup is how many characters sit between the hyphens.
const UserCodeGroup = 3
UserCodeGroup is how many characters sit before the hyphen: "H4T-9KQ".
const UserCodeLen = 6
UserCodeLen is how many characters the driver reads out to the operator. Six characters of the 32-letter alphabet below is thirty bits, which is far more than a code that lives for ten minutes behind a rate limiter needs, and short enough to say down a voice channel without repeating it.
Variables ¶
var DefaultParams = Params{Memory: 64 * 1024, Time: 3, Threads: 2, SaltLen: 16, KeyLen: 32}
DefaultParams is 64 MiB over three passes, comfortably above the OWASP minimum for argon2id. It costs roughly a tenth of a second on a laptop, which is what makes an offline attack on a stolen database expensive.
var ErrInvalidHash = errors.New("auth: stored password hash is not readable")
ErrInvalidHash is returned when a stored hash cannot be parsed. It means the row was corrupted or written by something else, not that the password was wrong, and it must never be reported to the person signing in as either.
var ErrMalformedToken = errors.New("auth: token is not the right shape")
ErrMalformedToken is returned when a presented token is not even the right shape. It is worth distinguishing from "no such token" only in a log line; the caller is told the same thing either way.
var ErrNoSecretKey = errors.New("auth: no secret key, so the stored API key cannot be read")
ErrNoSecretKey is returned when something sealed needs opening and no key is available — the usual cause being a data directory that was lost while the database survived. It is not corruption: the operator retypes the API key and the feature comes back.
var ErrSealed = errors.New("auth: the stored value does not open with this key")
ErrSealed is returned when a sealed value will not open with the key given.
Functions ¶
func CheckPasswordStrength ¶
CheckPasswordStrength reports whether a password may be used, phrased for the person who has to choose another one.
func EqualBytes ¶
EqualBytes compares two digests in constant time.
func EqualSetupToken ¶
EqualSetupToken compares a typed token against the real one in constant time, after normalising both.
func EqualString ¶
EqualString compares two secrets in constant time. Length is not secret, so leaking it through an early return is fine; which byte differs is, so the comparison itself must not stop at the first difference.
func HashPassword ¶
HashPassword hashes a password with DefaultParams and returns the PHC string that is stored in the database.
func HashPasswordWith ¶
HashPasswordWith hashes a password with explicit parameters. Tests use it to stay fast; the server uses HashPassword.
func MaxConcurrentHashes ¶
func MaxConcurrentHashes() int
MaxConcurrentHashes is how many password hashes may run at once.
func NeedsRehash ¶
NeedsRehash reports whether a stored hash was made with weaker parameters than DefaultParams and should be rewritten at the next successful sign-in.
func NewCSRFToken ¶
NewCSRFToken mints a cross-site request forgery token.
func NewSessionToken ¶
NewSessionToken mints an admin session token and returns it with its digest. The cookie carries the plain value; the database holds only the digest, so a dump cannot be used to sign in as anyone.
func NewSetupToken ¶
NewSetupToken mints the token printed on the terminal at first run, hyphenated into groups so it can be read aloud and typed without losing one's place.
func NewUserCode ¶
NewUserCode mints the short code on its own, for the case where a collision forces a second attempt.
func NormaliseSetupToken ¶
NormaliseSetupToken puts a token the operator typed into the one spelling the comparison uses: upper case, no hyphens or spaces, and with the four characters the alphabet leaves out folded onto the ones they are mistaken for. Someone who types a lower-case "l" for a "1" gets in; someone who types the wrong token does not.
func NormaliseUserCode ¶
NormaliseUserCode puts a code an operator typed into the one spelling the comparison uses, folding the four characters the alphabet leaves out onto the ones they are misread as. It is NormaliseSetupToken under a name that says what it is being used for.
func SplitDeviceToken ¶
SplitDeviceToken turns a presented token into the two things the database is queried with: its lookup prefix and its digest. It is the only way a token from a request becomes a row, so the hashing cannot be forgotten at a call site.
func VerifyPassword ¶
VerifyPassword reports whether password matches the stored hash. The comparison is constant time, and a hash that cannot be parsed is ErrInvalidHash rather than a quiet false, so a corrupted row shows up as a fault instead of as everyone's password being wrong.
Types ¶
type DeviceToken ¶
DeviceToken is a freshly minted device credential in its three forms.
Plain is shown to the client once, at pairing, and never again — it is not stored anywhere. Sum is what the database holds. Prefix is the clear-text fragment the lookup index is built on.
func NewDeviceToken ¶
func NewDeviceToken() (DeviceToken, error)
NewDeviceToken mints a device token.
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
Keyring holds the data key in a place the whole process shares, so that an operator who regenerates it in the admin panel does not have to restart the server for the API to notice.
Without it the key would be a value captured when the handlers were built, and a regenerated key would leave every sealed credential unreadable until a restart — which is the one thing the danger zone must not do quietly.
func NewKeyring ¶
NewKeyring holds k. A nil key is valid: it is a server whose configuration file carries no data key, and sealed values simply do not open.
type PairingCodes ¶
PairingCodes is one device-code grant in the three forms the flow needs.
DeviceCode is returned to the client once and never stored; DeviceCodeSum is what the pairings row holds. UserCode is the short code the driver reads out and an operator matches in the admin panel, and it is not a secret — it is useless without the device code.
func NewPairingCodes ¶
func NewPairingCodes() (PairingCodes, error)
NewPairingCodes mints one pairing's codes.
type Params ¶
type Params struct {
// Memory is the memory cost in KiB.
Memory uint32
// Time is the number of passes.
Time uint32
// Threads is the parallelism.
Threads uint8
// SaltLen and KeyLen are in bytes.
SaltLen, KeyLen uint32
}
Params are the argon2id cost parameters. They are encoded into every hash, so raising them later does not invalidate the passwords already stored: an old hash still verifies against the parameters it was made with, and is rewritten at the next successful sign-in.
type SecretKey ¶
type SecretKey []byte
SecretKey encrypts the values that must not be readable in a database dump — today every one of them belongs to a plugin: the core holds no vendor credential of its own.
It lives in the configuration file beside the binary, not in the database, which is the whole point: a dump of the database contains the ciphertext and nothing that opens it. The cost is that losing the data directory loses the stored API key, and the server says so and asks for it again rather than pretending the feature is on.
func ParseSecretKey ¶
ParseSecretKey reads a key back out of the configuration file. An empty string is not an error: it is a server that has never been given one.
func (SecretKey) Open ¶
Open decrypts a value produced by SecretKey.Seal. An empty input is an empty result, which is how "no key was ever stored" is spelled.