¿Cómo manejás el cifrado de datos en reposo y en tránsito en una app móvil?
El cifrado de disco completo y HTTPS son el piso, no el techo — la pregunta de la entrevista es en realidad sobre gestión de claves: dónde viven, quién puede desbloquearlas y qué pasa cuando un certificado rota.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
¿Cómo manejás el cifrado de datos en reposo y en tránsito en una app móvil?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#Dos problemas distintos, dos cajas de herramientas distintas
"Cifrado" en una entrevista mobile en realidad son dos preguntas distintas disfrazadas de una sola palabra. En reposo es proteger los datos que están en el dispositivo — una base de datos, un archivo de caché, un almacén de preferencias — frente a otra app, frente a un dispositivo rooteado/con jailbreak que inspecciona el sistema de archivos, o frente a alguien que tiene el teléfono desbloqueado en la mano. En tránsito es proteger los datos que viajan entre la app y tu backend frente a un atacante a nivel de red: wifi pública, un proxy malicioso, una CA comprometida o falsa. Usan herramientas completamente distintas, y confundirlas es la forma más rápida de dar una respuesta superficial — "usamos HTTPS" contesta la mitad de la pregunta y no dice nada sobre qué pasa con esos datos apenas tocan el disco.
#Cifrado en reposo: no construyas tu propia bóveda
Los sistemas operativos modernos te dan cifrado de disco completo gratis, pero eso sólo protege un dispositivo apagado o bloqueado — no hace nada una vez que el dispositivo está desbloqueado y una app maliciosa (o la shell de un dispositivo rooteado) puede leer el sandbox de otra app, y tampoco hace nada respecto a que esos datos terminen calladamente en un backup en la nube sin cifrar. La protección real a nivel de app significa guardar los secretos chicos — tokens, claves — en el almacenamiento seguro dedicado de la plataforma, y agregar una capa de cifrado sobre cualquier cosa más grande: el Keychain de iOS (respaldado por el Secure Enclave, con una clase kSecAttrAccessible explícita) más la Data Protection API para archivos; la librería Jetpack Security de Android (EncryptedSharedPreferences/EncryptedFile), respaldada por una clave maestra que nunca sale del Android Keystore.
Para cualquier cosa más grande que unos pocos pares clave-valor, el patrón que escala es el cifrado por sobres (envelope encryption): una clave de cifrado de datos (DEK) simétrica cifra el payload real con AES-GCM, y la DEK en sí se cifra ("envuelve") con una clave de cifrado de claves (KEK) que vive exclusivamente dentro del Keystore/Secure Enclave y nunca aparece en texto plano fuera de él.
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()val encryptedPrefs = EncryptedSharedPreferences.create(context,"secure_prefs",masterKey,EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)encryptedPrefs.edit().putString("refresh_token", token).apply()
func guardarToken(_ token: String) throws {let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,kSecAttrAccount as String: "refresh_token",kSecValueData as String: Data(token.utf8),kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,]SecItemDelete(query as CFDictionary)let status = SecItemAdd(query as CFDictionary, nil)guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }}
#Cifrado en tránsito: TLS es necesario, no suficiente
Exigí TLS 1.2 como piso (preferí 1.3) y rechazá el tráfico en texto plano por completo — App Transport Security de iOS viene así por defecto, y el Network Security Config de Android te deja poner cleartextTrafficPermitted="false" por dominio. Para cualquier cosa de alto valor — banca, datos de salud, herramientas de administración — agregá certificate pinning encima de la validación estándar de la cadena. Fijá el hash SPKI de la clave pública, no el certificado hoja completo: un pin sobre el certificado hoja se rompe apenas el servidor renueva su certificado, incluso con el mismo par de claves, mientras que un pin SPKI sobrevive a una renovación de rutina. Enviá siempre al menos un pin de respaldo (una segunda clave que controlás pero todavía no rotaste), y mantené el conjunto de pines actualizable desde tu backend — una rotación forzada por la autoridad certificadora sin un kill-switch remoto sobre tus pines deja a todas las copias instaladas de la app bloqueadas fuera de tu API al mismo tiempo.
val certificatePinner = CertificatePinner.Builder().add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=").add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // pin de respaldo.build()val client = OkHttpClient.Builder().certificatePinner(certificatePinner).build()
<!-- res/xml/network_security_config.xml --><network-security-config><domain-config cleartextTrafficPermitted="false"><domain includeSubdomains="true">api.example.com</domain><pin-set expiration="2027-01-01"><pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin><pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin></pin-set></domain-config></network-security-config>
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {guard let trust = challenge.protectionSpace.serverTrust,let certificate = SecTrustGetCertificateAtIndex(trust, 0) else {completionHandler(.cancelAuthenticationChallenge, nil); return}let hashClavePublica = spkiSha256(of: certificate)if pinnedHashes.contains(hashClavePublica) {completionHandler(.useCredential, URLCredential(trust: trust))} else {completionHandler(.cancelAuthenticationChallenge, nil)}}
#El antipatrón: "arreglar" un error de TLS de la forma incorrecta
Casi todos los TrustManager que confían en cualquier certificado en producción empezaron como un workaround sólo para debug frente a un certificado autofirmado de staging que nunca se quitó:
// ANTIPATRÓN — no publiques esto nuncaval trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()})
Una vez que esto se publica, TLS provee cifrado sin autenticación — cualquier máquina en el camino de red, incluyendo un proxy MITM en un café, es aceptada como si fuera el servidor. Si necesitás llegar a un endpoint de staging autofirmado, poné el override de confianza detrás de un flag de build de debug que jamás pueda compilarse dentro de un artefacto de release, nunca detrás de un interruptor en tiempo de ejecución.
🧠 Cifrado en reposo y en tránsito
0/3 · 0/3 answered1. ¿Por qué el cifrado de disco completo a nivel de sistema operativo no alcanza por sí solo para una app sensible?
2. ¿Por qué fijar el hash SPKI de la clave pública en vez del certificado hoja completo?
3. ¿Cuál es el principal peligro de dejar un TrustManager que confía en todo en un build de release?