eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3// πŸ€– The offline restore drill, as a test (docs/locked-vaults.md "Offline
4// restore β€” the hard gate"): seed a REAL instance DB through db.go β€” a member
5// with a public key, a personal Vault app-thread with a wrapped thread key,
6// and a locked vault's event log, all sealed with the real e2ee crypto β€” then
7// read the whole thing back through internal/vault's offline reader with the
8// server code out of the loop, exactly what `eleven vault get -db` does when
9// every server is dead. If this passes, the recovery path is crypto-complete:
10// S3 replica + ~/.eleven key + keychain KEK = secrets.
11
12import (
13 "path/filepath"
14 "testing"
15 "time"
16
17 "lchat/internal/e2ee"
18 "lchat/internal/vault"
19)
20
21func TestVaultOfflineRestore(t *testing.T) {
22 dbPath := filepath.Join(t.TempDir(), "lchat.db")
23 db, err := openDB(dbPath)
24 if err != nil {
25 t.Fatalf("openDB: %v", err)
26 }
27
28 // A member, exactly as join would leave them: keypair client-side, public
29 // half in the roster.
30 member, err := e2ee.GenerateUserKeypair()
31 if err != nil {
32 t.Fatal(err)
33 }
34 if _, err := db.sql.Exec("INSERT INTO users (id, label, created_at, public_key) VALUES ('u1','',?,?)",
35 time.Now().Unix(), member.PublicKey); err != nil {
36 t.Fatalf("seed user: %v", err)
37 }
38
39 // Their personal Vault app-thread: the app id lives INSIDE the encrypted
40 // name_blob (the server never learns which self-thread is a vault), and the
41 // thread key reaches the member only as an ECIES wrap to their public key.
42 rawKey, err := e2ee.GenerateKey()
43 if err != nil {
44 t.Fatal(err)
45 }
46 keyB64 := e2ee.B64uEncode(rawKey)
47 wrapped, err := e2ee.WrapThreadKey(keyB64, member.PublicKey)
48 if err != nil {
49 t.Fatal(err)
50 }
51 nameBlob, err := e2ee.EncryptJSON(rawKey, map[string]string{"app": "vault", "title": "Vault"})
52 if err != nil {
53 t.Fatal(err)
54 }
55 now := time.Now().Unix()
56 if _, err := db.sql.Exec("INSERT INTO threads (id, kind, name_blob, created_at) VALUES ('t1','self',?,?)", nameBlob, now); err != nil {
57 t.Fatal(err)
58 }
59 if _, err := db.sql.Exec("INSERT INTO thread_members (thread_id, user_id, created_at) VALUES ('t1','u1',?)", now); err != nil {
60 t.Fatal(err)
61 }
62 if _, err := db.sql.Exec("INSERT INTO thread_keys (thread_id, user_id, wrapped) VALUES ('t1','u1',?)", wrapped); err != nil {
63 t.Fatal(err)
64 }
65
66 // The vault's life so far: a plaintext add, then a lock (re-sealing that
67 // value and wrapping the vault key under a keychain-style KEK), then a
68 // sealed add β€” the operator-vault shape.
69 vaultKP, err := e2ee.GenerateUserKeypair()
70 if err != nil {
71 t.Fatal(err)
72 }
73 kek, err := e2ee.GenerateKey() // stands in for the Touch ID-guarded keychain KEK
74 if err != nil {
75 t.Fatal(err)
76 }
77 privRaw, err := e2ee.B64uDecode(vaultKP.PrivateKey)
78 if err != nil {
79 t.Fatal(err)
80 }
81 wrapBytes, err := e2ee.EncryptBytes(kek, privRaw)
82 if err != nil {
83 t.Fatal(err)
84 }
85 sealedWiFi, err := e2ee.SealForPub(vaultKP.PublicKey, map[string]string{"v": "hunter2"})
86 if err != nil {
87 t.Fatal(err)
88 }
89 sealedEnv, err := e2ee.SealForPub(vaultKP.PublicKey, map[string]string{"v": "LCHAT_ADMIN_TOKEN=abc\nADDR=:443\n"})
90 if err != nil {
91 t.Fatal(err)
92 }
93 events := []map[string]any{
94 {"type": "vault.add", "id": "s1", "aid": "a1", "label": "WiFi", "value": "hunter2", "uid": "u1", "name": "Paul", "ts": 1000},
95 {"type": "vault.lock", "aid": "a2", "pub": vaultKP.PublicKey,
96 "wrap": map[string]any{"id": "w1", "kind": "keychain", "label": "Paul β€” CLI", "wrapped": e2ee.B64uEncode(wrapBytes)},
97 "sealed": map[string]any{"s1": sealedWiFi}, "uid": "u1", "name": "Paul", "ts": 2000},
98 {"type": "vault.add", "id": "s2", "aid": "a3", "label": "router.env", "sealed": sealedEnv, "uid": "u1", "name": "Paul", "ts": 3000},
99 }
100 for _, e := range events {
101 blob, err := e2ee.EncryptJSON(rawKey, e)
102 if err != nil {
103 t.Fatal(err)
104 }
105 if _, err := db.insertMessage("t1", blob, "author"); err != nil {
106 t.Fatal(err)
107 }
108 }
109
110 // ---- the drill: server code out of the loop from here -------------------
111
112 o, err := vault.OpenOffline(dbPath)
113 if err != nil {
114 t.Fatalf("OpenOffline: %v", err)
115 }
116 defer o.Close()
117
118 uid, err := o.UserID(member.PublicKey)
119 if err != nil || uid != "u1" {
120 t.Fatalf("UserID = %q, %v β€” want u1", uid, err)
121 }
122 if id, err := o.UserID("someone-elses-key"); err != nil || id != "" {
123 t.Fatalf("a foreign key must not match a member (got %q, %v)", id, err)
124 }
125
126 threads, err := o.Threads(uid, member.PrivateKey)
127 if err != nil {
128 t.Fatalf("Threads: %v", err)
129 }
130 var vt *vault.OfflineThread
131 for i := range threads {
132 if threads[i].App == "vault" && threads[i].Kind == "self" {
133 vt = &threads[i]
134 }
135 }
136 if vt == nil {
137 t.Fatalf("no vault app-thread found offline; threads: %+v", threads)
138 }
139
140 s, err := o.FoldVault(vt.ID, vt.Key)
141 if err != nil {
142 t.Fatalf("FoldVault: %v", err)
143 }
144 if s.Keyring == nil || s.Keyring.Pub != vaultKP.PublicKey {
145 t.Fatalf("keyring didn't fold: %+v", s.Keyring)
146 }
147 if len(s.Items) != 2 {
148 t.Fatalf("want 2 items, got %d", len(s.Items))
149 }
150 wifi := s.FindItem("wifi")
151 if wifi == nil || wifi.Sealed == "" || wifi.Value != "" {
152 t.Fatalf("the lock event should have sealed WiFi: %+v", wifi)
153 }
154
155 // The get path: keychain KEK β†’ vault private key β†’ unseal both values.
156 wrapRaw, err := e2ee.B64uDecode(s.Keyring.Wraps[0].Wrapped)
157 if err != nil {
158 t.Fatal(err)
159 }
160 privOut, err := e2ee.DecryptBytes(kek, wrapRaw)
161 if err != nil {
162 t.Fatalf("KEK didn't open the wrap: %v", err)
163 }
164 privB64 := e2ee.B64uEncode(privOut)
165 var got struct {
166 V string `json:"v"`
167 }
168 if err := e2ee.OpenSealed(wifi.Sealed, privB64, &got); err != nil || got.V != "hunter2" {
169 t.Fatalf("unseal WiFi = %q, %v", got.V, err)
170 }
171 env := s.FindItem("router.env")
172 if env == nil {
173 t.Fatal("router.env missing offline")
174 }
175 if err := e2ee.OpenSealed(env.Sealed, privB64, &got); err != nil || got.V != "LCHAT_ADMIN_TOKEN=abc\nADDR=:443\n" {
176 t.Fatalf("unseal router.env = %q, %v", got.V, err)
177 }
178
179 // And the failure mode that matters: a WRONG kek opens nothing.
180 badKek, _ := e2ee.GenerateKey()
181 if _, err := e2ee.DecryptBytes(badKek, wrapRaw); err == nil {
182 t.Fatal("a wrong KEK must never open the vault key wrap")
183 }
184}
185