// Sirius.X persistent sign-in ("stay signed in on this device"). // // Problem: every visit asked for the seed phrase, the password or the PIN // again. A dashboard people come back to daily cannot work like that. // // Model: after one successful sign-in the wallet's recovery phrase is // encrypted with a random AES-GCM key that lives ONLY inside IndexedDB as a // non-extractable CryptoKey (the browser can use it, scripts cannot read // its bytes). The ciphertext sits in localStorage. On the next visit the // dashboard decrypts silently and the user is in — no prompt. Signing out, // or turning "stay signed in" off in Settings, destroys both halves. // // Trust boundary, stated honestly: this is a device session, like a // "remember me" cookie. Anyone who can run scripts in this browser profile // can use the wallet while the session exists. The password-encrypted // wallet blob (bns.wallet.v1) and the optional PIN escrow are unchanged and // still the durable secrets. Users who want a prompt before every payment // can keep that in Settings ("Ask for PIN before payments"). // // window.siriusSession = { isEnabled, setEnabled, remember, restore, forget, // requirePinForPayments, setRequirePinForPayments } (() => { const DB = "sirius-session"; const STORE = "keys"; const KEY_ID = "device"; const BLOB = "siriusSessionBlob"; const ENABLED = "siriusStaySignedIn"; // "0" = off; anything else = on (default on) const REQUIRE_PIN = "siriusRequirePin"; // "1" = ask PIN before payments (default off) const enc = new TextEncoder(); const dec = new TextDecoder(); const b64 = (bytes) => btoa(String.fromCharCode(...new Uint8Array(bytes))); const unb64 = (str) => Uint8Array.from(atob(str), (c) => c.charCodeAt(0)); function openDb() { return new Promise((resolve, reject) => { if (!("indexedDB" in window)) return reject(new Error("IndexedDB unavailable")); const req = indexedDB.open(DB, 1); req.onupgradeneeded = () => { req.result.createObjectStore(STORE); }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error || new Error("IndexedDB open failed")); }); } function idbGet(db, key) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readonly"); const req = tx.objectStore(STORE).get(key); req.onsuccess = () => resolve(req.result || null); req.onerror = () => reject(req.error); }); } function idbPut(db, key, value) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readwrite"); tx.objectStore(STORE).put(value, key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } function idbDel(db, key) { return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readwrite"); tx.objectStore(STORE).delete(key); tx.oncomplete = () => resolve(); tx.onerror = () => reject(tx.error); }); } async function deviceKey(create) { const db = await openDb(); try { let key = await idbGet(db, KEY_ID); if (!key && create) { key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]); await idbPut(db, KEY_ID, key); } return key; } finally { db.close(); } } function isEnabled() { try { return localStorage.getItem(ENABLED) !== "0"; } catch { return true; } } function setEnabled(on) { try { localStorage.setItem(ENABLED, on ? "1" : "0"); } catch {} if (!on) forget(); } function requirePinForPayments() { try { return localStorage.getItem(REQUIRE_PIN) === "1"; } catch { return false; } } function setRequirePinForPayments(on) { try { localStorage.setItem(REQUIRE_PIN, on ? "1" : "0"); } catch {} } function hasSession() { try { return !!localStorage.getItem(BLOB); } catch { return false; } } // Encrypt and store the recovery phrase for silent restore. `meta` may // carry { accountPath } for wallets derived on a non-default path. async function remember(mnemonic, meta = {}) { if (!isEnabled() || !mnemonic) return false; try { const key = await deviceKey(true); const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, enc.encode(mnemonic)); localStorage.setItem(BLOB, JSON.stringify({ v: 1, iv: b64(iv), ct: b64(ct), at: Date.now(), path: meta.accountPath || null })); return true; } catch { return false; } } // Silent restore. Resolves { mnemonic, accountPath } or null. Never throws. async function restore() { if (!isEnabled()) return null; let raw = null; try { raw = localStorage.getItem(BLOB); } catch {} if (!raw) return null; try { const blob = JSON.parse(raw); const key = await deviceKey(false); if (!key) { forget(); return null; } const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv: unb64(blob.iv) }, key, unb64(blob.ct)); return { mnemonic: dec.decode(pt), accountPath: blob.path || null }; } catch { // Key rotated / storage cleared / tampered — drop the stale blob. forget(); return null; } } async function forget() { try { localStorage.removeItem(BLOB); } catch {} try { const db = await openDb(); try { await idbDel(db, KEY_ID); } finally { db.close(); } } catch {} } window.siriusSession = { isEnabled, setEnabled, hasSession, remember, restore, forget, requirePinForPayments, setRequirePinForPayments, }; })();