eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "bytes"
5 "encoding/json"
6 "net/http"
7 "testing"
8)
9
10// A person's display name is stored only as an opaque blob (encrypted client-side
11// under the instance key); the plaintext label column is empty. Bots keep their
12// admin-authored plaintext name and are never overwritten by the blob path.
13func TestUserLabelsEncryptedAtRest(t *testing.T) {
14 db := newTestDB(t)
15
16 // A person: nameless in plaintext, name lives in label_blob.
17 alice, _ := db.createUser("")
18 db.setUserPublicKey(alice, "pub-alice", "")
19 if err := db.setUserLabelBlob(alice, "ENC(alice)"); err != nil {
20 t.Fatalf("setUserLabelBlob: %v", err)
21 }
22 var label, blob string
23 if err := db.sql.QueryRow("SELECT label, COALESCE(label_blob, '') FROM users WHERE id = ?", alice).
24 Scan(&label, &blob); err != nil {
25 t.Fatalf("read user: %v", err)
26 }
27 if label != "" || blob != "ENC(alice)" {
28 t.Fatalf("person row: label=%q blob=%q; want empty label + the blob", label, blob)
29 }
30
31 // The roster serves the blob, not a readable name.
32 find := func(uid string) *RosterEntry {
33 r, _ := db.roster()
34 for i := range r {
35 if r[i].UserID == uid {
36 return &r[i]
37 }
38 }
39 return nil
40 }
41 if e := find(alice); e == nil || e.Label != "" || e.LabelBlob != "ENC(alice)" {
42 t.Fatalf("roster entry for person: %+v", e)
43 }
44
45 // A bot keeps its plaintext name and is NOT overwritten by the label-blob path
46 // (setUserLabelBlob only touches non-agents), so an accidental/hostile call
47 // can't blank a bot's name.
48 bot, _ := db.createUser("Sunny")
49 db.setUserPublicKey(bot, "pub-bot", "")
50 if err := db.setUserAgent(bot); err != nil {
51 t.Fatalf("setUserAgent: %v", err)
52 }
53 if err := db.setUserLabelBlob(bot, "ENC(hijack)"); err != nil {
54 t.Fatalf("setUserLabelBlob(bot): %v", err)
55 }
56 if e := find(bot); e == nil || e.Label != "Sunny" || e.LabelBlob != "" {
57 t.Fatalf("bot label should stay plaintext + untouched: %+v", e)
58 }
59}
60
61// Legacy plaintext labels (names stored before label_blob existed) may be
62// sealed by any instance-key holder — but strictly fill-once: people only,
63// only while the plaintext is still there, never over an existing blob.
64func TestBackfillLegacyLabel(t *testing.T) {
65 db := newTestDB(t)
66
67 // A legacy person: plaintext label at rest, no blob.
68 legacy, _ := db.createUser("Legacy Name")
69 if ok, err := db.backfillUserLabelBlob(legacy, "ENC(legacy)"); err != nil || !ok {
70 t.Fatalf("backfill legacy: ok=%v err=%v", ok, err)
71 }
72 var label, blob string
73 db.sql.QueryRow("SELECT label, COALESCE(label_blob,'') FROM users WHERE id = ?", legacy).Scan(&label, &blob)
74 if label != "" || blob != "ENC(legacy)" {
75 t.Fatalf("after backfill: label=%q blob=%q", label, blob)
76 }
77 // Fill-once: a second call is a no-op — it cannot rename.
78 if ok, _ := db.backfillUserLabelBlob(legacy, "ENC(attacker)"); ok {
79 t.Fatal("backfill overwrote an existing blob")
80 }
81
82 // A person who already sealed their own name is untouchable.
83 self, _ := db.createUser("")
84 db.setUserLabelBlob(self, "ENC(self)")
85 if ok, _ := db.backfillUserLabelBlob(self, "ENC(other)"); ok {
86 t.Fatal("backfill touched a self-sealed name")
87 }
88
89 // A nameless person (nothing to seal) is a no-op.
90 fresh, _ := db.createUser("")
91 if ok, _ := db.backfillUserLabelBlob(fresh, "ENC(x)"); ok {
92 t.Fatal("backfill filled a nameless row")
93 }
94
95 // A bot keeps its admin-authored plaintext label.
96 bot, _ := db.createUser("Sunny")
97 db.setUserAgent(bot)
98 if ok, _ := db.backfillUserLabelBlob(bot, "ENC(bot)"); ok {
99 t.Fatal("backfill touched a bot")
100 }
101}
102
103// The endpoint: member-token gated, and the response says whether it filled.
104func TestLabelBackfillEndpoint(t *testing.T) {
105 ts, srv := newTestServer(t)
106 tok := mintDeviceToken(t, srv.db, "caller")
107 legacy, _ := srv.db.createUser("Legacy Name")
108
109 post := func(token, uid, blob string) (int, bool) {
110 body, _ := json.Marshal(map[string]string{"token": token, "user_id": uid, "label_blob": blob})
111 r, err := http.Post(ts.URL+"/api/label/backfill", "application/json", bytes.NewReader(body))
112 if err != nil {
113 t.Fatalf("post: %v", err)
114 }
115 defer r.Body.Close()
116 var out struct {
117 Filled bool `json:"filled"`
118 }
119 json.NewDecoder(r.Body).Decode(&out)
120 return r.StatusCode, out.Filled
121 }
122
123 if code, _ := post("not-a-token", legacy, "ENC(x)"); code != http.StatusForbidden {
124 t.Fatalf("bad token: want 403, got %d", code)
125 }
126 if code, filled := post(tok, legacy, "ENC(legacy)"); code != http.StatusOK || !filled {
127 t.Fatalf("fill: code=%d filled=%v", code, filled)
128 }
129 if code, filled := post(tok, legacy, "ENC(again)"); code != http.StatusOK || filled {
130 t.Fatalf("refill must no-op: code=%d filled=%v", code, filled)
131 }
132 var label, blob string
133 srv.db.sql.QueryRow("SELECT label, COALESCE(label_blob,'') FROM users WHERE id = ?", legacy).Scan(&label, &blob)
134 if label != "" || blob != "ENC(legacy)" {
135 t.Fatalf("at rest after endpoint fill: label=%q blob=%q", label, blob)
136 }
137}
138