Aegis: a BCH payment re-reads permissions after waiting for the PIN

signAndSend took a permissions snapshot, waited up to two minutes for the
PIN, then wrote the snapshot back. Revoking the site meanwhile still let
the allowance payment go out and restored the revoked allowance, and any
other permission change made during the wait was lost. After the wait it
now re-reads permissions, refuses an allowance payment whose allowance was
revoked, replaced or no longer covers it, and changes only this origin's
sendTx.
This commit is contained in:
Local Dev 2026-10-04 02:24:04 +02:00
parent 4511a933f4
commit 7447d57e96

View file

@ -3351,15 +3351,31 @@ function registerPageMessages(api) {
const perms = permissions(api); const perms = permissions(api);
const budget = perms[origin] && perms[origin].sendTx; const budget = perms[origin] && perms[origin].sendTx;
const remaining = budget ? Math.max(0, (budget.capSats | 0) - (budget.usedSats | 0)) : 0; const remaining = budget ? Math.max(0, (budget.capSats | 0) - (budget.usedSats | 0)) : 0;
// The PIN wait below can last two minutes, during which the user may
// revoke the site or other permissions may change. Everything after it
// re-reads permissions and changes only this origin's sendTx; writing
// back the snapshot taken here used to restore a revoked allowance and
// wipe any other change made meanwhile.
const patchSendTx = (fn) => {
const now = permissions(api);
const cur = { ...(now[origin] || {}) };
const next = fn(cur.sendTx);
if (!next && !now[origin]) return; // revoked meanwhile: nothing to change
if (next) cur.sendTx = next; else delete cur.sendTx;
now[origin] = cur;
api.storage.set("permissions", now);
};
if (budget && d.total <= remaining) { if (budget && d.total <= remaining) {
// An allowance skips the overlay, not the PIN the user asked for on // An allowance skips the overlay, not the PIN the user asked for on
// every transaction. // every transaction.
await requireDappTxPin(origin, "Bitcoin Cash payment"); await requireDappTxPin(origin, "Bitcoin Cash payment");
const fresh = (permissions(api)[origin] || {}).sendTx;
const left = fresh ? Math.max(0, (fresh.capSats | 0) - (fresh.usedSats | 0)) : 0;
if (!fresh || fresh.grantedAt !== budget.grantedAt || d.total > left) throw new Error("this site's allowance changed while waiting for the PIN; ask again");
const r = await rt.adapter.signAndBroadcast(plan); const r = await rt.adapter.signAndBroadcast(plan);
budget.usedSats = (budget.usedSats | 0) + d.total; patchSendTx((t) => (t ? { ...t, usedSats: (t.usedSats | 0) + d.total } : t));
api.storage.set("permissions", perms);
emitState(); emitState();
api.log(`silent send ${d.total} sat for ${origin}, ${remaining - d.total} sat of allowance left`); api.log(`silent send ${d.total} sat for ${origin}, ${left - d.total} sat of allowance left`);
return { txid: r.txid }; return { txid: r.txid };
} }
const rows = d.recipients.map((r, i) => ({ label: d.recipients.length > 1 ? `To #${i + 1}` : "To", value: r.to, mono: true })); const rows = d.recipients.map((r, i) => ({ label: d.recipients.length > 1 ? `To #${i + 1}` : "To", value: r.to, mono: true }));
@ -3384,13 +3400,8 @@ function registerPageMessages(api) {
await requireDappTxPin(origin, "Bitcoin Cash payment"); await requireDappTxPin(origin, "Bitcoin Cash payment");
const cap = flags.find((f) => f.startsWith("cap=")); const cap = flags.find((f) => f.startsWith("cap="));
const capSats = cap ? Number(cap.slice(4)) : 0; const capSats = cap ? Number(cap.slice(4)) : 0;
if (BCH_ALLOWANCES.includes(capSats)) { if (BCH_ALLOWANCES.includes(capSats)) patchSendTx(() => ({ capSats, usedSats: 0, grantedAt: Date.now() }));
perms[origin] = { ...(perms[origin] || {}), sendTx: { capSats, usedSats: 0, grantedAt: Date.now() } }; else if (budget) patchSendTx(() => null);
api.storage.set("permissions", perms);
} else if (budget) {
delete perms[origin].sendTx;
api.storage.set("permissions", perms);
}
emitState(); emitState();
const r = await rt.adapter.signAndBroadcast(plan); const r = await rt.adapter.signAndBroadcast(plan);
return { txid: r.txid }; return { txid: r.txid };