2026-05-23 15:29:05 +00:00
|
|
|
package internals
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/hmac"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-24 07:18:54 +00:00
|
|
|
// ── CSRF ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func newCSRFToken() string {
|
|
|
|
|
b := make([]byte, 24)
|
|
|
|
|
rand.Read(b) //nolint:errcheck
|
|
|
|
|
return hex.EncodeToString(b)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// setCSRFCookie writes a fresh CSRF token to a short-lived cookie and returns
|
|
|
|
|
// the token value so it can be embedded in the rendered HTML form.
|
|
|
|
|
func setCSRFCookie(w http.ResponseWriter) string {
|
|
|
|
|
tok := newCSRFToken()
|
|
|
|
|
http.SetCookie(w, &http.Cookie{
|
|
|
|
|
Name: csrfCookieName,
|
|
|
|
|
Value: tok,
|
|
|
|
|
Path: "/",
|
|
|
|
|
HttpOnly: true,
|
|
|
|
|
Secure: true,
|
|
|
|
|
SameSite: http.SameSiteStrictMode,
|
|
|
|
|
MaxAge: 900, // 15 min — covers slow typists
|
|
|
|
|
})
|
|
|
|
|
return tok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// checkCSRF returns true iff the submitted csrf_token form field matches the
|
|
|
|
|
// cookie value (constant-time compare to prevent timing side-channels).
|
|
|
|
|
func checkCSRF(r *http.Request) bool {
|
|
|
|
|
c, err := r.Cookie(csrfCookieName)
|
|
|
|
|
if err != nil || c.Value == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
formTok := r.FormValue("csrf_token")
|
|
|
|
|
if formTok == "" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return hmac.Equal([]byte(c.Value), []byte(formTok))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 15:29:05 +00:00
|
|
|
func checkCreds(username, password string) bool {
|
|
|
|
|
if username != appCreds.Username {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
got := hashPassword(password, appCreds.Salt)
|
|
|
|
|
return hmac.Equal([]byte(got), []byte(appCreds.Hash))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func initAuthSecret() {
|
|
|
|
|
authSecret = make([]byte, 32)
|
|
|
|
|
rand.Read(authSecret)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeAuthToken() string {
|
|
|
|
|
ts := fmt.Sprintf("%d", time.Now().Unix())
|
|
|
|
|
mac := hmac.New(sha256.New, authSecret)
|
|
|
|
|
mac.Write([]byte(ts))
|
|
|
|
|
return ts + "." + hex.EncodeToString(mac.Sum(nil))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func validAuthToken(token string) bool {
|
|
|
|
|
dot := strings.LastIndex(token, ".")
|
|
|
|
|
if dot < 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
ts, sig := token[:dot], token[dot+1:]
|
|
|
|
|
mac := hmac.New(sha256.New, authSecret)
|
|
|
|
|
mac.Write([]byte(ts))
|
|
|
|
|
if !hmac.Equal([]byte(sig), []byte(hex.EncodeToString(mac.Sum(nil)))) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
var t int64
|
|
|
|
|
fmt.Sscanf(ts, "%d", &t)
|
|
|
|
|
return time.Since(time.Unix(t, 0)) < authTokenTTL
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isAuthed(r *http.Request) bool {
|
|
|
|
|
if nopwMode {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
c, err := r.Cookie(authCookieName)
|
|
|
|
|
return err == nil && validAuthToken(c.Value)
|
|
|
|
|
}
|