diff --git a/bundled-addons/aegis/addon.json b/bundled-addons/aegis/addon.json index 222e9354..4a2306f0 100644 --- a/bundled-addons/aegis/addon.json +++ b/bundled-addons/aegis/addon.json @@ -1,7 +1,7 @@ { "id": "aegis", "name": "Aegis Wallet", - "version": "0.13.1", + "version": "0.13.2", "category": "plugin", "description": "Multi-chain wallet (BCH, BTC, TRX, ETH, SOL, SC, DGB) derived from your Theseus vault. Dapps get window.bitcoincash on .x sites; window.tronWeb / window.tronLink / window.ethereum / window.solana on any https page.", "author": "Silent Mode", diff --git a/bundled-addons/aegis/panel.js b/bundled-addons/aegis/panel.js index bc30eecc..09574d77 100644 --- a/bundled-addons/aegis/panel.js +++ b/bundled-addons/aegis/panel.js @@ -1483,18 +1483,42 @@ const IMPORT_COIN_CONFIG = { // phish; the user still reads what landed in the box and presses Import, and // the real validation happens in the host handler either way. const BIP39_LENGTHS = new Set([12, 15, 18, 21, 24]); +// A BIP39 English phrase is only lowercase a-z words, 3-8 letters each, in +// one of the defined lengths. Word membership is not checked here — the host +// handler does that when it derives; this only decides which box to fill. +function mnemonicIn(str) { + const words = String(str || "").toLowerCase().split(/\s+/).filter(Boolean); + if (!BIP39_LENGTHS.has(words.length)) return null; + if (!words.every((w) => /^[a-z]{3,8}$/.test(w))) return null; + return { value: words.join(" "), words: words.length }; +} function classifyScannedSecret(text) { const s = String(text || "").trim(); if (!s) return { kind: "empty" }; - // BIP39: only lowercase a-z words, in one of the defined lengths. - const words = s.toLowerCase().split(/\s+/).filter(Boolean); - if (BIP39_LENGTHS.has(words.length) && words.every((w) => /^[a-z]{3,8}$/.test(w))) { - return { kind: "mnemonic", value: words.join(" "), words: words.length }; - } + + const bare = mnemonicIn(s); + if (bare) return { kind: "mnemonic", value: bare.value, words: bare.words }; + // WIF: base58, 51-52 chars. 5/K/L = BTC-family mainnet, 9/c = testnet. if (/^[5KL9c][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(s)) return { kind: "wif", value: s }; // Raw 32-byte hex, with or without 0x. if (/^(0x)?[0-9a-fA-F]{64}$/.test(s)) return { kind: "hex", value: s }; + + // Wrapped phrases. Several wallets export the seed inside an envelope — + // a version marker, the words, sometimes a derivation path, pipe- or + // comma-separated ("1||m/44'/145'/0'"). Split on every run of + // non-letters (keeping spaces, which separate the words) and look for a + // BIP39-shaped run in any piece. This is tolerant of the wrapper without + // being loose about what counts as a phrase: a URL or an address still + // breaks into single-word pieces and matches nothing. + for (const piece of s.split(/[^A-Za-z ]+/)) { + const hit = mnemonicIn(piece); + if (!hit) continue; + // A path anywhere in the payload is worth carrying over — pulled from + // the ORIGINAL string, since splitting on non-letters shreds it. + const path = (s.match(/m(?:\/\d+'?)+/) || [])[0] || null; + return { kind: "mnemonic", value: hit.value, words: hit.words, path, wrapped: true }; + } return { kind: "unknown", value: s }; } @@ -1545,7 +1569,25 @@ function wireQrImport(overlay) { if (found.kind === "mnemonic") { const ta = overlay.querySelector("#imMnemonic"); if (ta) { ta.value = found.value; ta.dispatchEvent(new Event("input", { bubbles: true })); } - say(`Read a ${found.words}-word phrase. Check it, pick the coin and network, then press Import.`, false); + // A derivation path in the QR is worth surfacing, but it does not get + // to silently replace a path already in the box — that box is + // prefilled with this coin's default and may have been edited, and + // quietly changing which addresses get derived is the kind of thing + // that looks like lost funds. Fill it only when empty; otherwise say + // what the QR carried and let the user decide. + let pathNote = ""; + if (found.path) { + const pf = overlay.querySelector("#imPath"); + if (pf && !pf.value.trim()) { + pf.value = found.path; + pf.dispatchEvent(new Event("input", { bubbles: true })); + pathNote = ` Its derivation path ${found.path} went into the path box.`; + } else if (pf && pf.value.trim() !== found.path) { + pathNote = ` It also carried the path ${found.path} — the box says ${pf.value.trim()}, change it if that is wrong.`; + } + } + const unwrapped = found.wrapped ? " (unwrapped from the QR's own format)" : ""; + say(`Read a ${found.words}-word phrase${unwrapped}.${pathNote} Check it, pick the coin and network, then press Import.`, false); } else if (found.kind === "wif" || found.kind === "hex") { const raw = overlay.querySelector("#imRaw"); if (raw) { raw.value = found.value; raw.dispatchEvent(new Event("input", { bubbles: true })); } @@ -1553,7 +1595,7 @@ function wireQrImport(overlay) { } else { // Don't paste unrecognised payloads into a key field — show a short // preview so the user can see it was, say, a URL, and stop there. - const peek = found.value.length > 48 ? found.value.slice(0, 48) + "…" : found.value; + const peek = found.value.length > 90 ? found.value.slice(0, 90) + "…" : found.value; say(`That QR decoded to something that is not a seed phrase or key: "${peek}"`, true); } } catch (err) {