Restart.
Data foundation started, with updated guidelines and golangci-lint config.
This commit is contained in:
160
data/user.go
Normal file
160
data/user.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package data
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
// AccountType distinguishes human users from system/service accounts.
|
||||
type AccountType string
|
||||
|
||||
const (
|
||||
AccountHuman AccountType = "human"
|
||||
AccountSystem AccountType = "system"
|
||||
)
|
||||
|
||||
// User represents an identity in the system. It supports password and optional TOTP.
|
||||
type User struct {
|
||||
ID string
|
||||
Username string
|
||||
Type AccountType
|
||||
Roles []string
|
||||
|
||||
// Password hashing material
|
||||
pwdHash []byte
|
||||
pwdSalt []byte
|
||||
|
||||
// Base32 encoded TOTP secret (RFC 3548). Empty means TOTP disabled.
|
||||
TOTPSecret string
|
||||
}
|
||||
|
||||
// Scrypt parameters. Chosen for interactive logins; can be tuned later.
|
||||
const (
|
||||
scryptN = 1 << 15 // 32768
|
||||
scryptR = 8
|
||||
scryptP = 1
|
||||
keyLen = 32
|
||||
saltLen = 16
|
||||
)
|
||||
|
||||
// SetPassword hashes and stores the provided password using scrypt with a random salt.
|
||||
func (u *User) SetPassword(password string) error {
|
||||
if password == "" {
|
||||
return errors.New("password cannot be empty")
|
||||
}
|
||||
salt := make([]byte, saltLen)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return err
|
||||
}
|
||||
dk, err := scrypt.Key([]byte(password), salt, scryptN, scryptR, scryptP, keyLen)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.pwdSalt = salt
|
||||
u.pwdHash = dk
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPassword verifies a plaintext password against the stored scrypt hash.
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
if len(u.pwdSalt) == 0 || len(u.pwdHash) == 0 {
|
||||
return false
|
||||
}
|
||||
dk, err := scrypt.Key([]byte(password), u.pwdSalt, scryptN, scryptR, scryptP, keyLen)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare(dk, u.pwdHash) == 1
|
||||
}
|
||||
|
||||
// PasswordHash returns a portable string representation of the scrypt hash and salt.
|
||||
// The format is: scrypt:N:r:p:base64(salt):base64(hash).
|
||||
func (u *User) PasswordHash() string {
|
||||
if len(u.pwdSalt) == 0 || len(u.pwdHash) == 0 {
|
||||
return ""
|
||||
}
|
||||
return "scrypt:" +
|
||||
strconv.Itoa(scryptN) + ":" + strconv.Itoa(scryptR) + ":" + strconv.Itoa(scryptP) + ":" +
|
||||
base64.RawStdEncoding.EncodeToString(u.pwdSalt) + ":" +
|
||||
base64.RawStdEncoding.EncodeToString(u.pwdHash)
|
||||
}
|
||||
|
||||
const passwordHashParts = 5
|
||||
|
||||
// LoadPasswordHash parses the value produced by PasswordHash and loads it into the user.
|
||||
func (u *User) LoadPasswordHash(s string) error {
|
||||
if s == "" {
|
||||
u.pwdSalt = nil
|
||||
u.pwdHash = nil
|
||||
return nil
|
||||
}
|
||||
const prefix = "scrypt:"
|
||||
if len(s) < len(prefix) || s[:len(prefix)] != prefix {
|
||||
return errors.New("unsupported password hash format")
|
||||
}
|
||||
parts := splitN(s[len(prefix):], ':', passwordHashParts)
|
||||
if len(parts) != passwordHashParts {
|
||||
return errors.New("invalid password hash")
|
||||
}
|
||||
// We currently ignore parsed N,r,p and use compiled constants to avoid variable-time params.
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(hash) != keyLen {
|
||||
return errors.New("invalid hash length")
|
||||
}
|
||||
u.pwdSalt = salt
|
||||
u.pwdHash = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyTOTP checks a TOTP code for the user's TOTP secret. If no secret is set, it fails.
|
||||
// The window parameter defines the allowed step skew (+/- window steps).
|
||||
func (u *User) VerifyTOTP(code string, at time.Time, window int) bool {
|
||||
if u.TOTPSecret == "" || code == "" {
|
||||
return false
|
||||
}
|
||||
return defaultTOTPValidator.Validate(u.TOTPSecret, code, at, window)
|
||||
}
|
||||
|
||||
// TOTPValidator abstracts TOTP verification to allow swapping implementations.
|
||||
type TOTPValidator interface {
|
||||
Validate(secret, code string, at time.Time, window int) bool
|
||||
}
|
||||
|
||||
var defaultTOTPValidator TOTPValidator = builtinTOTPValidator{}
|
||||
|
||||
// splitN is like strings.SplitN but without importing another package here.
|
||||
func splitN(s string, sep rune, n int) []string {
|
||||
if n <= 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, n)
|
||||
start := 0
|
||||
count := 1
|
||||
for i, r := range s {
|
||||
if r == sep {
|
||||
out = append(out, s[start:i])
|
||||
start = i + 1
|
||||
count++
|
||||
if count == n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if start <= len(s) {
|
||||
out = append(out, s[start:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user