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.
This commit is contained in:
Local Dev 2026-10-04 03:49:40 +02:00
parent e72c0ed96b
commit d38da383d9
2 changed files with 16 additions and 5 deletions

View file

@ -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);

View file

@ -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.";
}
}
}