Android Keystore vs iOS Keychain: ¿cómo se guarda de verdad un secreto de forma segura en el dispositivo?
Guardar un secreto en el dispositivo no es una sola llamada a una API: es una decisión sobre claves respaldadas por hardware, biometría y qué pasa si el equipo está rooteado.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Android Keystore vs iOS Keychain: ¿cómo se guarda de verdad un secreto de forma segura en el dispositivo?
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 modelos de seguridad muy distintos
"Guardar un secreto de forma segura" esconde mucho detalle, porque Android e iOS resuelven el problema de forma estructuralmente distinta. El Keystore de Android es una API de gestión de claves: le pides que genere o importe una clave criptográfica y, a partir de ahí, nunca vuelves a ver los bytes de esa clave — solo le pides al proveedor que cifre, descifre, firme o verifique por ti. El Keychain de iOS es una API de almacenamiento de secretos: guarda valores cifrados arbitrarios (contraseñas, tokens, blobs pequeños) directamente, indexados por atributos como servicio y cuenta. Quien responde "son básicamente lo mismo" normalmente no ha tenido que construir la capa de cifrado encima del Keystore: Android te da la caja fuerte, iOS te da la caja fuerte y el objeto que querías meter dentro.
Esa diferencia importa en la práctica: en Android, guardar un token significa generar (o reutilizar) una clave AES respaldada por el Keystore, cifrar el token con ella, y persistir el texto cifrado en algún sitio — normalmente EncryptedSharedPreferences o una columna de Room, porque el Keystore no tiene dónde guardar tus datos. En iOS, SecItemAdd hace ambas cosas en una sola llamada: el sistema cifra tu valor en reposo bajo una clase de Data Protection, y lo recuperas con SecItemCopyMatching. Ambos enfoques terminan en el mismo hardware, eso sí: en dispositivos modernos, las operaciones con claves ocurren dentro de un Trusted Execution Environment (TEE) o un elemento seguro discreto (StrongBox en Android, Secure Enclave en iOS), así que el material de la clave nunca toca la memoria del proceso de la app ni se pagina al disco en claro.
#Android: genera la clave, nunca la toques
Aquí una clave AES-GCM respaldada por el Keystore que exige una autenticación biométrica reciente antes de poder usarse, y se invalida sola si el usuario registra una huella o rostro nuevo:
private const val KEY_ALIAS = "user_session_key"fun getOrCreateSecretKey(): SecretKey {val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }(keyStore.getKey(KEY_ALIAS, null) as? SecretKey)?.let { return it }val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")val spec = KeyGenParameterSpec.Builder(KEY_ALIAS,KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT).setBlockModes(KeyProperties.BLOCK_MODE_GCM).setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE).setUserAuthenticationRequired(true).setUserAuthenticationParameters(0, KeyProperties.AUTH_BIOMETRIC_STRONG).setInvalidatedByBiometricEnrollment(true).setIsStrongBoxBacked(true) // hace fallback con StrongBoxUnavailableException en equipos sin soporte.build()keyGenerator.init(spec)return keyGenerator.generateKey()}
El valor persistido en sí nunca pasa por SharedPreferences en claro. Jetpack Security envuelve exactamente este patrón por ti:
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()val securePrefs = EncryptedSharedPreferences.create(context,"secure_prefs",masterKey,EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)securePrefs.edit().putString("refresh_token", token).apply()
#iOS: una sola llamada cifra y guarda
El Keychain acepta un objeto de control de acceso que ata un ítem al enrolamiento biométrico actual, en paralelo a setInvalidatedByBiometricEnrollment:
func saveToken(_ token: String) throws {guard let access = SecAccessControlCreateWithFlags(nil,kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,.biometryCurrentSet,nil) else { throw KeychainError.accessControlFailed }let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,kSecAttrService as String: "com.myapp.session",kSecAttrAccount as String: "refresh_token",kSecValueData as String: token.data(using: .utf8)!,kSecAttrAccessControl as String: access]SecItemDelete(query as CFDictionary) // reemplaza cualquier ítem previolet status = SecItemAdd(query as CFDictionary, nil)guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }}
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly es el detalle que casi todos se saltan: sin el sufijo ThisDeviceOnly, un ítem puede restaurarse desde un backup de iCloud/iTunes en otro dispositivo distinto, que normalmente no es lo que quieres para un token de sesión.
#El error que lo tira todo abajo
La forma más común en que candidatos senior se equivocan aquí no es la API de almacenamiento — es proteger la cosa equivocada con biometría. Mostrar un BiometricPrompt y luego poner isUnlocked = true en un view model no es seguro: en un dispositivo rooteado o con jailbreak, ese booleano vive en memoria de proceso normal, y un script de Frida puede voltearlo sin tocar ningún sensor. La solución es pasar un CryptoObject (Android) o realizar el descifrado dentro del uso de una clave respaldada por Secure Enclave (iOS, con LAContext y control de acceso), de forma que la operación criptográfica misma falle a menos que el hardware acabe de confirmar la coincidencia biométrica. No hay booleano que enganchar, porque no hay ningún booleano en la cadena de confianza.
Ninguna de las dos garantías sobrevive a un sistema operativo comprometido: en un Android rooteado, un atacante con privilegios de sistema puede en ciertos escenarios coaccionar al propio demonio del Keystore, y las herramientas de jailbreak han logrado, en distintos momentos, extraer ítems del Keychain protegidos por debajo de las clases más fuertes. Por eso la atestación de dispositivo (Play Integrity API, DeviceCheck/App Attest) es un control aparte, no un reemplazo — Keystore y Keychain protegen el dato, la atestación le dice a tu backend si debería confiar en el entorno que lo está pidiendo.
🧠 Keystore vs Keychain
0/3 · 0/3 answered1. ¿Cuál es la diferencia estructural principal entre el Keystore de Android y el Keychain de iOS?
2. ¿Por qué proteger el descifrado con un CryptoObject es más fuerte que revisar un booleano 'isAuthenticated' tras un prompt biométrico?
3. ¿Qué aporta StrongBox en los dispositivos Android que lo soportan?