Aegis: a PIN clears only the transaction it was entered for
One global 90 s clearance was opened by every PIN proof. Opening the wallet or ticking a setting let the next dapp transaction from any site through without a PIN; a dapp waiting in its poll could take the clearance the user had just made for their own send; and with two sites waiting the second one's request was dropped. Each waiting dapp transaction now has an id, and only a proof naming that id releases it; a proof given for "transaction" in the panel clears the panel's next send only; any other proof clears nothing. The panel answers waiting sites one at a time. Promote to HD now asks for the PIN like any other spend instead of failing when PIN-per-transaction is on.
This commit is contained in:
parent
2faa3d808a
commit
4511a933f4
2 changed files with 53 additions and 20 deletions
|
|
@ -1282,33 +1282,43 @@ function noteMasterVerified(api) {
|
|||
// single-use fact recorded only after the host itself has verified the
|
||||
// master password the PIN unwraps (pinGateSatisfied). Panel sends consume
|
||||
// one; dapp transactions ask the open panel for one and wait.
|
||||
//
|
||||
// A clearance belongs to the thing the PIN was entered for. It used to be
|
||||
// one global window: any PIN proof (opening the wallet, a settings toggle)
|
||||
// let the next dapp transaction from any site through, a waiting dapp could
|
||||
// take the clearance the user had just made for their own send, and with
|
||||
// two sites waiting the second one's request was lost. Now:
|
||||
// - each waiting dapp transaction has its own id, and only a proof that
|
||||
// names that id releases it;
|
||||
// - a proof given for "transaction" in the panel clears the panel's next
|
||||
// send only;
|
||||
// - any other proof clears nothing.
|
||||
const TX_CLEARANCE_MS = 90_000;
|
||||
const DAPP_PIN_WAIT_MS = 120_000;
|
||||
let txClearanceUntil = 0;
|
||||
let pendingPinRequest = null; // { origin, what, at } while a dapp tx waits for the PIN
|
||||
let panelClearanceUntil = 0;
|
||||
const pendingPinRequests = new Map(); // id -> { id, origin, what, at, cleared }
|
||||
function txPinOwed() {
|
||||
try { return !!(ctx && ctx.pinNeeded && ctx.pinNeeded("transaction").needPin); }
|
||||
catch { return true; } // the safe direction for a lock is closed
|
||||
}
|
||||
function takeTxClearance() {
|
||||
if (Date.now() < txClearanceUntil) { txClearanceUntil = 0; return true; }
|
||||
return false;
|
||||
}
|
||||
function requirePanelTxPin() {
|
||||
if (txPinOwed() && !takeTxClearance()) throw new Error("PIN required — confirm your PIN to continue");
|
||||
if (!txPinOwed()) return;
|
||||
if (Date.now() < panelClearanceUntil) { panelClearanceUntil = 0; return; }
|
||||
throw new Error("PIN required — confirm your PIN to continue");
|
||||
}
|
||||
async function requireDappTxPin(origin, what) {
|
||||
if (!txPinOwed() || takeTxClearance()) return;
|
||||
pendingPinRequest = { origin: String(origin || ""), what: String(what || "transaction"), at: Date.now() };
|
||||
if (!txPinOwed()) return;
|
||||
const req = { id: nodeCrypto.randomBytes(8).toString("hex"), origin: String(origin || ""), what: String(what || "transaction"), at: Date.now(), cleared: false };
|
||||
pendingPinRequests.set(req.id, req);
|
||||
try {
|
||||
try { ctx.api.emit("pinRequest", pendingPinRequest); } catch {}
|
||||
try { ctx.api.emit("pinRequest", { id: req.id, origin: req.origin, what: req.what, at: req.at }); } catch {}
|
||||
const deadline = Date.now() + DAPP_PIN_WAIT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, 250));
|
||||
if (!ctx) break;
|
||||
if (takeTxClearance()) return;
|
||||
if (req.cleared) return;
|
||||
}
|
||||
} finally { pendingPinRequest = null; }
|
||||
} finally { pendingPinRequests.delete(req.id); }
|
||||
throw new Error("Aegis asks for your PIN on every transaction. Open the Aegis panel, confirm your PIN there, then try again.");
|
||||
}
|
||||
|
||||
|
|
@ -2717,14 +2727,24 @@ function registerPanelMessages(api) {
|
|||
catch { throw new Error("PIN proof rejected"); }
|
||||
noteMasterVerified(api);
|
||||
api.storage.set("aegis/pin/gate", { lastOkAt: Date.now(), bootId: BOOT_ID });
|
||||
txClearanceUntil = Date.now() + TX_CLEARANCE_MS;
|
||||
// What this proof clears (see requireDappTxPin).
|
||||
const forRequest = String((p && p.requestId) || "");
|
||||
if (forRequest) {
|
||||
const req = pendingPinRequests.get(forRequest);
|
||||
if (!req) throw new Error("that request is no longer waiting");
|
||||
req.cleared = true;
|
||||
} else if (String((p && p.event) || "") === "transaction") {
|
||||
panelClearanceUntil = Date.now() + TX_CLEARANCE_MS;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// A panel opened while a dapp transaction is waiting for the PIN picks the
|
||||
// request up here; one already open gets the "pinRequest" event instead.
|
||||
api.onMessage("pinRequestPending", (_p, m) => {
|
||||
fromPanel(m);
|
||||
return pendingPinRequest ? { ...pendingPinRequest } : null;
|
||||
// The oldest waiting request that is not answered yet.
|
||||
for (const r of pendingPinRequests.values()) if (!r.cleared) return { id: r.id, origin: r.origin, what: r.what, at: r.at };
|
||||
return null;
|
||||
});
|
||||
ctx.pinNeeded = pinNeeded;
|
||||
// Seal a plain blob left by an older build now, not when the panel next
|
||||
|
|
|
|||
|
|
@ -1196,6 +1196,9 @@ function openWalletManageModal(w) {
|
|||
+ `<br><br>The imported key is kept rather than deleted: the sweep still has to confirm, and anyone holding the old address can still pay into it. Remove it yourself once its balance reads zero.`,
|
||||
});
|
||||
if (!ok) return;
|
||||
// The sweep is a spend: with "PIN on every transaction" the host refuses
|
||||
// it without a proof given for a transaction.
|
||||
if (!await pinGate("transaction", "Confirm the sweep with your PIN.")) return;
|
||||
try {
|
||||
const r = await S.invoke("promoteToHd", { walletId: w.id, label: pv.suggestedLabel });
|
||||
close();
|
||||
|
|
@ -5139,19 +5142,29 @@ async function pinGate(event, subtitle) {
|
|||
// does not count, so a failure here is a failed gate.
|
||||
const proof = await verifyPinInteractively(subtitle || PIN_GATE_COPY[st.reason] || "Confirm with your PIN.");
|
||||
if (!proof) return false;
|
||||
try { await S.invoke("pinGateSatisfied", { masterPassword: proof }); }
|
||||
// `event` says what the proof is for: "transaction" clears the panel's
|
||||
// next send, anything else only records the gate.
|
||||
try { await S.invoke("pinGateSatisfied", { masterPassword: proof, event }); }
|
||||
catch { return false; }
|
||||
return true;
|
||||
}
|
||||
// A dapp transaction is waiting on the PIN ("ask on every transaction").
|
||||
// The host cannot draw a PIN pad, so it asks the panel: prove the PIN here
|
||||
// and the pending transaction goes through.
|
||||
// Requests are answered one at a time, each with its own PIN entry, and the
|
||||
// proof names the request it was entered for — so a PIN typed for one site
|
||||
// never releases another site's transaction.
|
||||
let pinRequestBusy = false;
|
||||
async function answerPinRequest(req) {
|
||||
if (!req || pinGateBlocked) return;
|
||||
if (!req || !req.id || pinGateBlocked || pinRequestBusy) return;
|
||||
pinRequestBusy = true;
|
||||
try {
|
||||
const where = String(req.origin || "A site").slice(0, 80);
|
||||
const proof = await verifyPinInteractively(`${where} is waiting — confirm the ${String(req.what || "transaction").slice(0, 60)} with your PIN.`);
|
||||
if (!proof) return;
|
||||
try { await S.invoke("pinGateSatisfied", { masterPassword: proof }); } catch { /* host keeps waiting, then refuses */ }
|
||||
if (proof) { try { await S.invoke("pinGateSatisfied", { masterPassword: proof, requestId: req.id }); } catch { /* host keeps waiting, then refuses */ } }
|
||||
} finally { pinRequestBusy = false; }
|
||||
// Another site may be waiting too.
|
||||
try { const next = await S.invoke("pinRequestPending"); if (next && next.id !== req.id) answerPinRequest(next); } catch {}
|
||||
}
|
||||
const PIN_GATE_COPY = {
|
||||
restart: "Theseus restarted — confirm your PIN to use this wallet.",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue