Un enllaç secret d'un sol ús és un URL que mostra el contingut exactament una vegada i després queda inactiu per sempre. El navegador de qui l'envia xifra el secret amb AES-GCM-256 i el servidor només desa el text xifrat. La clau viatja a la part de l'enllaç que va després de #k=, que els navegadors no envien mai. Quan el destinatari el mostra per primera vegada, el text es desxifra i la còpia desada es destrueix en la mateixa operació.
Podeu crear-ne un sense compte amb el generador gratuït d'enllaços secrets.
El mecanisme és senzill, però els casos límit són la part interessant, i és allà on les implementacions descuidades tenen fuites sense que ningú se n'adoni.
El flux bàsic
- Qui envia enganxa un secret. El navegador genera una clau aleatòria, xifra el contingut amb AES-GCM-256 mitjançant la Web Crypto API i només envia el text xifrat. El servidor el desa amb un àlies aleatori i difícil d'endevinar i retorna un URL; el navegador hi afegeix la clau com a fragment
#k=, que no surt mai del dispositiu de qui envia. - Qui envia comparteix l'URL per qualsevol canal: correu, xat, SMS.
- El destinatari obre l'URL. Un
GETnormal només retorna l'estat. Quan fa clic a Mostra, el servidor marca el secret com a consumit i retorna el text xifrat en una sola operació que no es pot dividir; el navegador del destinatari el desxifra amb la clau del fragment. - Qualsevol visita posterior rep una pàgina amb l'estat «destruït» en lloc del contingut.
El pas clau és el 3. Si la lectura i l'actualització de l'estat es fan com dues consultes separades (un SELECT per comprovar i després un UPDATE per marcar), dos visitants simultanis poden passar tots dos la comprovació abans que cap dels dos escrigui. Tots dos reben el contingut. És un error TOCTOU clàssic (time-of-check to time-of-use, és a dir, entre el moment de la comprovació i el de l'ús), i converteix la garantia d'«exactament una vegada» en «normalment una vegada».
Per què la destrucció i la lectura han de ser un sol pas?
Perquè, si no, dos visitants simultanis poden passar tots dos la comprovació abans que cap dels dos marqui el secret com a consumit. La solució és fondre la comprovació, la marca i la lectura en una sola operació de base de dades. A Postgres, amb UPDATE ... RETURNING, queda així:
UPDATE secrets
SET burned_at = now()
WHERE slug = $1
AND burned_at IS NULL
AND expires_at > now()
RETURNING payload;
El bloqueig de fila que fa l'UPDATE obliga les crides simultànies a anar una darrere l'altra. La primera compleix la clàusula WHERE, canvia burned_at i rep el contingut a través de RETURNING. Totes les crides posteriors troben burned_at ja definit, no coincideixen amb cap fila i reben l'estat de destruït. No hi ha cap interval entre «comprovar» i «consumir», perquè són la mateixa instrucció.
LinkPilot registra cada visualització en un sol pas indivisible, amb una funció de base de dades, precisament per això: diversos intents simultanis de mostrar el secret no el poden filtrar dues vegades. Si esteu avaluant una eina, és una pregunta legítima per fer directament al proveïdor: la destrucció i la lectura es fan en un sol pas indivisible?
Per què els enllaços destruïts retornen HTTP 410 i no 404?
Perquè 410 Gone indica que el recurs va existir i ja no està disponible de manera permanent, que és exactament el cas d'un secret destruït. Un cop consumit, l'enllaç ha de fallar de manera clara i definitiva. LinkPilot fa dues coses quan destrueix un secret:
- El contingut desat se sobreescriu amb
[REDACTED]en la mateixa escriptura que canvia l'estat. Així, ni tan sols un accés il·lícit posterior a la base de dades no pot recuperar un secret ja consumit. - Els intents posteriors de mostrar-lo retornen HTTP 410 Gone amb una pàgina clara que diu que el secret «ja s'ha vist i s'ha destruït».
410 és el codi d'estat correcte: vol dir que el recurs va existir i que ha deixat d'estar disponible de manera intencionada i permanent, a diferència del 404, que vol dir «no en sé res». La diferència també importa a les persones: un destinatari que veu «aquest secret ja s'ha vist» sap que ho ha de dir a qui l'ha enviat, que pot revisar la cronologia d'auditoria i rotar la credencial si qui l'ha vist no era el destinatari.
Àlies difícils d'endevinar, i per què no n'hi ha prou
L'àlies és l'única cosa que separa l'internet públic d'un text xifrat sense llegir (la clau és a la part de l'enllaç que va després del #, no al servidor), de manera que ha de tenir entropia real. Un àlies generat amb una font aleatòria criptogràficament segura amb, per exemple, 22 caràcters en base62 té aproximadament 128 bits. Provar-los tots no és un atac realista a cap ritme de peticions que un servidor pugui suportar.
Però l'entropia de l'àlies només protegeix contra qui l'endevina. No fa res si algú veu l'URL: l'historial del navegador, els registres d'un servidor intermediari, els correus reenviats, la pantalla compartida, els gestors del porta-retalls. Per això les implementacions serioses hi afegeixen capes:
- Caducitat, perquè un URL que algú ha vist deixi de funcionar quan toca.
- Una frase de contrasenya, perquè l'URL tot sol no sigui suficient.
- Destrucció després de la lectura, perquè l'URL no serveixi de res després de la lectura legítima.
Previsualitzacions d'enllaços i bots
Les aplicacions de xat (Slack, Teams, iMessage) i els analitzadors de seguretat del correu generen previsualitzacions dels URL: els obren amb una petició GET en el moment que arriba el missatge. Si el vostre punt d'accés per mostrar el secret el destrueix amb un GET, el previsualitzador destrueix el secret abans que cap persona el vegi. És la fallada real més habitual de les eines ingènues de destrucció després de la lectura.
Les defenses, tal com les implementa LinkPilot:
- Accions separades per a les metadades i per mostrar el secret. Un
GETal punt d'accés només retorna l'estat i si cal una frase de contrasenya. Una visualització només es registra amb unPOSTexplícit, que és l'acció que fa una persona quan fa clic al botó Mostra. - Frase de contrasenya obligatòria (Pro). Quan hi ha una frase de contrasenya, no es retorna el contingut a ningú, ni a un bot ni a una persona, sense el hash correcte.
Cache-Control: no-storea totes les respostes que mostren el secret, perquè els intermediaris no desin mai el contingut a la memòria cau.
La pàgina d'arquitectura de seguretat de LinkPilot documenta aquest tractament de principi a fi.
El cicle de vida, estat per estat
| Estat | Què el provoca | Què veu qui l'obre | Es pot recuperar el contingut? |
|---|---|---|---|
| Actiu | La creació | Pàgina per mostrar-lo (amb la petició de frase de contrasenya, si n'hi ha) | Sí, es pot mostrar una vegada |
| Destruït | S'arriba al límit de visualitzacions | «Ja s'ha vist i s'ha destruït», HTTP 410 | No, el contingut s'ha sobreescrit |
| Caducat | Passa la data de caducitat | Pàgina amb l'estat de caducat | No, se sobreescriu en el següent accés |
| Revocat | Qui l'ha enviat l'anul·la des del tauler | No es mostra, HTTP 410 | No, de manera immediata i permanent |
Cada canvi d'estat queda a la cronologia d'auditoria, una funció del pla Pro de LinkPilot. Els tipus d'esdeveniment són created, viewed, failed_passphrase, burned, expired i revoked, cadascun amb l'hora i una IP en hash (SHA-256 amb una sal que canvia cada dia; la IP original no es desa mai). Aquest rastre és el que converteix «crec que li ha arribat» en «vist a les 10:05 des del país esperat, una visualització i després destruït».
Extensions habituals
- Frase de contrasenya obligatòria: cal una frase de contrasenya enviada per un altre canal abans de mostrar el secret, de manera que un URL filtrat tot sol no és un secret filtrat. A LinkPilot és una funció del pla Pro (29 $ al mes), igual que la caducitat personalitzada i la cronologia d'auditoria; el generador sense compte no en permet posar cap. Limiteu els intents: LinkPilot en permet 5 per IP i per minut per a cada secret i registra totes les fallades.
- Caducitat per temps: el secret desapareix al cap d'un temps determinat encara que no s'hagi llegit mai. El formulari de LinkPilot amb la sessió iniciada ofereix 1 hora, 24 hores o 7 dies a tots els plans, i una data i hora personalitzades al pla Pro; el generador sense compte posa 7 dies per defecte. Cobreix el cas de l'enllaç oblidat a la safata d'entrada, que la destrucció després de la lectura no resol.
- Revocació manual: qui l'ha enviat pot anul·lar un secret sense llegir des del tauler tan bon punt alguna cosa no quadra. Una assegurança barata per quan «l'he enviat a l'Àlex equivocat».
Casos límit que distingeixen les bones implementacions
L'obre primer qui no toca
La destrucció després de la lectura no impedeix que l'obri qui no toca: fa visible l'error de lliurament. El destinatari previst arriba a una pàgina de secret destruït, la cronologia d'auditoria mostra una visualització que no esperàveu i roteu la credencial. És molest, però limitat: una sola exposició, detectada de pressa. Compareu-ho amb un secret enganxat en un xat, on no ho sabríeu mai.
El problema de les captures de pantalla
D'un sol ús vol dir que es mostra una vegada, no que se'n faci una sola còpia. El destinatari pot fer una captura de pantalla, copiar el secret o tornar-lo a escriure, i l'enllaç no ho impedeix de cap manera. Els proveïdors que donen a entendre el contrari venen ficció. La manera honesta de dir-ho: la destrucció després de la lectura controla el canal de lliurament, no el destinatari. La confiança en el destinatari és un punt de partida, no un resultat.
Precàrrega del navegador i càrrega especulativa
Alguns navegadors i aplicacions carreguen per avançat els URL que és probable que s'obrin. Com que una eina ben feta només destrueix el secret amb un POST explícit, la precàrrega es comporta exactament com un previsualitzador: pot obtenir l'estructura de la pàgina, però mai el contingut.
Quan no s'ha de fer servir un enllaç d'un sol ús
- El valor es tornarà a necessitar. Poder recuperar-lo és tot l'objectiu d'un gestor de contrasenyes i just el contrari del que vol un enllaç d'un sol ús. Feu servir un element de la caixa forta.
- Ho necessiten diversos destinataris. Un enllaç, una lectura. Si l'heu de repartir, o bé creeu un enllaç per persona (correcte i auditable) o bé reconeixeu que el que necessiteu és un control d'accés compartit.
- Qui el consumeix és una màquina. Els serveis i la CI han de llegir d'un gestor de secrets, no fer clic a enllaços.
Si voleu veure el mecanisme en acció, creeu-ne un amb el generador gratuït d'enllaços secrets: sense compte, xifrat al vostre navegador i destruït després de mostrar-lo per primera vegada.
Lectures relacionades
- Compartir contrasenyes de manera segura: 5 mètodes comparats
- One time secret, enllaç d'un sol ús i enllaç secret: les pàgines breus amb l'eina a dalt de tot
- És segur OneTimeSecret?
- Les millors alternatives a OneTimeSecret
- OneTimeSecret vs Privnote vs Password Pusher
- Enllaços que caduquen: el secret es destrueix, no només es redirigeix
- Generador gratuït d'enllaços secrets
Referències
- W3C, Web Cryptography API.
- IETF, RFC 3986, secció 3.5 Fragment.
- IETF, RFC 9110: HTTP Semantics, secció 15.5.11 410 Gone.
- Documentació de PostgreSQL, UPDATE.
