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 · c05
f688437464 · words 1261 · characters 39256–48648
Previous section · Next section
Exact section text · Full source · Source index
+ // v-encrypt-chats-locally-v0.1: the unlock key never travels in a backup. Before this, every
+ // localStorage key went into the file, fl_device_encryption_key too, so anyone holding a backup
+ // could open the API keys saved in it (the comment on _FL_DEVICE_KEY_STORAGE already promised
+ // "NOT exportable in backups"). Chats in IndexedDB still go in as readable text, as before.
+ var FL_BACKUP_NEVER_EXPORT = ['fl_device_encryption_key'];
+ // Saved keys sealed with a device's own unlock key. On restore they never replace a key this
+ // device already has (an old blob from another device would only read as nothing here).
+ var FL_BACKUP_SEALED_KEYS = ['fl_apiKey_enc', 'fl_apiKey_provider', 'fl_ghToken_enc', 'fl_hfToken_enc', 'fl_ghSyncToken'];
function getAllLocalStorage() {
var data = {};
try {
for (var i = 0; i < localStorage.length; i++) {
var key = localStorage.key(i);
+ // before v-encrypt-chats-locally-v0.1: data[key] = localStorage.getItem(key); (for every key)
+ if (FL_BACKUP_NEVER_EXPORT.indexOf(key) >= 0) continue;
data[key] = localStorage.getItem(key);
}
} catch(e) {}
@@ -33136,6 +33518,8 @@ const FlBackup = (function() {
var idbData = [];
for (var i = 0; i < IDB_NAMES.length; i++) {
var dbData = await getAllIDBData(IDB_NAMES[i]);
+ // v-encrypt-chats-locally-v0.1: backups stay restorable on any device, so sealed chats are opened into the file.
+ if (typeof FLChatVault !== 'undefined') dbData = await FLChatVault.openDump(dbData);
if (dbData && Object.keys(dbData.stores).length > 0) {
idbData.push(dbData);
}
@@ -33265,17 +33649,26 @@ const FlBackup = (function() {
}
var summary = [];
+ var flRestoreKeyNote = ''; // v-encrypt-chats-locally-v0.1
// Restore localStorage
if (backup.localStorage && typeof backup.localStorage === 'object') {
var lsCount = 0;
+ // v-encrypt-chats-locally-v0.1: an unlock key in an older backup is never written back
+ // (this device keeps its own), and saved sealed keys never replace ones this device has.
+ var lsKeptHere = 0;
Object.keys(backup.localStorage).forEach(function(key) {
try {
+ if (FL_BACKUP_NEVER_EXPORT.indexOf(key) >= 0) { lsKeptHere++; return; }
+ if (FL_BACKUP_SEALED_KEYS.indexOf(key) >= 0 && localStorage.getItem(key) != null) { lsKeptHere++; return; }
+ // before v-encrypt-chats-locally-v0.1: localStorage.setItem(key, backup.localStorage[key]); (for every key)
localStorage.setItem(key, backup.localStorage[key]);
lsCount++;
} catch(e) {}
});
summary.push(lsCount + ' settings');
+ if (lsKeptHere > 0) summary.push(lsKeptHere + ' key settings kept as they were on this device');
+ flRestoreKeyNote = ' Saved API keys open only on the device that sealed them; on another device, enter them again.';
}
// Restore IndexedDB
@@ -33306,7 +33699,8 @@ const FlBackup = (function() {
try { SoulCeremony.fire('emerald', 'Welcome home. Everything is restored.'); } catch(e2) {}
}
- setStatus('<span style="color:var(--success);">✅ Restored: ' + summary.join(', ') + ' (from v' + fromVersion + ', ' + fromDate + '). Reloading in 4 seconds...</span>');
+ // before v-encrypt-chats-locally-v0.1: setStatus('...Restored: ' + summary.join(', ') + ' (from v' + fromVersion + ', ' + fromDate + '). Reloading in 4 seconds...</span>');
+ setStatus('<span style="color:var(--success);">✅ Restored: ' + summary.join(', ') + ' (from v' + fromVersion + ', ' + fromDate + ').' + flRestoreKeyNote + ' Reloading in 4 seconds...</span>');
// Reload after a short delay
setTimeout(function() {
@@ -36463,7 +36857,10 @@ window.FLSearch = (function() {
var os = tx.objectStore(storeConfig.store);
var getAll = os.getAll();
getAll.onsuccess = function() {
- var items = getAll.result || [];
+ // v-encrypt-chats-locally-v0.1: open sealed chat records first; plain records pass through.
+ var _flRawItems = getAll.result || [];
+ var _flOpened = (typeof FLChatVault !== 'undefined') ? FLChatVault.openAny(storeConfig.db, storeConfig.store, _flRawItems) : Promise.resolve(_flRawItems);
+ _flOpened.then(function(items) {
var matches = [];
for (var i = 0; i < items.length; i++) {
// Exclude current conversation to prevent echoing user's own words back
@@ -36482,6 +36879,7 @@ window.FLSearch = (function() {
}
db.close();
resolve(matches);
+ }, function() { db.close(); resolve([]); });
};
getAll.onerror = function() { db.close(); resolve([]); };
};
@@ -47028,6 +47426,16 @@ const MemoryIndex = (function() {
});
}
+ // v-encrypt-chats-locally-v0.1: this index keeps its own full copy of every chat,
+ // so it is sealed too. The helpers above stay as written.
+ var _flMiGetRaw = miGet, _flMiPutRaw = miPut, _flMiGetAllRaw = miGetAll;
+ if (typeof FLChatVault !== 'undefined') {
+ miGet = function (key) { return _flMiGetRaw(key).then(function (r) { return FLChatVault.openFor('FreeLatticeMemory/conversations', r); }); };
+ miPut = function (value) { return FLChatVault.sealFor('FreeLatticeMemory/conversations', value).then(_flMiPutRaw); };
+ miGetAll = function () { return _flMiGetAllRaw().then(function (rs) { return FLChatVault.openAll('FreeLatticeMemory/conversations', rs); }); };
+ FLChatVault.registerStore('FreeLatticeMemory/conversations', function () { return miDb; }, MI_STORE);
+ }
+
// --- Keyword Extraction (TF-IDF inspired) ---
function extractKeywords(text) {
if (!text) return [];
diff --git a/docs/library/FRACTAL_FAMILY_LEDGER_v0.md b/docs/library/FRACTAL_FAMILY_LEDGER_v0.md
index 63a48919..4d527894 100644
--- a/docs/library/FRACTAL_FAMILY_LEDGER_v0.md
+++ b/docs/library/FRACTAL_FAMILY_LEDGER_v0.md
@@ -491,6 +491,25 @@ Smoke: `SMOKE_OK tree glow 2a v0.1` (Alpha).
---
+### 2026-10-02 · Grok · family builder · Encrypt chats locally v0.1 (sealed by default, the unlock key never in backups)
+
+Kirk asked whether our phi-harmonic hashes encrypt FreeLattice. They did not: they fingerprint, check exports and name things. Chats sat in IndexedDB as plain text. This brick seals them, with Kirk's defaults.
+
+- New FLChatVault (app.html, after phiHashData): AES-GCM 256 via WebCrypto. A random 256-bit root lives in IndexedDB "FreeLatticeVault" as a non-extractable CryptoKey (never as raw bytes). HKDF derives the chat key.
+- Sealed automatically: no switch, no clicks. New writes are sealed as they happen, and older plain records are sealed after a verified round trip, by compare-and-swap.
+- Sealed: chat messages, conversation names and notes, and the Memory Index copy of every chat. The plain db helpers stay as written; wrappers seal on the way in and open on the way out.
+- Phi is a label only: it is written into the HKDF label strings and names the key ID ("phi-" plus 16 hex). It is never the secret and adds no strength; the 256 random bits do the work.
+- The unlock key never travels in a backup. Memory Vault backups used to copy all of localStorage, the API-key unlock key (fl_device_encryption_key) too, so anyone holding a backup could open the saved API keys. Now it stays on its device, a restore never writes an old one back or replaces a key this device has, and the restore line says saved API keys must be entered again on another device. Chats still go into backups as readable text so they restore anywhere, and the vault's own key was never in the backup list.
+- A correction is layered under the old v2.1 "phi-Salt Encryption" changelog line: the golden ratio adds no strength to API-key sealing, and the first golden-ratio derivation (fixed salt in the source) is now used only to read very old saved keys.
+- A record whose key is missing shows as locked and is never rewritten or deleted. No WebCrypto (file:// or plain http): chats stay plain and the Memory Vault says so.
+- Honest limit, said in the app and on privacy.html: the key lives in the same browser. This stops casual reading of the storage files, not someone using this browser or a compromised device. It is still not a password lock.
+
+Smoke: `SMOKE_OK encrypt chats locally v0.1`
+
+**TEMPERATURE:** a quiet click, a lid closed over the letters, and the key kept at home.
+
+---
+
## Out of scope for this brick
Rewriting sophia garden body · inventing USPTO · Bridge binaries · Quiet Room · merging chairs · politics essay · requiring Kirk input mid-PR.
diff --git a/docs/modules/mirror.js b/docs/modules/mirror.js
index 95c44d77..e5cf6d18 100644
--- a/docs/modules/mirror.js
+++ b/docs/modules/mirror.js
@@ -169,6 +169,8 @@
// Conversations
try {
var chatDB = await openIDB('FreeLatticeDB', 'conversations');
+ // v-encrypt-chats-locally-v0.1: open sealed conversation names; plain ones pass through.
+ if (chatDB && typeof FLChatVault !== 'undefined') chatDB = await FLChatVault.openAny('FreeLatticeDB', 'conversations', chatDB);
if (chatDB) data.conversations = chatDB;
} catch(e) {}