¿Qué es el Android Keystore?
El Android Keystore es un sistema para generar y almacenar claves criptográficas de forma que nunca puedan extraerse del dispositivo. En hardware compatible (la mayoría de los dispositivos modernos), las claves viven en un Trusted Execution Environment (TEE) o Secure Element — un chip dedicado aislado del procesador principal.
La diferencia fundamental con guardar una clave en SharedPreferences:
- En SharedPreferences: la clave es bytes en disco. Con root, se puede leer.
- En Keystore: la clave nunca sale del hardware. Solo podés pedirle al Keystore que cifre/descifre — nunca accedés a la clave en sí.
El Keystore no es invulnerableEn dispositivos rooteados, un atacante con acceso físico y tiempo puede extraer material de claves en algunos casos. Pero hace el ataque órdenes de magnitud más difícil que guardar claves en texto plano o en SharedPreferences sin cifrar.
Generar una clave AES en el Keystore
private fun generarClave(alias: String) {
// Verificar si ya existe
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
if (keyStore.containsAlias(alias)) return
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
"AndroidKeyStore"
)
keyGenerator.init(
KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
// La clave se invalida si el usuario agrega un nuevo biométrico o PIN
.setInvalidatedByBiometricEnrollment(true)
// Requerir pantalla bloqueada para usar la clave (Android 9+)
// .setUnlockedDeviceRequired(true)
.build()
)
keyGenerator.generateKey()
}
private fun obtenerClave(alias: String): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
return (keyStore.getEntry(alias, null) as KeyStore.SecretKeyEntry).secretKey
}
Cifrar y descifrar con AES-GCM
AES-GCM es el modo recomendado: proporciona confidencialidad (nadie puede leer el contenido) e integridad (detecta si los datos fueron modificados). Cada operación de cifrado genera un IV único que debe guardarse junto con el dato cifrado:
class CifradoManager(context: Context) {
companion object {
private const val KEY_ALIAS = "mi_app_clave_maestra"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
}
init {
generarClave(KEY_ALIAS)
}
fun cifrar(texto: String): CifradoResult {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, obtenerClave(KEY_ALIAS))
val iv = cipher.iv // IV único generado automáticamente
val textoCifrado = cipher.doFinal(texto.toByteArray(Charsets.UTF_8))
return CifradoResult(
iv = Base64.encodeToString(iv, Base64.DEFAULT),
dato = Base64.encodeToString(textoCifrado, Base64.DEFAULT)
)
}
fun descifrar(resultado: CifradoResult): String {
val iv = Base64.decode(resultado.iv, Base64.DEFAULT)
val textoCifrado = Base64.decode(resultado.dato, Base64.DEFAULT)
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(128, iv) // 128-bit authentication tag
cipher.init(Cipher.DECRYPT_MODE, obtenerClave(KEY_ALIAS), spec)
val textoDescifrado = cipher.doFinal(textoCifrado)
return String(textoDescifrado, Charsets.UTF_8)
}
}
data class CifradoResult(val iv: String, val dato: String)
// Uso:
val manager = CifradoManager(context)
val cifrado = manager.cifrar("mi token secreto")
// Guardar cifrado.iv y cifrado.dato en SharedPreferences/Room
val original = manager.descifrar(cifrado)
// "mi token secreto"
Nunca reutilices el IVReutilizar el mismo IV con la misma clave en AES-GCM rompe la seguridad criptográfica completamente. Siempre generá un IV nuevo para cada operación de cifrado y guardalo junto al dato cifrado.
Claves RSA para firma digital
// Generar par de claves RSA en el Keystore
private fun generarClaveRSA(alias: String) {
val keyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_RSA,
"AndroidKeyStore"
)
keyPairGenerator.initialize(
KeyGenParameterSpec.Builder(
alias,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
)
.setDigests(KeyProperties.DIGEST_SHA256)
.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1)
.setKeySize(2048)
.build()
)
keyPairGenerator.generateKeyPair()
}
// Firmar datos
fun firmar(datos: ByteArray, alias: String): ByteArray {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val clavePrivada = keyStore.getKey(alias, null) as PrivateKey
val signature = Signature.getInstance("SHA256withRSA")
signature.initSign(clavePrivada)
signature.update(datos)
return signature.sign()
}
// Verificar firma con la clave pública
fun verificar(datos: ByteArray, firma: ByteArray, alias: String): Boolean {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
val clavePublica = keyStore.getCertificate(alias).publicKey
val signature = Signature.getInstance("SHA256withRSA")
signature.initVerify(clavePublica)
signature.update(datos)
return signature.verify(firma)
}
Protección biométrica de claves
Podés requerir autenticación biométrica para usar una clave del Keystore. La clave solo puede usarse si el usuario autenticó con huella o cara:
// Generar clave que requiere biometría
val spec = KeyGenParameterSpec.Builder(alias, PURPOSE_ENCRYPT or PURPOSE_DECRYPT)
.setBlockModes(BLOCK_MODE_GCM)
.setEncryptionPaddings(ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // requiere auth
.setUserAuthenticationParameters(
30, // válida por 30 segundos después de auth
KeyProperties.AUTH_BIOMETRIC_STRONG or
KeyProperties.AUTH_DEVICE_CREDENTIAL // o PIN/patrón
)
.build()
// Al intentar cifrar/descifrar, si el usuario no autenticó recientemente:
// throws UserNotAuthenticatedException
// → Mostrar BiometricPrompt y reintentar
Buenas prácticas
- Usá un alias descriptivo por clave:
"mi_app_token_sesion", no"clave1". - Usá AES-256 con GCM para cifrado simétrico. No uses ECB o CBC sin HMAC.
- Usá RSA-2048 o superior para cifrado asimétrico y firmas.
- Manejá
KeyPermanentlyInvalidatedException: ocurre cuando el usuario cambia el PIN/biometría y la clave fue creada consetInvalidatedByBiometricEnrollment(true). Tenés que regenerar la clave y pedirle al usuario que se vuelva a autenticar. - No guardes el resultado de
cipher.iven la misma clave de SharedPreferences que el dato cifrado — guardá el IV y el dato en campos separados.