Files
takana/recipes/atuq/extensions/boveda/fondo.js
T
sergioandsergio 6f0ba974c7 feat(atuq): la boveda de la suite guarda las contrasenas, y el gestor de Gecko se aparta
La decima extension de atuq. Ofrece la credencial que corresponde al sitio
abierto y la pone en el formulario, pero NO puede sacar una contrasena de la
boveda por su cuenta: pregunta cuales corresponden —y eso contesta titulo y
usuario, nada mas— y la contrasena sale por otro verbo que pregunta en el
escritorio antes de contestar. Si la extension queda comprometida, o si una
pagina consigue hablarle, lo que obtiene es la lista de titulos de los sitios
que coinciden con su propia direccion.

La direccion siempre la pone el chrome y nunca la pagina. Lo unico que el
trozo que corre dentro del sitio aporta es que HAY un formulario y lo que el
usuario tipeo; si pudiera decir de quien es la pagina, podria decir que es el
banco.

Y la extension no decide nada: quien sabe que credencial va en que sitio es
la funcion del otro lado del cable. Ponerlo en JS seria reimplementar el
original y, peor, poner en la pagina la decision de a quien se le entrega una
contrasena.

EL GESTOR DE GECKO SE APAGA, pero como valor de arranque y no como politica.
Dos gestores peleando por el mismo campo es la peor experiencia que puede
tener alguien que solo quiere entrar a su correo: el sitio se rellena dos
veces, o ninguna, y no hay forma de saber cual tiene la buena. De fabrica
manda la boveda; el que prefiera el de Gecko lo prende y listo. Prohibirselo
seria decidir por el, que es lo que este fichero dice en su cabecera que atuq
no quiere ser.

Y un guardian nuevo, que corre en un segundo y sin construir nada. Una
extension esta enchufada en CUATRO lugares distintos y ninguno da error si
falta: el identificador que ella declara, la politica que la instala, el
permiso para hablarle al host, y las preferencias que la acompanan. Si falta
la politica no se instala. Si falta el permiso se instala y se conecta a
nada, en silencio, y la insignia no aparece nunca. Si faltan las preferencias
los dos gestores se pelean. Ninguno de los tres se ve como un error: se ven
como "no anda". El guardian los mira todos y trae su control negativo, que le
saca el permiso del host y exige que lo detecte.

No reemplaza al guardian de metal —servidor, navegador real, login real, el
dialogo a la vista—: lo precede, y ahorra descubrir construyendo que faltaba
un renglon en un JSON.

Los cinco verbos del otro lado ya estan en tawasuyu (bc6903f9e), con trece
tests que leen el cable.
2026-09-15 10:08:20 -04:00

158 lines
5.7 KiB
JavaScript

"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();