Public, free to read, reuse, and adapt — Kirk Patrick Miller's notice for this edition. Not a patent grant.
GROK_NEXT_ENCRYPT_CHATS_LOCALLY_2026-10-02.txt · c06
f688437464 · words 1200 · characters 48648–57866
Previous section · Next section
Exact section text · Full source · Source index
diff --git a/docs/privacy.html b/docs/privacy.html
index e4b21b6d..f1d24ffd 100644
--- a/docs/privacy.html
+++ b/docs/privacy.html
@@ -59,6 +59,7 @@ footer p { color: var(--muted); }
<ul>
<li><strong>Chats, memory, garden, settings and Lattice Points</strong> are kept in your browser's storage (localStorage and IndexedDB), or in the desktop app on your computer. We never receive them.</li>
<li><strong>They are not locked with a password by default.</strong> Anyone who can use your browser profile or your computer could read them. If other people share your computer, keep that in mind.</li>
+<li><strong>Newer (v-encrypt-chats-locally-v0.1): chats in the main app are sealed on your device.</strong> In browsers that support WebCrypto (https or localhost), your chat messages, conversation names and the memory index are sealed with AES-GCM before they are written to storage. The key is made in your browser, stays in your browser, and never leaves it. Because the key sits in the same browser, this stops casual reading of the storage files. It does not stop someone who can use your browser profile, harmful code running on the page, or a compromised device, and it is still not a password lock. Saved backups (Memory Vault) are readable files, so keep them somewhere safe. A backup never carries the unlock key, so saved API keys must be entered again after restoring on another device.</li>
<li><strong>API keys and tokens are scrambled, not unbreakable.</strong> They're encrypted with AES-GCM, but the key that unlocks them is kept in the same browser storage. That stops casual snooping and copy-paste accidents. It won't stop someone, or harmful code, with access to your browser profile. Use keys you can easily revoke, with the smallest permissions that work.</li>
<li><strong>Some exports ask for a password</strong> (like the soul file). Those files are locked with the password you choose.</li>
<li><strong>Walk the Garden</strong> (<a href="love-logic-v4.html">love-logic-v4.html</a>) stores nothing at all. Your garden lives only in its link.</li>
@@ -109,7 +110,7 @@ footer p { color: var(--muted); }
<tr><td class="old">“API keys are encrypted” / “Encrypted with φ-salt”</td><td>Keys are scrambled with AES-GCM, and the unlocking key sits in the same browser. That protects against casual snooping, not against someone with access to your browser profile.</td></tr>
<tr><td class="old">“We have no servers” / “Servers required: 0”</td><td>No account servers and no chat database. A small search helper, the mesh broker, font and code hosts, the site host, and any cloud AI you choose do see some traffic (table above).</td></tr>
<tr><td class="old">“100% Private” / “Zero Data Collection”</td><td>We collect no copy of your chats and run no analytics. The services in the table above still see what they need to work.</td></tr>
-<tr><td class="old">“Every conversation is encrypted locally”</td><td>Conversations are kept in your browser's storage on your device. They're not password-locked by default.</td></tr>
+<tr><td class="old">“Every conversation is encrypted locally”</td><td>Conversations are kept in your browser's storage on your device. They're not password-locked by default. <span class="newer-inline">Newer (v-encrypt-chats-locally-v0.1): in the main app they are now sealed with AES-GCM on your device, with the key kept in the same browser. That stops casual reading, not a compromised device.</span></td></tr>
<tr class="v-honesty-truth-pass-v0.4.2"><td class="old">“No API key”</td><td>A mind on your own computer (Ollama, LM Studio) or in your browser needs no key. A cloud AI uses your own key.</td></tr>
<tr class="v-honesty-truth-pass-v0.4.2"><td class="old">“The helper keeps no logs”</td><td>The helper's code writes no logs and caches nothing. Cloudflare's dashboard logs are set to off, which the code can't prove. Today search ships switched off.</td></tr>
<tr><td class="old">“privilege-grade conditions” (Continuity Seal)</td><td><!-- RETIRED 2026-09-30 v-honesty-truth-pass-v0.4.2: "our ledgers are tamper-evident". True for the desktop ledger (desktop/lattice-ledger.js: hash chain + Ed25519 signature, verifyChain fails on an edited entry; desktop/scripts/smoke-ledger.js). The browser chain (docs/modules/lattice-chain.js) says "provenance, not anti-tampering". -->An ethic plus local tools: your chats stay on your device. The desktop app's ledger is hash-chained and signed, so an edited entry fails its check. The browser's chain is a record of where things came from, not tamper-proof. It is not legal privilege, not end-to-end encryption, and the K-of-N vault is not built yet.</td></tr>
diff --git a/docs/scripts/smoke-encrypt-chats-locally.js b/docs/scripts/smoke-encrypt-chats-locally.js
new file mode 100644
index 00000000..ed921974
--- /dev/null
+++ b/docs/scripts/smoke-encrypt-chats-locally.js
@@ -0,0 +1,189 @@
+#!/usr/bin/env node
+// smoke-encrypt-chats-locally.js (v-encrypt-chats-locally-v0.1)
+// 1. FLChatVault seals and opens chat records with AES-GCM (WebCrypto in Node).
+// 2. Migration seals plain records only after a verified round trip, and skips
+// a record the app changed in the meantime (compare-and-swap).
+// 3. A missing key never deletes or overwrites: records show as locked, and a
+// locked placeholder written back restores the original sealed blob.
+// 4. No WebCrypto: honest passthrough, chats stay plain and readable.
+// 5. Locks: root index.html == docs/app.html; fl-connect.js and sw.js untouched;
+// honest note present; status uses textContent; no em dash in the new block.
+'use strict';
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+const assert = require('assert');
+
+const ROOT = path.resolve(__dirname, '..', '..');
+const app = fs.readFileSync(path.join(ROOT, 'docs', 'app.html'), 'utf8');
+
+const b = app.indexOf('// v-encrypt-chats-locally-v0.1 BEGIN: FLChatVault');
+const e = app.indexOf('// v-encrypt-chats-locally-v0.1 END: FLChatVault');
+assert.ok(b > 0 && e > b, 'FLChatVault block present');
+const block = app.slice(app.lastIndexOf('\n', b), e);
+const ph = app.indexOf('async function phiHashData(data) {');
+assert.ok(ph > 0, 'phiHashData present');
+const phiFn = app.slice(ph, app.indexOf('\n}\n', ph) + 3);
+
+function makeVault(opts) {
+ opts = opts || {};
+ const ctx = {
+ crypto: opts.noCrypto ? {} : globalThis.crypto,
+ TextEncoder, TextDecoder, setTimeout, clearTimeout, console,
+ btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
+ atob: (s) => Buffer.from(s, 'base64').toString('binary'),
+ Promise, Uint8Array, Array, Object, JSON, Error, String
+ };
+ vm.createContext(ctx);
+ vm.runInContext(phiFn + '\n' + block + '\nthis.FLChatVault = FLChatVault;', ctx);
+ const V = ctx.FLChatVault;
+ V._setKeyStoreForTest(opts.keyStore || memKeyStore());
+ return V;
+}
+function eq(a, b, msg) { assert.deepStrictEqual(JSON.parse(JSON.stringify(a)), JSON.parse(JSON.stringify(b)), msg); }
+function memKeyStore() {
+ const m = new Map();
+ return { m, getAll: async () => Array.from(m.values()), put: async (r) => { m.set(r.id, r); } };
+}
+function memAdapter(records, keyPath) {
+ return {
+ rows: records,
+ getAll: async () => records.map((r) => JSON.parse(JSON.stringify(r))),
+ cas: async (snap, next) => {
+ const i = records.findIndex((r) => r[keyPath] === snap[keyPath]);
+ if (i < 0 || JSON.stringify(records[i]) !== JSON.stringify(snap)) return false;
+ records[i] = JSON.parse(JSON.stringify(next));
+ return true;
+ }
+ };
+}
+
+(async () => {
+ // 1. round trip
+ const ks = memKeyStore();
+ const V = makeVault({ keyStore: ks });
+ await V.init();
+ assert.strictEqual(V.status().mode, 'ready', 'vault ready');
+ assert.ok(/^phi-[0-9a-f]{16}$/.test(V.status().kid), 'phi key ID shape');
+ for (const v of ks.m.values()) { if (v.root) assert.ok(!(v.root instanceof Uint8Array), 'root stored as CryptoKey, not raw bytes'); }
+ const msg = { conversationId: 'c1', role: 'user', content: 'my secret garden note', timestamp: '2026-09-30T12:00:00.000Z' };
+ const sealed = await V.sealFor('FreeLatticeDB/messages', msg);
+ assert.ok(sealed.sealed && sealed.sealed.ct, 'message sealed');
+ assert.ok(!JSON.stringify(sealed).includes('secret garden'), 'no plaintext in the sealed record');
+ assert.strictEqual(msg.content, 'my secret garden note', 'caller object untouched');
+ const opened = await V.openFor('FreeLatticeDB/messages', sealed);
+ assert.strictEqual(opened.content, msg.content, 'message opens');
+ const conv = { id: 'c1', name: 'Talk about bees', title: 'Talk about bees', lastMessage: 'bees', contextNote: '', createdAt: 'x', updatedAt: 'y' };
+ const sc = await V.sealFor('FreeLatticeDB/conversations', conv);
+ assert.ok(!JSON.stringify(sc).includes('bees') && sc.updatedAt === 'y' && sc.id === 'c1', 'conversation names sealed, ids and dates kept');
+ eq(await V.openFor('FreeLatticeDB/conversations', sc), conv, 'conversation opens');
+ const mi = { id: 'c1', timestamp: