tiktak: keygen на HMAC вместо предсказуемого legacy-алгоритма
build-and-push / detect (push) Successful in 12s
build-and-push / build (${{ fromJSON(needs.detect.outputs.services) }}) (push) Successful in 30s

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:
2026-08-26 11:55:53 +03:00
co-authored by Claude Opus 5
parent b647951199
commit c0089b2c11
2 changed files with 65 additions and 16 deletions
+31 -16
View File
@@ -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)))
}