[{"data":1,"prerenderedAt":822},["ShallowReactive",2],{"\u002Fposts\u002Fabr-cipher-por-dentro":3},{"id":4,"title":5,"body":6,"date":807,"description":808,"draft":809,"extension":810,"meta":811,"navigation":247,"path":812,"seo":813,"stem":814,"tags":815,"__hash__":821},"posts\u002Fposts\u002Fabr-cipher-por-dentro.md","ABR Cipher por dentro: criptografía sin servidor, de la URL al disco",{"type":7,"value":8,"toc":793},"minimark",[9,24,32,37,40,73,76,83,87,101,107,127,130,138,142,149,510,525,532,537,540,562,565,569,572,575,605,608,615,625,632,639,643,650,667,674,707,710,714,721,741,744,754,760,764,771,774,780,789],[10,11,12,13,18,19,23],"p",{},"Necesitaba enviar contraseñas sin que pasaran por ningún servidor y destruir el archivo original después de cifrarlo. Lo que encontraba subía los datos a su nube o pedía instalar algo. Así que construí ",[14,15,17],"a",{"href":16},"\u002Fprojects\u002Fcipher","ABR Cipher",", y este artículo cuenta cómo está hecho por dentro. Si buscas qué hace y cómo se usa, eso está en el ",[14,20,22],{"href":21},"\u002Fposts\u002Fabr-cipher-cifrado-en-tu-navegador","artículo para usuarios",".",[10,25,26,27,31],{},"Hay una regla que lo condiciona todo: ",[28,29,30],"strong",{},"ningún dato sensible sale del dispositivo",". No hay backend, ni cuentas, ni telemetría. Es una SPA estática, y si algo no se puede hacer en el cliente, la aplicación lo dice en vez de enviarlo a un servidor.",[33,34,36],"h2",{"id":35},"que-nada-sensible-pase-por-un-servidor","«Que nada sensible pase por un servidor»",[10,38,39],{},"Lo primero era decidir con qué se cifra. Las opciones eran una librería en JavaScript (CryptoJS), una en WASM (libsodium.js) o la Web Crypto API que trae el navegador. Elegí la última, y solo la última: no hay ninguna librería criptográfica de terceros en el proyecto.",[41,42,43,50,56,62],"ul",{},[44,45,46,49],"li",{},[28,47,48],{},"La mantienen los fabricantes de navegadores",", no una sola persona. CryptoJS lleva años sin mantenerse y no es de tiempo constante.",[44,51,52,55],{},[28,53,54],{},"Usa la aceleración por hardware"," del procesador (AES-NI).",[44,57,58,61],{},[28,59,60],{},"No añade nada al bundle"," y está disponible también dentro de los Web Workers.",[44,63,64,67,68,72],{},[28,65,66],{},"No hay riesgo de cadena de suministro."," Cada librería criptográfica en ",[69,70,71],"code",{},"package.json"," es confiar la seguridad de los usuarios en las prácticas de su autor.",[10,74,75],{},"Tiene un coste, y está documentado en su ADR: la API es asíncrona de punta a punta, lo que hace verboso encadenar varias contraseñas, y no ofrece algunos algoritmos modernos como Argon2id o ChaCha20-Poly1305. Por eso las claves se derivan con PBKDF2 (100.000 iteraciones para archivos, 200.000 por defecto para notas) y el cifrado por defecto es AES-256-GCM.",[10,77,78,79,82],{},"Solo hay una excepción, también documentada: para leer certificados X.509 uso ",[69,80,81],{},"@peculiar\u002Fx509",", porque interpretar ese formato no es criptografía. Las operaciones criptográficas de esa librería también se delegan en la Web Crypto API.",[33,84,86],{"id":85},"compartir-un-secreto-sin-un-servidor-que-lo-guarde","«Compartir un secreto sin un servidor que lo guarde»",[10,88,89,90,93,94,97,98,23],{},"Servicios como PrivNote guardan el mensaje cifrado en su servidor y lo borran tras la primera lectura. Yo no tengo servidor que guarde nada ni que borre nada. La solución está en la propia especificación de las URL: el ",[28,91,92],{},"fragmento",", lo que va después de ",[69,95,96],{},"#",", nunca se envía en las peticiones HTTP. No aparece en los logs del servidor, ni en proxies, ni en la cabecera ",[69,99,100],{},"Referer",[10,102,103,104,106],{},"Así que la nota entera viaja ahí: un JSON cifrado y codificado en base64url, pegado detrás del ",[69,105,96],{},". Sobre esa base se combinan capas de protección:",[41,108,109,115,121],{},[44,110,111,114],{},[28,112,113],{},"Una o varias contraseñas",", encadenadas: cada una con su sal y su propia derivación. Hacen falta todas para leerla.",[44,116,117,120],{},[28,118,119],{},"La clave pública del destinatario",", que envuelve la clave de la nota con RSA-OAEP. Se puede combinar con contraseñas.",[44,122,123,126],{},[28,124,125],{},"Una fecha de caducidad",", que se comprueba al abrirla.",[10,128,129],{},"Sin ninguna de esas capas, la clave va dentro del enlace y el propio enlace es el secreto.",[10,131,132,133,137],{},"Lo honesto es contar también los límites, y están escritos en el ADR. La caducidad se comprueba en el cliente, así que la «autodestrucción» es ",[134,135,136],"em",{},"best-effort",": quien tenga el enlace completo antes de que caduque puede leerlo. El enlace también puede quedar en el historial del navegador o en la vista previa de una app de mensajería, y una nota larga produce una URL larga. Es el precio de que no exista ninguna base de datos que filtrar.",[33,139,141],{"id":140},"cifrar-archivos-de-varios-gb","«Cifrar archivos de varios GB»",[10,143,144,145,148],{},"Cargar un archivo de 4 GB en un ",[69,146,147],{},"ArrayBuffer"," falla en muchos navegadores, a veces sin dar ningún error. Para que el tamaño no importe, el cifrado trabaja por trozos de 64 MB y escribe el resultado según va saliendo:",[150,151,156],"pre",{"className":152,"code":153,"language":154,"meta":155,"style":155},"language-ts shiki shiki-themes github-light github-dark","while (offset \u003C fileSize) {\n  const chunk = file.slice(offset, offset + CHUNK_SIZE)\n  let currentData = new Uint8Array(await chunk.arrayBuffer())\n\n  \u002F\u002F Cada contraseña añade una capa: sal + IV + texto cifrado\n  for (const password of validPasswords) {\n    const salt = window.crypto.getRandomValues(new Uint8Array(SALT_LENGTH))\n    const iv = window.crypto.getRandomValues(new Uint8Array(ivLength))\n    const key = await this.deriveKey(password, salt, options.algorithm)\n    const encrypted = await window.crypto.subtle.encrypt(this.getAlgorithmParams(options.algorithm, iv), key, currentData)\n    \u002F\u002F ...currentData = salt + iv + encrypted\n  }\n\n  \u002F\u002F El trozo crece al cifrarse, así que se escribe su tamaño delante\n  const chunkHeader = new Uint8Array(4)\n  new DataView(chunkHeader.buffer).setUint32(0, currentData.length, false)\n  await writer.write(chunkHeader)\n  await writer.write(currentData)\n  offset += CHUNK_SIZE\n}\n","ts","",[69,157,158,177,209,242,249,256,277,309,332,356,387,393,399,404,410,431,465,480,492,504],{"__ignoreMap":155},[159,160,163,167,171,174],"span",{"class":161,"line":162},"line",1,[159,164,166],{"class":165},"szBVR","while",[159,168,170],{"class":169},"sVt8B"," (offset ",[159,172,173],{"class":165},"\u003C",[159,175,176],{"class":169}," fileSize) {\n",[159,178,180,183,187,190,193,197,200,203,206],{"class":161,"line":179},2,[159,181,182],{"class":165},"  const",[159,184,186],{"class":185},"sj4cs"," chunk",[159,188,189],{"class":165}," =",[159,191,192],{"class":169}," file.",[159,194,196],{"class":195},"sScJk","slice",[159,198,199],{"class":169},"(offset, offset ",[159,201,202],{"class":165},"+",[159,204,205],{"class":185}," CHUNK_SIZE",[159,207,208],{"class":169},")\n",[159,210,212,215,218,221,224,227,230,233,236,239],{"class":161,"line":211},3,[159,213,214],{"class":165},"  let",[159,216,217],{"class":169}," currentData ",[159,219,220],{"class":165},"=",[159,222,223],{"class":165}," new",[159,225,226],{"class":195}," Uint8Array",[159,228,229],{"class":169},"(",[159,231,232],{"class":165},"await",[159,234,235],{"class":169}," chunk.",[159,237,238],{"class":195},"arrayBuffer",[159,240,241],{"class":169},"())\n",[159,243,245],{"class":161,"line":244},4,[159,246,248],{"emptyLinePlaceholder":247},true,"\n",[159,250,252],{"class":161,"line":251},5,[159,253,255],{"class":254},"sJ8bj","  \u002F\u002F Cada contraseña añade una capa: sal + IV + texto cifrado\n",[159,257,259,262,265,268,271,274],{"class":161,"line":258},6,[159,260,261],{"class":165},"  for",[159,263,264],{"class":169}," (",[159,266,267],{"class":165},"const",[159,269,270],{"class":185}," password",[159,272,273],{"class":165}," of",[159,275,276],{"class":169}," validPasswords) {\n",[159,278,280,283,286,288,291,294,296,299,301,303,306],{"class":161,"line":279},7,[159,281,282],{"class":165},"    const",[159,284,285],{"class":185}," salt",[159,287,189],{"class":165},[159,289,290],{"class":169}," window.crypto.",[159,292,293],{"class":195},"getRandomValues",[159,295,229],{"class":169},[159,297,298],{"class":165},"new",[159,300,226],{"class":195},[159,302,229],{"class":169},[159,304,305],{"class":185},"SALT_LENGTH",[159,307,308],{"class":169},"))\n",[159,310,312,314,317,319,321,323,325,327,329],{"class":161,"line":311},8,[159,313,282],{"class":165},[159,315,316],{"class":185}," iv",[159,318,189],{"class":165},[159,320,290],{"class":169},[159,322,293],{"class":195},[159,324,229],{"class":169},[159,326,298],{"class":165},[159,328,226],{"class":195},[159,330,331],{"class":169},"(ivLength))\n",[159,333,335,337,340,342,345,348,350,353],{"class":161,"line":334},9,[159,336,282],{"class":165},[159,338,339],{"class":185}," key",[159,341,189],{"class":165},[159,343,344],{"class":165}," await",[159,346,347],{"class":185}," this",[159,349,23],{"class":169},[159,351,352],{"class":195},"deriveKey",[159,354,355],{"class":169},"(password, salt, options.algorithm)\n",[159,357,359,361,364,366,368,371,374,376,379,381,384],{"class":161,"line":358},10,[159,360,282],{"class":165},[159,362,363],{"class":185}," encrypted",[159,365,189],{"class":165},[159,367,344],{"class":165},[159,369,370],{"class":169}," window.crypto.subtle.",[159,372,373],{"class":195},"encrypt",[159,375,229],{"class":169},[159,377,378],{"class":185},"this",[159,380,23],{"class":169},[159,382,383],{"class":195},"getAlgorithmParams",[159,385,386],{"class":169},"(options.algorithm, iv), key, currentData)\n",[159,388,390],{"class":161,"line":389},11,[159,391,392],{"class":254},"    \u002F\u002F ...currentData = salt + iv + encrypted\n",[159,394,396],{"class":161,"line":395},12,[159,397,398],{"class":169},"  }\n",[159,400,402],{"class":161,"line":401},13,[159,403,248],{"emptyLinePlaceholder":247},[159,405,407],{"class":161,"line":406},14,[159,408,409],{"class":254},"  \u002F\u002F El trozo crece al cifrarse, así que se escribe su tamaño delante\n",[159,411,413,415,418,420,422,424,426,429],{"class":161,"line":412},15,[159,414,182],{"class":165},[159,416,417],{"class":185}," chunkHeader",[159,419,189],{"class":165},[159,421,223],{"class":165},[159,423,226],{"class":195},[159,425,229],{"class":169},[159,427,428],{"class":185},"4",[159,430,208],{"class":169},[159,432,434,437,440,443,446,448,451,454,457,460,463],{"class":161,"line":433},16,[159,435,436],{"class":165},"  new",[159,438,439],{"class":195}," DataView",[159,441,442],{"class":169},"(chunkHeader.buffer).",[159,444,445],{"class":195},"setUint32",[159,447,229],{"class":169},[159,449,450],{"class":185},"0",[159,452,453],{"class":169},", currentData.",[159,455,456],{"class":185},"length",[159,458,459],{"class":169},", ",[159,461,462],{"class":185},"false",[159,464,208],{"class":169},[159,466,468,471,474,477],{"class":161,"line":467},17,[159,469,470],{"class":165},"  await",[159,472,473],{"class":169}," writer.",[159,475,476],{"class":195},"write",[159,478,479],{"class":169},"(chunkHeader)\n",[159,481,483,485,487,489],{"class":161,"line":482},18,[159,484,470],{"class":165},[159,486,473],{"class":169},[159,488,476],{"class":195},[159,490,491],{"class":169},"(currentData)\n",[159,493,495,498,501],{"class":161,"line":494},19,[159,496,497],{"class":169},"  offset ",[159,499,500],{"class":165},"+=",[159,502,503],{"class":185}," CHUNK_SIZE\n",[159,505,507],{"class":161,"line":506},20,[159,508,509],{"class":169},"}\n",[10,511,512,513,516,517,520,521,524],{},"El archivo resultante empieza con una ",[28,514,515],{},"cabecera mágica"," que dice cómo se cifró: ",[69,518,519],{},"ABR_CHUNKED_V1"," si es solo con contraseñas, ",[69,522,523],{},"ABR_HYBRID_V1"," si la clave AES va envuelta con la clave pública de alguien. Al descifrar se leen los primeros bytes, se detecta el formato y se descifra trozo a trozo. Los archivos del formato antiguo, sin cabecera, se siguen abriendo por otro camino.",[10,526,527,528,531],{},"Para escribir a disco sin pasar todo por memoria uso la ",[28,529,530],{},"File System Access API",": el usuario elige el destino y el resultado se escribe directamente en el archivo. Solo funciona completa en Chrome y Edge, así que se detecta en tiempo de ejecución y, si no está, la aplicación cifra en memoria, con el límite de tamaño que eso implica.",[533,534,536],"h3",{"id":535},"dos-bugs-que-enseñan-algo","Dos bugs que enseñan algo",[10,538,539],{},"Las operaciones pesadas (cifrar archivos, calcular hashes y la esteganografía) corren en Web Workers para no congelar la interfaz. Todos hablan el mismo protocolo tipado y, si no pueden arrancar, la operación se hace en el hilo principal. Dos bugs salieron justo de ahí:",[41,541,542,556],{},[44,543,544,547,548,551,552,555],{},[28,545,546],{},"Archivos cifrados de 0 bytes."," Al cifrar un único archivo con el worker, nadie cerraba el flujo de escritura. Un ",[69,549,550],{},"FileSystemWritableFileStream"," no guarda nada hasta que se cierra, así que el ",[69,553,554],{},".enc"," quedaba vacío. En el cifrado por lotes no pasaba porque esa ruta sí lo cerraba. Lo corregí con un test que reproduce exactamente ese caso.",[44,557,558,561],{},[28,559,560],{},"Workers que fallaban en silencio."," El composable base solo escuchaba los mensajes del worker. Si el worker no conseguía cargar su módulo, nunca enviaba un mensaje de error: la promesa se quedaba esperando para siempre, el indicador de carga no se apagaba y el respaldo en el hilo principal nunca se ejecutaba. Ahora los errores del worker llegan al mismo manejador que el resto. Al estar en la base, el arreglo cubre los cuatro workers.",[10,563,564],{},"Los dos tienen algo en común: el caso feliz funcionaba y los tests de ese caso pasaban. Lo que fallaba era lo que pasa cuando algo no va como se espera.",[33,566,568],{"id":567},"destruir-el-original-de-verdad","«Destruir el original de verdad»",[10,570,571],{},"El módulo de borrado seguro sobrescribe el archivo varias veces mediante la File System Access API: ceros, unos y, en la última pasada, datos aleatorios. Después le pone un nombre al azar. Contra los programas de recuperación habituales es suficiente.",[10,573,574],{},"Pero el navegador tiene un techo. El sistema operativo puede conservar copias del contenido en sitios a los que una página web no llega:",[41,576,577,584,591,598],{},[44,578,579,580,583],{},"la ",[28,581,582],{},"caché de páginas"," del sistema, por la que pasan los datos antes de llegar al disco;",[44,585,586,587,590],{},"el ",[28,588,589],{},"journal"," del sistema de archivos;",[44,592,593,594,597],{},"los ",[28,595,596],{},"sectores remapeados"," por el firmware de un SSD;",[44,599,600,601,604],{},"las ",[28,602,603],{},"instantáneas"," de volumen de Windows.",[10,606,607],{},"Además, el navegador no puede borrar la entrada del archivo en el directorio. La aplicación muestra este límite en el propio módulo, en vez de prometer más de lo que hace.",[533,609,611,614],{"id":610},"cipher-shred-un-compañero-en-rust",[69,612,613],{},"cipher-shred",": un compañero en Rust",[10,616,617,618,620,621,624],{},"Para llegar más abajo escribí ",[69,619,613],{},", una herramienta nativa en Rust, opcional. Abre el archivo con ",[69,622,623],{},"FILE_FLAG_NO_BUFFERING"," para saltarse la caché del sistema y, después de sobrescribir, borra el archivo de verdad.",[10,626,627,628,631],{},"La aplicación web habla con ella a través de un servidor HTTP mínimo que solo escucha en ",[69,629,630],{},"127.0.0.1",". Cada vez que arranca genera un token de sesión aleatorio, que todas las peticiones deben llevar y que se compara en tiempo constante. El progreso llega por Server-Sent Events, y no se guardan rutas ni logs. Si la herramienta no está instalada, la web funciona exactamente igual.",[10,633,634,635,638],{},"También documenté lo que no consigue. En NTFS, los archivos muy pequeños (por debajo de unos 700–900 bytes) se guardan dentro de su propio registro de la tabla maestra de archivos (",[69,636,637],{},"$MFT","). Sobrescribirlos obliga al sistema a moverlos fuera, y los bytes originales quedan en el espacio sobrante de ese registro, donde una API de archivos normal no llega.",[533,640,642],{"id":641},"recuperar-en-vez-de-destruir","Recuperar en vez de destruir",[10,644,645,646,649],{},"Sobre la misma base añadí el subcomando contrario: ",[69,647,648],{},"recover",", para rescatar archivos borrados por error. Tiene dos motores:",[41,651,652,661],{},[44,653,654,657,658,660],{},[28,655,656],{},"El rápido"," lee la ",[69,659,637],{}," y busca entradas de archivos borrados cuyos clústeres no se hayan reutilizado, comprobándolo contra el mapa de bits del volumen. Si están libres, la confianza es alta; si se han reutilizado en parte, baja.",[44,662,663,666],{},[28,664,665],{},"El forense"," recorre los sectores en bruto buscando firmas de archivo (JPG, PNG, PDF, ZIP) y reconstruye por el marcador de fin o por tamaño.",[10,668,669,670,673],{},"El parser de NTFS es mío: unas 1.600 líneas de Rust sin dependencias externas para leer el sector de arranque, los registros, sus atributos y los ",[134,671,672],{},"data runs",". Leer un disco es leer datos en los que no se puede confiar, así que el parser está escrito a la defensiva:",[41,675,676,686,693,704],{},[44,677,678,679,682,683,685],{},"El volumen se abre ",[28,680,681],{},"siempre en solo lectura",": ningún código de ",[69,684,648],{}," pide permiso de escritura sobre el disco que analiza.",[44,687,688,689,692],{},"El destino de una recuperación tiene que estar en ",[28,690,691],{},"otro volumen",", y eso lo comprueba el binario, no la interfaz. Así no se puede saltar llamando al servidor directamente.",[44,694,695,696,699,700,703],{},"Las correcciones de sector de cada registro (",[134,697,698],{},"fixups",") se ",[28,701,702],{},"validan todas antes de aplicar ninguna",", para no dejar un registro modificado a medias si una falla.",[44,705,706],{},"Toda la aritmética de clústeres y desplazamientos que viene del disco usa operaciones con comprobación de desbordamiento, y hay topes de tamaño para que un registro corrupto no provoque una reserva de memoria enorme.",[10,708,709],{},"Este código pasó varias rondas de revisión, y cada hallazgo (un búfer que podía exponer memoria sin inicializar, un tamaño sacado del atributo equivocado, un sector de arranque que no era NTFS) se corrigió con su test. La herramienta tiene 64 tests en Rust. Por ahora funciona solo en Windows y no está publicada.",[33,711,713],{"id":712},"cómo-está-organizado","Cómo está organizado",[10,715,716,717,720],{},"Con diecinueve módulos, lo que mantiene el proyecto ordenado es una regla de capas que llamo ",[28,718,719],{},"la regla de oro",": un componente nunca importa la lógica directamente.",[41,722,723,729,735,738],{},[44,724,725,728],{},[69,726,727],{},"utils\u002F"," son funciones puras en TypeScript, sin Vue, testeables en Node.",[44,730,731,734],{},[69,732,733],{},"composables\u002F"," tienen el estado reactivo y orquestan; hay uno por módulo.",[44,736,737],{},"Los componentes solo pintan, y cada uno importa exactamente un composable.",[44,739,740],{},"Pinia solo entra cuando dos módulos que no tienen relación necesitan el mismo estado. El vault desbloqueado vive solo en memoria y nunca se guarda en claro.",[10,742,743],{},"Añadir un módulo tiene siempre los mismos pasos: composable, componente, sus tests, las traducciones a los 4 idiomas y un test end-to-end.",[10,745,746,747,264,750,753],{},"Las decisiones importantes están escritas en ",[28,748,749],{},"9 ADR",[134,751,752],{},"Architecture Decision Records","): por qué no hay servidor, por qué solo Web Crypto, por qué la nota va en el fragmento, por qué un worker por módulo... Cada uno cuenta el contexto, la decisión y, sobre todo, sus consecuencias negativas.",[10,755,756,757,759],{},"En tests, unos 670 casos con Vitest y Testing Library repartidos en 73 archivos, más los flujos principales en Playwright, con objetivos de cobertura del 90 % en ",[69,758,727],{}," y del 80 % en los composables. La accesibilidad es un requisito para publicar: navegación por teclado, WAI-ARIA y Lighthouse 100 como objetivo.",[33,761,763],{"id":762},"qué-haría-distinto","Qué haría distinto",[10,765,766,767,770],{},"Ahora mismo, al cifrar un archivo, cada trozo de 64 MB vuelve a derivar su clave con PBKDF2 y sal propia. Es seguro, pero repite un trabajo deliberadamente caro en cada trozo. Hoy derivaría la clave una sola vez por archivo y sacaría una subclave por trozo con HKDF: misma seguridad y bastante menos tiempo con archivos grandes. Al haber cabecera de versión, sería un ",[69,768,769],{},"ABR_CHUNKED_V2"," que conviviría con los archivos antiguos.",[10,772,773],{},"También queda una deuda conocida: el módulo de RSA es anterior a la regla de oro y todavía importa la lógica directamente. Está apuntado como excepción pendiente de refactorizar.",[10,775,776,777,779],{},"Lo siguiente es llevar ",[69,778,613],{}," a Linux y macOS, y publicarlo.",[10,781,782,783,23],{},"Puedes probar ABR Cipher en ",[14,784,788],{"href":785,"rel":786},"https:\u002F\u002Fabdell1999.github.io\u002Fcipher-public\u002F",[787],"nofollow","abdell1999.github.io\u002Fcipher-public",[790,791,792],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":155,"searchDepth":179,"depth":179,"links":794},[795,796,797,800,805,806],{"id":35,"depth":179,"text":36},{"id":85,"depth":179,"text":86},{"id":140,"depth":179,"text":141,"children":798},[799],{"id":535,"depth":211,"text":536},{"id":567,"depth":179,"text":568,"children":801},[802,804],{"id":610,"depth":211,"text":803},"cipher-shred: un compañero en Rust",{"id":641,"depth":211,"text":642},{"id":712,"depth":179,"text":713},{"id":762,"depth":179,"text":763},"2026-10-18","Por qué ABR Cipher solo usa la Web Crypto API, cómo viaja un secreto en el fragmento de una URL, cómo se cifra un archivo de varios GB en el navegador y qué hace falta para destruir un archivo de verdad, con un compañero en Rust que lee NTFS.",false,"md",{},"\u002Fposts\u002Fabr-cipher-por-dentro",{"title":5,"description":808},"posts\u002Fabr-cipher-por-dentro",[816,817,818,819,820],"cipher","seguridad","arquitectura","typescript","rust","eAHLSU8qj_ktLYYRRMsNm_BVTJLQdrJAdRM1nb__xZA",1792430529685]