// Package auth has password hashing, session tokens and a login rate limiter. package auth import ( "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "sync" "time" "golang.org/x/crypto/bcrypt" ) const MinPasswordLength = 8 func HashPassword(pw string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(pw), 12) return string(b), err } func CheckPassword(hash, pw string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pw)) == nil } // dummyHash lets failed lookups of unknown users take as long as real ones. var dummyHash, _ = bcrypt.GenerateFromPassword([]byte("not-a-real-password"), 12) func BurnTime(pw string) { _ = bcrypt.CompareHashAndPassword(dummyHash, []byte(pw)) } // NewToken returns a random session token and the hash stored server side. func NewToken() (token, hash string) { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic(err) } token = base64.RawURLEncoding.EncodeToString(b) return token, HashToken(token) } func HashToken(token string) string { sum := sha256.Sum256([]byte(token)) return hex.EncodeToString(sum[:]) } // Limiter allows a fixed number of failed attempts per key per window. type Limiter struct { mu sync.Mutex max int window time.Duration hits map[string][]time.Time } func NewLimiter(max int, window time.Duration) *Limiter { return &Limiter{max: max, window: window, hits: map[string][]time.Time{}} } func (l *Limiter) prune(key string, now time.Time) []time.Time { kept := l.hits[key][:0] for _, t := range l.hits[key] { if now.Sub(t) < l.window { kept = append(kept, t) } } if len(kept) == 0 { delete(l.hits, key) return nil } l.hits[key] = kept return kept } // Blocked reports whether key has used up its attempts. func (l *Limiter) Blocked(key string) bool { l.mu.Lock() defer l.mu.Unlock() return len(l.prune(key, time.Now())) >= l.max } func (l *Limiter) Fail(key string) { l.mu.Lock() defer l.mu.Unlock() now := time.Now() l.hits[key] = append(l.prune(key, now), now) } func (l *Limiter) Reset(key string) { l.mu.Lock() defer l.mu.Unlock() delete(l.hits, key) }