118 lines
3.7 KiB
Go
118 lines
3.7 KiB
Go
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
|
|
}
|