¿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 con setInvalidatedByBiometricEnrollment(true). Tenés que regenerar la clave y pedirle al usuario que se vuelva a autenticar.
  • No guardes el resultado de cipher.iv en la misma clave de SharedPreferences que el dato cifrado — guardá el IV y el dato en campos separados.