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 "database/sql"
5 "fmt"
6 "sync/atomic"
7 "testing"
8)
9
10// A pre-existing DB with the old plaintext invites table has it dropped on open,
11// and a token that used to authenticate via it is no longer accepted.
12func TestLegacyInvitesRemovedOnOpen(t *testing.T) {
13 path := t.TempDir() + "/legacy.db"
14 raw, err := sql.Open("sqlite", path)
15 if err != nil {
16 t.Fatalf("open raw: %v", err)
17 }
18 if _, err := raw.Exec(`CREATE TABLE invites (token TEXT PRIMARY KEY, label TEXT NOT NULL DEFAULT '', created_at INTEGER NOT NULL, claimed_at INTEGER, revoked INTEGER NOT NULL DEFAULT 0)`); err != nil {
19 t.Fatalf("create legacy table: %v", err)
20 }
21 if _, err := raw.Exec(`INSERT INTO invites(token, label, created_at) VALUES('legacytok', 'x', 0)`); err != nil {
22 t.Fatalf("seed legacy token: %v", err)
23 }
24 raw.Close()
25
26 db, err := openDB(path)
27 if err != nil {
28 t.Fatalf("openDB: %v", err)
29 }
30 if _, err := db.sql.Exec("SELECT 1 FROM invites LIMIT 1"); err == nil {
31 t.Fatal("legacy invites table was not dropped on open")
32 }
33 srv := &Server{db: db}
34 if srv.tokenOK("legacytok") {
35 t.Fatal("a legacy invite token is still accepted for auth")
36 }
37}
38
39var mintSeq atomic.Int64
40
41// mintDeviceToken creates a joined member and returns a valid device (session)
42// token via the key-invite claim flow — the standard way to get an authenticated
43// caller in tests now that legacy invites are gone.
44func mintDeviceToken(t *testing.T, db *DB, label string) string {
45 t.Helper()
46 uid, err := db.createUser(label)
47 if err != nil {
48 t.Fatalf("createUser: %v", err)
49 }
50 id := fmt.Sprintf("mint-%s-%d", label, mintSeq.Add(1))
51 if err := db.createKeyInvite(id, uid, "", testClaimHash, 0); err != nil {
52 t.Fatalf("createKeyInvite: %v", err)
53 }
54 tok, ok, err := db.claimKeyInvite(id, label, "", testClaimSecret)
55 if err != nil || !ok {
56 t.Fatalf("claimKeyInvite: ok=%v err=%v", ok, err)
57 }
58 return tok
59}
60
61// Shared across the invite tests: a claim secret and its stored hash. Creation
62// stores testClaimHash; claiming presents testClaimSecret.
63const testClaimSecret = "test-claim-secret-vector"
64
65var testClaimHash = hashToken(testClaimSecret)
66
67// Cross-implementation vector: the browser's deriveInvite("AAAAAAAAAAAAAAAAAAAAAA")
68// (T = 16 zero bytes, base64url) produces this claim secret + hash. The server must
69// accept that secret against that hash — i.e. the client's sha256hex and the
70// server's hashToken agree, so real browser claims verify. (Regenerate via:
71// node --input-type=module -e "import('./web/static/crypto.js').then(...)")
72func TestClaimHashCrossImpl(t *testing.T) {
73 const jsClaimSecret = "zgoEhtdOkw8sCcWC7LhWHMItVCulazloEuXRG7hz_Zw"
74 const jsClaimHash = "a4553e1d5770adf87427741af0fbcf6e42a223a82403edd6752c0c39fa43a7e3"
75 if got := hashToken(jsClaimSecret); got != jsClaimHash {
76 t.Fatalf("hashToken(jsClaimSecret) = %q, want %q — JS and Go hashing disagree", got, jsClaimHash)
77 }
78 if !validClaimSecret(jsClaimHash, jsClaimSecret) {
79 t.Fatal("server would reject a browser-derived claim secret")
80 }
81}
82
83func TestValidClaimSecret(t *testing.T) {
84 if !validClaimSecret(hashToken("s"), "s") {
85 t.Error("valid preimage rejected")
86 }
87 for _, bad := range [][2]string{{"", ""}, {"", "s"}, {hashToken("s"), ""}, {hashToken("s"), "wrong"}} {
88 if validClaimSecret(bad[0], bad[1]) {
89 t.Errorf("validClaimSecret(%q, %q) = true, want false", bad[0], bad[1])
90 }
91 }
92}
93
94// Claiming requires proving knowledge of the invite secret — knowing only the id
95// (which the server stores, and a DB thief would have) is not enough.
96func TestClaimKeyInviteRequiresSecret(t *testing.T) {
97 db := newTestDB(t)
98 if err := db.createKeyInvite("inv1", "u1", "", testClaimHash, 0); err != nil {
99 t.Fatalf("create: %v", err)
100 }
101 // A DB thief who only has the stored id can't produce the preimage.
102 if _, ok, _ := db.claimKeyInvite("inv1", "dev", "", "wrong-secret"); ok {
103 t.Fatal("claim succeeded with the wrong secret")
104 }
105 if _, ok, _ := db.claimKeyInvite("inv1", "dev", "", ""); ok {
106 t.Fatal("claim succeeded with an empty secret")
107 }
108 // The real invitee (who derived the secret from T in the URL fragment) can.
109 tok, ok, err := db.claimKeyInvite("inv1", "dev", "", testClaimSecret)
110 if err != nil || !ok || tok == "" {
111 t.Fatalf("claim with correct secret: ok=%v err=%v tok=%q", ok, err, tok)
112 }
113}
114
115// An invite stored without a claim_hash (e.g. created before this migration) is
116// unclaimable — we don't fall back to unauthenticated claiming.
117func TestClaimKeyInviteRejectsInviteWithoutHash(t *testing.T) {
118 db := newTestDB(t)
119 if err := db.createKeyInvite("inv2", "u2", "", "", 0); err != nil {
120 t.Fatalf("create: %v", err)
121 }
122 if _, ok, _ := db.claimKeyInvite("inv2", "dev", "", testClaimSecret); ok {
123 t.Fatal("claimed an invite that has no claim_hash")
124 }
125}
126