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