"use strict"; // bóveda — las contraseñas las guarda la suite, no el navegador (SDD-BOVEDA §8), v1. // // Esta extensión **no sabe nada**. Pregunta y muestra; quien decide qué credencial // corresponde a qué página es la función pura de `pacha-boveda-core`, del otro lado // del cable. Reimplementar acá la regla de coincidencia sería fabricar un sustituto // paralelo del original —lo que la regla 10 de tawasuyu prohíbe— y, peor, sería // poner en JS la decisión de a quién se le entrega una contraseña. // // ── LO QUE ESTA EXTENSIÓN NO PUEDE HACER, Y ES A PROPÓSITO ─────────────────── // No puede sacar una contraseña de la bóveda. Pide `vault.match`, que contesta // títulos y usuarios; la contraseña sale por `vault.fill`, y ese verbo **pregunta // en el escritorio** antes de contestar. Si esta extensión queda comprometida, o si // una página consigue hablarle, lo que obtiene es la lista de títulos de los sitios // que coinciden con su propia dirección. // // ── LA URL LA PONE EL CHROME, NUNCA LA PÁGINA ──────────────────────────────── // Todo lo que se le manda al host lleva `tab.url`, que lo dice el navegador. Si la // dirección viniera del content script, una página podría decir que es el banco y // pedir lo que no es suyo. El content script sólo avisa que **hay** un formulario; // nunca dice de quién. const HOST = "puriy_costura"; let puerto = null; let proximoId = 1; // id de petición → qué hacer con la respuesta. const esperando = new Map(); function di(s) { try { dump("BOVEDA " + s + "\n"); } catch (e) {} } function conectar() { try { puerto = browser.runtime.connectNative(HOST); puerto.onMessage.addListener(alRecibir); puerto.onDisconnect.addListener(p => { di("DESCONECTADO " + (p.error ? p.error.message : "sin error")); puerto = null; // Lo que quedó esperando no va a llegar nunca: se le contesta que no, en // vez de dejar a la página esperando para siempre. for (const [, f] of esperando) { try { f(null); } catch (e) {} } esperando.clear(); }); di("CONECTADO " + HOST); } catch (e) { di("SIN HOST " + e.message); puerto = null; } } function pedir(mensaje) { return new Promise(resolver => { if (!puerto) { conectar(); } if (!puerto) { resolver(null); return; } const id = proximoId++; esperando.set(id, resolver); try { puerto.postMessage(Object.assign({ id }, mensaje)); } catch (e) { esperando.delete(id); di("NO PUDE PEDIR " + e.message); resolver(null); } }); } function alRecibir(r) { if (!r || typeof r.id !== "number") { return; } const f = esperando.get(r.id); if (!f) { return; } esperando.delete(r.id); f(r); } // ── La insignia: cuántas credenciales hay para lo que estás mirando ────────── async function mirar(tabId, url) { if (!url || !/^https?:/.test(url)) { browser.browserAction.setBadgeText({ tabId, text: "" }); return 0; } const r = await pedir({ verb: "vault.match", url }); if (!r || r.ok !== true) { browser.browserAction.setBadgeText({ tabId, text: "" }); return 0; } if (r.locked === true) { // Cerrada no es lo mismo que vacía, y se dice: si no, el usuario cree que // no tiene guardada la contraseña que sí tiene. browser.browserAction.setBadgeText({ tabId, text: "🔒" }); return 0; } const n = (r.items || []).length; browser.browserAction.setBadgeText({ tabId, text: n ? String(n) : "" }); return n; } browser.tabs.onUpdated.addListener((tabId, cambio, tab) => { if (cambio.status === "complete") { mirar(tabId, tab.url); } }); // ── Rellenar: el usuario lo pide, el escritorio lo autoriza ────────────────── browser.browserAction.onClicked.addListener(async tab => { const r = await pedir({ verb: "vault.match", url: tab.url }); if (!r || r.ok !== true || r.locked === true) { di("no hay bóveda abierta"); return; } const items = r.items || []; if (items.length === 0) { di("nada para " + tab.url); return; } // v1: si hay varias, se usa la primera —que viene de la más específica a la // menos, decidido del otro lado—. Elegir entre varias es una pantalla, y la // pantalla es F2c. const elegida = items[0]; const f = await pedir({ verb: "vault.fill", url: tab.url, item: elegida.id }); if (!f || f.ok !== true) { di("fill falló"); return; } if (f.denied === true) { di("el usuario dijo que no"); return; } if (!f.password) { di("sin contraseña en la respuesta"); return; } browser.tabs.sendMessage(tab.id, { tipo: "rellenar", username: f.username || "", password: f.password, totp: f.totp || null, }); }); // ── Guardar lo que el usuario acaba de tipear ──────────────────────────────── browser.runtime.onMessage.addListener(async (msg, sender) => { if (!msg || !sender || !sender.tab) { return; } // ⚠ La URL sale de `sender.tab.url` —lo que dice el navegador— y NO del // mensaje. Lo único que la página aporta es lo que el usuario tipeó en ella. const url = sender.tab.url; if (msg.tipo === "hay-formulario") { mirar(sender.tab.id, url); return; } if (msg.tipo === "se-envio" && msg.password) { const r = await pedir({ verb: "vault.save", url, username: msg.username || "", password: msg.password, title: sender.tab.title || "", }); if (r && r.denied === true) { di("no se guardó: el usuario dijo que no"); } else if (r && r.ok === true) { di("guardada para " + url); mirar(sender.tab.id, url); } } }); conectar();