Extraer secretos de un APK: lo que un atacante ve en tu binario
Un APK de Android no es una caja fuerte. Es un archivo ZIP con extensión .apk, y cualquiera con unzip y diez minutos libres puede mirar dentro. Si tu app lleva una API key, un token de backend o cualquier secreto escrito directamente en el código, descompilar el APK suele ser suficiente para conseguirlo.
No es un ataque teórico. Los APK públicos se desmontan constantemente: investigadores de seguridad, competidores y bots que rastrean las tiendas de apps buscando credenciales expuestas. Si en algún momento tu pipeline de CI o tu asistente de IA escribió un secreto directamente en el código fuente, probablemente sigue ahí en cada versión que has publicado.
Tu APK es solo un ZIP
Compruébalo tú mismo:
unzip app-release.apk -d app_unzipped
Obtienes classes.dex, resources.arsc, AndroidManifest.xml, una carpeta assets/ y cualquier librería nativa .so. Nada de eso está cifrado por defecto. La firma de release protege la integridad, no la confidencialidad.
Descompilar en minutos: las herramientas del atacante
Primero grep, luego descompilar
Antes de tocar un descompilador serio, la mayoría de atacantes simplemente buscan patrones con grep:
strings app-release.apk | grep -Ei "sk_live_|AIza|xox[baprs]-|secret|apikey|token"
Solo con strings aparecen sorprendentemente muchas claves hardcodeadas: claves live de Stripe, API keys de Google, tokens de Slack, en texto plano dentro de resources.arsc o assets/.
Descompilación completa
Para recuperar código fuente de verdad:
jadx -d app_src app-release.apk
o, para recursos y manifest:
apktool d app-release.apk -o app_decoded
grep -r -i "api_key\|secret\|token" app_decoded/res/
jadx reconstruye Java/Kotlin legible a partir del bytecode DEX. No revierte perfectamente el código minificado, pero renombrar variables no oculta los literales de texto: un secreto asignado a una variable llamada a sigue siendo el mismo secreto.
Las librerías nativas tampoco están a salvo
Si moviste una clave a un .so nativo pensando que sería más difícil de alcanzar, el obstáculo es menor de lo que parece:
strings libnative.so | grep -i "key\|secret"
Los strings estáticos en código nativo son igual de visibles; solo has añadido un paso de desensamblado, no protección real.
Qué encuentran realmente los atacantes
En la práctica, los APK descompilados suelen exponer:
- API keys de terceros hardcodeadas (pasarelas de pago, mapas, analítica, proveedores de IA)
- URLs de backend y tokens estáticos pensados para APIs "internas"
- Configuraciones de Firebase con reglas demasiado permisivas
- Client secrets de OAuth que nunca deberían vivir en el cliente
- Claves de firma o cifrado copiadas de un
.envdirectamente aBuildConfig
Esto último es cada vez más habitual. Los asistentes de IA integran SDKs muy rápido, y el camino más corto para que "funcione" suele ser pegar la clave directamente en el código. Hablamos de este patrón con más detalle en seguridad en la era de la IA: el asistente no sabe que esa clave va a viajar dentro de un binario público.
Por qué la ofuscación no soluciona esto
ProGuard/R8 renombra clases y métodos; no cifra los literales de texto salvo que añadas explícitamente herramientas de cifrado de strings, y aun así un atacante decidido puede enganchar la app en tiempo de ejecución (Frida, Xposed) y volcar el valor descifrado justo cuando tu código lo usa. La ofuscación eleva el coste de clonar el código fuente completo; no protege un secreto estático compilado en el binario. Trátala como un disuasorio, no como una defensa.
La solución real: no enviar el secreto
La única solución duradera es arquitectónica: el secreto no debería existir dentro del APK, cifrado o no. Si no está ahí, no hay nada que descompilar.
Eso significa que tu app obtiene los secretos en tiempo de ejecución, desde un servicio que:
- Nunca tiene acceso al valor en claro (servidor zero-knowledge).
- Solo entrega secretos a un cliente que ha verificado como una instalación genuina y no manipulada de tu app.
- Añade una barrera humana —biometría— antes de descifrar en el dispositivo.
Este es el modelo sobre el que está construido Koove. En lugar de STRIPE_KEY=sk_live_... metido en tu bundle, tu código y tu asistente de IA referencian el secreto por nombre, y el valor real se almacena en el servidor como ciphertext que el servidor nunca puede leer.
Cómo Koove saca los secretos del binario
En el lado del desarrollador, los secretos se envían por CLI, sin pasar nunca por un repositorio ni empaquetarse en un build:
koove set STRIPE_SECRET_KEY sk_live_xxx --env prod
En el lado del cliente, el SDK móvil solo pide el secreto después de demostrar que el dispositivo es real:
import { KooveClient } from '@koove/sdk';
const koove = new KooveClient({
apiUrl: 'https://api.koove.io',
appId: 'app_xxxx',
appToken: 'xxxx',
});
await koove.init(); // Apple App Attest / Play Integrity, luego registro
const stripeKey = await koove.decryptSecret(envelope); // protegido por biometría, descifrado en el dispositivo
Por debajo, los secretos se cifran con un envelope X25519 + AES-256-GCM derivado con HKDF-SHA256, sobre primitivas de código abierto (@koove/crypto) que puedes leer en lugar de tener que confiar en una afirmación. La attestation se verifica contra hardware real en ambas plataformas, no contra un simulador o un bypass de desarrollo, que es exactamente lo que importa en este modelo de amenaza: aunque un atacante descompile tu app por completo, las copias clonadas o repackagedas fallan la attestation y no reciben ningún secreto. Los detalles completos están en el centro de seguridad y confianza.
Los límites, con honestidad
Dos cosas que conviene decir claramente. Primero, el envelope de Koove no tiene forward secrecy: no es un ratchet estilo Signal, y no lo vendemos como tal. Segundo, una vez que un secreto se ha descifrado en un dispositivo legítimo y verificado, vive en la memoria de esa app como cualquier valor en runtime; ningún gestor de secretos puede "deshacer" retroactivamente un valor que ya llegó a la RAM de un dispositivo. Lo que Koove sí resuelve es el fallo mucho más común: un secreto estático guardado en un binario público, esperando a que cualquiera que descargue tu APK lo saque con un grep.
Si quieres la referencia completa de CLI y SDK, está en la documentación; los precios están en /plans y las preguntas frecuentes en el FAQ.
Para cerrar
Descompilar un APK no es un ataque sofisticado: es unzip, jadx y grep, y lleva minutos. La única defensa fiable es asegurarte de que no haya nada que encontrar: sin claves hardcodeadas, sin tokens en assets/, sin secretos viviendo cerca de tu código fuente.
Si tu app o tu asistente de IA ha puesto alguna vez una clave real directamente en el código, merece la pena revisar hoy mismo tu último APK publicado. Regístrate y saca esos secretos del binario antes de que alguien más los encuentre por ti.