All checks were successful
release-tag / release-image (push) Successful in 2m3s
144 lines
3.7 KiB
Go
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})
|
|
}
|