Cómo cifra RedaktPDF tus PDFs: un análisis a fondo de nuestra arquitectura E2EE
La mayoría de los editores de PDF online procesan tus archivos en sus servidores. Reciben tu documento en texto en claro, lo manipulan y te lo devuelven. En el momento en que tu archivo sale de tu navegador, queda legible por la infraestructura de esa empresa — sus ingenieros, sus logs y cualquiera que obtenga acceso no autorizado a sus sistemas.
RedaktPDF funciona de forma distinta. Cada PDF que subes como usuario registrado se cifra en tu navegador antes de que un solo byte llegue a nuestros servidores. El servidor almacena texto cifrado que no puede leer. Este post explica exactamente cómo funciona — la jerarquía de claves, las primitivas criptográficas y las decisiones de diseño detrás de cada elección.
Para una visión no técnica, consulta Por qué el cifrado de extremo a extremo importa para la edición de PDFs.
El modelo zero-knowledge
El término "zero-knowledge" está sobrecargado en criptografía, pero en este contexto significa algo específico: el servidor guarda datos que son criptográficamente inútiles sin información que solo el usuario posee.
Esto es lo que el servidor de RedaktPDF sabe sobre tus documentos cifrados:
- La KEK envuelta (cifrada) — inútil sin tu clave maestra
- Las claves de archivo envueltas (cifradas) — inútiles sin la KEK
- Blobs cifrados en S3 — ilegibles sin las claves de archivo
- El salt de PBKDF2 — no es secreto; necesario para la derivación de claves
- Tu dirección de correo y metadatos de la cuenta
Esto es lo que el servidor nunca ve:
- Tu contraseña (solo se almacena un hash bcrypt para la autenticación)
- La clave maestra — derivada en tu navegador, nunca transmitida
- La KEK en texto en claro — desenvuelta solo en tu navegador
- Las claves de archivo en texto en claro — desenvueltas solo en tu navegador
- El contenido del PDF, las imágenes de las páginas y el texto extraído — cifrados antes de la subida
¿Por qué importa esto en la práctica? Considera una brecha en el peor escenario: un atacante exfiltra toda la base de datos y cada objeto de S3. Tiene claves envueltas y blobs cifrados. Sin tu contraseña, no puede derivar la clave maestra. Sin la clave maestra, la KEK envuelta no puede descifrarse. Sin la KEK, las claves por archivo permanecen selladas. La brecha no produce nada accionable.
Esto no es un compromiso contractual — es una propiedad criptográfica de la arquitectura.
La jerarquía de claves
RedaktPDF usa una jerarquía de claves de tres niveles. Cada nivel cumple un propósito distinto.
User Password
|
v PBKDF2(password, salt, 600,000 iterations, SHA-256)
Master Key (MK) ---- stored nowhere (derived on-demand)
|
v AES-GCM wrapKey
Key Encryption Key (KEK) ---- wrapped copy stored in DB
|
v AES-GCM wrapKey (per document)
File Key (FK) ---- wrapped copy stored in DB per document
|
v AES-256-GCM encrypt
Encrypted PDF, page images, text metadata ---- stored in S3
Clave maestra (MK): Derivada de tu contraseña mediante PBKDF2. Nunca se almacena en ninguna parte — ni en el servidor, ni en el navegador más allá de la sesión actual. Cada vez que inicias sesión en una sesión cifrada, la MK se vuelve a derivar de tu contraseña y existe solo en memoria. Si cierras la pestaña, desaparece.
Clave de cifrado de claves (KEK): Una clave AES-GCM de 256 bits generada aleatoriamente. Se genera una vez al activar el cifrado e inmediatamente se envuelve (cifra) con la MK. La forma envuelta se almacena en la base de datos. La forma en texto en claro existe en la memoria del navegador solo durante una sesión activa.
La KEK existe por un motivo operativo: los cambios de contraseña. Cuando cambias tu contraseña, se deriva una nueva MK. Sin una capa de KEK, la clave de archivo de cada documento tendría que volver a cifrarse con la nueva MK. Con la capa de KEK, solo hay que actualizar el envoltorio de la KEK — una única escritura en base de datos sin importar cuántos documentos tengas. Todas las claves de archivo existentes siguen siendo válidas.
Clave de archivo (FK): Una clave AES-GCM de 256 bits generada aleatoriamente, única por documento. Cifra los bytes reales del PDF, las imágenes de página, las miniaturas y el texto extraído. La forma envuelta se almacena en la base de datos junto con los metadatos del documento. Aislar claves por documento significa que un compromiso hipotético de clave afecta exactamente a un documento, no a toda tu biblioteca de archivos.
Derivación de claves: de la contraseña a la clave maestra
La derivación de la clave maestra usa la implementación PBKDF2 de la Web Crypto API. Este es el código de producción de packages/crypto/src/index.ts:
const PBKDF2_ITERATIONS = 600_000;
export async function deriveMasterKey(
password: string,
salt: Uint8Array,
): Promise<CryptoKey> {
const keyMaterial = await globalThis.crypto.subtle.importKey(
'raw',
new TextEncoder().encode(password),
'PBKDF2',
false,
['deriveKey'],
);
return globalThis.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: salt as unknown as ArrayBuffer,
iterations: PBKDF2_ITERATIONS,
hash: 'SHA-256',
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false, // not extractable
['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
);
}
Repasemos el código:
-
importKey('raw', ...)— importa la contraseña codificada en UTF-8 como material de clave en bruto. Esto todavía no es una clave usable; son solo bytes que se introducen en la función de derivación. -
deriveKey(...)— aplica PBKDF2 con SHA-256 y 600.000 iteraciones para producir la clave maestra. -
{ name: 'AES-GCM', length: 256 }— especifica que la salida es una clave AES-GCM de 256 bits. -
false(extraíble) — esto es crítico. La clave maestra es no extraíble. Ni siquiera el código JavaScript que se ejecuta en el mismo contexto del navegador puede leer los bytes de la clave en bruto. Existe dentro del almacén de claves interno de la Web Crypto API y solo puede usarse para operaciones criptográficas, nunca exportarse. Esto impide que un script malicioso lea la clave de la memoria. -
['encrypt', 'decrypt', 'wrapKey', 'unwrapKey']— los usos de la clave. La clave maestra puede envolver y desenvolver la KEK, y puede cifrar/descifrar directamente si es necesario.
¿Por qué 600.000 iteraciones? PBKDF2 es intencionalmente lento — cada iteración adicional hace que los ataques de fuerza bruta sean proporcionalmente más caros. La recomendación de OWASP a 2023 es de 600.000 iteraciones para PBKDF2-SHA256. Esto hace que adivinar una sola contraseña suponga un tiempo de CPU medible. A 600.000 iteraciones, un atacante que intente un ataque de diccionario contra un salt robado debe pagar ese coste de CPU por cada contraseña candidata. Incluso con un clúster de GPU, romper una contraseña fuerte es inviable.
El salt aleatorio de 16 bytes garantiza que contraseñas idénticas produzcan claves maestras distintas, frustrando los ataques con tablas rainbow.
Cifrando un PDF
Una vez disponible una clave de archivo, todo el cifrado de datos usa la misma función. Aquí está el encryptBlob de producción del paquete crypto:
export async function encryptBlob(
data: Uint8Array,
key: CryptoKey,
): Promise<Uint8Array> {
const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
const ciphertext = await globalThis.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
data as unknown as ArrayBuffer,
);
return concat(iv, new Uint8Array(ciphertext));
}
Tres cosas a destacar:
IV aleatorio por cifrado. IV_BYTES es 12 (bytes), la longitud de IV recomendada por NIST para AES-GCM. Se genera un IV fresco de 12 bytes para cada llamada a encryptBlob. Esto significa que cifrar dos veces el mismo PDF produce textos cifrados distintos. Si se reutilizaran IVs con la misma clave, la seguridad de AES-GCM colapsaría — dos textos cifrados bajo la misma clave e IV filtran el XOR de los textos en claro y permiten al atacante falsificar tags de autenticación. Los IVs aleatorios eliminan este riesgo.
Cifrado autenticado. AES-GCM es un cifrado AEAD (Authenticated Encryption with Associated Data). Proporciona tanto confidencialidad (el contenido no puede leerse) como integridad (el contenido no puede modificarse silenciosamente). El tag de autenticación GCM adjuntado al texto cifrado detecta cualquier manipulación. Si un blob cifrado almacenado se modifica en S3, el descifrado lanzará un error en lugar de devolver texto en claro corrupto.
Formato autodescriptivo. El IV se antepone al texto cifrado (concat(iv, new Uint8Array(ciphertext))). La salida es un único array de bytes: [12 bytes IV][bytes restantes: texto cifrado + tag de autenticación de 16 bytes]. El descifrado lee los primeros 12 bytes como IV y el resto como texto cifrado autenticado. No es necesario almacenar el IV por separado.
Esta misma función cifra los bytes del archivo PDF, cada imagen de página renderizada, las miniaturas y el JSON de texto extraído — todo lo que podría exponer el contenido del documento.
Cuando usas el editor de PDF seguro para aplicar ediciones, esas operaciones se ensamblan y exportan completamente en tu navegador. El documento editado nunca se vuelve a subir en forma descifrada.
El flujo de subida
Esta es la secuencia para subir un PDF cifrado:
- Tu navegador lee el archivo PDF como un
Uint8Array. - PDF.js renderiza cada página localmente a 150 DPI (resolución completa) y 72 DPI (miniatura). No se renderiza nada del lado del servidor.
- PDF.js extrae las posiciones del texto localmente para soportar búsqueda y selección.
- El navegador genera una clave de archivo (FK) aleatoria fresca.
- La FK cifra: los bytes del PDF, cada PNG de página renderizada, cada PNG de miniatura y el JSON de posiciones de texto — todo mediante
encryptBlob. - La FK se envuelve con la KEK del usuario usando AES-GCM
wrapKey. - El navegador sube el PDF cifrado a S3 directamente mediante una URL pre-firmada — el servidor emite la URL pero nunca recibe el contenido del archivo.
- El navegador envía los blobs de página cifrados y la FK envuelta a
/api/documents/confirm-encrypted. - El servidor almacena los blobs cifrados en S3 y la FK envuelta en la base de datos. En ningún momento ve el PDF en texto en claro, las imágenes de página o la clave de archivo.
Incluso las operaciones de redacción siguen este modelo: las redacciones se aplican del lado del cliente sobre la representación del PDF en el navegador antes de que el documento se cifre y se suba.
Por qué AES-GCM para envolver claves
Una nota sobre la implementación del wrapping de claves que difiere de la tabla de documentación en docs/SECURITY.md.
La tabla de SECURITY.md lista "AES-KW (Key Wrap)" para la primitiva de wrapping — una referencia al AES Key Wrap del RFC 3394. La implementación real usa wrapKey con AES-GCM. Esta es la función wrapKek:
export async function wrapKek(
kek: CryptoKey,
masterKey: CryptoKey,
): Promise<string> {
const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
const wrapped = await globalThis.crypto.subtle.wrapKey(
'raw',
kek,
masterKey,
{
name: 'AES-GCM',
iv,
},
);
return toBase64(concat(iv, new Uint8Array(wrapped)));
}
La distinción importa: AES-KW (RFC 3394) no usa IV. Proporciona integridad mediante un valor de comprobación de integridad incorporado, pero su mecanismo de autenticación es más estrecho que el de GCM. AES-GCM con un IV aleatorio proporciona AEAD completo — confidencialidad, integridad y autenticación con un tag de autenticación de 128 bits. Cualquier modificación a una clave envuelta es detectable.
Usar AES-GCM en toda la pila (tanto para el cifrado de datos como para el wrapping de claves) tiene otro beneficio práctico: la consistencia. La implementación usa una única primitiva con propiedades bien entendidas y excelente soporte en navegadores. No hay sobrecarga cognitiva de razonar sobre dos modos criptográficos distintos.
La tabla de SECURITY.md es una laguna documental; la implementación real es más robusta. Este post documenta lo que el código hace realmente.
Qué significa esto en la práctica
Atémoslo con un escenario concreto. Supongamos que mañana se exfiltraran la base de datos de RedaktPDF y todos los objetos de S3. El atacante obtiene:
- Tu dirección de correo
- Un hash bcrypt de tu contraseña (factor de coste 12 — no usable directamente)
- El salt de PBKDF2
- La KEK envuelta
- FKs envueltas para cada uno de tus documentos
- Blobs cifrados para cada documento
Para descifrar un único documento, el atacante debe:
- Romper tu contraseña a partir del hash bcrypt (o adivinarla a partir del salt de PBKDF2, que no acelera significativamente la adivinación de contraseñas)
- Usar la contraseña obtenida + el salt para ejecutar PBKDF2 a 600.000 iteraciones y derivar la clave maestra
- Usar la clave maestra para desenvolver la KEK
- Usar la KEK para desenvolver la FK del documento objetivo
- Usar la FK para descifrar los blobs cifrados
Cada paso requiere la finalización exitosa del paso anterior. El coste de PBKDF2 está diseñado para que el paso 2 tarde un tiempo de cómputo significativo por cada intento, haciendo impracticables los ataques de fuerza bruta contra contraseñas fuertes.
Una brecha completa expone metadatos pero no contenido. Esa es la garantía práctica de la arquitectura zero-knowledge. Para una mirada más amplia a las prácticas de privacidad en la edición de PDFs, consulta nuestra guía de privacidad en PDF.
Lecturas adicionales
Las especificaciones de la Web Crypto API, PBKDF2 y AES-GCM detrás de esta implementación:
- Web Crypto API — MDN — referencia de criptografía nativa del navegador
- NIST SP 800-132 — Recomendaciones PBKDF — estándar de derivación de claves
- AES-GCM — NIST SP 800-38D — especificación de cifrado autenticado
¿Listo para probar RedaktPDF?
Edita, redacta y anota PDF directamente en tu navegador — gratis y cifrado.
EmpezarHerramientas relacionadas
Secure Editor
Edit PDFs with AES-256 end-to-end encryption. Encrypted before upload — servers never see plaintext. Passkey and MFA supported. Zero-knowledge architecture.
Redakt PDF
Redact PDFs online free. Cover sensitive text or images with flattened whiteout areas, then export a clean PDF. Private, browser-based, no sign-up.