You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

107 lines
3.7 KiB

package echomeet
import (
"encoding/base64"
"encoding/json"
"os"
"strings"
"testing"
"time"
)
// 票据是公开链接里的唯一凭据,下面每条都是「错了就等于任何人能读任意纪要」级别的性质,
// 所以全部钉死在测试里。
func TestShareTicketRoundTrip(t *testing.T) {
os.Setenv("FIELD_ENCRYPT_KEY", "test-field-key-0123456789")
defer os.Unsetenv("FIELD_ENCRYPT_KEY")
exp := time.Now().Add(time.Hour).Unix()
ticket, err := signShareTicket(sharePayload{Id: 42, Uid: "1234567890", Expire: exp})
if err != nil {
t.Fatalf("签发失败: %v", err)
}
got, err := parseShareTicket(ticket)
if err != nil {
t.Fatalf("校验失败: %v", err)
}
if got.Id != 42 || got.Uid != "1234567890" || got.Expire != exp {
t.Fatalf("载荷不一致: %+v", got)
}
}
func TestShareTicketRejects(t *testing.T) {
os.Setenv("FIELD_ENCRYPT_KEY", "test-field-key-0123456789")
defer os.Unsetenv("FIELD_ENCRYPT_KEY")
good, _ := signShareTicket(sharePayload{Id: 42, Uid: "u1", Expire: time.Now().Add(time.Hour).Unix()})
body := good[:strings.LastIndex(good, ".")]
// 改载荷不改签名:这是最直接的攻击——把 id 换成别人的记录。
tampered := base64.RawURLEncoding.EncodeToString([]byte(`{"i":43,"u":"u1","e":9999999999}`))
cases := map[string]string{
"空串": "",
"没有分隔点": body,
"签名被截断": body + ".",
"签名乱填": body + ".AAAA",
"载荷被篡改": tampered + good[strings.LastIndex(good, "."):],
"整段不是base64": "!!!.@@@",
}
for name, tk := range cases {
if _, err := parseShareTicket(tk); err == nil {
t.Errorf("%s: 本该被拒,却通过了", name)
}
}
// 过期票据必须拒绝:签名是对的,只有时间不对。
expired, _ := signShareTicket(sharePayload{Id: 42, Uid: "u1", Expire: time.Now().Add(-time.Second).Unix()})
if _, err := parseShareTicket(expired); err == nil {
t.Error("过期票据本该被拒")
}
}
// 换密钥必须让旧票据全部失效——否则「换密钥」这个应急手段是假的。
func TestShareTicketKeyIsolation(t *testing.T) {
os.Setenv("FIELD_ENCRYPT_KEY", "key-A")
ticket, err := signShareTicket(sharePayload{Id: 1, Uid: "u", Expire: time.Now().Add(time.Hour).Unix()})
if err != nil {
t.Fatal(err)
}
os.Setenv("FIELD_ENCRYPT_KEY", "key-B")
defer os.Unsetenv("FIELD_ENCRYPT_KEY")
if _, err = parseShareTicket(ticket); err == nil {
t.Fatal("换密钥后旧票据仍然有效")
}
}
// 没有 FIELD_ENCRYPT_KEY 时必须报错,绝不能退化成用空密钥签名(那样谁都能自己造票据)。
func TestShareTicketRefusesEmptyKey(t *testing.T) {
old := os.Getenv("FIELD_ENCRYPT_KEY")
os.Unsetenv("FIELD_ENCRYPT_KEY")
defer os.Setenv("FIELD_ENCRYPT_KEY", old)
if _, err := signShareTicket(sharePayload{Id: 1, Uid: "u", Expire: 1}); err == nil {
t.Fatal("缺密钥时本该拒绝签发")
}
}
// 票据里不该出现任何登录凭据的痕迹:它进 URL、会被转发、会进浏览器历史。
func TestShareTicketCarriesOnlyScopedPayload(t *testing.T) {
os.Setenv("FIELD_ENCRYPT_KEY", "test-field-key-0123456789")
defer os.Unsetenv("FIELD_ENCRYPT_KEY")
ticket, _ := signShareTicket(sharePayload{Id: 7, Uid: "u7", Expire: time.Now().Add(time.Hour).Unix()})
raw, err := base64.RawURLEncoding.DecodeString(ticket[:strings.LastIndex(ticket, ".")])
if err != nil {
t.Fatal(err)
}
m := map[string]any{}
if err = json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if len(m) != 3 {
t.Fatalf("载荷字段数变了(%d),新增字段前先确认它可以公开: %v", len(m), m)
}
for _, k := range []string{"i", "u", "e"} {
if _, ok := m[k]; !ok {
t.Fatalf("载荷缺字段 %s: %v", k, m)
}
}
}