feat(aegis): choose when Aegis asks for the PIN
Reported: after a restart Aegis opens without asking for a PIN, then demands
one as soon as you click something. That came from the only control being
requirePinForSending — safeStorage remembers the master password, so the
wallet reopens unlocked, and the PIN prompt then ambushes the first action.
Asking at the door is coherent. Asking nothing is coherent. Asking once the
user is already inside is not.
"Ask for PIN" is now four independent triggers, because wanting one at
startup and again per transaction is a normal combination:
- On each browser restart (compares a per-process boot id; a timestamp
cannot tell a restart from a long idle)
- On each wallet launch (hide/show — one panel load is one launch)
- Every 6 hours
- Each transaction
With none ticked, an open wallet is never interrupted again. The decision is
made host-side: the panel reloads on every hide/show and must not be the
thing that remembers a gate was cleared. A clearance is only recorded after
the panel has actually decrypted the PIN blob, which is proof rather than a
claim, and ticking a trigger does not fire it retroactively.
restart defaults ON for a wallet that otherwise reopens fully unlocked, and
transaction inherits the old requirePinForSending so nobody loses a gate they
had chosen. Removing the PIN clears every trigger and the clearance record.
This commit is contained in:
parent
123b7ad8e9
commit
be05fe67d4
3 changed files with 201 additions and 36 deletions
|
|
@ -18,6 +18,14 @@ const path = require("node:path");
|
||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
|
|
||||||
const LEGACY_BCH_PURPOSE = "bchwallet/mainnet/0";
|
const LEGACY_BCH_PURPOSE = "bchwallet/mainnet/0";
|
||||||
|
|
||||||
|
// New each time the add-on's main process starts, i.e. once per Theseus
|
||||||
|
// launch. Comparing it against the id stored the last time a PIN was
|
||||||
|
// accepted is how "ask again after a browser restart" is detected — a
|
||||||
|
// timestamp cannot tell a restart from a long idle, and the panel cannot be
|
||||||
|
// trusted to report its own restarts.
|
||||||
|
const BOOT_ID = require("node:crypto").randomBytes(8).toString("hex");
|
||||||
|
const PIN_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
||||||
const LEGACY_BCH_WALLET_ID = "bch-default";
|
const LEGACY_BCH_WALLET_ID = "bch-default";
|
||||||
|
|
||||||
let ctx = null;
|
let ctx = null;
|
||||||
|
|
@ -2200,6 +2208,9 @@ function registerPanelMessages(api) {
|
||||||
fromPanel(m);
|
fromPanel(m);
|
||||||
api.storage.set("aegis/pin/v1", null);
|
api.storage.set("aegis/pin/v1", null);
|
||||||
api.storage.set("aegis/pin/failCount", 0);
|
api.storage.set("aegis/pin/failCount", 0);
|
||||||
|
// Drop the gate record as well, so enrolling a new PIN later starts from
|
||||||
|
// "not yet satisfied" rather than inheriting the old PIN's clearance.
|
||||||
|
api.storage.set("aegis/pin/gate", null);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
// Track failed PIN attempts in the addon so a panel reload cannot bypass
|
// Track failed PIN attempts in the addon so a panel reload cannot bypass
|
||||||
|
|
@ -2226,11 +2237,74 @@ function registerPanelMessages(api) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When to ask for the PIN. These are independent triggers, not a single
|
||||||
|
// mode: wanting one at startup and one per transaction is a normal
|
||||||
|
// combination. Previously the only control was requirePinForSending, which
|
||||||
|
// produced the behaviour the user reported — Aegis opens unlocked after a
|
||||||
|
// restart (safeStorage remembered the password) and then demands a PIN the
|
||||||
|
// moment you touch something. Asking at the door or not at all is
|
||||||
|
// coherent; asking only once you are inside is not.
|
||||||
|
//
|
||||||
|
// `restart` defaults ON for a wallet that otherwise reopens fully unlocked.
|
||||||
|
// `transaction` inherits the old requirePinForSending so nobody silently
|
||||||
|
// loses a gate they had chosen.
|
||||||
|
function pinPolicy() {
|
||||||
|
const cfg = api.storage.get("aegis/security/v1", {}) || {};
|
||||||
|
const on = (cfg.pinOn && typeof cfg.pinOn === "object") ? cfg.pinOn : null;
|
||||||
|
return {
|
||||||
|
restart: on ? !!on.restart : true,
|
||||||
|
launch: on ? !!on.launch : false,
|
||||||
|
interval: on ? !!on.interval : false,
|
||||||
|
transaction: on ? !!on.transaction : !!cfg.requirePinForSending,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const pinGate = () => {
|
||||||
|
const g = api.storage.get("aegis/pin/gate", null);
|
||||||
|
return (g && typeof g === "object") ? g : {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Does the user have to prove the PIN right now? Decided host-side: the
|
||||||
|
// panel reloads freely and must not be the thing that remembers whether a
|
||||||
|
// gate was already satisfied.
|
||||||
|
function pinNeeded(event) {
|
||||||
|
if (!api.storage.get("aegis/pin/v1", null)) return { needPin: false, reason: "no-pin" };
|
||||||
|
const on = pinPolicy();
|
||||||
|
const g = pinGate();
|
||||||
|
if (on.interval) {
|
||||||
|
// Never satisfied, or satisfied too long ago. Checked for every event
|
||||||
|
// so a six-hour expiry also lands on the next transaction.
|
||||||
|
if (!g.lastOkAt || (Date.now() - Number(g.lastOkAt)) > PIN_INTERVAL_MS) {
|
||||||
|
return { needPin: true, reason: "interval" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event === "transaction" && on.transaction) return { needPin: true, reason: "transaction" };
|
||||||
|
if (event === "panel-load") {
|
||||||
|
if (on.launch) return { needPin: true, reason: "launch" };
|
||||||
|
if (on.restart && g.bootId !== BOOT_ID) return { needPin: true, reason: "restart" };
|
||||||
|
}
|
||||||
|
return { needPin: false, reason: "satisfied" };
|
||||||
|
}
|
||||||
|
|
||||||
|
api.onMessage("pinGateStatus", (p, m) => {
|
||||||
|
fromPanel(m);
|
||||||
|
const event = String(p && p.event || "panel-load");
|
||||||
|
return { ...pinNeeded(event), event, policy: pinPolicy() };
|
||||||
|
});
|
||||||
|
// Called only after the panel has actually decrypted the PIN blob, which
|
||||||
|
// is proof of the PIN and not merely a claim about it.
|
||||||
|
api.onMessage("pinGateSatisfied", (_p, m) => {
|
||||||
|
fromPanel(m);
|
||||||
|
api.storage.set("aegis/pin/gate", { lastOkAt: Date.now(), bootId: BOOT_ID });
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
api.onMessage("securityGet", (_p, m) => {
|
api.onMessage("securityGet", (_p, m) => {
|
||||||
fromPanel(m);
|
fromPanel(m);
|
||||||
const cfg = api.storage.get("aegis/security/v1", {}) || {};
|
const cfg = api.storage.get("aegis/security/v1", {}) || {};
|
||||||
return {
|
return {
|
||||||
hasPin: !!api.storage.get("aegis/pin/v1", null),
|
hasPin: !!api.storage.get("aegis/pin/v1", null),
|
||||||
|
pinOn: pinPolicy(),
|
||||||
|
pinIntervalHours: PIN_INTERVAL_MS / 3600000,
|
||||||
requirePinForSending: !!cfg.requirePinForSending,
|
requirePinForSending: !!cfg.requirePinForSending,
|
||||||
// Defaults ON (note the !== false), unlike the send flag: a send is
|
// Defaults ON (note the !== false), unlike the send flag: a send is
|
||||||
// already fronted by an approval overlay, whereas revealing a key is
|
// already fronted by an approval overlay, whereas revealing a key is
|
||||||
|
|
@ -2246,9 +2320,25 @@ function registerPanelMessages(api) {
|
||||||
const next = { ...cur };
|
const next = { ...cur };
|
||||||
if (p && typeof p.requirePinForSending === "boolean") next.requirePinForSending = p.requirePinForSending;
|
if (p && typeof p.requirePinForSending === "boolean") next.requirePinForSending = p.requirePinForSending;
|
||||||
if (p && typeof p.requirePinForReveal === "boolean") next.requirePinForReveal = p.requirePinForReveal;
|
if (p && typeof p.requirePinForReveal === "boolean") next.requirePinForReveal = p.requirePinForReveal;
|
||||||
|
if (p && p.pinOn && typeof p.pinOn === "object") {
|
||||||
|
// Write the whole set from the current policy plus the keys given, so
|
||||||
|
// the first edit materialises the migrated defaults instead of leaving
|
||||||
|
// three triggers undefined and one set.
|
||||||
|
const cur = pinPolicy();
|
||||||
|
const merged = { ...cur };
|
||||||
|
for (const k of ["restart", "launch", "interval", "transaction"]) {
|
||||||
|
if (typeof p.pinOn[k] === "boolean") merged[k] = p.pinOn[k];
|
||||||
|
}
|
||||||
|
next.pinOn = merged;
|
||||||
|
// The legacy flag now lives in pinOn.transaction; keep them in step so
|
||||||
|
// an older build reading this store still gates sends the same way.
|
||||||
|
next.requirePinForSending = merged.transaction;
|
||||||
|
}
|
||||||
api.storage.set("aegis/security/v1", next);
|
api.storage.set("aegis/security/v1", next);
|
||||||
return {
|
return {
|
||||||
hasPin: !!api.storage.get("aegis/pin/v1", null),
|
hasPin: !!api.storage.get("aegis/pin/v1", null),
|
||||||
|
pinOn: pinPolicy(),
|
||||||
|
pinIntervalHours: PIN_INTERVAL_MS / 3600000,
|
||||||
requirePinForSending: !!next.requirePinForSending,
|
requirePinForSending: !!next.requirePinForSending,
|
||||||
requirePinForReveal: next.requirePinForReveal !== false,
|
requirePinForReveal: next.requirePinForReveal !== false,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,13 @@
|
||||||
/* Network selector — indicator and switch in one control, directly under
|
/* Network selector — indicator and switch in one control, directly under
|
||||||
the balance and present whenever a wallet is. The active chip IS the
|
the balance and present whenever a wallet is. The active chip IS the
|
||||||
"you are here" marker the status row used to carry. */
|
"you are here" marker the status row used to carry. */
|
||||||
|
/* The "Ask for PIN" trigger list. Stacked under its own label rather than
|
||||||
|
one switch per row: four rows of switches read as four unrelated
|
||||||
|
settings, when they are one question with four answers. */
|
||||||
|
.gsec .gline .pinon { display: flex; flex-direction: column; gap: 5px; margin-top: 7px; }
|
||||||
|
.gsec .gline .pinon label { display: flex; align-items: center; gap: 7px; font-size: 12px;
|
||||||
|
color: var(--ink); cursor: pointer; font-weight: 400; }
|
||||||
|
.gsec .gline .pinon input { margin: 0; }
|
||||||
.netsel { display: flex; gap: 6px; margin-top: 12px; padding: 0 2px; flex-wrap: wrap; }
|
.netsel { display: flex; gap: 6px; margin-top: 12px; padding: 0 2px; flex-wrap: wrap; }
|
||||||
.netsel[hidden] { display: none; }
|
.netsel[hidden] { display: none; }
|
||||||
.netselchip { background: transparent; border: 1px solid var(--line); color: var(--mut);
|
.netselchip { background: transparent; border: 1px solid var(--line); color: var(--mut);
|
||||||
|
|
@ -990,13 +997,22 @@
|
||||||
<button class="btn sm danger" id="gsPinRemove" hidden>Remove</button>
|
<button class="btn sm danger" id="gsPinRemove" hidden>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gline" id="gsRequirePinLine" hidden>
|
<!-- Independent triggers, not a mode. Wanting a PIN at startup and
|
||||||
|
again per transaction is a normal combination, and the old
|
||||||
|
single "require PIN for sending" flag produced the worst of both:
|
||||||
|
the wallet reopened unlocked after a restart and then demanded a
|
||||||
|
PIN the moment you touched something. With none of these ticked
|
||||||
|
the wallet never asks again once it is open. -->
|
||||||
|
<div class="gline" id="gsPinOnLine" hidden>
|
||||||
<div class="glabel">
|
<div class="glabel">
|
||||||
Require PIN for sending
|
Ask for PIN
|
||||||
<span class="ghint">Prompts for your PIN on every panel-initiated Send. Dapp-driven approvals still use the standard approval overlay.</span>
|
<span class="ghint">When Aegis should make you re-enter your PIN. Untick everything and it only asks when the vault itself is locked.</span>
|
||||||
</div>
|
<div class="pinon">
|
||||||
<div class="gactions">
|
<label><input type="checkbox" id="gsPinOnRestart"> On each browser restart</label>
|
||||||
<label class="switch"><input type="checkbox" id="gsRequirePin"><span></span></label>
|
<label><input type="checkbox" id="gsPinOnLaunch"> On each wallet launch (hide / show)</label>
|
||||||
|
<label><input type="checkbox" id="gsPinOnInterval"> Every 6 hours</label>
|
||||||
|
<label><input type="checkbox" id="gsPinOnTransaction"> Each transaction</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Turning this OFF does not make a secret free to read: it moves
|
<!-- Turning this OFF does not make a secret free to read: it moves
|
||||||
|
|
|
||||||
|
|
@ -2165,10 +2165,8 @@ function openConsolidateModal() {
|
||||||
const msg = overlay.querySelector("#conMsg"); msg.hidden = true;
|
const msg = overlay.querySelector("#conMsg"); msg.hidden = true;
|
||||||
// PIN gate fires ONCE for the whole batch — a batch send-max operation
|
// PIN gate fires ONCE for the whole batch — a batch send-max operation
|
||||||
// is a single user intent.
|
// is a single user intent.
|
||||||
if (!securityLoaded) await refreshSecurityState();
|
if (!await pinGate("transaction", `Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`)) {
|
||||||
if (securityState.requirePinForSending && securityState.hasPin) {
|
msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return;
|
||||||
const ok = await verifyPinInteractively(`Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`);
|
|
||||||
if (!ok) { msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return; }
|
|
||||||
}
|
}
|
||||||
setBusy(true, "Sending…");
|
setBusy(true, "Sending…");
|
||||||
try {
|
try {
|
||||||
|
|
@ -2285,10 +2283,8 @@ async function renderConsolidateInline(hostEl) {
|
||||||
const checked = Array.from(hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]:checked')).map((c) => c.dataset.inconwid);
|
const checked = Array.from(hostEl.querySelectorAll('input[type="checkbox"][data-inconwid]:checked')).map((c) => c.dataset.inconwid);
|
||||||
if (!checked.length) return;
|
if (!checked.length) return;
|
||||||
const msg = hostEl.querySelector("#inconMsg"); msg.hidden = true;
|
const msg = hostEl.querySelector("#inconMsg"); msg.hidden = true;
|
||||||
if (!securityLoaded) await refreshSecurityState();
|
if (!await pinGate("transaction", `Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`)) {
|
||||||
if (securityState.requirePinForSending && securityState.hasPin) {
|
msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return;
|
||||||
const ok = await verifyPinInteractively(`Confirm consolidating ${checked.length} wallet${checked.length === 1 ? "" : "s"} with your PIN.`);
|
|
||||||
if (!ok) { msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return; }
|
|
||||||
}
|
}
|
||||||
const go = hostEl.querySelector("#inconGo");
|
const go = hostEl.querySelector("#inconGo");
|
||||||
go.disabled = true; go.textContent = "Sending…";
|
go.disabled = true; go.textContent = "Sending…";
|
||||||
|
|
@ -4412,13 +4408,11 @@ async function updatePlan() {
|
||||||
$("sendBtn").addEventListener("click", async () => {
|
$("sendBtn").addEventListener("click", async () => {
|
||||||
if (!lastPlan) return;
|
if (!lastPlan) return;
|
||||||
const msg = $("sendMsg"); msg.hidden = true;
|
const msg = $("sendMsg"); msg.hidden = true;
|
||||||
// PIN approval gate: when the user has opted into "Require PIN for
|
// PIN gate. Whether a transaction needs one is the policy's call, not a
|
||||||
// sending", panel-initiated sends must clear a PIN check before the
|
// single flag's — and if the user only asked for a PIN at startup, this
|
||||||
// approval overlay even shows. Cancel if PIN check fails.
|
// check passes without a prompt.
|
||||||
if (!securityLoaded) await refreshSecurityState();
|
if (!await pinGate("transaction", "Confirm this send with your PIN.")) {
|
||||||
if (securityState.requirePinForSending && securityState.hasPin) {
|
msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return;
|
||||||
const ok = await verifyPinInteractively("Confirm this send with your PIN.");
|
|
||||||
if (!ok) { msg.className = "msg err"; msg.textContent = "Cancelled — PIN not confirmed."; msg.hidden = false; return; }
|
|
||||||
}
|
}
|
||||||
$("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…";
|
$("sendBtn").disabled = true; $("sendBtn").textContent = "Waiting for approval…";
|
||||||
try {
|
try {
|
||||||
|
|
@ -4547,15 +4541,21 @@ async function renderGeneralSecurity() {
|
||||||
const hasPin = !!securityState.hasPin;
|
const hasPin = !!securityState.hasPin;
|
||||||
const set = $("gsPinSet"), chg = $("gsPinChange"), rm = $("gsPinRemove");
|
const set = $("gsPinSet"), chg = $("gsPinChange"), rm = $("gsPinRemove");
|
||||||
const hint = $("pinStatusHint");
|
const hint = $("pinStatusHint");
|
||||||
const line = $("gsRequirePinLine"), rp = $("gsRequirePin");
|
|
||||||
if (set) set.hidden = hasPin;
|
if (set) set.hidden = hasPin;
|
||||||
if (chg) chg.hidden = !hasPin;
|
if (chg) chg.hidden = !hasPin;
|
||||||
if (rm) rm.hidden = !hasPin;
|
if (rm) rm.hidden = !hasPin;
|
||||||
if (hint) hint.textContent = hasPin
|
if (hint) hint.textContent = hasPin
|
||||||
? "On — Aegis accepts a 6-digit PIN as an alias for your master password."
|
? "On — Aegis accepts a 6-digit PIN as an alias for your master password."
|
||||||
: "Off — Aegis asks for the master password every time.";
|
: "Off — Aegis asks for the master password every time.";
|
||||||
if (line) line.hidden = !hasPin;
|
// "Ask for PIN" triggers. Meaningless without a PIN to ask for.
|
||||||
if (rp) rp.checked = !!securityState.requirePinForSending;
|
const onLine = $("gsPinOnLine");
|
||||||
|
if (onLine) onLine.hidden = !hasPin;
|
||||||
|
const pol = securityState.pinOn || {};
|
||||||
|
for (const [id, key] of PIN_ON_FIELDS) {
|
||||||
|
const box = $(id);
|
||||||
|
if (box) box.checked = !!pol[key];
|
||||||
|
}
|
||||||
// Both PIN policies are meaningless without a PIN to use.
|
// Both PIN policies are meaningless without a PIN to use.
|
||||||
const revLine = $("gsRequirePinRevealLine"), rpr = $("gsRequirePinReveal");
|
const revLine = $("gsRequirePinRevealLine"), rpr = $("gsRequirePinReveal");
|
||||||
if (revLine) revLine.hidden = !hasPin;
|
if (revLine) revLine.hidden = !hasPin;
|
||||||
|
|
@ -4655,22 +4655,37 @@ $("gsPinRemove") && $("gsPinRemove").addEventListener("click", async () => {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await S.invoke("pinBlobClear");
|
await S.invoke("pinBlobClear");
|
||||||
// Also disable the send-time PIN policy — it depends on having a PIN.
|
// Every trigger depends on there being a PIN, so clear them all rather
|
||||||
await S.invoke("securitySet", { requirePinForSending: false });
|
// than leaving a policy armed against a credential that no longer exists.
|
||||||
|
await S.invoke("securitySet", { pinOn: { restart: false, launch: false, interval: false, transaction: false } });
|
||||||
await refreshSecurityState();
|
await refreshSecurityState();
|
||||||
renderGeneralSecurity();
|
renderGeneralSecurity();
|
||||||
} catch (e) { aegisAlert("Could not remove PIN: " + cleanErr(e)); }
|
} catch (e) { aegisAlert("Could not remove PIN: " + cleanErr(e)); }
|
||||||
});
|
});
|
||||||
$("gsRequirePin") && $("gsRequirePin").addEventListener("change", async () => {
|
// Checkbox id → policy key, used by both the paint and the wiring below.
|
||||||
const on = $("gsRequirePin").checked;
|
const PIN_ON_FIELDS = [
|
||||||
try {
|
["gsPinOnRestart", "restart"],
|
||||||
securityState = await S.invoke("securitySet", { requirePinForSending: on });
|
["gsPinOnLaunch", "launch"],
|
||||||
renderGeneralSecurity();
|
["gsPinOnInterval", "interval"],
|
||||||
} catch (e) {
|
["gsPinOnTransaction", "transaction"],
|
||||||
$("gsRequirePin").checked = !on;
|
];
|
||||||
aegisAlert("Could not save setting: " + cleanErr(e));
|
for (const [id, key] of PIN_ON_FIELDS) {
|
||||||
}
|
const box = $(id);
|
||||||
});
|
if (!box) continue;
|
||||||
|
box.addEventListener("change", async () => {
|
||||||
|
const want = box.checked;
|
||||||
|
try {
|
||||||
|
securityState = await S.invoke("securitySet", { pinOn: { [key]: want } });
|
||||||
|
// Ticking a trigger should not fire it retroactively: the user is
|
||||||
|
// sitting in Settings having just proved whatever got them here.
|
||||||
|
if (want) { try { await S.invoke("pinGateSatisfied"); } catch {} }
|
||||||
|
renderGeneralSecurity();
|
||||||
|
} catch (e) {
|
||||||
|
box.checked = !want;
|
||||||
|
aegisAlert("Could not save setting: " + cleanErr(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
$("gsRequirePinReveal") && $("gsRequirePinReveal").addEventListener("change", async () => {
|
$("gsRequirePinReveal") && $("gsRequirePinReveal").addEventListener("change", async () => {
|
||||||
const on = $("gsRequirePinReveal").checked;
|
const on = $("gsRequirePinReveal").checked;
|
||||||
try {
|
try {
|
||||||
|
|
@ -4907,6 +4922,46 @@ async function openRevealSecretModal(w) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ask for the PIN if the configured policy says this event needs it. The
|
||||||
|
// host decides — the panel reloads whenever it is hidden and shown, so it
|
||||||
|
// cannot be what remembers that a gate was already cleared.
|
||||||
|
//
|
||||||
|
// Returns true to proceed, false if the user cancelled or failed. A passing
|
||||||
|
// check is recorded, which is what stops Aegis asking again until one of the
|
||||||
|
// user's triggers fires. Any failure to reach the host is treated as "ask",
|
||||||
|
// since the safe direction for a lock is closed.
|
||||||
|
async function pinGate(event, subtitle) {
|
||||||
|
let st;
|
||||||
|
try { st = await S.invoke("pinGateStatus", { event }); }
|
||||||
|
catch { st = { needPin: true, reason: "unknown" }; }
|
||||||
|
if (!st.needPin) return true;
|
||||||
|
const ok = await verifyPinInteractively(subtitle || PIN_GATE_COPY[st.reason] || "Confirm with your PIN.");
|
||||||
|
if (ok) { try { await S.invoke("pinGateSatisfied"); } catch { /* re-asks next time */ } }
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
const PIN_GATE_COPY = {
|
||||||
|
restart: "Theseus restarted — confirm your PIN to use this wallet.",
|
||||||
|
launch: "Confirm your PIN to open the wallet.",
|
||||||
|
interval: "It has been a while — confirm your PIN to carry on.",
|
||||||
|
transaction: "Confirm this transaction with your PIN.",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gate the panel itself on load. Runs once per panel script load, which is
|
||||||
|
// also once per hide/show, so "on each wallet launch" is simply this check
|
||||||
|
// with that trigger enabled.
|
||||||
|
let pinGateChecked = false;
|
||||||
|
async function pinGateOnLoad() {
|
||||||
|
if (pinGateChecked) return;
|
||||||
|
pinGateChecked = true;
|
||||||
|
if (!securityLoaded) await refreshSecurityState();
|
||||||
|
if (!securityState.hasPin) return;
|
||||||
|
// Nothing to protect yet if the vault is locked — the master-password gate
|
||||||
|
// is already in the way, and stacking a PIN prompt on top of it is two
|
||||||
|
// locks on one door.
|
||||||
|
if (state && (state.overallPhase === "locked" || state.overallPhase === "nosetup")) return;
|
||||||
|
await pinGate("panel-load");
|
||||||
|
}
|
||||||
|
|
||||||
// How many wrong PINs before a sensitive reveal stops asking for the PIN and
|
// How many wrong PINs before a sensitive reveal stops asking for the PIN and
|
||||||
// asks for the master password instead. Lower than PIN_MAX_FAILS on purpose:
|
// asks for the master password instead. Lower than PIN_MAX_FAILS on purpose:
|
||||||
// someone fumbling their own PIN gets a way through that does not cost them a
|
// someone fumbling their own PIN gets a way through that does not cost them a
|
||||||
|
|
@ -5411,6 +5466,10 @@ S.on("state", (s) => { state = s; render(); if (tab === "settings") fillSettings
|
||||||
try { state = await S.invoke("state"); render(); }
|
try { state = await S.invoke("state"); render(); }
|
||||||
catch (e) { $("gate").hidden = false; $("gate").innerHTML = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`; }
|
catch (e) { $("gate").hidden = false; $("gate").innerHTML = `<div class="big">⚠</div><div>${esc(cleanErr(e))}</div>`; }
|
||||||
bindIdleAutoLock();
|
bindIdleAutoLock();
|
||||||
|
// Ask for the PIN at the door if the policy says so, rather than letting
|
||||||
|
// the wallet open and then interrupting the first thing the user clicks.
|
||||||
|
// This runs once per panel load, which is also once per hide/show.
|
||||||
|
pinGateOnLoad();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Persistent footer: aegis.x brand link + version marker + update check.
|
// Persistent footer: aegis.x brand link + version marker + update check.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue