package rtc import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/binary" "errors" "fmt" "math" "time" ) const volcengineTokenVersion = "001" // VolcengineTokenIssuer creates the production RTC token format documented by // Volcengine. The issuer is deterministic for a room/user/expiry tuple so an // idempotent API replay can reconstruct the same short-lived credential // without persisting it. type VolcengineTokenIssuer struct { appID string appKey []byte ttl time.Duration } func NewVolcengineTokenIssuer(appID, appKey string, ttl time.Duration) (*VolcengineTokenIssuer, error) { if len(appID) != 24 { return nil, errors.New("Volcengine RTC app ID must contain exactly 24 characters") } if len(appKey) < 16 { return nil, errors.New("Volcengine RTC app key must contain at least 16 characters") } if ttl <= 0 { return nil, errors.New("Volcengine RTC token TTL must be positive") } return &VolcengineTokenIssuer{appID: appID, appKey: []byte(appKey), ttl: ttl}, nil } func (i *VolcengineTokenIssuer) Issue(roomID, userID string, expiresAt time.Time) (string, error) { if roomID == "" || userID == "" || expiresAt.IsZero() { return "", errors.New("room, user and expiry are required") } issuedAt := expiresAt.Add(-i.ttl).Unix() expiresUnix := expiresAt.Unix() if issuedAt < 0 || issuedAt > math.MaxUint32 || expiresUnix <= issuedAt || expiresUnix > math.MaxUint32 { return "", errors.New("RTC token issue and expiry times must fit unsigned 32-bit Unix timestamps") } message := bytes.NewBuffer(nil) nonce := i.nonce(roomID, userID, uint32(expiresUnix)) writeUint32(message, nonce) writeUint32(message, uint32(issuedAt)) writeUint32(message, uint32(expiresUnix)) if err := writeString(message, roomID); err != nil { return "", fmt.Errorf("encode RTC room ID: %w", err) } if err := writeString(message, userID); err != nil { return "", fmt.Errorf("encode RTC user ID: %w", err) } // Privilege 0 expands to publish audio/video/data (1/2/3); privilege 4 // permits subscription. All privileges expire with the token itself. privileges := []uint16{0, 1, 2, 3, 4} writeUint16(message, uint16(len(privileges))) for _, privilege := range privileges { writeUint16(message, privilege) writeUint32(message, uint32(expiresUnix)) } mac := hmac.New(sha256.New, i.appKey) _, _ = mac.Write(message.Bytes()) signature := mac.Sum(nil) content := bytes.NewBuffer(nil) if err := writeBytes(content, message.Bytes()); err != nil { return "", err } if err := writeBytes(content, signature); err != nil { return "", err } return volcengineTokenVersion + i.appID + base64.StdEncoding.EncodeToString(content.Bytes()), nil } func (i *VolcengineTokenIssuer) nonce(roomID, userID string, expiresAt uint32) uint32 { mac := hmac.New(sha256.New, i.appKey) _, _ = mac.Write([]byte("rtc-token-nonce\x00" + roomID + "\x00" + userID + "\x00")) var expiry [4]byte binary.LittleEndian.PutUint32(expiry[:], expiresAt) _, _ = mac.Write(expiry[:]) return binary.LittleEndian.Uint32(mac.Sum(nil)[:4]) } func writeUint16(buffer *bytes.Buffer, value uint16) { var encoded [2]byte binary.LittleEndian.PutUint16(encoded[:], value) _, _ = buffer.Write(encoded[:]) } func writeUint32(buffer *bytes.Buffer, value uint32) { var encoded [4]byte binary.LittleEndian.PutUint32(encoded[:], value) _, _ = buffer.Write(encoded[:]) } func writeString(buffer *bytes.Buffer, value string) error { return writeBytes(buffer, []byte(value)) } func writeBytes(buffer *bytes.Buffer, value []byte) error { if len(value) > math.MaxUint16 { return errors.New("RTC token field exceeds 65535 bytes") } writeUint16(buffer, uint16(len(value))) _, _ = buffer.Write(value) return nil }