From 7447d57e963f0212b811167e9304b636a88001df Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 4 Oct 2026 02:24:04 +0200 Subject: [PATCH] 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. --- bundled-addons/aegis/index.js | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/bundled-addons/aegis/index.js b/bundled-addons/aegis/index.js index df8bebea..2eff0291 100644 --- a/bundled-addons/aegis/index.js +++ b/bundled-addons/aegis/index.js @@ -3351,15 +3351,31 @@ function registerPageMessages(api) { const perms = permissions(api); const budget = perms[origin] && perms[origin].sendTx; 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) { // An allowance skips the overlay, not the PIN the user asked for on // every transaction. 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); - budget.usedSats = (budget.usedSats | 0) + d.total; - api.storage.set("permissions", perms); + patchSendTx((t) => (t ? { ...t, usedSats: (t.usedSats | 0) + d.total } : t)); 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 }; } 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"); const cap = flags.find((f) => f.startsWith("cap=")); const capSats = cap ? Number(cap.slice(4)) : 0; - if (BCH_ALLOWANCES.includes(capSats)) { - perms[origin] = { ...(perms[origin] || {}), sendTx: { capSats, usedSats: 0, grantedAt: Date.now() } }; - api.storage.set("permissions", perms); - } else if (budget) { - delete perms[origin].sendTx; - api.storage.set("permissions", perms); - } + if (BCH_ALLOWANCES.includes(capSats)) patchSendTx(() => ({ capSats, usedSats: 0, grantedAt: Date.now() })); + else if (budget) patchSendTx(() => null); emitState(); const r = await rt.adapter.signAndBroadcast(plan); return { txid: r.txid };