FLINT NEXT: ENCRYPT CHATS LOCALLY v0.1 (FreeLattice only) Written Wed Sep 30, 2026, evening MDT, by Grok Bot for Kirk. Research and plan only. Nothing pushed, no PR opened. SMOKE LINE SMOKE_OK encrypt chats locally v0.1 (new file docs/scripts/smoke-encrypt-chats-locally.js; runs in Node 20 with real WebCrypto) LOCKS - Root index.html == docs/app.html, byte for byte. After applying, run: cp docs/app.html index.html - docs/modules/fl-connect.js: untouched (not needed). Stays byte-identical with Alpha. - sw.js (root and docs): untouched (not needed; the vault is inline in app.html, so there is no new file to cache). - Kimi pages, the lawyer packet, Alpha: untouched. No lawyer mention anywhere. - Layer, never delete: the old db helpers stay exactly as written. Wrappers are added after them. privacy.html keeps the older line and gains a "Newer" line. - No em dashes in new text. Status text goes in with textContent. No confirm(). No bare *. - Kirk's name stays out of it. ORDER NOTE (it touches app.html) Apply AFTER the Connect queue: 1. Tree any-model (fl-connect.js; this brick does not touch that file) 2. Model choice survives reload v0.1.1 (FLINT_HEAL_MODEL_CHOICE_SURVIVES_RELOAD_v0.1.1.diff) 3. Connect boot probe (connect-boot-probe-v0.1-reference/fl-app.html.diff) 4. Scan only when asked v0.1 (FLINT_HEAL_SCAN_ONLY_WHEN_ASKED_v0.1-fl.diff) 5. THIS brick Verified tonight on a scratch tree from origin/main ba53b415: 2, 3 and 4 applied in that order, then this diff applied clean, and the smoke passed. It also applies clean on main alone. The hunks live at about lines 25600-25800 (after phiHashData and dbCount), 33,000 (backup and soul export), 36,460 (cross-store search) and 47,020 (Memory Index). The boot probe's hunk inside switchToConversation (about line 25898) is not touched. If the earlier bricks shift lines, every hunk below is anchored on exact text, so applying by hand also works. FILES /workspace/flint-pastes/FLINT_NEXT_ENCRYPT_CHATS_LOCALLY_v0.1-fl.diff (the whole change, git apply-able) Prototype worktree on the box: /workspace/encrypt-proto (detached at origin/main ba53b415, uncommitted) docs/app.html +383 (vault module, wrappers, status line, 4 reader patches) index.html copy of docs/app.html docs/modules/mirror.js +2 (opens sealed conversation names before the Mirror reads them) docs/privacy.html Newer line + Newer note on the "encrypted locally" row docs/library/FRACTAL_FAMILY_LEDGER_v0.md one ledger entry docs/scripts/smoke-encrypt-chats-locally.js new ====================================================================== WHY (THE AUDIT IN ONE SCREEN) ====================================================================== Kirk asked: "I thought we had encryption for FreeLattice using our phi-harmonic hashes?" Plain answer: no phi-harmonic hash encrypts anything today. The phi pieces fingerprint, check, or name things. Real encryption exists in three places, none of them for chats, and none use phi: a. API keys and tokens (app.html "phi-Root Encryption", about line 25499): AES-GCM. The key comes from a random device key kept in localStorage (fl_device_encryption_key). There is no phi in the current derivation. The old derivation (provider + "1.6180339887" plus a fixed salt in the source) is still a decrypt fallback, and anyone with the source can recompute it. The v2.1 changelog text "phi-Salt Encryption, golden-ratio derivation" is no longer true. b. Soul file export: AES-GCM with PBKDF2 (100k) from a password you type. Real password encryption, for that file only. c. Desktop: the Ed25519 companion seed and the pair seed are encrypted at rest by Electron safeStorage (the OS keychain). Chats themselves (IndexedDB FreeLatticeDB: messages and conversations, plus the Memory Index copy in FreeLatticeMemory) were plain text. That is what this brick fixes. ====================================================================== WHAT THIS BRICK DOES ====================================================================== 1. FLChatVault (inline in app.html, right after phiHashData) - On first run it makes 32 random bytes (crypto.getRandomValues), imports them as a NON-EXTRACTABLE HKDF CryptoKey, and wipes the bytes. The CryptoKey is stored in a separate IndexedDB database "FreeLatticeVault" (store "keys"). Raw key bytes are never stored anywhere and can't be read back out by page code. - A random 32-byte salt is stored next to it. - HKDF-SHA-256 derives the AES-GCM 256 chat key. Label: "freelattice.chats.v1|phi=1.6180339887". - Before the key is trusted with a single chat, it's written, read back, and its key ID checked. - Each record gets a fresh 12-byte IV. Additional authenticated data (AAD) ties a message to its conversationId, role and timestamp, and a conversation to its id. So a sealed message moved or edited outside the app reads as "locked", not as wrong text. 2. Where phi is honest, and not decorative - Domain label: phi is in the HKDF labels. Chat keys can never collide with keys derived for any other purpose from the same root. This is real domain separation. Any unique string would do the same job, and the paste says so. - Key ID: a one-way 128-bit check value (HKDF with a second label) goes through phiHashData and is shown as "phi-xxxxxxxxxxxxxxxx". The vault uses it to tell the right key from a wrong or missing one before trying to open anything. It reveals nothing usable about the key. - Phi is never the secret and adds no strength. The strength is the 256 random bits. 3. What is sealed - FreeLatticeDB/messages: content - FreeLatticeDB/conversations: name, title, lastMessage, contextNote - FreeLatticeMemory/conversations (the Memory Index keeps a full second copy of every chat): title, messages, tags, summary, keywords Left readable on purpose (the app needs them to sort and find things): ids, conversationId, role, timestamps, counts. Say so plainly: the fact that a chat exists, and when, is still visible. 4. How it plugs in (smallest change) - The plain helpers dbGet / dbPut / dbGetAll / dbGetAllByIndex stay as written. Right after dbCount, wrappers seal on the way in and open on the way out. Every existing reader keeps working: switchToConversation, export, memory summary, GitHub sync, memory index sync. - The same pattern wraps miGet / miPut / miGetAll inside the MemoryIndex module. - Four places read IndexedDB directly and now open sealed records first: * FLSearch cross-store search (searchStore) * Soul file export (readDB of conversations) * Memory Vault backup (getAllIDBData via FLChatVault.openDump) * mirror.js (openIDB of conversations) 5. Migration (layer, never lose data) - Plain records keep working forever. Readers accept mixed plain and sealed. - New writes are sealed as they happen. - A background pass runs 3 s and 20 s after load. For each plain record: 1) seal it 2) open the sealed copy and compare every field with the original 3) only if they match, compare-and-swap inside one readwrite transaction: replace only if the stored record is still exactly the plain one read If the app changed the record meanwhile, it's skipped (and picked up next load). A crash at any point leaves either the old plain record or the new sealed one. Both are readable. - Key missing (site data partly cleared, a different browser): sealed records are shown as "Locked conversation" and "Locked on this device. This was sealed with a key this browser no longer has. It has not been deleted." They are never re-sealed, rewritten or deleted. If app code writes a locked placeholder back, the original sealed blob is put back instead. A new key gets a new key ID, so new chats still work. - No WebCrypto (file://, plain http on a LAN address): nothing is sealed, chats stay readable exactly as before, and the Memory Vault says so. - Backups stay readable files so they restore on any device. That was true before; it's now said out loud in the status line. 6. What the person sees Settings, Memory Vault section, one line (textContent): "Chats sealed on this device with AES-GCM: N records sealed, N still plain (they seal on the next pass). Key ID phi-.... The key lives in this same browser, so this stops casual reading of the storage files, not someone using this browser or a compromised device. Saved backups are readable files: keep them somewhere safe." privacy.html: the older line "They are not locked with a password by default." stays. A Newer line follows it (exact text is in the diff). The "Every conversation is encrypted locally" row gets a short Newer note. ====================================================================== THE HONEST NOTE (say this wherever we mention it) ====================================================================== The key lives in the same browser as the chats. That protects against casual reading: someone copying the browser's storage folder, a sync or backup tool that grabs the database files, a curious glance at DevTools storage. It does NOT protect against: - someone who can open this browser profile (the app opens chats for them like it does for you) - harmful code running on the page (an extension or injected script can ask the vault to open things) - a compromised device It is still not a password lock. The only mode that resists someone holding the device is a passphrase mode (see "later"). Do not write "encrypted" without "on this device, key in the same browser" next to it. ====================================================================== APPLY ====================================================================== git apply /workspace/flint-pastes/FLINT_NEXT_ENCRYPT_CHATS_LOCALLY_v0.1-fl.diff cp docs/app.html index.html node docs/scripts/smoke-encrypt-chats-locally.js -> SMOKE_OK encrypt chats locally v0.1 run the full smoke list (docs/scripts/smoke-*.js, tests/smoke.js, tests/smoke-history.js, desktop/scripts/smoke-*.js) Tonight on the prototype tree: 67 pass, plus the same 8 known carry-forward failures as main (carry-forward, connect-card-yesno-wizard, fable-tech, phone-shine, proof-60s, tests/smoke-history, desktop reseed, desktop swarm). No new failures. Main has 74 smoke files and passes 66; this adds 1. What the smoke checks: - seal and open round trip for messages, conversations and the Memory Index; no plaintext left in a sealed record; the caller's object isn't mutated; uncovered stores pass through - the root is stored as a CryptoKey, never raw bytes; the key ID has the shape phi- plus 16 hex - AAD: a moved record reads as locked (no throw); a locked placeholder written back restores the original blob byte for byte - migration: seals plain, leaves sealed alone, and compare-and-swap skips a record changed mid-pass (the newer text survives) - reload with the same key store gives the same key ID and still opens - fresh key store: old records show locked and are never rewritten; the new key gets a new ID - no WebCrypto: plaintext passthrough, and the status says "not sealed" - the backup dump opens sealed stores - static: the honest note is in the code; the status uses textContent; no em dash or confirm() in the block; root index == app.html; the privacy Newer line is present and the older line kept; fl-connect.js and sw.js carry no vault code Browser walk, done tonight in headless Chrome on http://localhost (a secure context): W1 fresh load: mode ready, key ID phi-36b7..., no page errors W2 a legacy plain message and conversation written raw (as an old build would), plus a new chat through the app: the new message is sealed on write; the old one is still plain until the pass W3 sealExisting: 2 sealed, 3 already sealed. The raw IndexedDB holds no plaintext in messages, conversations or the Memory Index W4 reload: conversation names open ("Old chat name", "Bees and honey"); switchToConversation('legacy1') shows "OLD PLAIN TEXT"; state.chatHistory is right W5 FLSearch finds "NEW SECRET hello" through the sealed store W6 FreeLatticeVault deleted, then reload: the chat shows "Locked conversation" with the locked line; nothing is deleted; a new key ID appears; the status says "3 locked (their key is not in this browser; kept, not deleted)" Flint should repeat W1, W4 and W6 on the live preview at 390 px and 1280 px (Settings > Memory Vault status line). ====================================================================== OUT OF SCOPE FOR v0.1 (named, not hidden) ====================================================================== - Companion chat store (about line 62946), Memory Vault module (FreeLatticeMemoryVault), Memory Garden, Living Context, STORE_META memorySummary (an AI-written summary of your chats), meta/memoryIndex store, Round Table (fl_rtMemory in localStorage), Quiet Room. These are still plain. The status line counts only what is sealed, and privacy.html says "chat messages, conversation names and the memory index", not "everything". - GitHub sync still pushes readable conversation data to the person's own repo, as before. - Alpha (theLatticeTree) garden-thread chats stay plain localStorage. A sister brick could reuse this module later. - The API-key fallback (_phiDeriveKeyLegacy) and the stale "phi-Salt" changelog line are not changed here (see decisions). LATER (v0.2 ideas, only if Kirk wants) - Passphrase mode: PBKDF2-SHA-256 (600k) or Argon2 from a passphrase you choose wraps the root. Chats can't be opened without it, even by someone holding the device. Cost: forget the passphrase and those chats are gone. That's why it must be opt-in, with a readable backup offered first. - Seal the remaining stores listed above with the same vault. - A real re-encrypt for API keys found only under the legacy derivation, then drop the legacy fallback. ====================================================================== DECISIONS FOR KIRK ====================================================================== 1. On by default (recommended; it never loses data and needs no clicks), or opt-in behind a Settings switch? 2. Add the passphrase mode as v0.2 (opt-in, with a warning that a forgotten passphrase loses chats)? 3. Phi's role: keep it as the HKDF label plus the phi-hash key ID (honest), or drop phi from crypto entirely and keep it for art and the economy? 4. Fix the old wording: layer a Newer note on the v2.1 "phi-Salt Encryption" changelog line, and plan the API-key legacy re-encrypt? 5. Memory Vault backups include ALL of localStorage, including fl_device_encryption_key. So anyone holding a backup file can unlock the API keys in it. Warn in the backup UI, or leave the device key out of backups (keys would then need re-entering after a restore)? 6. Which remaining plain stores come next (companion chat, memorySummary, Memory Vault module, Living Context)? 7. Ledger entry tone OK? (It's in the diff, with a temperature line like the others.) ====================================================================== APPENDIX: THE FULL DIFF (same as the -fl.diff file) ====================================================================== diff --git a/docs/app.html b/docs/app.html index 50035267..b719395c 100644 --- a/docs/app.html +++ b/docs/app.html @@ -18078,6 +18078,8 @@ textarea.ag-input { min-height: 60px; resize: vertical; font-family: inherit; }