snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
117
internal/client/track_signature.go
Normal file
117
internal/client/track_signature.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const gracePeriodSeconds = 5
|
||||
|
||||
func parseSignatureTimestamps(sig string) (iat int64, expiry int64) {
|
||||
first := strings.IndexByte(sig, ':')
|
||||
if first < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
rest := sig[first+1:]
|
||||
second := strings.IndexByte(rest, ':')
|
||||
if second < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
iatVal, err := strconv.ParseInt(sig[:first], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
expiryVal, err := strconv.ParseInt(rest[:second], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return iatVal, expiryVal
|
||||
}
|
||||
|
||||
// trackSignatureVerifier reuses HMAC hashers and buffers for signature verification.
|
||||
// Thread-safe via sync.Pool.
|
||||
type trackSignatureVerifier struct {
|
||||
macPool sync.Pool
|
||||
bufPool sync.Pool
|
||||
}
|
||||
|
||||
type verifyBufs struct {
|
||||
payload []byte
|
||||
hexBuf []byte
|
||||
expectedHex []byte
|
||||
}
|
||||
|
||||
func newTrackSignatureVerifier(secret string) *trackSignatureVerifier {
|
||||
return &trackSignatureVerifier{
|
||||
macPool: sync.Pool{
|
||||
New: func() any {
|
||||
return hmac.New(sha256.New, []byte(secret))
|
||||
},
|
||||
},
|
||||
bufPool: sync.Pool{
|
||||
New: func() any {
|
||||
return &verifyBufs{
|
||||
payload: make([]byte, 0, 256),
|
||||
hexBuf: make([]byte, hex.EncodedLen(sha256.Size)),
|
||||
expectedHex: make([]byte, hex.EncodedLen(sha256.Size)),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyTrackSignature(secret string, channel string, sig string, keys []string, userID string) bool {
|
||||
return newTrackSignatureVerifier(secret).verify(channel, sig, keys, userID)
|
||||
}
|
||||
|
||||
func (v *trackSignatureVerifier) verify(channel string, sig string, keys []string, userID string) bool {
|
||||
first := strings.IndexByte(sig, ':')
|
||||
if first < 0 {
|
||||
return false
|
||||
}
|
||||
rest := sig[first+1:]
|
||||
second := strings.IndexByte(rest, ':')
|
||||
if second < 0 {
|
||||
return false
|
||||
}
|
||||
iatStr := sig[:first]
|
||||
expiryStr := rest[:second]
|
||||
hmacHex := rest[second+1:]
|
||||
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
|
||||
mac := v.macPool.Get().(hash.Hash)
|
||||
bufs := v.bufPool.Get().(*verifyBufs)
|
||||
|
||||
// Payload fields are NUL-separated rather than ':'-separated so colons
|
||||
// inside userID or channel (e.g. "news:tech", "alice:org") can't shift
|
||||
// between fields and yield a colliding HMAC for a different (userID,
|
||||
// channel) tuple. The outer signature string itself stays ':'-separated
|
||||
// because its fields (iat / exp / hmac_hex) are colon-free by construction.
|
||||
mac.Reset()
|
||||
bufs.payload = bufs.payload[:0]
|
||||
bufs.payload = append(bufs.payload, iatStr...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
bufs.payload = append(bufs.payload, expiryStr...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
bufs.payload = append(bufs.payload, userID...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
bufs.payload = append(bufs.payload, channel...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
hex.Encode(bufs.hexBuf, keysHash[:])
|
||||
bufs.payload = append(bufs.payload, bufs.hexBuf...)
|
||||
mac.Write(bufs.payload)
|
||||
hex.Encode(bufs.expectedHex, mac.Sum(nil))
|
||||
|
||||
result := hmac.Equal([]byte(hmacHex), bufs.expectedHex)
|
||||
|
||||
v.macPool.Put(mac)
|
||||
v.bufPool.Put(bufs)
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user