The vault re-locks on every restart and only the master password opened
it, so every extension that needs it (Aegis, now Pithos) either asked for
the master password itself or grew its own PIN. Theseus now owns one:
- Settings > Passwords sets, changes or removes a 6-digit PIN. The PIN
wraps the master password (PBKDF2-SHA256, 600k iterations, AES-256-GCM)
and the result is sealed with the OS keystore (safeStorage: DPAPI /
Keychain / libsecret), so a copied vault-pin.json cannot be brute-forced
elsewhere. Every unlock still ends at the master password.
- Three wrong PINs in a row require the master password. The strike count
lives in the same file, so a restart does not reset it; a successful
master-password unlock does. A PIN whose password no longer opens the
vault (password changed) is dropped.
- unlock.html is Theseus's own prompt, over the whole window: PIN pad, or
the master password. Extensions call api.vault.requestUnlock({ reason })
(vault-derive capability) and get { ok } back; what the user typed never
reaches them. Settings' locked screen offers "Unlock with PIN" through
the same prompt.
10 lines
657 B
JavaScript
10 lines
657 B
JavaScript
// Preload for the vault unlock overlay (unlock.html). Main pushes one request
|
|
// via `unlock-show`; the page sends a PIN or the master password back with
|
|
// `unlock-submit` and main checks it. The secret goes straight to main — it
|
|
// is never handed to the add-on that asked for the unlock.
|
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
contextBridge.exposeInMainWorld("unlock", {
|
|
onShow: (cb) => ipcRenderer.on("unlock-show", (_e, req) => cb(req)),
|
|
submit: (reqId, mode, value) => ipcRenderer.invoke("unlock-submit", reqId, String(mode || ""), String(value || "")),
|
|
cancel: (reqId) => ipcRenderer.invoke("unlock-cancel", reqId),
|
|
});
|