Una caixa forta, i la resta del shell
La meitat que no s'havia mogut
Durant gairebé tot l'any el quadern ha parlat de tramuntana, perquè era allà on hi havia la feina. El shell es va quedar en els grups de pestanyes de la v0.2.0, el 2026-06-29, i allà va continuar mentre el motor aprenia a reproduir vídeo i a cedir les seves decisions a l'incrustador.
Entre llavors i el 2026-10-19 el shell ha rebut 71 commits. Un de sol suma 17 720 línies: una caixa forta de contrasenyes amb passkeys, codis d'un sol ús i un agent SSH, perfils temporals, adreces d'interès i disposicions de diverses pestanyes alhora. Aquesta entrada va d'això, i sobretot de la caixa forta, perquè és la part on equivocar-se surt més car.
Una caixa forta que obre un element cada vegada
La caixa forta és una base de dades inro xifrada a $XDG_DATA_HOME/rumb/vault.inro, que s'obre amb una contrasenya mestra que mai no es desa. Argon2id estira la contrasenya, amb la sal i el cost desats al costat de la base de dades a vault.kdf, fins a una clau mestra de 32 bytes.
Amb aquesta clau no es xifra res. HKDF en deriva una clau per a les pàgines de la base de dades i una clau per a cada tipus d'element, i la clau mestra s'esborra tan bon punt existeixen. Després cada element se segella per separat, amb una clau aleatòria que és només seva i que es desa embolcallada amb la clau del seu tipus. L'id i el tipus de l'element s'autentiquen amb totes dues, així que un secret segellat no s'obre com si fos un altre element.
El que en surt és el sentit del disseny. Llistar la caixa forta llegeix per a què és cada element —lloc, usuari, nom de la clau— i no desxifra cap secret. Vault::secret obre un element, i no hi ha cap crida que els obri tots. La clau d'un tipus no obre res d'un altre tipus. Una caixa forta desbloquejada guarda les claus per tipus i cap secret en clar, cosa que acota el que exposa un error o una lectura de la memòria del procés.
No acota el que exposa la contrasenya mestra: qui la tingui obre tots els elements. El quadre que la demana el dibuixa la mateixa interfície del shell, mai una pàgina, i el que s'hi tecleja va a memòries intermèdies de mida fixa que s'esborren quan es deixen anar: un String que creix deixa enrere la seva còpia anterior, sense esborrar.
Emplenar, i on s'hi nega
Un inici de sessió només s'ofereix a https, o a http cap a aquesta mateixa màquina. S'escriu al primer camp de contrasenya visible de la pàgina i al camp de text anterior, però només si la pàgina continua al lloc per al qual es va fer l'oferta: una navegació que arribi entremig no rep cap contrasenya. L'script corre només al document de dalt, així que un marc d'un altre lloc mai no s'emplena. Els codis d'un sol ús van igual, al camp on és l'usuari o al que la pàgina marca com el del codi.
El quadre d'afegir té un generador de contrasenyes —longitud, tipus de caràcter, sense caràcters semblants, caràcters exclosos, sorteigs sense biaix amb getrandom— i cada contrasenya rep un veredicte sobre com és de difícil d'endevinar. El veredicte és una estimació feta al moment, i no pregunta a ningú.
Fins i tot les icones diuen alguna cosa. La caixa forta ensenya cada element amb la icona del seu lloc, i aquestes icones es desen dins de la caixa forta i se serveixen a la seva pàgina des de rumb://vault/icon amb un testimoni. Una carpeta amb elles seria la llista de llocs en clar.
Passkeys, lligades pel shell
Una passkey és un parell de claus ES256 fet per a un lloc, la part de confiança. La clau privada és un element de la caixa forta d'un tipus propi, segellat com la resta; el lloc guarda la clau pública i envia un repte per signar. No hi ha res a teclejar ni res que pugui demanar un lloc que se'n faci passar per un altre, mentre es compleixi una regla: una passkey només signa per a la part de confiança per a la qual es va fer.
Tota la protecció és a decidir en nom de quines parts de confiança pot parlar una pàgina, i a partir de què. L'autenticador rep l'origen de la pàgina com a argument, i el shell li dona l'origen que Servo coneix del document —el delegat que el motor va guanyar aquesta tardor—, mai un que declari la pàgina.
A partir d'aquest origen, relying_party resol la resta. L'esquema ha de ser https, o http cap a localhost. L'amfitrió ha de ser un nom de domini, perquè una adreça no té pare amb qui compartir una passkey ni certificat que ho demostri. L'id de part de confiança que demana la pàgina ha de ser el mateix amfitrió o un dels seus pares, i registrable segons la llista de sufixos públics que porta Servo, així que cap pàgina no pot reclamar co.uk. Una pàgina que falla en qualsevol d'aquestes coses rep un no, i només se n'assabenta d'això.
El quadre que pregunta a l'usuari anomena el lloc per aquesta part de confiança i aquest origen, mai per un nom que hagi triat la pàgina. Dir-hi que sí és la marca d'usuari present; que la caixa forta estigui oberta, cosa que va demanar la contrasenya mestra, és la d'usuari verificat.
Codis i claus
La caixa forta guarda les llavors dels codis d'un sol ús per temps, el segon factor que la majoria de llocs anomena app d'autenticació. Accepta el que lliura el lloc —una llavor en base32 en majúscules o minúscules, amb espais i guions o sense, o un enllaç otpauth://— i genera el codi del moment segons l'RFC 6238.
I guarda claus SSH. rumb porta un agent SSH sobre elles: ssh, git i torii hi arriben per SSH_AUTH_SOCK i demanen a rumb que signi. Les claus són ed25519. La meitat privada mai no surt de la caixa forta: una signatura obre aquella única clau, signa i l'esborra. Amb la caixa forta bloquejada, l'agent no té claus i no signa res.
Del protocol de l'agent parla el que necessita ssh: llistar claus i signar. Tota la resta es rebutja, entre altres afegir i treure claus, que es fa a rumb://vault. Com qualsevol agent SSH, respon a tots els programes d'aquest usuari que arribin al sòcol; el sòcol és a $XDG_RUNTIME_DIR i només el pot llegir l'usuari, i aquest és tot el límit.
Entrar, i tornar
Una caixa forta que ningú no pot omplir és una caixa forta que ningú no fa servir, així que els inicis de sessió entren des d'una exportació CSV, des de navegadors Chromium —el seu Login Data, amb la clau desada al clauer de l'escriptori— i des de perfils de Firefox, a través de logins.json i key4.db, en AES-256-CBC o 3DES. Una importació mai no escriu damunt d'un inici de sessió que la caixa forta ja tingui.
Hi ha dues coses que es guarden fora de la caixa forta per al dia que no s'obri, i es distingeixen perquè fallen per separat. Una clau de recuperació és la mateixa clau mestra, escrita en un fitxer. Obre la caixa forta sense contrasenya i sense vault.kdf, així que serveix per a una contrasenya oblidada o una sal perduda, i és tan secreta com la contrasenya. Posar una contrasenya nova torna a embolcallar aquesta mateixa clau mestra: res de la base de dades no es xifra dues vegades, i la clau de recuperació continua funcionant.
Una còpia de seguretat és vault.kdf i la base de dades tal com són al disc, en un sol fitxer. És text xifrat, només s'obre amb la contrasenya o la clau de recuperació, i només es restaura on no hi ha caixa forta. Al contrari que l'historial i les adreces d'interès, res de la caixa forta no funciona «en la mesura del possible»: una caixa forta que no es pot llegir és un error que l'usuari veu, mai una caixa forta buida, perquè una caixa forta buida convida a desar damunt de la de veritat.
Perfils, adreces d'interès i disposicions
Un perfil temporal viu a la memòria, sobre les sessions temporals que va guanyar tramuntana, i desapareix quan s'acaba: el seu nom, els seus ajustos i el que va visitar mai no s'escriuen al disc. Se li pot concedir la caixa forta per a la sessió o fins a una data, sencera o només algunes categories. Dins del que se li ha concedit pot emplenar, però no copiar ni ensenyar una contrasenya, i no pot crear una passkey. Als ajustos del perfil principal hi caben fins a dotze perfils predefinits amb nom.
Les adreces d'interès són una sola llista per a tot el procés, en carpetes d'un nivell, amb una estrella a la barra i una pàgina rumb://bookmarks. L'historial guarda una entrada per adreça, amb les hores de les visites anteriors a l'última. I les icones de la pàgina d'inici es desen només per als llocs que hi ha, perquè una icona per cada lloc visitat seria un segon historial, un que esborrar l'historial deixa enrere.
Una disposició ensenya diverses pestanyes alhora, com un arbre de divisions: qualsevol plafó es pot tornar a dividir, en horitzontal o en vertical, en la proporció que l'usuari arrossegui. Al voltant: icones de Lucide en lloc de glifs que depenien de les fonts instal·lades, una barra de desplaçament que dibuixa el shell perquè Servo no en pinta cap, i la memòria de cada pestanya quan s'hi deixa el punter a sobre.
Per sota, i el que queda
Un script que llegeix screen.width espera la resposta del shell amb la pàgina aturada, i el shell només responia un cop per fotograma, així que cada lectura costava 16,67 ms. Ara executa el bucle d'esdeveniments de Servo quan Servo ho demana, i un canvi de vídeo a YouTube arriba a playing en 1,35 s en lloc de 3,0 s. SpiderMonkey recull ara les escombraries després de 8 MB de malloc en lloc de 38 MB, cosa que deixa una pestanya reproduint vídeo uns 60 MB més petita amb la mateixa CPU. I SIGTERM —tancar la sessió, apagar— tanca les finestres com el botó de tancar, així que el motor s'acaba net.
RUMB_MULTIPROCESS=1 executa l'script i la maquetació de cada pàgina en un procés fill, i RUMB_SANDBOX=1 posa aquests fills en un sandbox de Landlock i seccomp. Totes dues estan desactivades per defecte fins que funcioni l'aïllament per lloc, i fins llavors la caixa forta comparteix procés amb les pàgines.
Aquest és el límit honest de tot l'anterior. La caixa forta acota el que exposa un error, no el que exposa la contrasenya mestra, i inro guarda la seva còpia de la clau de pàgines fins que es tanca la base de dades i tampoc no l'esborra llavors. El següent al backlog: baixades amb quarantena, un mode de bloqueig, un gestor d'RSS, sessions col·laboratives i sincronització entre dispositius a la xarxa local.