En las dos partes anteriores protegimos datos del usuario en su dispositivo: tokens en el Keychain y archivos con Data Protection. Ahora el problema se invierte. No se trata de proteger datos del usuario, sino de proteger secretos tuyos que metes dentro de la app. Y aquí la conclusión incomoda: no puedes.
Todo lo que va en el binario se puede leer
La app que subes a la App Store se descarga y se ejecuta en un dispositivo que no controlas. Ese binario, con todo lo que lleva dentro, está en manos del usuario. Y extraer cadenas de un binario iOS no requiere un atacante sofisticado.
Un strings sobre el ejecutable descifrado lista buena parte de las constantes de texto. Herramientas como Hopper o Ghidra desensamblan el código. Frida engancha la app en ejecución y lee valores en memoria mientras corre. Nada de esto es exótico; es lo primero que hace cualquiera que audite una app.
Así que cuando escribes esto:
let apiKey = "sk_live_9f2a8c...b41" // no
acabas de publicar esa clave. No importa que esté en una constante, en un .plist, en un .xcconfig compilado dentro del bundle, o partida en pedazos y recompuesta en tiempo de ejecución. Todo eso vive en el binario o corre en el dispositivo, y ambos son territorio del atacante.
La ofuscación sube el listón, no cierra la puerta
Es tentador pensar que si partes la clave en trozos, la codificas en base64, o la reconstruyes con un algoritmo enrevesado, la escondes lo suficiente. Lo que consigues con eso es que extraerla cueste diez minutos en vez de diez segundos.
El problema de fondo es que, en algún momento de la ejecución, la clave tiene que existir completa en memoria para poder usarla. Un atacante con un breakpoint o con Frida la lee justo ahí, ya reconstruida, sin importar lo enrevesado que fuera el camino para armarla. La ofuscación tiene su lugar contra el análisis automatizado a gran escala, pero no convierte un secreto embebido en un secreto seguro.
Dónde va de verdad un secreto
La regla es simple: un secreto que debe seguir siendo secreto no puede ejecutarse en el cliente. Vive en tu servidor.
En la práctica esto significa que tu app no habla directamente con el servicio que requiere la clave secreta. Habla con tu backend, y tu backend, que sí corre en un entorno que controlas, guarda la clave y hace la llamada al tercero. La app nunca ve el secreto.
App → tu backend (guarda el secreto) → servicio de terceros
Sí, es más trabajo que meter la clave en una constante. Es también la única forma de que la clave no acabe en el binario de miles de dispositivos. Si el servicio permite emitir tokens de corta duración por usuario, mejor todavía: tu backend emite un token limitado y temporal, y la app trabaja con eso en lugar de con la clave maestra.
Las claves que sí tienes que enviar
Hay un caso que no se puede evitar: SDKs de terceros que exigen una clave en el cliente. Un SDK de mapas, de analítica, de notificaciones. Esa clave tiene que ir en la app porque el SDK corre en la app.
Para estas, cambia el enfoque: trátalas como públicas, porque en la práctica lo son. La seguridad no está en esconderlas, está en restringir qué pueden hacer.
- Restringe la clave del lado del servicio. Casi todos los proveedores te dejan atar una clave a tu bundle identifier, limitar qué APIs puede llamar, y ponerle límites de uso. Una clave restringida a tu app y a una sola API, filtrada, sirve de mucho menos a quien la robe.
- Rota y monitorea. Ten un plan para rotar la clave sin publicar una versión nueva, y alertas de uso anómalo. Una clave filtrada que puedes rotar en minutos es un incidente; una que no, es una filtración permanente.
- No des la clave de administración. El SDK necesita la clave pública o de cliente, no la de administración de tu cuenta. Confundirlas es cómo una fuga menor se vuelve catastrófica.
Mantener los secretos fuera del repositorio
Un problema distinto, y relacionado, es no filtrar secretos a tu propio repositorio de git. Aunque una clave termine siendo pública en el binario, no quieres su historial en git, ni quieres que las claves de tu backend aparezcan en un commit.
Aquí sí ayudan los archivos .xcconfig: mantienen la configuración fuera del código, y puedes dejarlos fuera del control de versiones e inyectar los valores desde tu CI en tiempo de compilación.
// Secrets.xcconfig (en .gitignore, inyectado por CI)
API_BASE_URL = https:/$()/api.midominio.com
// Leído desde Info.plist, alimentado por el xcconfig
let baseURL = Bundle.main.object(forInfoDictionaryKey: "API_BASE_URL") as? String
Que quede claro qué resuelve esto y qué no. Resuelve que tus secretos no vivan en el historial de git ni en el código fuente. No resuelve que un valor puesto en el Info.plist deje de ser extraíble del binario. Son dos problemas separados, y el .xcconfig solo ataca el primero.
Un consejo que vale oro: si alguna vez commiteaste una clave por error, rótala. Borrarla del último commit no basta, porque sigue en el historial y en cada clon del repositorio. Un secreto que tocó git alguna vez hay que tratarlo como comprometido.
Lo que llevamos
- Todo lo que va en el binario, ofuscado o no, se puede extraer. La ofuscación solo sube el costo.
- Un secreto que debe seguir siéndolo vive en tu servidor; la app habla con tu backend, no con el tercero.
- Las claves que sí deben ir en el cliente se tratan como públicas: restríngelas del lado del servicio, rótalas y monitoréalas.
- Mantén los secretos fuera de git con
.xcconfigy CI, y rota cualquier clave que haya tocado el historial.
Qué sigue
Ya cubrimos dónde guardar secretos, cómo proteger archivos y qué hacer con las claves de API. Queda una última vía de fuga, y es de las más silenciosas: los datos sensibles que se escapan por los logs. Un token impreso en la consola, un cuerpo de respuesta completo en un crash report, un valor marcado como público “solo para depurar” que se quedó en producción.
En la parte 4, que cierra el bloque de fundamentos, vemos la higiene de logs con os.Logger, el sistema de niveles de privacidad, y un par de fugas vecinas: el portapapeles y la captura de pantalla del multitarea.
Fuentes: Security (Apple) · App Attest (Apple)