package main import ( "bytes" "encoding/json" "net/http" "testing" ) // A person's display name is stored only as an opaque blob (encrypted client-side // under the instance key); the plaintext label column is empty. Bots keep their // admin-authored plaintext name and are never overwritten by the blob path. func TestUserLabelsEncryptedAtRest(t *testing.T) { db := newTestDB(t) // A person: nameless in plaintext, name lives in label_blob. alice, _ := db.createUser("") db.setUserPublicKey(alice, "pub-alice", "") if err := db.setUserLabelBlob(alice, "ENC(alice)"); err != nil { t.Fatalf("setUserLabelBlob: %v", err) } var label, blob string if err := db.sql.QueryRow("SELECT label, COALESCE(label_blob, '') FROM users WHERE id = ?", alice). Scan(&label, &blob); err != nil { t.Fatalf("read user: %v", err) } if label != "" || blob != "ENC(alice)" { t.Fatalf("person row: label=%q blob=%q; want empty label + the blob", label, blob) } // The roster serves the blob, not a readable name. find := func(uid string) *RosterEntry { r, _ := db.roster() for i := range r { if r[i].UserID == uid { return &r[i] } } return nil } if e := find(alice); e == nil || e.Label != "" || e.LabelBlob != "ENC(alice)" { t.Fatalf("roster entry for person: %+v", e) } // A bot keeps its plaintext name and is NOT overwritten by the label-blob path // (setUserLabelBlob only touches non-agents), so an accidental/hostile call // can't blank a bot's name. bot, _ := db.createUser("Sunny") db.setUserPublicKey(bot, "pub-bot", "") if err := db.setUserAgent(bot); err != nil { t.Fatalf("setUserAgent: %v", err) } if err := db.setUserLabelBlob(bot, "ENC(hijack)"); err != nil { t.Fatalf("setUserLabelBlob(bot): %v", err) } if e := find(bot); e == nil || e.Label != "Sunny" || e.LabelBlob != "" { t.Fatalf("bot label should stay plaintext + untouched: %+v", e) } } // Legacy plaintext labels (names stored before label_blob existed) may be // sealed by any instance-key holder — but strictly fill-once: people only, // only while the plaintext is still there, never over an existing blob. func TestBackfillLegacyLabel(t *testing.T) { db := newTestDB(t) // A legacy person: plaintext label at rest, no blob. legacy, _ := db.createUser("Legacy Name") if ok, err := db.backfillUserLabelBlob(legacy, "ENC(legacy)"); err != nil || !ok { t.Fatalf("backfill legacy: ok=%v err=%v", ok, err) } var label, blob string db.sql.QueryRow("SELECT label, COALESCE(label_blob,'') FROM users WHERE id = ?", legacy).Scan(&label, &blob) if label != "" || blob != "ENC(legacy)" { t.Fatalf("after backfill: label=%q blob=%q", label, blob) } // Fill-once: a second call is a no-op — it cannot rename. if ok, _ := db.backfillUserLabelBlob(legacy, "ENC(attacker)"); ok { t.Fatal("backfill overwrote an existing blob") } // A person who already sealed their own name is untouchable. self, _ := db.createUser("") db.setUserLabelBlob(self, "ENC(self)") if ok, _ := db.backfillUserLabelBlob(self, "ENC(other)"); ok { t.Fatal("backfill touched a self-sealed name") } // A nameless person (nothing to seal) is a no-op. fresh, _ := db.createUser("") if ok, _ := db.backfillUserLabelBlob(fresh, "ENC(x)"); ok { t.Fatal("backfill filled a nameless row") } // A bot keeps its admin-authored plaintext label. bot, _ := db.createUser("Sunny") db.setUserAgent(bot) if ok, _ := db.backfillUserLabelBlob(bot, "ENC(bot)"); ok { t.Fatal("backfill touched a bot") } } // The endpoint: member-token gated, and the response says whether it filled. func TestLabelBackfillEndpoint(t *testing.T) { ts, srv := newTestServer(t) tok := mintDeviceToken(t, srv.db, "caller") legacy, _ := srv.db.createUser("Legacy Name") post := func(token, uid, blob string) (int, bool) { body, _ := json.Marshal(map[string]string{"token": token, "user_id": uid, "label_blob": blob}) r, err := http.Post(ts.URL+"/api/label/backfill", "application/json", bytes.NewReader(body)) if err != nil { t.Fatalf("post: %v", err) } defer r.Body.Close() var out struct { Filled bool `json:"filled"` } json.NewDecoder(r.Body).Decode(&out) return r.StatusCode, out.Filled } if code, _ := post("not-a-token", legacy, "ENC(x)"); code != http.StatusForbidden { t.Fatalf("bad token: want 403, got %d", code) } if code, filled := post(tok, legacy, "ENC(legacy)"); code != http.StatusOK || !filled { t.Fatalf("fill: code=%d filled=%v", code, filled) } if code, filled := post(tok, legacy, "ENC(again)"); code != http.StatusOK || filled { t.Fatalf("refill must no-op: code=%d filled=%v", code, filled) } var label, blob string srv.db.sql.QueryRow("SELECT label, COALESCE(label_blob,'') FROM users WHERE id = ?", legacy).Scan(&label, &blob) if label != "" || blob != "ENC(legacy)" { t.Fatalf("at rest after endpoint fill: label=%q blob=%q", label, blob) } }