The portal was a flat page with tabs that asked for the password or PIN on every visit and offered little beyond a records form. Owners need a control panel they can live in. Sign in once: the recovery phrase is kept encrypted under a non-extractable browser key (IndexedDB) so the next visit opens the dashboard silently; sign-out or the Settings toggle destroys it. Payments approve with one click unless "Ask for PIN before payments" is on. Dashboard: left menu (Overview, Domain names, My TLD list, Wallet, Register name/TLD, Settings). Per-name detail with a summary, a DNS record table (A, AAAA, CNAME, MX, TXT, NS, SRV, CAA) that signs the manifest, content & hosting (h, s3, p, ip, tls), a redirect tab (u), ownership transfer (UPD that re-issues the certificate to the recipient) and zone-file/JSON export. Per-TLD detail with policy, on/off, owner registration and the public list of names under it. Overview flags names that point nowhere or lack DNS.
141 lines
5.5 KiB
JavaScript
141 lines
5.5 KiB
JavaScript
// 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,
|
|
};
|
|
})();
|