Jetpack Security — la abstracción correcta

La librería Jetpack Security (androidx.security:security-crypto) envuelve la complejidad del Keystore y la criptografía en APIs simples. Por debajo usa AES-256 para cifrar datos y el Keystore para proteger la clave maestra.

// build.gradle (app)
dependencies {
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
}

MasterKey — la clave que protege todo

El MasterKey es una clave AES-256-GCM en el Keystore que se usa para cifrar las claves de datos individuales. Se crea una sola vez y se reutiliza:

// Crear o recuperar el MasterKey
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    // Opciones adicionales:
    // .setUserAuthenticationRequired(true, 30)  // requiere biometría
    // .setRequestStrongBoxBacked(true)  // usar StrongBox si está disponible
    .build()

// El MasterKey se guarda en el Keystore bajo el alias "_androidx_security_master_key"
// No tenés que gestionar su ciclo de vida

EncryptedSharedPreferences

Drop-in replacement de SharedPreferences que cifra tanto las claves como los valores automáticamente. La API es idéntica a la de SharedPreferences:

// Crear EncryptedSharedPreferences
val securePrefs = EncryptedSharedPreferences.create(
    context,
    "prefs_seguras",        // nombre del archivo (se cifra internamente)
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,    // cifra las claves
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM   // cifra los valores
)

// Usarlas exactamente igual que SharedPreferences normales:
securePrefs.edit {
    putString("token_sesion", "eyJhbGciOi...")
    putString("refresh_token", "eyJhbGciOi...")
    putLong("expiracion", System.currentTimeMillis() + 3600_000)
    putBoolean("usuario_verificado", true)
}

val token = securePrefs.getString("token_sesion", null)

// Con Hilt — inyectar como SharedPreferences normal
@Module @InstallIn(SingletonComponent::class)
object SecurityModule {

    @Provides @Singleton
    @Named("secure")
    fun provideSecurePrefs(@ApplicationContext context: Context): SharedPreferences {
        val masterKey = MasterKey.Builder(context)
            .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
            .build()
        return EncryptedSharedPreferences.create(
            context, "prefs_seguras", masterKey,
            EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
            EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
        )
    }
}

Cuidado con el backupPor default Android incluye SharedPreferences (incluso EncryptedSharedPreferences) en el backup de Google. Si el MasterKey se regenera en un dispositivo nuevo y el backup contiene datos cifrados con la clave vieja, los datos serán inaccesibles. Considerá excluir los archivos sensibles del backup.

EncryptedFile — cifrar archivos completos

Para archivos más grandes: documentos, imágenes, PDFs, cachés de datos:

// Escribir un archivo cifrado
fun escribirArchivoCifrado(context: Context, nombre: String, contenido: ByteArray) {
    val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    val archivo = File(context.filesDir, nombre)

    val encryptedFile = EncryptedFile.Builder(
        context,
        archivo,
        masterKey,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()

    encryptedFile.openFileOutput().use { outputStream ->
        outputStream.write(contenido)
    }
}

// Leer un archivo cifrado
fun leerArchivoCifrado(context: Context, nombre: String): ByteArray {
    val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    val archivo = File(context.filesDir, nombre)
    if (!archivo.exists()) return byteArrayOf()

    val encryptedFile = EncryptedFile.Builder(
        context, archivo, masterKey,
        EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
    ).build()

    return encryptedFile.openFileInput().use { it.readBytes() }
}

// Uso para guardar un JWT en un archivo en lugar de SharedPreferences:
escribirArchivoCifrado(context, "auth_token.enc", token.toByteArray())
val token = String(leerArchivoCifrado(context, "auth_token.enc"))

DataStore con cifrado manual

DataStore Preferences no tiene soporte nativo de cifrado en Jetpack Security, pero podés cifrar los valores manualmente usando el Keystore:

// Cifrar antes de guardar en DataStore
val TOKEN_KEY = stringPreferencesKey("token")

suspend fun guardarToken(token: String) {
    val tokenCifrado = cifradoManager.cifrar(token)  // usa el Keystore
    dataStore.edit { prefs ->
        prefs[TOKEN_KEY] = "${tokenCifrado.iv}:${tokenCifrado.dato}"
    }
}

// Descifrar al leer
val tokenFlow: Flow<String?> = dataStore.data.map { prefs ->
    prefs[TOKEN_KEY]?.let { cifrado ->
        val (iv, dato) = cifrado.split(":")
        cifradoManager.descifrar(CifradoResult(iv, dato))
    }
}

Excluir datos sensibles del backup

<!-- res/xml/backup_rules.xml -->
<full-backup-content>
    <!-- Excluir archivos sensibles del backup -->
    <exclude domain="sharedpref" path="prefs_seguras.xml" />
    <exclude domain="file" path="auth_token.enc" />
    <exclude domain="database" path="tokens.db" />
</full-backup-content>

<!-- AndroidManifest.xml -->
<application
    android:allowBackup="true"
    android:fullBackupContent="@xml/backup_rules"
    android:dataExtractionRules="@xml/backup_rules" />

<!-- O deshabilitar el backup completamente para apps muy sensibles -->
<application android:allowBackup="false" />

Cuándo usar cada herramienta

# Token de sesión, configuración del usuario, flags
# → EncryptedSharedPreferences

# Documentos, PDFs, imágenes descargadas, cache de datos privados
# → EncryptedFile

# Claves criptográficas (nunca deben salir del dispositivo)
# → Android Keystore directamente (sin Jetpack Security)

# Contraseñas del usuario (si tenés que guardarlas — idealmente no)
# → Hash con bcrypt o Argon2, NO guardar en texto plano NI cifradas

# Tokens OAuth (access token + refresh token)
# → EncryptedSharedPreferences o EncryptedFile
# → El access token puede tener expiración corta (1h)
# → El refresh token debe ser bien protegido