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.
This commit is contained in:
Local Dev 2026-10-04 04:26:39 +02:00
parent 2f7d42d077
commit 455e468186

53
main.js
View file

@ -2224,6 +2224,57 @@ function migrateExtensionDirs() {
catch (e) { console.warn(`[addons] could not move ${from} -> ${to}:`, e?.message); } 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"); } function bundledAddonsDir() { return path.join(RES_DIR, "bundled-addons"); }
// Ids that ship inside Theseus, and the legacy ids those add-ons absorb // Ids that ship inside Theseus, and the legacy ids those add-ons absorb
// (Aegis absorbs "bchwallet" and "siawallet": the wallet's key namespace). // (Aegis absorbs "bchwallet" and "siawallet": the wallet's key namespace).
@ -8285,6 +8336,8 @@ if (!process.env.THESEUS_NO_AUTOSTART && !app.requestSingleInstanceLock()) {
installDownloadTracker(); installDownloadTracker();
installRequestFilter(); installRequestFilter();
migrateExtensionDirs(); migrateExtensionDirs();
scrubStaleStoreCopies();
setTimeout(() => { try { pruneAddonBackups(); } catch {} }, 20000);
initAddons(); initAddons();
// Custom engines saved before icons were cached (or whose fetch never // Custom engines saved before icons were cached (or whose fetch never
// landed) get theirs once the startup burst is over. // landed) get theirs once the startup burst is over.