From d38da383d9919a805976dbba5eac048d7f429cd3 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 03:49:40 +0200 Subject: [PATCH] Aegis: stay-unlocked needs a real keystore and the right password Remember-me sealed the master password with whatever safeStorage offered, which on Linux without a keyring is a constant key, i.e. the password in the clear in the add-on store; and it stored any string without checking it. It now uses the same keystore test as the PIN, verifies the password with the vault first, and drops a blob sealed under no real keystore. Settings says that remember-me leaves the master password readable to anything running as the user, which the PIN's TPM protection does not change. --- bundled-addons/aegis/index.js | 19 +++++++++++++++---- bundled-addons/aegis/panel.js | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index 47b79921..2158551c 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -2988,12 +2988,18 @@ function registerPanelMessages(api) { if (next.lockOnClose) api.storage.set("aegis/session/enc", null); return sessionStatusFor(api); }); - api.onMessage("sessionEnable", (p, m) => { + api.onMessage("sessionEnable", async (p, m) => { fromPanel(m); const pw = String(p && p.masterPassword || ""); if (!pw) throw new Error("master password required"); const ss = safeStorageOr(api); - if (!ss || !ss.isEncryptionAvailable()) throw new Error("OS keystore unavailable — remember-me needs Windows DPAPI / macOS Keychain / libsecret"); + // Same bar as the PIN: Linux's basic_text "keystore" is a constant key, + // i.e. the master password in the clear. + if (!osSealUsable(ss)) throw new Error("OS keystore unavailable — remember-me needs Windows DPAPI / macOS Keychain / libsecret"); + // Store only a password the vault accepts. + try { await api.vault.lifecycle.unlock(pw); } + catch { throw new Error("wrong master password"); } + noteMasterVerified(api); const enc = ss.encryptString(pw).toString("base64"); api.storage.set("aegis/session/enc", { encPwB64: enc, savedAt: Date.now() }); // Force lockOnClose = false alongside — semantically they're the same @@ -3034,7 +3040,7 @@ function sessionStatusFor(api) { lockOnClose: !!cfg.lockOnClose, idleMinutes: Number(cfg.idleMinutes) || 0, hasSession: !!(blob && blob.encPwB64), - safeStorageAvailable: !!(ss && ss.isEncryptionAvailable && ss.isEncryptionAvailable()), + safeStorageAvailable: osSealUsable(ss), }; } @@ -3064,7 +3070,12 @@ async function tryAutoUnlock(api) { const blob = api.storage.get("aegis/session/enc", null); if (!blob || !blob.encPwB64) return; const ss = safeStorageOr(api); - if (!ss || !ss.isEncryptionAvailable()) return; + if (!osSealUsable(ss)) { + // Sealed under no real keystore: it is the master password in the + // clear on disk. Never use it; remove it. + api.storage.set("aegis/session/enc", null); + return; + } try { const pw = ss.decryptString(Buffer.from(blob.encPwB64, "base64")); await api.vault.lifecycle.unlock(pw); diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index 5ac60308..683e2f93 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -4820,7 +4820,7 @@ function renderSessionSettings() { } else if (sessionState.lockOnClose) { hint.textContent = "On — Aegis asks for the master password (or PIN) every time Theseus starts."; } else { - hint.textContent = "Off — Aegis stays signed in across Theseus restarts. Master password is stored in the OS keystore under this user only."; + hint.textContent = "Off — Aegis stays signed in across Theseus restarts. The master password is stored in the OS keystore under this user only, so anything that runs as your Windows account can read it; the PIN's security-chip protection does not cover it."; } } }