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
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)
|
|
}
|
|
}
|
|
}
|
|
|