Una bóveda, y el resto del shell
La mitad que no se había movido
Durante casi todo el año el cuaderno ha hablado de tramuntana, porque ahí estaba el trabajo. El shell se quedó en los grupos de pestañas de la v0.2.0, el 2026-06-29, y ahí siguió mientras el motor aprendía a reproducir vídeo y a ceder sus decisiones al embebedor.
Entre entonces y el 2026-10-19 el shell ha recibido 71 commits. Uno solo suma 17 720 líneas: una bóveda de contraseñas con passkeys, códigos de un solo uso y un agente SSH, perfiles temporales, marcadores y disposiciones de varias pestañas a la vez. Esta entrada va de eso, y sobre todo de la bóveda, porque es la parte donde equivocarse sale más caro.
Una bóveda que abre un elemento cada vez
La bóveda es una base de datos inro cifrada en $XDG_DATA_HOME/rumb/vault.inro, que se abre con una contraseña maestra que nunca se guarda. Argon2id estira la contraseña, con la sal y el coste guardados junto a la base de datos en vault.kdf, hasta una clave maestra de 32 bytes.
Con esa clave no se cifra nada. HKDF deriva de ella una clave para las páginas de la base de datos y una clave por cada tipo de elemento, y la clave maestra se borra en cuanto existen. Después cada elemento se sella por separado, con una clave aleatoria que es solo suya y que se guarda envuelta con la clave de su tipo. El id y el tipo del elemento se autentican con ambas, así que un secreto sellado no se abre como si fuera otro elemento.
Lo que sale de ahí es el sentido del diseño. Listar la bóveda lee para qué es cada elemento —sitio, usuario, nombre de la clave— y no descifra ningún secreto. Vault::secret abre un elemento, y no hay ninguna llamada que los abra todos. La clave de un tipo no abre nada de otro tipo. Una bóveda desbloqueada guarda las claves por tipo y ningún secreto en claro, lo que acota lo que expone un fallo o una lectura de la memoria del proceso.
No acota lo que expone la contraseña maestra: quien la tenga abre todos los elementos. El cuadro que la pide lo dibuja la propia interfaz del shell, nunca una página, y lo que se teclea va a búferes de tamaño fijo que se borran al soltarse: un String que crece deja atrás su copia anterior, sin borrar.
Rellenar, y dónde se niega
Un inicio de sesión solo se ofrece en https, o en http hacia esta misma máquina. Se escribe en el primer campo de contraseña visible de la página y en el campo de texto anterior, pero solo si la página sigue en el sitio para el que se hizo la oferta: una navegación que llegue entretanto no recibe contraseña. El script corre solo en el documento de arriba, así que un marco de otro sitio nunca se rellena. Los códigos de un solo uso van igual, al campo donde está el usuario o al que la página marca como el del código.
El cuadro de añadir tiene un generador de contraseñas —longitud, tipos de carácter, sin caracteres parecidos, caracteres excluidos, sorteos sin sesgo con getrandom— y cada contraseña recibe un veredicto sobre lo difícil que es de adivinar. El veredicto es una estimación hecha en el momento, y no pregunta a nadie.
Hasta los iconos dicen algo. La bóveda enseña cada elemento con el icono de su sitio, y esos iconos se guardan dentro de la bóveda y se sirven a su página desde rumb://vault/icon con un token. Una carpeta con ellos sería la lista de sitios en claro.
Passkeys, atadas por el shell
Una passkey es un par de claves ES256 hecho para un sitio, la parte confiante. La clave privada es un elemento de la bóveda de un tipo propio, sellado como los demás; el sitio guarda la clave pública y manda un reto para firmar. No hay nada que teclear ni nada que pueda pedir un sitio que se haga pasar por otro, mientras se cumpla una regla: una passkey solo firma para la parte confiante para la que se hizo.
Toda la protección está en decidir en nombre de qué partes confiantes puede hablar una página, y a partir de qué. El autenticador recibe el origen de la página como argumento, y el shell le da el origen que Servo conoce del documento —el delegado que el motor ganó este otoño—, nunca uno que declare la página.
A partir de ese origen, relying_party resuelve el resto. El esquema tiene que ser https, o http hacia localhost. El host tiene que ser un nombre de dominio, porque una dirección no tiene padre con quien compartir una passkey ni certificado que lo demuestre. El id de parte confiante que pide la página tiene que ser el propio host o uno de sus padres, y registrable según la lista de sufijos públicos que lleva Servo, así que ninguna página puede reclamar co.uk. Una página que falla en cualquiera de estas cosas recibe un no, y solo se entera de eso.
El cuadro que pregunta al usuario nombra el sitio por esa parte confiante y ese origen, nunca por un nombre que haya elegido la página. Decir que sí ahí es la marca de usuario presente; que la bóveda esté abierta, lo que pidió la contraseña maestra, es la de usuario verificado.
Códigos y claves
La bóveda guarda las semillas de los códigos de un solo uso por tiempo, el segundo factor que la mayoría de sitios llama app de autenticación. Acepta lo que entrega el sitio —una semilla en base32 en mayúsculas o minúsculas, con o sin espacios y guiones, o un enlace otpauth://— y genera el código del momento según el RFC 6238.
Y guarda claves SSH. rumb lleva un agente SSH sobre ellas: ssh, git y torii llegan a él por SSH_AUTH_SOCK y piden a rumb que firme. Las claves son ed25519. La mitad privada nunca sale de la bóveda: una firma abre esa única clave, firma y la borra. Con la bóveda bloqueada, el agente no tiene claves y no firma nada.
Del protocolo del agente habla lo que necesita ssh: listar claves y firmar. Todo lo demás se rechaza, entre ello añadir y quitar claves, que se hace en rumb://vault. Como cualquier agente SSH, responde a todos los programas de este usuario que lleguen al socket; el socket está en $XDG_RUNTIME_DIR y solo lo puede leer el usuario, y ese es todo el límite.
Entrar, y volver
Una bóveda que nadie puede llenar es una bóveda que nadie usa, así que los inicios de sesión entran desde una exportación CSV, desde navegadores Chromium —su Login Data, con la clave guardada en el llavero del escritorio— y desde perfiles de Firefox, a través de logins.json y key4.db, en AES-256-CBC o 3DES. Una importación nunca escribe encima de un inicio de sesión que la bóveda ya tenga.
Hay dos cosas que se guardan fuera de la bóveda para el día en que no abra, y se distinguen porque fallan por separado. Una clave de recuperación es la propia clave maestra, escrita en un archivo. Abre la bóveda sin contraseña y sin vault.kdf, así que sirve para una contraseña olvidada o una sal perdida, y es tan secreta como la contraseña. Poner una contraseña nueva vuelve a envolver esa misma clave maestra: nada de la base de datos se cifra dos veces, y la clave de recuperación sigue funcionando.
Una copia de seguridad es vault.kdf y la base de datos tal como están en el disco, en un solo archivo. Es texto cifrado, solo se abre con la contraseña o la clave de recuperación, y solo se restaura donde no hay bóveda. Al contrario que el historial y los marcadores, nada de la bóveda funciona «en lo posible»: una bóveda que no se puede leer es un error que el usuario ve, nunca una bóveda vacía, porque una bóveda vacía invita a guardar encima de la de verdad.
Perfiles, marcadores y disposiciones
Un perfil temporal vive en memoria, sobre las sesiones temporales que ganó tramuntana, y desaparece cuando termina: su nombre, sus ajustes y lo que visitó nunca se escriben en el disco. Se le puede conceder la bóveda para la sesión o hasta una fecha, entera o solo algunas categorías. Dentro de lo concedido puede rellenar, pero no copiar ni enseñar una contraseña, y no puede crear una passkey. En los ajustes del perfil principal caben hasta doce perfiles predefinidos con nombre.
Los marcadores son una sola lista para todo el proceso, en carpetas de un nivel, con una estrella en la barra y una página rumb://bookmarks. El historial guarda una entrada por dirección, con las horas de las visitas anteriores a la última. Y los iconos de la página de inicio se guardan solo para los sitios que hay en ella, porque un icono por cada sitio visitado sería un segundo historial, uno que borrar el historial deja atrás.
Una disposición enseña varias pestañas a la vez, como un árbol de divisiones: cualquier panel se puede volver a dividir, en horizontal o en vertical, en la proporción que el usuario arrastre. Alrededor: iconos de Lucide en lugar de glifos que dependían de las fuentes instaladas, una barra de desplazamiento que dibuja el shell porque Servo no pinta ninguna, y la memoria de cada pestaña al dejar el puntero encima.
Por debajo, y lo que queda
Un script que lee screen.width espera la respuesta del shell con la página parada, y el shell solo respondía una vez por fotograma, así que cada lectura costaba 16,67 ms. Ahora ejecuta el bucle de eventos de Servo cuando Servo lo pide, y un cambio de vídeo en YouTube llega a playing en 1,35 s en lugar de 3,0 s. SpiderMonkey recoge ahora la basura tras 8 MB de malloc en lugar de 38 MB, lo que deja una pestaña reproduciendo vídeo unos 60 MB más pequeña con la misma CPU. Y SIGTERM —cerrar sesión, apagar— cierra las ventanas como el botón de cerrar, así que el motor termina limpio.
RUMB_MULTIPROCESS=1 ejecuta el script y la maquetación de cada página en un proceso hijo, y RUMB_SANDBOX=1 mete esos hijos en un sandbox de Landlock y seccomp. Las dos están desactivadas por defecto hasta que funcione el aislamiento por sitio, y hasta entonces la bóveda comparte proceso con las páginas.
Ese es el límite honesto de todo lo anterior. La bóveda acota lo que expone un fallo, no lo que expone la contraseña maestra, e inro guarda su copia de la clave de páginas hasta que se cierra la base de datos y tampoco la borra entonces. Lo siguiente en el backlog: descargas con cuarentena, un modo de bloqueo, un gestor de RSS, sesiones colaborativas y sincronización entre dispositivos en la red local.