Files
loginsrv/htpasswd/auth.go
T

126 lines
2.7 KiB
Go

package htpasswd
import (
"bytes"
"crypto/sha1"
"crypto/subtle"
"encoding/base64"
"encoding/csv"
"fmt"
"github.com/abbot/go-http-auth"
"golang.org/x/crypto/bcrypt"
"io"
"os"
"strings"
"sync"
"time"
)
// Auth is the htpassword authenticater
type Auth struct {
filename string
userHash map[string]string
// Used in func reloadIfChanged to reload htpasswd file if it changed
modTime time.Time
mu sync.RWMutex
}
// NewAuth creates an htpassword authenticater
func NewAuth(filename string) (*Auth, error) {
a := &Auth{
filename: filename,
}
return a, a.parse(filename)
}
func (a *Auth) parse(filename string) error {
r, err := os.Open(filename)
if err != nil {
return err
}
fileInfo, err := os.Stat(filename)
if err != nil {
return err
}
a.modTime = fileInfo.ModTime()
cr := csv.NewReader(r)
cr.Comma = ':'
cr.Comment = '#'
cr.TrimLeadingSpace = true
a.mu.Lock()
defer a.mu.Unlock()
a.userHash = map[string]string{}
for {
record, err := cr.Read()
if err == io.EOF {
break
}
if err != nil {
return err
}
if len(record) != 2 {
return fmt.Errorf("password file in wrong format (%v)", filename)
}
a.userHash[record[0]] = record[1]
}
return nil
}
// Authenticate the user
func (a *Auth) Authenticate(username, password string) (bool, error) {
reloadIfChanged(a)
a.mu.RLock()
defer a.mu.RUnlock()
if hash, exist := a.userHash[username]; exist {
h := []byte(hash)
p := []byte(password)
if strings.HasPrefix(hash, "$2y$") || strings.HasPrefix(hash, "$2b$") {
matchErr := bcrypt.CompareHashAndPassword(h, p)
return (matchErr == nil), nil
}
if strings.HasPrefix(hash, "{SHA}") {
return compareSha(h, p), nil
}
if strings.HasPrefix(hash, "$apr1$") {
return compareMD5(h, p), nil
}
return false, fmt.Errorf("unknown algorithm for user %q", username)
}
return false, nil
}
// Reload htpasswd file if it changed during current run
func reloadIfChanged(a *Auth) {
fileInfo, err := os.Stat(a.filename)
if err != nil {
//On error, retain current file
return
}
currentmodTime := fileInfo.ModTime()
if currentmodTime != a.modTime {
a.modTime = currentmodTime
a.parse(a.filename)
}
}
func compareSha(hashedPassword, password []byte) bool {
d := sha1.New()
d.Write(password)
return 1 == subtle.ConstantTimeCompare(hashedPassword[5:], []byte(base64.StdEncoding.EncodeToString(d.Sum(nil))))
}
func compareMD5(hashedPassword, password []byte) bool {
parts := bytes.SplitN(hashedPassword, []byte("$"), 4)
if len(parts) != 4 {
return false
}
magic := []byte("$" + string(parts[1]) + "$")
salt := parts[2]
return 1 == subtle.ConstantTimeCompare(hashedPassword, auth.MD5Crypt(password, salt, magic))
}