package main // 🤖 The offline restore drill, as a test (docs/locked-vaults.md "Offline // restore — the hard gate"): seed a REAL instance DB through db.go — a member // with a public key, a personal Vault app-thread with a wrapped thread key, // and a locked vault's event log, all sealed with the real e2ee crypto — then // read the whole thing back through internal/vault's offline reader with the // server code out of the loop, exactly what `eleven vault get -db` does when // every server is dead. If this passes, the recovery path is crypto-complete: // S3 replica + ~/.eleven key + keychain KEK = secrets. import ( "path/filepath" "testing" "time" "lchat/internal/e2ee" "lchat/internal/vault" ) func TestVaultOfflineRestore(t *testing.T) { dbPath := filepath.Join(t.TempDir(), "lchat.db") db, err := openDB(dbPath) if err != nil { t.Fatalf("openDB: %v", err) } // A member, exactly as join would leave them: keypair client-side, public // half in the roster. member, err := e2ee.GenerateUserKeypair() if err != nil { t.Fatal(err) } if _, err := db.sql.Exec("INSERT INTO users (id, label, created_at, public_key) VALUES ('u1','',?,?)", time.Now().Unix(), member.PublicKey); err != nil { t.Fatalf("seed user: %v", err) } // Their personal Vault app-thread: the app id lives INSIDE the encrypted // name_blob (the server never learns which self-thread is a vault), and the // thread key reaches the member only as an ECIES wrap to their public key. rawKey, err := e2ee.GenerateKey() if err != nil { t.Fatal(err) } keyB64 := e2ee.B64uEncode(rawKey) wrapped, err := e2ee.WrapThreadKey(keyB64, member.PublicKey) if err != nil { t.Fatal(err) } nameBlob, err := e2ee.EncryptJSON(rawKey, map[string]string{"app": "vault", "title": "Vault"}) if err != nil { t.Fatal(err) } now := time.Now().Unix() if _, err := db.sql.Exec("INSERT INTO threads (id, kind, name_blob, created_at) VALUES ('t1','self',?,?)", nameBlob, now); err != nil { t.Fatal(err) } if _, err := db.sql.Exec("INSERT INTO thread_members (thread_id, user_id, created_at) VALUES ('t1','u1',?)", now); err != nil { t.Fatal(err) } if _, err := db.sql.Exec("INSERT INTO thread_keys (thread_id, user_id, wrapped) VALUES ('t1','u1',?)", wrapped); err != nil { t.Fatal(err) } // The vault's life so far: a plaintext add, then a lock (re-sealing that // value and wrapping the vault key under a keychain-style KEK), then a // sealed add — the operator-vault shape. vaultKP, err := e2ee.GenerateUserKeypair() if err != nil { t.Fatal(err) } kek, err := e2ee.GenerateKey() // stands in for the Touch ID-guarded keychain KEK if err != nil { t.Fatal(err) } privRaw, err := e2ee.B64uDecode(vaultKP.PrivateKey) if err != nil { t.Fatal(err) } wrapBytes, err := e2ee.EncryptBytes(kek, privRaw) if err != nil { t.Fatal(err) } sealedWiFi, err := e2ee.SealForPub(vaultKP.PublicKey, map[string]string{"v": "hunter2"}) if err != nil { t.Fatal(err) } sealedEnv, err := e2ee.SealForPub(vaultKP.PublicKey, map[string]string{"v": "LCHAT_ADMIN_TOKEN=abc\nADDR=:443\n"}) if err != nil { t.Fatal(err) } events := []map[string]any{ {"type": "vault.add", "id": "s1", "aid": "a1", "label": "WiFi", "value": "hunter2", "uid": "u1", "name": "Paul", "ts": 1000}, {"type": "vault.lock", "aid": "a2", "pub": vaultKP.PublicKey, "wrap": map[string]any{"id": "w1", "kind": "keychain", "label": "Paul — CLI", "wrapped": e2ee.B64uEncode(wrapBytes)}, "sealed": map[string]any{"s1": sealedWiFi}, "uid": "u1", "name": "Paul", "ts": 2000}, {"type": "vault.add", "id": "s2", "aid": "a3", "label": "router.env", "sealed": sealedEnv, "uid": "u1", "name": "Paul", "ts": 3000}, } for _, e := range events { blob, err := e2ee.EncryptJSON(rawKey, e) if err != nil { t.Fatal(err) } if _, err := db.insertMessage("t1", blob, "author"); err != nil { t.Fatal(err) } } // ---- the drill: server code out of the loop from here ------------------- o, err := vault.OpenOffline(dbPath) if err != nil { t.Fatalf("OpenOffline: %v", err) } defer o.Close() uid, err := o.UserID(member.PublicKey) if err != nil || uid != "u1" { t.Fatalf("UserID = %q, %v — want u1", uid, err) } if id, err := o.UserID("someone-elses-key"); err != nil || id != "" { t.Fatalf("a foreign key must not match a member (got %q, %v)", id, err) } threads, err := o.Threads(uid, member.PrivateKey) if err != nil { t.Fatalf("Threads: %v", err) } var vt *vault.OfflineThread for i := range threads { if threads[i].App == "vault" && threads[i].Kind == "self" { vt = &threads[i] } } if vt == nil { t.Fatalf("no vault app-thread found offline; threads: %+v", threads) } s, err := o.FoldVault(vt.ID, vt.Key) if err != nil { t.Fatalf("FoldVault: %v", err) } if s.Keyring == nil || s.Keyring.Pub != vaultKP.PublicKey { t.Fatalf("keyring didn't fold: %+v", s.Keyring) } if len(s.Items) != 2 { t.Fatalf("want 2 items, got %d", len(s.Items)) } wifi := s.FindItem("wifi") if wifi == nil || wifi.Sealed == "" || wifi.Value != "" { t.Fatalf("the lock event should have sealed WiFi: %+v", wifi) } // The get path: keychain KEK → vault private key → unseal both values. wrapRaw, err := e2ee.B64uDecode(s.Keyring.Wraps[0].Wrapped) if err != nil { t.Fatal(err) } privOut, err := e2ee.DecryptBytes(kek, wrapRaw) if err != nil { t.Fatalf("KEK didn't open the wrap: %v", err) } privB64 := e2ee.B64uEncode(privOut) var got struct { V string `json:"v"` } if err := e2ee.OpenSealed(wifi.Sealed, privB64, &got); err != nil || got.V != "hunter2" { t.Fatalf("unseal WiFi = %q, %v", got.V, err) } env := s.FindItem("router.env") if env == nil { t.Fatal("router.env missing offline") } if err := e2ee.OpenSealed(env.Sealed, privB64, &got); err != nil || got.V != "LCHAT_ADMIN_TOKEN=abc\nADDR=:443\n" { t.Fatalf("unseal router.env = %q, %v", got.V, err) } // And the failure mode that matters: a WRONG kek opens nothing. badKek, _ := e2ee.GenerateKey() if _, err := e2ee.DecryptBytes(badKek, wrapRaw); err == nil { t.Fatal("a wrong KEK must never open the vault key wrap") } }