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 "crypto/rand"
5 "crypto/sha256"
6 "database/sql"
7 "encoding/base64"
8 "encoding/hex"
9 "time"
10
11 _ "modernc.org/sqlite"
12)
13
14// DB holds the relay's only state: the set of per-instance bearer tokens the
15// admin has minted. It stores NO push traffic — no device tokens, threads, or
16// blobs ever touch disk. Tokens are stored hashed (cf. the messenger's admin
17// token), so a stolen DB yields no live credential.
18type DB struct{ sql *sql.DB }
19
20func openDB(path string) (*DB, error) {
21 d, err := sql.Open("sqlite", path)
22 if err != nil {
23 return nil, err
24 }
25 // One writer at a time keeps WAL happy and avoids "database is locked".
26 d.SetMaxOpenConns(1)
27 for _, p := range []string{
28 "PRAGMA journal_mode=WAL",
29 "PRAGMA busy_timeout=5000",
30 "PRAGMA synchronous=NORMAL",
31 } {
32 if _, err := d.Exec(p); err != nil {
33 return nil, err
34 }
35 }
36 db := &DB{sql: d}
37 if err := db.migrate(); err != nil {
38 return nil, err
39 }
40 return db, nil
41}
42
43func (db *DB) migrate() error {
44 _, err := db.sql.Exec(`
45CREATE TABLE IF NOT EXISTS relay_tokens (
46 id TEXT PRIMARY KEY, -- random public handle, used to revoke
47 label TEXT NOT NULL, -- e.g. "family.example.com" — for logs/listing, not auth
48 token_hash TEXT NOT NULL UNIQUE, -- sha256(bearer token); the secret itself is never stored
49 created_at INTEGER NOT NULL,
50 last_used_at INTEGER
51);`)
52 return err
53}
54
55func hashToken(t string) string {
56 h := sha256.Sum256([]byte(t))
57 return hex.EncodeToString(h[:])
58}
59
60func randToken() (string, error) {
61 b := make([]byte, 32)
62 if _, err := rand.Read(b); err != nil {
63 return "", err
64 }
65 return base64.RawURLEncoding.EncodeToString(b), nil
66}
67
68// RelayToken is a token row for the admin listing. It deliberately never carries
69// the secret (only mintToken ever returns that, once).
70type RelayToken struct {
71 ID string `json:"id"`
72 Label string `json:"label"`
73 CreatedAt int64 `json:"created_at"`
74 LastUsedAt *int64 `json:"last_used_at,omitempty"`
75}
76
77// mintToken creates a per-instance token, stores only its hash, and returns the
78// plaintext ONCE (the caller shows it to the operator; it's unrecoverable after).
79func (db *DB) mintToken(label string) (id, plaintext string, err error) {
80 if id, err = randToken(); err != nil {
81 return "", "", err
82 }
83 if plaintext, err = randToken(); err != nil {
84 return "", "", err
85 }
86 _, err = db.sql.Exec(
87 `INSERT INTO relay_tokens(id, label, token_hash, created_at) VALUES(?, ?, ?, ?)`,
88 id, label, hashToken(plaintext), time.Now().Unix(),
89 )
90 if err != nil {
91 return "", "", err
92 }
93 return id, plaintext, nil
94}
95
96// authInstance resolves a bearer token to its instance label, bumping
97// last_used_at. Returns ""/false for an empty or unknown token.
98func (db *DB) authInstance(token string) (label string, ok bool) {
99 if token == "" {
100 return "", false
101 }
102 var id string
103 err := db.sql.QueryRow(
104 `SELECT id, label FROM relay_tokens WHERE token_hash = ?`, hashToken(token),
105 ).Scan(&id, &label)
106 if err != nil {
107 return "", false
108 }
109 // Best-effort; a failed bump doesn't deny an otherwise-valid token.
110 _, _ = db.sql.Exec(`UPDATE relay_tokens SET last_used_at = ? WHERE id = ?`, time.Now().Unix(), id)
111 return label, true
112}
113
114func (db *DB) listTokens() ([]RelayToken, error) {
115 rows, err := db.sql.Query(
116 `SELECT id, label, created_at, last_used_at FROM relay_tokens ORDER BY created_at`,
117 )
118 if err != nil {
119 return nil, err
120 }
121 defer rows.Close()
122 out := []RelayToken{}
123 for rows.Next() {
124 var t RelayToken
125 var last sql.NullInt64
126 if err := rows.Scan(&t.ID, &t.Label, &t.CreatedAt, &last); err != nil {
127 return nil, err
128 }
129 if last.Valid {
130 t.LastUsedAt = &last.Int64
131 }
132 out = append(out, t)
133 }
134 return out, rows.Err()
135}
136
137func (db *DB) revokeToken(id string) (bool, error) {
138 res, err := db.sql.Exec(`DELETE FROM relay_tokens WHERE id = ?`, id)
139 if err != nil {
140 return false, err
141 }
142 n, _ := res.RowsAffected()
143 return n > 0, nil
144}
145