// Package auth implements identity: password hashing, sessions, per-library // grants, and the middleware that turns a cookie into a current user. // // Nothing outside this package may inspect a password or a raw session token. package auth import ( "crypto/rand" "crypto/subtle" "encoding/base64" "errors" "fmt" "strings" "golang.org/x/crypto/argon2" ) // Argon2id parameters. These are the OWASP-recommended baseline for // interactive logins (memory cost is the dominant factor). They are encoded // into every hash, so changing them here does not invalidate old passwords — // existing hashes keep verifying with their own stored parameters. const ( argonTime = 2 argonMemory = 64 * 1024 // KiB, i.e. 64 MiB argonThreads = 4 argonKeyLen = 32 argonSaltLen = 16 ) var ( // ErrInvalidHash means a stored password hash is not in the PHC format // this package writes. It indicates database corruption, not a bad login. ErrInvalidHash = errors.New("auth: password hash is not in PHC format") // ErrIncompatibleVersion means the hash was made by a newer argon2 version. ErrIncompatibleVersion = errors.New("auth: incompatible argon2 version") ) // HashPassword derives an argon2id hash and returns it as a PHC string: // // $argon2id$v=19$m=65536,t=2,p=4$$ // // The parameters travel with the hash, so verification never needs them passed // in and parameter changes are backward compatible. func HashPassword(password string) (string, error) { salt := make([]byte, argonSaltLen) if _, err := rand.Read(salt); err != nil { return "", fmt.Errorf("auth: read salt: %w", err) } key := argon2.IDKey([]byte(password), salt, argonTime, argonMemory, argonThreads, argonKeyLen) return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s", argon2.Version, argonMemory, argonTime, argonThreads, base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(key), ), nil } // VerifyPassword reports whether password matches the PHC-encoded hash. // It always performs the full derivation, so a malformed hash and a wrong // password cost the same — there is no timing oracle for "user exists". func VerifyPassword(password, encoded string) (bool, error) { salt, want, params, err := decodeHash(encoded) if err != nil { return false, err } got := argon2.IDKey([]byte(password), salt, params.time, params.memory, params.threads, uint32(len(want))) return subtle.ConstantTimeCompare(got, want) == 1, nil } type argonParams struct { time uint32 memory uint32 threads uint8 } func decodeHash(encoded string) (salt, hash []byte, p argonParams, err error) { parts := strings.Split(encoded, "$") // Leading empty field from the leading '$' gives 6 fields. if len(parts) != 6 || parts[0] != "" || parts[1] != "argon2id" { return nil, nil, p, ErrInvalidHash } var version int if _, err := fmt.Sscanf(parts[2], "v=%d", &version); err != nil { return nil, nil, p, ErrInvalidHash } if version != argon2.Version { return nil, nil, p, ErrIncompatibleVersion } if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &p.memory, &p.time, &p.threads); err != nil { return nil, nil, p, ErrInvalidHash } salt, err = base64.RawStdEncoding.Strict().DecodeString(parts[4]) if err != nil { return nil, nil, p, ErrInvalidHash } hash, err = base64.RawStdEncoding.Strict().DecodeString(parts[5]) if err != nil { return nil, nil, p, ErrInvalidHash } if len(hash) == 0 || len(salt) == 0 { return nil, nil, p, ErrInvalidHash } return salt, hash, p, nil }