diff --git a/services/tiktak/keygen/keygen.go b/services/tiktak/keygen/keygen.go index 61aba65..169d17b 100644 --- a/services/tiktak/keygen/keygen.go +++ b/services/tiktak/keygen/keygen.go @@ -1,27 +1,42 @@ package keygen -/* -#cgo LDFLAGS: ${SRCDIR}/legacy/keygen.a -lm -#include -*/ -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))) } diff --git a/services/tiktak/keygen/keygen_test.go b/services/tiktak/keygen/keygen_test.go new file mode 100644 index 0000000..6aa3685 --- /dev/null +++ b/services/tiktak/keygen/keygen_test.go @@ -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) + } + } +}