feat(bskyweb): add HMAC download token for invite passes
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user