2026-04-29 12:43:05 +08:00

113 lines
3.4 KiB
Go

// Package tencentsig implements Tencent UserSig v2 signing shared by Chat and RTC.
package tencentsig
import (
"bytes"
"compress/zlib"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
)
// Config contains the paid Tencent application credentials required to sign UserSig.
type Config struct {
// SDKAppID is the application ID assigned by Tencent Cloud.
SDKAppID int64
// SecretKey is the application key; it must never leave backend configuration.
SecretKey string
// TTL controls how long the generated UserSig remains valid.
TTL time.Duration
}
// Result is the transport-neutral UserSig output shared by Chat and RTC wrappers.
type Result struct {
// SDKAppID lets clients initialize the matching Tencent SDK.
SDKAppID int64 `json:"sdk_app_id"`
// UserID is the Tencent identifier derived from the immutable internal user_id.
UserID string `json:"user_id"`
// UserSig is the signed credential passed to Tencent SDK login or enterRoom.
UserSig string `json:"user_sig"`
// ExpireAtMS lets clients refresh before Tencent rejects the credential.
ExpireAtMS int64 `json:"expire_at_ms"`
}
// GenerateUserSig signs a Tencent UserSig with the HMAC-SHA256 v2 algorithm.
func GenerateUserSig(cfg Config, userID string, now time.Time) (Result, error) {
userID = strings.TrimSpace(userID)
if cfg.SDKAppID <= 0 || cfg.SecretKey == "" || cfg.TTL <= 0 || userID == "" {
// UserSig protects paid Tencent resources; incomplete config must fail closed.
return Result{}, fmt.Errorf("tencent usersig config is incomplete")
}
current := now.Unix()
expire := int64(cfg.TTL.Seconds())
rawSig := signPayload(cfg.SecretKey, userID, cfg.SDKAppID, current, expire)
payload := map[string]any{
"TLS.ver": "2.0",
"TLS.identifier": userID,
"TLS.sdkappid": cfg.SDKAppID,
"TLS.expire": expire,
"TLS.time": current,
"TLS.sig": rawSig,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return Result{}, err
}
compressed, err := zlibCompress(payloadBytes)
if err != nil {
return Result{}, err
}
return Result{
SDKAppID: cfg.SDKAppID,
UserID: userID,
UserSig: encodeTencentBase64(compressed),
ExpireAtMS: now.Add(cfg.TTL).UnixMilli(),
}, nil
}
func signPayload(secret string, userID string, sdkAppID int64, current int64, expire int64) string {
// Tencent v2 signing string is line-based and key names are case-sensitive.
content := "TLS.identifier:" + userID + "\n" +
"TLS.sdkappid:" + strconv.FormatInt(sdkAppID, 10) + "\n" +
"TLS.time:" + strconv.FormatInt(current, 10) + "\n" +
"TLS.expire:" + strconv.FormatInt(expire, 10) + "\n"
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte(content))
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}
func zlibCompress(payload []byte) ([]byte, error) {
var buffer bytes.Buffer
writer := zlib.NewWriter(&buffer)
if _, err := writer.Write(payload); err != nil {
_ = writer.Close()
return nil, err
}
if err := writer.Close(); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func encodeTencentBase64(payload []byte) string {
// Tencent UserSig uses standard base64 with URL-hostile characters remapped.
encoded := base64.StdEncoding.EncodeToString(payload)
encoded = strings.ReplaceAll(encoded, "+", "*")
encoded = strings.ReplaceAll(encoded, "/", "-")
encoded = strings.ReplaceAll(encoded, "=", "_")
return encoded
}