Saltar al contenido principal
Móvil Seguridad

Almacenamiento seguro de tokens: mejores prácticas para desarrolladores móviles

Mejores prácticas de almacenamiento seguro de tokens para desarrolladores móviles: Keychain, DataStore con Keystore, rotación de refresh tokens y migración de EncryptedSharedPreferences.

Almacenamiento seguro de tokens: mejores prácticas para desarrolladores móviles

Guarda los tokens de autenticación en almacenamiento cifrado nativo, no en el localStorage de la WebView o en SharedPreferences sin cifrar. En iOS, usa el Keychain. En Android, protege las llaves con Android Keystore (software, TEE o StrongBox según el dispositivo) y persiste el ciphertext con Jetpack DataStore + Tink. EncryptedSharedPreferences está obsoleta en androidx.security.crypto; planifica las migraciones ya. Para Capacitor apps, combina la seguridad de transporte (HTTPS, pinning) con plugins de almacenamiento nativos en lugar de duplicar la criptografía en JavaScript.

Cómo elegimos estos patrones de almacenamiento

Cada recomendación aquí pasa por cuatro filtros. Primero, respaldo por hardware: el material secreto debe vivir en Keychain/Keystore o cifrarse con claves que sí lo hagan. Segundo, una ruta de migración con soporte oficial: si Google o Apple han declarado obsoleto el enfoque, no lo recomendamos para nuevos code. Tercero, compatibilidad con Capacitor: el patrón debe funcionar desde el límite del plugin sin exponer claves en bruto a JavaScript. Cuarto, revocabilidad: una credencial filtrada debe poder invalidarse del lado del servidor.

Ese último filtro es la razón por la que la rotación de refresh tokens aparece en todo este artículo. En nuestra revisión de los datos de incidentes de 2024 recopilados por Chainalysis, las vulneraciones de claves privadas y credenciales provocaron la mayor parte del robo de criptomonedas —incluyendo más de $305 millones de DMM Bitcoin en Japón y $235 millones de WazirX en India— y el fallo común fue confiar ciegamente en una única credencial válida de forma continua (informe de delitos cripto de Chainalysis 2024). Los tokens de auth móvil fallan igual: un bearer token de larga duración en un almacenamiento en texto plano es una invitación abierta. Dejaríamos de recomendar cualquier arquitectura que persista una credencial sin rotación indefinidamente.

Principios aplicables a cualquier plataforma

La siguiente tabla se resume en snapshots; la lógica subyacente es la división entre access/refresh token. Un token de acceso tiene una vida corta —minutos, no días— y es lo que tu API llama 'present'. Como su radio de impacto es pequeño y expira rápido, debe residir solo en memoria, reconstruyéndose en cada inicio de la app. El token de actualización es el secreto duradero que permite generar nuevos access tokens, por lo que solo este merece un lugar en un almacenamiento cifrado persistente y respaldado por hardware. Diferentes tiempos de vida justifican diferentes ubicaciones; guardarlos ambos de la misma forma es donde empiezan la mayoría de las filtraciones.

La secuencia completa en runtime es así: la app inicia sin access token, llama a tu servidor de auth con el refresh token persistido, recibe un nuevo access token en memoria y destruye el viejo refresh token porque el servidor lo rota en cada uso. Si el servidor detecta que un refresh token ya consumido se presenta de nuevo, lo trata como evidencia de robo y revoca toda la familia de tokens. Al cerrar sesión, el cliente limpia el local storage y el servidor revoca el grant; borrar datos localmente sin la revocación del servidor es tener fe, no seguridad.

Práctica Por qué es importante
Access tokens de corta duración Limita el daño si un token se filtra
Rotación de refresh tokens Invalida refresh tokens robados tras su uso legítimo
Solo HTTPS Evita la interceptación de red casual
Certificate pinning (si es necesario) Reduce riesgos de certificados mal emitidos; ver guía de OWASP sobre certificate pinning
Sin tokens en logs, URLs o analytics Tokens en herramientas de observabilidad causan brechas
Revocación al cerrar sesión La invalidación en el servidor es mejor que esperar que el cliente borre datos

@capgo/capacitor-ssl-pinning puede hacer pinning de TLS para Capacitor HTTP en iOS y Android si necesitas controles de transporte más estrictos.

Android: Keystore y DataStore (no EncryptedSharedPreferences)

Google marcó como deprecated androidx.security.crypto helpers, incluyendo EncryptedSharedPreferences y MasterKey. El resumen del paquete indica a los desarrolladores usar KeyGenerator con el proveedor AndroidKeyStore para nuevo material de clave.

Usa un pipeline basado en Tink de extremo a extremo:

  1. Crea un keyset de Tink con AndroidKeysetManager, guardando la master key en Android Keystore mediante un android-keystore:// URI (ver las datastore-tink notas de la versión).
  2. Obtén un Aead primitivo de ese conjunto de claves.
  3. Añadir androidx.datastore:datastore-tink (disponible desde DataStore 1.3.0-rc01; la versión estable 1.2.x no incluye este artefacto), envuelve tu Serializer con AeadSerializer, y pásalo a DataStore. Cuando varios stores comparten una Aead clave, asigna a cada store una associatedDataúnica, como un valor estable derivado de su nombre de archivo.

Google documenta esto como la ruta de reemplazo soportada para los androidx.security.crypto helpers obsoletos. No mezcles un JCA independiente SecretKey con AeadSerializer; el serializador espera una instancia Aead de Tink.

Nota sobre el versionado: como el datastore-tink artefacto solo existe a partir de la versión 1.3.0-rc01, los equipos que usan versiones estables 1.2.x no encontrarán el reemplazo documentado en su árbol de dependencias. Ese desfase ya lleva varias versiones y, según nuestro criterio, es la razón principal por la que los planes de migración se estancan: decide conscientemente si usar una release candidate o esperar, en lugar de que ocurra por accidente.

Opciones de Keystore que debes conocer

  • setIsStrongBoxBacked(true) cuando FEATURE_STRONGBOX_KEYSTORE está disponible para un mayor aislamiento de claves.
  • Autenticación de usuario binding (setUserAuthenticationRequired) cuando los tokens solo deben ser accesibles tras biometría o credenciales del dispositivo.
  • Evita el Auto Backup para archivos que contienen keysets o ciphertext, a menos que entiendas el comportamiento de restauración; la doc de EncryptedSharedPreferences de Google advierte sobre la restauración de ciphertext sin las llaves.

Los módulos nativos en Kotlin o Java en Capacitor deben implementar este patrón dentro del plugin; JavaScript solo debe recibir handles opacos o el estado de la sesión, no las llaves en bruto.

iOS: Keychain y Secure Enclave

El Keychain sigue siendo el lugar correcto para los tokens en iOS. Los elementos se cifran y el sistema los protege. Usa una constante de accesibilidad con un alcance limitado y Control de acceso cuando necesites Face ID, Touch ID o validación por passcode.

La elección de accesibilidad es una decisión real, no un valor por defecto para copiar. Elige kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly cuando los tokens deban usarse en procesos de fondo —refrescos por push, background fetch— pero nunca deban migrarse a un nuevo dispositivo mediante backup. Elige kSecAttrAccessibleWhenUnlockedThisDeviceOnly para uso de tokens solo en primer plano, donde la ventana más restrictiva vale la pena. En ambos casos, habilita kSecUseDataProtectionKeychain en targets modernos para obtener el Keychain con protección de datos en lugar del legacy basado en archivos.

CryptoKit ayuda cuando cifras payloads por tu cuenta, pero para tokens bearer estándar tipo OAuth, basta con guardar el string del token en el Keychain con la accesibilidad correcta. Las llaves respaldadas por Secure Enclave (una llave P-256 generada con SecureEnclave en CryptoKit) solo valen la pena cuando la llave misma no debe salir nunca del hardware seguro — como en operaciones de firma o device attestation. No pueden guardar un string de token por ti; protegen tu propia llave de sobre (envelope key). Si no estás rotando sobres y gestionando ese ciclo de vida, el almacenamiento simple en Keychain es más sencillo y no menos seguro. El cifrado extra en la capa de la app solo ayuda si también rotas llaves y gestionas los sobres con cuidado.

de Capacitor @capacitor/preferences escribe en UserDefaults, que es no un almacenamiento sin seguridad. No guardes tokens ahí.

Capacitor: opciones de almacenamiento seguro en la org de Capgo

Capgo hace no lanzar un paquete llamado “secure storage”, pero dos plugins cubren las necesidades comunes de Capacitor:

Plugin Ideal para Respaldo nativo
@capgo/capacitor-native-biometric setData / getSecureData Secretos pequeños (session tokens, llaves API) iOS Keychain; cifrado en SharedPreferences con Android Keystore
@capgo/capacitor-data-storage-sqlite con opciones de cifrado Datos locales grandes o estructurados SQLite con SQLCipher opcional

El guía de almacenamiento key-value compara Preferences, SQLite stores y NativeBiometric.setData para secretos.

Requiere @capgo/capacitor-native-biometric 8.6.0 o superior (ajusta la versión mayor del plugin a la de tu Capacitor. Ejemplo con lecturas con biometría:

import {
  AccessControl,
  NativeBiometric,
} from '@capgo/capacitor-native-biometric';

const TOKEN_KEY = 'auth.refresh_token';

export async function saveRefreshToken(token: string) {
  await NativeBiometric.setData({
    key: TOKEN_KEY,
    value: token,
    accessControl: AccessControl.BIOMETRY_CURRENT_SET,
  });
}

export async function loadRefreshToken(): Promise<string | null> {
  try {
    const { value } = await NativeBiometric.getSecureData({
      key: TOKEN_KEY,
      reason: 'Unlock your session',
      title: 'Authenticate',
    });
    return value ?? null;
  } catch {
    return null;
  }
}

getData solo devuelve valores almacenados sin accessControl. Para protección biométrica, usa siempre BIOMETRY_CURRENT_SET o BIOMETRY_ANY con getSecureData.

En web builds, nunca guardes tokens de producción en localStorage. Usa mejor el modelo de sesión web de tu backend.

Ciclo de vida del token en servidor y cliente

Sí, las apps móviles deben guardar refresh tokens; la alternativa es obligar al usuario a reautenticarse en cada inicio, lo cual es puro teatro de seguridad. El matiz es dónde y cómo: persiste solo el refresh token, solo en almacenamiento respaldado por Keychain o Keystore, y solo con rotación y revocación en el servidor.

La rotación es lo que hace segura la persistencia. Emite refresh tokens como one-time-use : cada canje devuelve un nuevo refresh token e invalida el anterior. El servidor mantiene una breve ventana de detección de reuso — si se presenta un token supuestamente consumido, marca toda la familia como comprometida y revoca la cadena completa, forzando un nuevo login en cada dispositivo que lo compartiera. Permite un pequeño periodo de gracia (de unos segundos a un minuto) para peticiones simultáneas o reconexiones offline, donde un cliente legítimo podría presentar el token viejo una última vez antes de recibir el nuevo. No dar ningún periodo de gracia bloquea a usuarios con conexiones inestables; un periodo demasiado largo amplía la superficie de ataque.

Dónde se guarda cada token depende directamente de su tiempo de vida:

  • Emite tokens de acceso con TTLs cortos (minutos, no días), y mantenlos solo en memoria durante la sesión.
  • Persiste el token de actualización en Keychain o en almacenamiento cifrado respaldado por Keystore, y rótalo como se indicó.
  • Al cambiar la contraseña, cerrar sesión o perder el dispositivo, revoca los refresh tokens en el servidor; que el cliente borre los datos locales no es revocación.
  • Considera una estrategia de token binding ligera (device key attestation o app instance IDs) si tu modelo de amenazas lo requiere.

Al lanzar parches de seguridad para el manejo de tokens en apps con Capacitor, Live Updates de Capgo puedes desplegar cambios en la capa web rápidamente si cumples las políticas de la tienda. Los cambios en el storage nativo requieren un build de la tienda.

Comparativa de opciones de storage

Método Seguridad Capacitor encaja
Variable JS en memoria Alta en ejecución, se pierde al cerrar la app OK solo para tokens de sesión
localStorage / Preferencias Bajo Solo ajustes, nunca secretos
Keychain de iOS / Keystore de Android Alto Vía code nativo o plugins de Capgo
DataStore + Tink (Android nativo) Alto Plugin Capacitor custom o SDK nativo
Criptografía custom en JS Variable Evítalo; las keys en JS son extraíbles

Un detalle importante sobre esa última fila: el fallo de la criptografía en JavaScript no son los algoritmos débiles, sino su ubicación. Cualquier key que un runtime de JS pueda leer puede ser exfiltrada por cualquier otro proceso en el runtime, por lo que la “alta seguridad” es estructuralmente inalcanzable desde esa capa, sin importar la librería que elijas.

Checklist de migración desde EncryptedSharedPreferences

  1. Identifica qué claves contienen secretos frente a las de configuración pública.
  2. Implementa DataStore + AeadSerializer con un keyset de Tink respaldado por Keystore.
  3. Migra con SharedPreferencesMigration usando un producer que abra la instancia legacy de EncryptedSharedPreferences (no la versión plain SharedPreferences), luego borra los archivos legacy tras una lectura exitosa.
  4. Lanza el cambio nativo en un release de la store; testea backup y restore en un dispositivo físico.
  5. Eliminar androidx.security.crypto dependencias cuando nada las referencie.

FAQ

¿Debería cifrar los tokens antes de guardarlos en el Keychain?

Normalmente no se requiere una capa extra para tokens bearer estándar si la accesibilidad del Keychain coincide con tu modelo de amenazas. Añade cifrado de sobre (envelope encryption) cuando las regulaciones o las políticas de rotación de claves lo exijan.

¿El SQLCipher en un plugin de Capacitor reemplaza a Keystore?

SQLCipher protege el archivo de la base de datos, pero sigues necesitando una clave fuerte en Keystore o Keychain para descifrarla. Trata el manejo de la passphrase de cifrado como parte de tu diseño de almacenamiento seguro.

¿Puedo guardar tokens en Capacitor Preferences usando ofuscación?

La ofuscación no es cifrado. Preferences mapea a UserDefaults y SharedPreferences sin protección respaldada por hardware.

¿Deberían las apps móviles guardar refresh tokens?

Sí, pero solo en almacenamiento cifrado respaldado por Keychain o Keystore, junto con rotación y revocación en el servidor. No guardarlos implica reautenticarse en cada inicio, algo que los usuarios evitan exigiendo requisitos de login más débiles. Lo que nunca es aceptable es guardar el refresh token en Preferences, localStorage o cualquier archivo en texto plano.

¿Cuál es la forma más segura de guardar access tokens en una app moderna?

No los persistas en absoluto. Mantén el access token de corta duración en memoria durante la sesión y reconstrúyelo usando el refresh token en cada inicio. Persiste solo el refresh token —en el iOS Keychain o en un almacenamiento cifrado respaldado por Android Keystore— y deja que la rotación y la revocación limiten el daño si un dispositivo llega a verse comprometido.

Continuar con Secure Token Storage: Best Practices for Mobile Developers

Si estás usando Almacenamiento seguro de tokens: mejores prácticas para desarrolladores móviles para planificar la seguridad y el cumplimiento, conéctalo con Cifrado para los detalles de implementación en Encryption, Cumplimiento para el detalle de implementación en Compliance, Escáner de seguridad de Capgo para el flujo de trabajo del producto en Capgo Security Scanner, Seguridad de Capgo para el flujo de trabajo del producto en Capgo Security, y Centro de confianza de Capgo para el flujo de trabajo del producto en Capgo Trust Center.

Live Updates para apps de Capacitor

Si hay un bug en la capa web, publica el fix con Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el proceso de revisión normal.

Lo último de nuestro blog

Capgo te da los mejores insights para crear una app móvil realmente profesional.