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