Themis vs Web Crypto API: probé ambas para cifrado en mi app de identidad digital y los tradeoffs no son obvios
Cometí un error que me costó dos semanas de rediseño: asumí que Web Crypto API era suficiente para todo. Lo asumí sin probarlo contra mis casos reales, sin medir, sin cuestionar. Lo asumí porque "es nativo del browser" y eso suena a garantía. No lo cuento para hacer catarsis, lo cuento porque es exactamente el tipo de error silencioso que destruye un sprint de seguridad sin que nadie lo note hasta que ya es tarde.
El contexto importa: estoy construyendo Lakaut ID, un sistema de validación de identidad biométrica. No es una app de notas. No es un SaaS de formularios. Es una Autoridad de Certificación digital argentina donde la criptografía no es una feature más —es la razón por la que el producto existe. Cuando elegís mal tu primitiva criptográfica acá, no perdés uptime: perdés la cadena de confianza completa.
Eso me forzó a hacer algo que debería haber hecho desde el principio: comparar Themis (de Cossack Labs) contra Web Crypto API en casos concretos, con código real, con métricas reales, sin dejarme seducir por el marketing de ninguno de los dos.
Cifrado TypeScript en aplicación web de producción: el contexto que cambia todo
Hay una trampa en cómo se discute criptografía en el ecosistema JavaScript: la mayoría de los posts hablan de "cifrar datos" como si fuera un problema homogéneo. No lo es. En Lakaut ID tengo tres casos distintos con requerimientos que no se superponen:
- Datos biométricos en reposo — templates faciales, hashes de documentos. Necesito AES-GCM con claves derivadas por usuario, sin que el servidor pueda leer el plaintext.
- Mensajería segura entre componentes — el frontend de captura biométrica hablando con el backend de validación en Railway. Necesito algo más parecido a un canal seguro que a cifrado de archivo.
- Generación y manejo de claves — derivación de claves desde passphrase del usuario, rotación, exportación segura para backup.
Web Crypto API cubre los tres en papel. Themis también. El problema está en los detalles.
Web Crypto API: qué funciona bien y dónde me clavé
La API es poderosa y bien diseñada. Vivir en el browser como API nativa tiene ventajas reales: cero dependencias, sin bundle penalty, y las operaciones se delegan a la implementación del runtime (en Node.js 18+ es el mismo engine que el browser).
Para datos biométricos en reposo, la implementación en TypeScript quedó así:
// cifrado-biometrico.ts — Lakaut ID
// Cifrado AES-GCM de templates faciales antes de persistir en Railway
async function cifrarTemplateBiometrico(
templateBuffer: ArrayBuffer,
claveUsuario: CryptoKey
): Promise<{ cifrado: ArrayBuffer; iv: Uint8Array }> {
// IV aleatorio de 12 bytes — recomendado para AES-GCM
const iv = crypto.getRandomValues(new Uint8Array(12));
const cifrado = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
// tagLength por defecto: 128 bits — no lo cambies sin saber qué hacés
tagLength: 128,
},
claveUsuario,
templateBuffer
);
return { cifrado, iv };
}
async function derivarClaveDesdePIN(
pin: string,
sal: Uint8Array
): Promise<CryptoKey> {
// Importamos el PIN como material de clave base
const materialBase = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(pin),
{ name: "PBKDF2" },
false, // no exportable — intencional
["deriveKey"]
);
// PBKDF2 con 310.000 iteraciones — recomendación OWASP 2023
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: sal,
iterations: 310_000,
hash: "SHA-256",
},
materialBase,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
Esto funcionó. Funciona bien. Las 310.000 iteraciones de PBKDF2 siguen la recomendación actualizada de OWASP y el cifrado AES-GCM 256 es sólido.
El problema silencioso que casi no detecto
Web Crypto API tiene un comportamiento que me costó caro: falla silenciosamente en contextos no-HTTPS.
Durante desarrollo local estaba probando el flujo de captura biométrica en una VM de red interna, sin HTTPS. crypto.subtle estaba disponible en el objeto global, pero todas las llamadas retornaban undefined sin lanzar excepciones. No había error en consola. No había rechazo de promesa. Simplemente: silencio.
El spec dice que crypto.subtle solo está disponible en secure contexts. Pero la forma en que algunos browsers manejan esto —especialmente en redes internas y Chrome con flags— es inconsistente. Me enteré cuando un QA interno reportó que "el cifrado no funcionaba" y yo no podía reproducirlo desde mi máquina local con localhost (que sí es un secure context por spec).
El fix fue agregar un guard explícito:
// guard-crypto.ts — validación de contexto seguro antes de operar
function validarContextoSeguro(): void {
if (!window.isSecureContext) {
// No lanzamos error genérico — queremos saber exactamente qué pasó
throw new Error(
`[Lakaut ID] Operación criptográfica bloqueada: contexto no seguro. ` +
`Protocolo actual: ${window.location.protocol}. ` +
`Se requiere HTTPS o localhost.`
);
}
if (!crypto.subtle) {
throw new Error(
`[Lakaut ID] crypto.subtle no disponible en este entorno. ` +
`Verificá la versión del browser y el contexto de seguridad.`
);
}
}
Esto debería ser obligatorio en cualquier app que use Web Crypto API. La falla silenciosa es el peor tipo de bug en criptografía.
Themis: dónde gana y por qué lo incorporé igual
Themis de Cossack Labs es una librería de criptografía de alto nivel. No te expone primitivas: te expone casos de uso. No elegís AES-GCM ni RSA-OAEP. Elegís "SecureCell" (datos en reposo) o "SecureMessage" (mensajería asimétrica) o "SecureSession" (canal forward-secret). La librería toma las decisiones criptográficas por vos.
Eso es exactamente su propuesta: reducir la superficie de error del desarrollador.
Instalación en el proyecto:
# Themis para Node.js — binding JS del core en C/C++
npm install jsthemis
# Para el frontend (WASM build)
npm install wasm-themis
El bundle penalty es real
Acá no voy a mentirte: incorporar Themis en Next.js tiene un costo. El bundle de wasm-themis agrega aproximadamente 1.2 MB al lado del cliente (antes de compresión con gzip, que lo baja a ~400 KB). Es significativo.
Mi decisión en Lakaut ID fue no usar Themis en el frontend y usar Web Crypto API para el cifrado del lado del cliente. Themis vive en el backend de Node.js donde el peso del bundle no importa.
// backend/cifrado-canal.ts — Lakaut ID, solo en Node.js
// SecureMessage de Themis para comunicación entre servicios
import { SecureMessage } from "jsthemis";
// Cada componente tiene su par de claves — generado en setup
const mensajero = new SecureMessage(
clavePrivadaBackend,
clavePublicaFrontend
);
function cifrarRespuestaValidacion(payload: ValidacionResult): Buffer {
const serializado = Buffer.from(JSON.stringify(payload));
// Themis elige el cifrado internamente — ECDH + AES-GCM bajo el capó
return mensajero.wrap(serializado);
}
function descifrarSolicitudCaptura(mensaje: Buffer): SolicitudCaptura {
const decifrado = mensajero.unwrap(mensaje);
return JSON.parse(decifrado.toString());
}
La ventaja que no esperaba: portabilidad entre plataformas. Themis tiene bindings para iOS (Swift/ObjC), Android (Kotlin/Java), Python y Go. Si en algún momento Lakaut ID agrega una app mobile nativa —algo que está en el roadmap— el protocolo de mensajería segura entre mobile y backend va a funcionar sin reescribir nada. Web Crypto API en el frontend no me daría esa garantía de interoperabilidad.
Forward secrecy: el gap que Web Crypto no cierra fácil
Para el canal de mensajería entre el servicio de captura biométrica y el servicio de validación, necesitaba forward secrecy: que si alguien roba las claves hoy, no pueda descifrar el tráfico del mes pasado.
Web Crypto API tiene las primitivas para construirlo (ECDH + derivación de claves efímeras), pero requiere que yo implemente el protocolo completo. Themis SecureSession lo implementa out of the box.
Aquí está el tradeoff honesto: "out of the box" significa que confío en que Cossack Labs lo implementó bien. Themis es open source, auditado, y el repo tiene una historia respetable. Pero sigue siendo una dependencia de terceros con todo lo que eso implica en términos de supply chain. Ya escribí sobre los vectores de supply chain en npm y sobre cómo npm audit no alcanza para detectarlos — aplica acá también.
Mi mitigación: jsthemis está pinneado a hash exacto en package.json y el proceso de actualización requiere revisión manual de changelog y diff de binarios.
Benchmark: AES-GCM cifrado en Node.js
Medí el throughput de cifrado de 1 MB de datos biométricos simulados (100 iteraciones, mediana):
Entorno: Node.js 22.4, Railway (512 MB RAM, 1 vCPU compartida)
Dataset: 1 MB de ArrayBuffer con datos aleatorios
Web Crypto API (AES-GCM-256):
Mediana: 2.1 ms
P95: 3.4 ms
P99: 5.8 ms
Themis SecureCell (Seal mode):
Mediana: 2.9 ms
P95: 4.2 ms
P99: 7.1 ms
// benchmark-cifrado.ts — script de medición local
import { performance } from "perf_hooks";
import { SecureCell } from "jsthemis";
const ITERACIONES = 100;
const PAYLOAD_SIZE = 1024 * 1024; // 1 MB
async function benchmarkWebCrypto(clave: CryptoKey): Promise<number[]> {
const tiempos: number[] = [];
const datos = crypto.getRandomValues(new Uint8Array(PAYLOAD_SIZE));
for (let i = 0; i < ITERACIONES; i++) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const inicio = performance.now();
await crypto.subtle.encrypt({ name: "AES-GCM", iv }, clave, datos);
tiempos.push(performance.now() - inicio);
}
return tiempos;
}
function benchmarkThemis(clave: Buffer): number[] {
const tiempos: number[] = [];
const celda = SecureCell.SealWithSymmetricKey(clave);
const datos = Buffer.allocUnsafe(PAYLOAD_SIZE);
for (let i = 0; i < ITERACIONES; i++) {
const inicio = performance.now();
celda.encrypt(datos);
tiempos.push(performance.now() - inicio);
}
return tiempos;
}
Web Crypto gana en velocidad pura —lo esperable, dado que delega al runtime C++ subyacente directamente. Themis tiene overhead del binding pero es marginal para casos de uso reales. Para cifrar templates biométricos de 10-50 KB (el caso típico de Lakaut ID), la diferencia es imperceptible.
Los gotchas que nadie documenta
1. Themis y TypeScript types incompletos
Los tipos de jsthemis están desactualizados en algunos métodos. Encontré que SecureCell.SealWithSymmetricKey no tiene overloads para Buffer y Uint8Array declarados correctamente —terminé extendiendo el módulo con un .d.ts local.
2. Web Crypto API y la exportación de claves
deriveKey con extractable: false es lo correcto para producción —la clave nunca sale del contexto seguro. Pero si necesitás hacer backup de claves para recovery de usuario, necesitás extractable: true y un flujo de exportación explícito. Mezclar los dos casos en el mismo flujo de código es una fuente de bugs. En Lakaut ID los separé en módulos distintos con comentarios de advertencia.
3. Themis en Edge Runtime de Next.js
jsthemis tiene bindings nativos (N-API). No funciona en Edge Runtime de Next.js (que ejecuta V8 sin bindings nativos). Si usás App Router con export const runtime = 'edge', Themis está descartado. Esto me limitó a usar Themis solo en API Routes con Node.js runtime, no en middleware.
Este tipo de incompatibilidad de runtime es el mismo problema que documenté cuando estuve revisando la Clipboard API en TypeScript —las APIs que "deberían funcionar" tienen contextos donde simplemente no están disponibles, y el error no siempre es obvio.
4. La surface de ataque de Themis vs Web Crypto
Web Crypto API es una API estandarizada con implementaciones en múltiples browsers y runtimes. Los bugs son públicos, el spec es público, las implementaciones son auditadas por equipos enormes. Themis es una librería C/C++ con bindings, mantenida por un equipo más pequeño. La surface de ataque es diferente, no necesariamente mayor, pero distinta.
Para decisiones de arquitectura de seguridad como las que tomo en Lakaut ID, esa diferencia importa. Mis agentes autónomos también tienen guardrails explícitos exactamente por este tipo de razonamiento sobre superficie de ataque.
FAQ: cifrado TypeScript en aplicaciones web de producción
¿Themis o Web Crypto API para una app nueva en 2026?
Depende del caso. Si el cifrado es solo en el browser y no necesitás interoperabilidad con mobile o backend no-JS, Web Crypto API alcanza y te ahorrás la dependencia. Si tenés un stack heterogéneo (mobile nativo + Node.js + quizás Python en algún microservicio), Themis cierra el gap de interoperabilidad mejor que cualquier alternativa que hayas encontrado.
¿Es Web Crypto API segura para datos biométricos?
Sí, si la usás correctamente: AES-GCM-256, IV aleatorio por operación, PBKDF2 o Argon2 para derivación de claves, y el guard de secure context que mencioné arriba. El problema no es la API en sí —es la facilidad de usarla mal.
¿Puedo usar Themis en el frontend con Next.js?
Sí, pero con wasm-themis (el build en WebAssembly), no con jsthemis. El costo en bundle es ~400 KB gzipped. Para una app de identidad digital donde la criptografía es core, ese tradeoff puede valer. Para una app SaaS genérica, probablemente no.
¿Qué pasa si necesito forward secrecy en Web Crypto API?
Podés construirlo con ECDH efímero y deriveKey, pero tenés que implementar el protocolo completo vos mismo. Es factible, está documentado, y si lo hacés bien funciona. Themis SecureSession te lo da empaquetado. El costo de Themis es la dependencia; el costo de la implementación propia es el riesgo de hacerlo mal.
¿Cómo manejás la rotación de claves en producción?
En Lakaut ID tengo un proceso separado de key management: las claves de datos en reposo tienen un ID de versión embebido en el ciphertext. Cuando roto claves, el servicio de descifrado sabe qué versión usar para cada registro. No hay re-encriptación masiva —solo los registros que se acceden post-rotación se re-encriptan con la clave nueva. Es una decisión de diseño que tiene tradeoffs propios, pero evita una operación de migración costosa.
¿Vale la pena el overhead operacional de Themis vs el overhead conceptual de Web Crypto API?
Esta es la pregunta honesta. Web Crypto API requiere que entiendas criptografía suficiente para no cometer errores sutiles (IV reuse, parámetros incorrectos, manejo de material de clave). Themis requiere que confíes en Cossack Labs y gestiones una dependencia C/C++ con sus bindings. Ninguno es "fácil" de verdad. En Lakaut ID uso los dos: Web Crypto para el frontend, Themis para el backend. No es elegante, pero es honesto con los tradeoffs.
Mi tesis, sin rodeos
Web Crypto API es suficiente para el 80% de los casos web. Es sólida, está bien especificada, y no te agrega superficie de ataque de terceros. Pero tiene dos límites reales que me importan en Lakaut ID: la interoperabilidad entre plataformas cuando eventualmente lleguemos a mobile nativo, y la complejidad de implementar forward secrecy correctamente sin abstracciones.
Themis cierra esos gaps. El precio que pagás es un bundle más pesado en el frontend (si lo usás ahí) y una dependencia C/C++ que requiere gestión cuidadosa en el backend —especialmente en un contexto donde ya escribí sobre cómo los ataques de supply chain en npm son más peligrosos de lo que parece.
Lo incómodo que nadie dice: no existe la opción "segura por default" en criptografía aplicada. Cada elección que hacés —primitiva, librería, parámetro, contexto de ejecución— es una decisión que puede ser correcta o incorrecta según el contexto. Yo pasé dos semanas aprendiendo eso de la manera cara. Ahora lo sé.
Si estás construyendo algo donde la criptografía importa de verdad, probá ambas opciones contra tus casos concretos antes de decidir. No confíes en benchmarks genéricos ni en posts de blog —incluido este. Probá con tus datos, en tu runtime, en tu infraestructura.
Fuentes originales:
- Themis — Cossack Labs: https://github.com/cossacklabs/themis
Artículos Relacionados
pnpm vs npm vs yarn vs bun: la comparativa definitiva que nadie te va a dar en 2025
Usé los cuatro en proyectos reales. Uno me rompió un monorepo a las 3am. Otro me salvó la vida en producción. Te cuento todo sin filtros.
Functional programming en TypeScript: lo apliqué en mi codebase real y esto sobrevivió (y esto no)
Apliqué functors, monads y pipe() de fp-ts en mi codebase real de Next.js con Server Actions y Prisma. Documenté qué patrones sobrevivieron producción y cuáles quedaron enterrados en el README. La brecha entre los ejemplos bonitos y el código real es enorme — y nadie la documenta honestamente.
Spring Boot en producción real: lo que mi codebase de Lakaut me enseñó que la documentación oficial omite
Tres años laburando con Spring Boot en producción real en Lakaut AC me dejaron logs, incidentes y métricas que ningún tutorial va a mostrarte. Los defaults de la doc oficial asumen un entorno que no existe en Railway con JVM tuning real.
Comentarios (0)
¿Qué pensás de esto?
Dejá tu comentario en 10 segundos.
Usamos tu login solo para mostrar tu nombre y avatar. Nada de spam.