tiktak: keygen на HMAC вместо предсказуемого legacy-алгоритма
Share-токен приватного видео считался в legacy/keygen.a только от video id, без ключа: seed=(seed*17+42)%62, out[i]=alpha[seed]. После первой итерации состояние схлопывалось в id%62, то есть на весь сервис приходилось 62 различных токена. Разбирать бинарь не требовалось: 62 своих приватных видео дают таблицу токенов ко всем чужим. Форж токена => POST /access => строка в таблице access => haveAccess() отдаёт и description, и субтитры, и .webm совершенно легально, мимо патча /vtt/. Теперь HMAC-SHA256(secret, vid), те же сигнатуры и тот же 30-символьный base62, cgo и keygen.a из сборки выпали (образ собирается под любую архитектуру). Сверка constant-time через hmac.Equal. Совместимость со старыми токенами намеренно не сохранена: раунды ещё не начинались, приватных видео нет. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,27 +1,42 @@
|
||||
package keygen
|
||||
|
||||
/*
|
||||
#cgo LDFLAGS: ${SRCDIR}/legacy/keygen.a -lm
|
||||
#include <keygen.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"strings"
|
||||
"unsafe"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// The legacy C implementation (legacy/keygen.a, no source shipped) derived the
|
||||
// share token from the video id ALONE -- no key, no salt. Worse, its state
|
||||
// collapsed to (seed*17+42)%62 after the first round, so the token depended
|
||||
// only on id%62: 62 distinct tokens for the whole service. Anyone could upload
|
||||
// 62 private videos, copy the tokens off their own /home and unlock every
|
||||
// private video on every team's box. Replaced with a keyed HMAC.
|
||||
//
|
||||
// Same signatures, same 30-char base62 alphabet, so nothing else changes.
|
||||
// keygen.h / legacy/keygen.a are no longer part of the build (cgo is gone).
|
||||
const (
|
||||
tokenLen = 30
|
||||
alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
|
||||
// Server-side secret. Rotating it invalidates every token already handed
|
||||
// out, so change it only between rounds.
|
||||
secret = "D1gkxNv7_HphLJjNuFag_stjaByXLZ1Y0l12RGppZtw"
|
||||
)
|
||||
|
||||
func GenerateKey(vid int) string {
|
||||
var key [C.TOK_SIZE]byte
|
||||
keyPtr := (*C.char)(unsafe.Pointer(&key[0]))
|
||||
C.GenerateToken(C.int(vid), keyPtr)
|
||||
res := strings.Builder{}
|
||||
for _, v := range key {
|
||||
res.WriteByte(v)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(strconv.Itoa(vid)))
|
||||
sum := mac.Sum(nil)
|
||||
|
||||
out := make([]byte, tokenLen)
|
||||
for i := 0; i < tokenLen; i++ {
|
||||
out[i] = alphabet[int(sum[i%len(sum)])%len(alphabet)]
|
||||
}
|
||||
return res.String()
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func ValidateKey(token string, vid int) bool {
|
||||
res := C.ValidateToken(C.int(vid), C.CString(token))
|
||||
return res == 1
|
||||
// Constant-time: never leak how much of the token was correct.
|
||||
return hmac.Equal([]byte(token), []byte(GenerateKey(vid)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package keygen
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestTokenProperties(t *testing.T) {
|
||||
seen := map[string]int{}
|
||||
for vid := 1; vid <= 5000; vid++ {
|
||||
tok := GenerateKey(vid)
|
||||
if len(tok) != 30 {
|
||||
t.Fatalf("vid %d: len %d, want 30", vid, len(tok))
|
||||
}
|
||||
for _, ch := range tok {
|
||||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9')) {
|
||||
t.Fatalf("vid %d: bad char %q", vid, ch)
|
||||
}
|
||||
}
|
||||
if prev, dup := seen[tok]; dup {
|
||||
t.Fatalf("COLLISION: vid %d and vid %d share a token", prev, vid)
|
||||
}
|
||||
seen[tok] = vid
|
||||
if !ValidateKey(tok, vid) {
|
||||
t.Fatalf("vid %d: own token rejected", vid)
|
||||
}
|
||||
if ValidateKey(tok, vid+1) || ValidateKey(tok, vid+62) {
|
||||
t.Fatalf("vid %d: token accepted for another video", vid)
|
||||
}
|
||||
}
|
||||
t.Logf("5000 ids -> %d distinct tokens (legacy gave 62)", len(seen))
|
||||
for _, bad := range []string{"", "aaaa", GenerateKey(1)[:29], GenerateKey(1) + "x"} {
|
||||
if ValidateKey(bad, 1) {
|
||||
t.Fatalf("garbage token %q accepted", bad)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user