Files
ts3-monolith-go/internal/web/tokens.go
T
2026-10-30 10:56:13 +01:00

110 lines
3.0 KiB
Go

package web
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"strconv"
"time"
"ts3monolith/internal/ts3"
)
type tokenManager struct{ secret []byte }
type tokenPayload struct {
Kind string `json:"k"`
IP string `json:"ip"`
Exp int64 `json:"exp"`
Nonce string `json:"n"`
ClID int `json:"clid,omitempty"`
DBID int `json:"dbid,omitempty"`
Nick string `json:"nick,omitempty"`
UID string `json:"uid,omitempty"`
}
func newTokenManager(secret string) *tokenManager { return &tokenManager{secret: []byte(secret)} }
func (m *tokenManager) sign(p tokenPayload) (string, error) {
raw, err := json.Marshal(p)
if err != nil {
return "", err
}
body := base64.RawURLEncoding.EncodeToString(raw)
mac := hmac.New(sha256.New, m.secret)
_, _ = mac.Write([]byte(body))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return body + "." + sig, nil
}
func (m *tokenManager) verify(tok, kind, ip string) (tokenPayload, error) {
var p tokenPayload
body, sig, ok := splitToken(tok)
if !ok {
return p, errors.New("invalid token")
}
mac := hmac.New(sha256.New, m.secret)
_, _ = mac.Write([]byte(body))
got, err := base64.RawURLEncoding.DecodeString(sig)
if err != nil || !hmac.Equal(got, mac.Sum(nil)) {
return p, errors.New("invalid token signature")
}
raw, err := base64.RawURLEncoding.DecodeString(body)
if err != nil {
return p, err
}
if err = json.Unmarshal(raw, &p); err != nil {
return p, err
}
if p.Kind != kind || p.IP != ip || time.Now().Unix() > p.Exp {
return p, errors.New("expired or mismatched token")
}
return p, nil
}
func splitToken(s string) (string, string, bool) {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == '.' {
return s[:i], s[i+1:], true
}
}
return "", "", false
}
func randomNonce() string {
b := make([]byte, 18)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
func (m *tokenManager) formToken(ip string) string {
t, _ := m.sign(tokenPayload{Kind: "form", IP: ip, Exp: time.Now().Add(10 * time.Minute).Unix(), Nonce: randomNonce()})
return t
}
func (m *tokenManager) clientToken(ip string, c ts3.Client) string {
t, _ := m.sign(tokenPayload{Kind: "client", IP: ip, Exp: time.Now().Add(10 * time.Minute).Unix(), Nonce: randomNonce(), ClID: c.ID, DBID: c.DatabaseID, Nick: c.Nick, UID: c.UID})
return t
}
func (m *tokenManager) challengeToken(ip string, c ts3.Client) (token string, code string, err error) {
p := tokenPayload{Kind: "challenge", IP: ip, Exp: time.Now().Add(5 * time.Minute).Unix(), Nonce: randomNonce(), ClID: c.ID, DBID: c.DatabaseID, Nick: c.Nick, UID: c.UID}
token, err = m.sign(p)
if err != nil {
return "", "", err
}
return token, m.challengeCode(p), nil
}
func (m *tokenManager) challengeCode(p tokenPayload) string {
mac := hmac.New(sha256.New, m.secret)
_, _ = mac.Write([]byte("verify|" + p.Nonce + "|" + strconv.Itoa(p.ClID) + "|" + p.IP))
sum := mac.Sum(nil)
n := (uint32(sum[0])<<24 | uint32(sum[1])<<16 | uint32(sum[2])<<8 | uint32(sum[3])) % 1000000
return fmt.Sprintf("%06d", n)
}