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