| 1 | package main |
| 2 | |
| 3 | import ( |
| 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. |
| 13 | func 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. |
| 64 | func 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. |
| 104 | func 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 | |