From 455e46818663bc4afa91a4f4bdc01e031035a92e Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 04:26:39 +0200 Subject: [PATCH] Theseus: scrub stale add-on store copies of the PIN and session secrets Stores nothing reads any more kept whatever they held when they were copied: the pre-rename addons-data/ folder, the bchwallet.json left by the Aegis absorb, and parse-failure copies. For Aegis before 0.31 that could include the master password behind only an unsealed 6-digit PIN and the stay-unlocked blob. Those two keys are removed from such copies at startup; nothing else in them is touched, and the live store is not among them. extensions-backups/, which kept a copy of an add-on on every reseed and update forever, is pruned to the newest three per add-on. --- main.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/main.js b/main.js index f1ee5a38..e74e5757 100644 --- a/main.js +++ b/main.js @@ -2224,6 +2224,57 @@ function migrateExtensionDirs() { catch (e) { console.warn(`[addons] could not move ${from} -> ${to}:`, e?.message); } } } +// Copies of add-on stores that nothing reads any more still hold whatever +// was in them when they were copied — for Aegis builds before 0.31 that +// included the master password behind only a 6-digit PIN (aegis/pin/v1, +// unsealed) and the stay-unlocked blob. The stale pre-rename addons-data/ +// folder (kept when extensions-data/ already existed), the bchwallet.json +// left behind by the Aegis absorb, and parse-failure copies are scrubbed of +// those keys; everything else in them is left as it was. +const SECRET_STORE_KEYS = ["aegis/pin/v1", "aegis/session/enc"]; +function scrubStaleStoreCopies() { + const ud = app.getPath("userData"); + const files = []; + const listJson = (d, test) => { try { for (const n of fs.readdirSync(d)) if (test(n)) files.push(path.join(d, n)); } catch {} }; + listJson(path.join(ud, "addons-data"), (n) => /\.json(\.tmp)?$/i.test(n)); + listJson(path.join(ud, "extensions-data"), (n) => /^bchwallet\.json$/i.test(n) || /\.json\.(corrupt-\d+|tmp)$/i.test(n)); + for (const f of files) { + try { + const raw = fs.readFileSync(f, "utf8"); + if (!SECRET_STORE_KEYS.some((k) => raw.includes(JSON.stringify(k)))) continue; + let data; + try { data = JSON.parse(raw); } catch { continue; } + if (!data || typeof data !== "object") continue; + let n = 0; + for (const k of SECRET_STORE_KEYS) if (k in data) { delete data[k]; n++; } + if (!n) continue; + fs.writeFileSync(f, JSON.stringify(data)); + console.log(`[addons] removed ${n} stale secret key(s) from ${path.relative(ud, f)}`); + } catch (e) { console.warn("[addons] scrub failed for", f, e?.message); } + } +} +// extensions-backups/ gets a copy of an add-on's folder on every reseed, +// promote and community update and was never pruned (118 copies, 93 MB on +// one profile). Keep the newest three per add-on. +function pruneAddonBackups(keep = 3) { + const d = addonsBackupDir(); + let names = []; + try { names = fs.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).map((x) => x.name); } catch { return; } + const byId = new Map(); + for (const name of names) { + const m = /^(.+?)-(?:\d[\w.]*|unknown|migrated)-/.exec(name); + const id = m ? m[1] : name; + let mtime = 0; try { mtime = fs.statSync(path.join(d, name)).mtimeMs; } catch {} + if (!byId.has(id)) byId.set(id, []); + byId.get(id).push({ name, mtime }); + } + for (const [, list] of byId) { + list.sort((a, b) => b.mtime - a.mtime); + for (const { name } of list.slice(keep)) { + try { fs.rmSync(path.join(d, name), { recursive: true, force: true }); } catch {} + } + } +} function bundledAddonsDir() { return path.join(RES_DIR, "bundled-addons"); } // Ids that ship inside Theseus, and the legacy ids those add-ons absorb // (Aegis absorbs "bchwallet" and "siawallet": the wallet's key namespace). @@ -8285,6 +8336,8 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) { installDownloadTracker(); installRequestFilter(); migrateExtensionDirs(); + scrubStaleStoreCopies(); + setTimeout(() => { try { pruneAddonBackups(); } catch {} }, 20000); initAddons(); // Custom engines saved before icons were cached (or whose fetch never // landed) get theirs once the startup burst is over.