Files
patchpinglite/internal/auth/auth.go
jbergner 270c13af5b
All checks were successful
release-tag / release-image (push) Successful in 2m3s
init
2026-05-04 22:25:50 +02:00

144 lines
3.7 KiB
Go

package auth
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"hash"
"net/http"
"strconv"
"strings"
"time"
"releasewatcher/internal/store"
)
const CookieName = "rw_session"
func randomBytes(n int) []byte {
b := make([]byte, n)
_, _ = rand.Read(b)
return b
}
func HashPassword(password string) string {
salt := randomBytes(16)
iters := 210000
dk := pbkdf2Key([]byte(password), salt, iters, 32, sha256.New)
return fmt.Sprintf("pbkdf2-sha256$%d$%s$%s", iters, hex.EncodeToString(salt), hex.EncodeToString(dk))
}
func VerifyPassword(password, encoded string) bool {
parts := strings.Split(encoded, "$")
if len(parts) != 4 || parts[0] != "pbkdf2-sha256" {
return false
}
iters, err := strconv.Atoi(parts[1])
if err != nil {
return false
}
salt, err := hex.DecodeString(parts[2])
if err != nil {
return false
}
want, err := hex.DecodeString(parts[3])
if err != nil {
return false
}
got := pbkdf2Key([]byte(password), salt, iters, len(want), sha256.New)
return hmac.Equal(got, want)
}
func pbkdf2Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
prf := hmac.New(h, password)
hLen := prf.Size()
numBlocks := (keyLen + hLen - 1) / hLen
var dk []byte
for block := 1; block <= numBlocks; block++ {
prf.Reset()
prf.Write(salt)
prf.Write([]byte{byte(block >> 24), byte(block >> 16), byte(block >> 8), byte(block)})
u := prf.Sum(nil)
t := append([]byte(nil), u...)
for i := 1; i < iter; i++ {
prf.Reset()
prf.Write(u)
u = prf.Sum(nil)
for x := range t {
t[x] ^= u[x]
}
}
dk = append(dk, t...)
}
return dk[:keyLen]
}
type Manager struct {
Secret []byte
Store *store.FileStore
}
func New(secret string, st *store.FileStore) *Manager {
if secret == "" {
secret = base64.RawURLEncoding.EncodeToString(randomBytes(32))
}
return &Manager{Secret: []byte(secret), Store: st}
}
func (m *Manager) Sign(userID string) string {
exp := time.Now().Add(12 * time.Hour).Unix()
payload := fmt.Sprintf("%s.%d.%s", userID, exp, base64.RawURLEncoding.EncodeToString(randomBytes(12)))
mac := hmac.New(sha256.New, m.Secret)
mac.Write([]byte(payload))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return base64.RawURLEncoding.EncodeToString([]byte(payload + "." + sig))
}
func (m *Manager) Parse(token string) (store.User, error) {
decoded, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return store.User{}, err
}
parts := strings.Split(string(decoded), ".")
if len(parts) != 4 {
return store.User{}, errors.New("invalid token")
}
payload := strings.Join(parts[:3], ".")
mac := hmac.New(sha256.New, m.Secret)
mac.Write([]byte(payload))
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(want), []byte(parts[3])) {
return store.User{}, errors.New("bad signature")
}
exp, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil || time.Now().Unix() > exp {
return store.User{}, errors.New("expired")
}
u, ok := m.Store.FindUserByID(parts[0])
if !ok {
return store.User{}, errors.New("unknown user")
}
return u, nil
}
func (m *Manager) CurrentUser(r *http.Request) (store.User, bool) {
c, err := r.Cookie(CookieName)
if err != nil {
return store.User{}, false
}
u, err := m.Parse(c.Value)
return u, err == nil
}
func (m *Manager) SetCookie(w http.ResponseWriter, userID string) {
http.SetCookie(w, &http.Cookie{Name: CookieName, Value: m.Sign(userID), Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Secure: false, MaxAge: 12 * 60 * 60})
}
func ClearCookie(w http.ResponseWriter) {
http.SetCookie(w, &http.Cookie{Name: CookieName, Value: "", Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, MaxAge: -1})
}