sergio/takana
> Una distribución Linux construida con un laboratorio funcional y hermético en el > sótano, y una terminal mutable, clásica y anárquica en el piso de arriba — diseñada > desde el suelo para que una IA programadora entienda, modifique y co…
⎇ 84e3737e10e17927380ed3e232e50cebfdd39123
recipes/atuq/extensions/boveda/fondo.js
History
Who touched what
raw
feat(atuq): la boveda de la suite guarda las contrasenas, y el gestor de Gecko se aparta
1"use strict";
2// bóveda — las contraseñas las guarda la suite, no el navegador (SDD-BOVEDA §8), v1.
3//
4// Esta extensión **no sabe nada**. Pregunta y muestra; quien decide qué credencial
5// corresponde a qué página es la función pura de `pacha-boveda-core`, del otro lado
6// del cable. Reimplementar acá la regla de coincidencia sería fabricar un sustituto
7// paralelo del original —lo que la regla 10 de tawasuyu prohíbe— y, peor, sería
8// poner en JS la decisión de a quién se le entrega una contraseña.
9//
10// ── LO QUE ESTA EXTENSIÓN NO PUEDE HACER, Y ES A PROPÓSITO ───────────────────
11// No puede sacar una contraseña de la bóveda. Pide `vault.match`, que contesta
12// títulos y usuarios; la contraseña sale por `vault.fill`, y ese verbo **pregunta
13// en el escritorio** antes de contestar. Si esta extensión queda comprometida, o si
14// una página consigue hablarle, lo que obtiene es la lista de títulos de los sitios
15// que coinciden con su propia dirección.
16//
17// ── LA URL LA PONE EL CHROME, NUNCA LA PÁGINA ────────────────────────────────
18// Todo lo que se le manda al host lleva `tab.url`, que lo dice el navegador. Si la
19// dirección viniera del content script, una página podría decir que es el banco y
20// pedir lo que no es suyo. El content script sólo avisa que **hay** un formulario;
21// nunca dice de quién.
22
23const HOST = "puriy_costura";
24
25let puerto = null;
26let proximoId = 1;
27// id de petición → qué hacer con la respuesta.
28const esperando = new Map();
29
30function di(s) {
31 try { dump("BOVEDA " + s + "\n"); } catch (e) {}
32}
33
34function conectar() {
35 try {
36 puerto = browser.runtime.connectNative(HOST);
37 puerto.onMessage.addListener(alRecibir);
38 puerto.onDisconnect.addListener(p => {
39 di("DESCONECTADO " + (p.error ? p.error.message : "sin error"));
40 puerto = null;
41 // Lo que quedó esperando no va a llegar nunca: se le contesta que no, en
42 // vez de dejar a la página esperando para siempre.
43 for (const [, f] of esperando) { try { f(null); } catch (e) {} }
44 esperando.clear();
45 });
46 di("CONECTADO " + HOST);
47 } catch (e) {
48 di("SIN HOST " + e.message);
49 puerto = null;
50 }
51}
52
53function pedir(mensaje) {
54 return new Promise(resolver => {
55 if (!puerto) { conectar(); }
56 if (!puerto) { resolver(null); return; }
57 const id = proximoId++;
58 esperando.set(id, resolver);
59 try {
60 puerto.postMessage(Object.assign({ id }, mensaje));
61 } catch (e) {
62 esperando.delete(id);
63 di("NO PUDE PEDIR " + e.message);
64 resolver(null);
65 }
66 });
67}
68
69function alRecibir(r) {
70 if (!r || typeof r.id !== "number") { return; }
71 const f = esperando.get(r.id);
72 if (!f) { return; }
73 esperando.delete(r.id);
74 f(r);
75}
76
77// ── La insignia: cuántas credenciales hay para lo que estás mirando ──────────
78
79async function mirar(tabId, url) {
80 if (!url || !/^https?:/.test(url)) {
81 browser.browserAction.setBadgeText({ tabId, text: "" });
82 return 0;
83 }
84 const r = await pedir({ verb: "vault.match", url });
85 if (!r || r.ok !== true) {
86 browser.browserAction.setBadgeText({ tabId, text: "" });
87 return 0;
88 }
89 if (r.locked === true) {
90 // Cerrada no es lo mismo que vacía, y se dice: si no, el usuario cree que
91 // no tiene guardada la contraseña que sí tiene.
92 browser.browserAction.setBadgeText({ tabId, text: "🔒" });
93 return 0;
94 }
95 const n = (r.items || []).length;
96 browser.browserAction.setBadgeText({ tabId, text: n ? String(n) : "" });
97 return n;
98}
99
100browser.tabs.onUpdated.addListener((tabId, cambio, tab) => {
101 if (cambio.status === "complete") { mirar(tabId, tab.url); }
102});
103
104// ── Rellenar: el usuario lo pide, el escritorio lo autoriza ──────────────────
105
106browser.browserAction.onClicked.addListener(async tab => {
107 const r = await pedir({ verb: "vault.match", url: tab.url });
108 if (!r || r.ok !== true || r.locked === true) {
109 di("no hay bóveda abierta");
110 return;
111 }
112 const items = r.items || [];
113 if (items.length === 0) { di("nada para " + tab.url); return; }
114
115 // v1: si hay varias, se usa la primera —que viene de la más específica a la
116 // menos, decidido del otro lado—. Elegir entre varias es una pantalla, y la
117 // pantalla es F2c.
118 const elegida = items[0];
119 const f = await pedir({ verb: "vault.fill", url: tab.url, item: elegida.id });
120 if (!f || f.ok !== true) { di("fill falló"); return; }
121 if (f.denied === true) { di("el usuario dijo que no"); return; }
122 if (!f.password) { di("sin contraseña en la respuesta"); return; }
123
124 browser.tabs.sendMessage(tab.id, {
125 tipo: "rellenar",
126 username: f.username || "",
127 password: f.password,
128 totp: f.totp || null,
129 });
130});
131
132// ── Guardar lo que el usuario acaba de tipear ────────────────────────────────
133
134browser.runtime.onMessage.addListener(async (msg, sender) => {
135 if (!msg || !sender || !sender.tab) { return; }
136 // ⚠ La URL sale de `sender.tab.url` —lo que dice el navegador— y NO del
137 // mensaje. Lo único que la página aporta es lo que el usuario tipeó en ella.
138 const url = sender.tab.url;
139
140 if (msg.tipo === "hay-formulario") {
141 mirar(sender.tab.id, url);
142 return;
143 }
144 if (msg.tipo === "se-envio" && msg.password) {
145 const r = await pedir({
146 verb: "vault.save",
147 url,
148 username: msg.username || "",
149 password: msg.password,
150 title: sender.tab.title || "",
151 });
152 if (r && r.denied === true) { di("no se guardó: el usuario dijo que no"); }
153 else if (r && r.ok === true) { di("guardada para " + url); mirar(sender.tab.id, url); }
154 }
155});
156
157conectar();