feat(bskyweb): add HMAC download token for invite passes

This commit is contained in:
vineyardbovines
2026-06-25 15:57:28 -04:00
parent 3b2e609761
commit b9d19295c1
2 changed files with 119 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
// bskyweb/cmd/bskyweb/passtoken.go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"strings"
"time"
)
var (
ErrTokenExpired = errors.New("invite pass token expired")
ErrTokenInvalid = errors.New("invite pass token invalid")
)
type passTokenPayload struct {
Did string `json:"d"`
Theme string `json:"t"`
Exp int64 `json:"e"`
}
func MintPassToken(secret []byte, did, theme string, now time.Time, ttl time.Duration) (string, error) {
p := passTokenPayload{Did: did, Theme: theme, Exp: now.Add(ttl).Unix()}
body, err := json.Marshal(p)
if err != nil {
return "", err
}
bodyB64 := base64.RawURLEncoding.EncodeToString(body)
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(bodyB64))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return bodyB64 + "." + sig, nil
}
func VerifyPassToken(secret []byte, token string, now time.Time) (string, string, error) {
parts := strings.SplitN(token, ".", 2)
if len(parts) != 2 {
return "", "", ErrTokenInvalid
}
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(parts[0]))
want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(want), []byte(parts[1])) {
return "", "", ErrTokenInvalid
}
body, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return "", "", ErrTokenInvalid
}
var p passTokenPayload
if err := json.Unmarshal(body, &p); err != nil {
return "", "", ErrTokenInvalid
}
if now.Unix() > p.Exp {
return "", "", ErrTokenExpired
}
return p.Did, p.Theme, nil
}
+58
View File
@@ -0,0 +1,58 @@
// bskyweb/cmd/bskyweb/passtoken_test.go
package main
import (
"errors"
"testing"
"time"
)
func TestPassToken_RoundTrip(t *testing.T) {
secret := []byte("test-secret-32-bytes-of-random!!")
now := time.Date(2026, 6, 25, 12, 0, 0, 0, time.UTC)
tok, err := MintPassToken(secret, "did:plc:abc", "dusk", now, 60*time.Second)
if err != nil {
t.Fatalf("mint: %v", err)
}
gotDid, gotTheme, err := VerifyPassToken(secret, tok, now.Add(10*time.Second))
if err != nil {
t.Fatalf("verify: %v", err)
}
if gotDid != "did:plc:abc" || gotTheme != "dusk" {
t.Fatalf("mismatch: %q %q", gotDid, gotTheme)
}
}
func TestPassToken_Expired(t *testing.T) {
secret := []byte("test-secret-32-bytes-of-random!!")
now := time.Date(2026, 6, 25, 12, 0, 0, 0, time.UTC)
tok, _ := MintPassToken(secret, "did:plc:abc", "day", now, 60*time.Second)
_, _, err := VerifyPassToken(secret, tok, now.Add(61*time.Second))
if !errors.Is(err, ErrTokenExpired) {
t.Fatalf("want ErrTokenExpired, got %v", err)
}
}
func TestPassToken_BadSignature(t *testing.T) {
secretA := []byte("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
secretB := []byte("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
now := time.Date(2026, 6, 25, 12, 0, 0, 0, time.UTC)
tok, _ := MintPassToken(secretA, "did:plc:abc", "day", now, 60*time.Second)
_, _, err := VerifyPassToken(secretB, tok, now)
if !errors.Is(err, ErrTokenInvalid) {
t.Fatalf("want ErrTokenInvalid, got %v", err)
}
}
func TestPassToken_Mutated(t *testing.T) {
secret := []byte("test-secret-32-bytes-of-random!!")
now := time.Date(2026, 6, 25, 12, 0, 0, 0, time.UTC)
tok, _ := MintPassToken(secret, "did:plc:abc", "day", now, 60*time.Second)
// flip a byte in the middle of the payload portion
mutated := []byte(tok)
mutated[10] ^= 0x01
_, _, err := VerifyPassToken(secret, string(mutated), now)
if !errors.Is(err, ErrTokenInvalid) {
t.Fatalf("want ErrTokenInvalid, got %v", err)
}
}