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}) }