Dónde NO guardar API keys en una app React Native (y dónde sí)
Si has escrito EXPO_PUBLIC_API_KEY o importado un .env en un componente React Native, probablemente ya tengas un problema de seguridad React Native sin saberlo. El motivo es simple: todo lo que compilas en el bundle de JavaScript viaja al dispositivo del usuario, y ese dispositivo no es de confianza.
Esto no es teórico. Cualquiera con el APK o el IPA puede descomprimirlo y leer el bundle como texto plano. Vamos a ver exactamente dónde falla la gente al gestionar React Native API keys, y qué alternativas funcionan de verdad.
El bundle de JavaScript no es una caja fuerte
Metro y Hermes compilan tu código, pero no lo cifran. Un .env cargado con react-native-dotenv o react-native-config se sustituye en tiempo de build por su valor literal, que queda incrustado en el bundle final. Da igual si tu backend nunca ve el .env: el valor ya está en el .apk que subes a Google Play.
Los sitios donde nunca deberías guardar una API key
1. Variables de entorno empaquetadas en el cliente
EXPO_PUBLIC_*, REACT_APP_* o cualquier variable inyectada por react-native-config termina en texto plano dentro del JS bundle. Sirven para configuración pública (un flag, una URL de API), nunca para secretos.
2. Hardcodeadas en el código fuente
const STRIPE_SECRET_KEY = "sk_live_51H...";
Esto es lo primero que un asistente de IA sugiere si le pides "conecta esto con Stripe", y lo primero que un atacante busca con un grep.
3. AsyncStorage o el sistema de archivos sin cifrar
AsyncStorage guarda datos en SQLite (Android) o un plist (iOS) sin cifrado real. En un dispositivo rooteado o con jailbreak, se lee directamente.
4. Info.plist, strings.xml o google-services.json
Muchos añaden claves de terceros aquí "porque es configuración nativa". Sigue siendo el mismo binario que se distribuye públicamente.
5. El historial de git
Aunque borres la clave del archivo actual, sigue viva en commits anteriores. git log -p | grep sk_ la encuentra en segundos.
Cómo comprobarlo tú mismo
Con un build de release ya generado:
# Android
unzip app-release.apk -d app-release
strings app-release/assets/index.android.bundle | grep -Ei "sk_live|AIza|api[_-]?key"
# iOS
strings MyApp.app/MyApp | grep -Ei "sk_live|AIza"
Si algo aparece, esa clave ya está comprometida en cuanto publicas la app. Revocarla evita nuevo daño, pero no borra las copias que ya se descargaron.
Dónde SÍ deben vivir las API keys
Las claves públicas, en el cliente (sin problema)
Una clave publicable de Stripe, la config de Firebase o un ID de analítica están diseñadas para ser públicas. Ahí el cliente es el sitio correcto.
Los tokens de sesión, en el Keychain/Keystore
Para tokens emitidos tras el login (JWT de corta duración, refresh tokens), usa react-native-keychain o el Keystore nativo. Es cifrado a nivel de sistema operativo y pensado exactamente para esto.
Las claves secretas de terceros: nunca en el móvil
Una API key de OpenAI, la contraseña de tu base de datos o una clave secreta de Stripe no deberían llegar al dispositivo en ningún caso. Si tu app necesita ese servicio, la llamada la hace tu backend, no el cliente.
El patrón correcto: backend + gestor de secretos
La app React Native llama a tu API; tu API llama a OpenAI, Stripe o tu base de datos. El móvil nunca ve esas claves. El problema que queda es: ¿dónde guarda tu backend (y tu CI/CD, y el asistente de IA que escribe el código) esas claves sin que acaben en un .env commiteado?
Aquí es donde entra Koove. En vez de pegar la clave en un archivo, la guardas una vez:
koove set OPENAI_API_KEY sk-proj-xxxx --env prod
El valor se cifra en el cliente con envelope encryption (X25519 + AES-256-GCM) antes de salir de tu máquina; el servidor de Koove solo almacena ciphertext, nunca la clave en claro. Tu código de backend deja de tener el secreto embebido y solo lo referencia por nombre:
import { koove } from "@koove/sdk";
const apiKey = await koove.get("OPENAI_API_KEY");
const client = new OpenAI({ apiKey });
La clave se descifra solo para consumidores verificados: un backend autorizado, o un consumidor móvil validado con App Attest / Play Integrity + biometría, según el caso de uso. Los detalles de configuración están en /docs/sdk.
Sé honesto sobre los límites
Aquí conviene ser claro, no vendedor. Koove protege el secreto hasta el momento de la descarga y descifrado: no puede hacer que un secreto que ya se entregó a un dispositivo comprometido "desaparezca", ni convierte tu app en inhackeable. Lo que sí hace es reducir drásticamente la superficie de exposición: el secreto no vive en tu repo, no vive en el bundle, no vive en texto plano en ningún servidor, y solo llega a quien puede demostrar que es un cliente legítimo. Si tu caso de uso exige que el secreto viaje al propio dispositivo móvil (poco habitual, pero existe), la atenuación con attestation eleva mucho el listón frente a un emulador o una app modificada, pero una vez descifrado en memoria del dispositivo, ese dispositivo lo tiene.
En la era del código generado por IA
Cuando le pides a Copilot o Cursor que "integre Stripe", la sugerencia más probable es una clave hardcodeada directamente en el componente. Es el patrón que más ha visto en su entrenamiento. El asistente de IA puede usar la misma CLI (koove set NAME value) para guardar el secreto, y tu código solo referencia el nombre. Profundizamos en este problema específico en /ai-security.
Checklist rápido
- ¿Hay algún
sk_,AIza,Bearero contraseña en tu bundle? Compruébalo constringsantes de cada release. - ¿Tus claves de terceros pasan por el cliente en algún momento? Deberían pasar solo por tu backend.
- ¿Guardas tokens de sesión en AsyncStorage? Muévelos al Keychain/Keystore.
- ¿Tu
.envestá en.gitignorepero ya fue commiteado antes? Revísalo congit log -p. - ¿Tu asistente de IA tiene acceso directo a producir código con secretos en claro? Dale una forma de guardarlos sin exponerlos.
Si quieres dejar de decidir caso por caso dónde va cada clave y centralizar esto con cifrado extremo a extremo, mira los planes en /plans o crea tu cuenta directamente en /register. Si tienes dudas concretas sobre cómo encaja con tu stack, el /faq cubre los casos más habituales.