En muchas ocasiones es importante proteger la información local de nuestras Apps. Al ser información sensible nuestro objetivo debe ser que, aunque un tercero pueda acceder a ella, no sea legible.
Una de las formas de conseguirlo es cifrando la base de datos. A continuación vamos a guiarte en un paso a paso para cumplir ese objetivo.
Qué necesitamos
- Room Database Es la la base de datos oficial de Android, una capa de abstracción de SQLite.
- SQLCipher Es una extensión de SQLite que posibilita cifrar la información.
- Android Keystore Almacén de claves criptográficas protegido de Android.
Empecemos
Lo primero que tenemos que añadir son las dependencias de Gradle:
// Room
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-rxjava2:$room_version"
// SQLCipher
implementation "net.zetetic:android-database-sqlcipher:$sqlcipher_version"
implementation "androidx.sqlite:sqlite-ktx:$sqlite_version"
Ahora vamos con la implementación. En el inyector de dependencias (Koin en este caso) se especifica a Room que debe usar SQLCipher:
single {
Room.databaseBuilder(
androidContext(),
Database::class.java,
BuildConfig.DB_NAME
)
.openHelperFactory(get<SupportFactory>())
.build()
}
single {
// importado desde net.sqlcipher.database.SupportFactory
SupportFactory(get<Passphrase>().getPassphrase())
}
single {
Passphrase(
androidContext()
.getSharedPreferences(
"sharedPreferencesRoomSqlCypher",
Context.MODE_PRIVATE
)
)
}
Passphrase es la clase que proporciona la clave para cifrar la base de datos.
Para conseguir una clave segura se utiliza un generador pseudoaleatorio y una KeyStore. Los pasos para la primera ejecución son:
- Generar la clave pseudoaleatoria.
- Proporcionar la clave a SQLCypher.
- Crear la clave criptográfica de la KeyStore.
- Cifrar la clave de la base de datos con la clave criptográfica de la KeyStore.
- Guardar la clave cifrada, por ejemplo, en SharedPreferences.
En las siguientes ejecuciones lo que haremos será:
- Recuperar la clave de la base de datos, que está almacenada cifrada.
- Descifrar la clave usando la clave criptográfica de la KeyStore.
- Proporcionar la clave a SQLCipher.
El contenido de Passphrase es el siguiente:
class Passphrase constructor(
private val sharedPreferences: SharedPreferences,
) {
companion object {
private const val ALIAS = "aliaskeystore"
private const val TRANSFORMATION = "AES/CBC/NoPadding"
private const val ANDROID_KEY_STORE = "AndroidKeyStore"
private val CHARSET = Charsets.ISO_8859_1
private const val IV_KEY = "ivkey"
private const val DB_KEY = "dbkey"
}
private var passphrase: ByteArray? = null
fun getPassphrase(): ByteArray {
this.passphrase?.let {
return it
} ?: run {
return if (sharedPreferences.contains(DB_KEY) && sharedPreferences.contains(IV_KEY)) {
val encryptedData = sharedPreferences.getString(DB_KEY, null)
val iv = Base64.decode(sharedPreferences.getString(IV_KEY, null), Base64.DEFAULT)
decryptData(encryptedData!!.toByteArray(CHARSET), iv)
} else {
this.passphrase = Random.nextBytes(128)
val encryptedData = encrypt(this.passphrase!!)
sharedPreferences.edit().putString(DB_KEY, encryptedData.toString(CHARSET)).apply()
return this.passphrase!!
}
}
}
Las funciones de cifrado y descrifrado de la clave son:
@Throws(
UnrecoverableEntryException::class,
NoSuchAlgorithmException::class,
KeyStoreException::class,
NoSuchProviderException::class,
NoSuchPaddingException::class,
InvalidKeyException::class,
IOException::class,
InvalidAlgorithmParameterException::class,
SignatureException::class,
BadPaddingException::class,
IllegalBlockSizeException::class
)
private fun encrypt(byteArray: ByteArray): ByteArray {
val cipher: Cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey())
sharedPreferences.edit().remove(IV_KEY).apply()
sharedPreferences.edit().putString(IV_KEY, Base64.encodeToString(cipher.iv, Base64.DEFAULT)).apply()
return cipher.doFinal(byteArray)
}
@Throws(
UnrecoverableEntryException::class,
NoSuchAlgorithmException::class,
KeyStoreException::class,
NoSuchProviderException::class,
NoSuchPaddingException::class,
InvalidKeyException::class,
IOException::class,
BadPaddingException::class,
IllegalBlockSizeException::class,
InvalidAlgorithmParameterException::class
)
fun decryptData(encryptedData: ByteArray, encryptionIv: ByteArray): ByteArray {
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = IvParameterSpec(encryptionIv)
cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec)
return cipher.doFinal(encryptedData)
}
Para obtener la clave de la KeyStore:
@NonNull
@Throws(
NoSuchAlgorithmException::class,
NoSuchProviderException::class,
InvalidAlgorithmParameterException::class
)
private fun getSecretKey(): SecretKey {
val ks: KeyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply {
load(null)
}
return if (ks.containsAlias(ALIAS)) {
val secretKey = ks.getEntry(ALIAS, null) as KeyStore.SecretKeyEntry
secretKey.secretKey
} else {
val keyGenerator: KeyGenerator = KeyGenerator
.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
keyGenerator.init(
KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
)
return keyGenerator.generateKey()
}
}
}
Conclusiones
Aplicando esta guía en nuestros desarrollos, tendremos la seguridad de que toda la información local de nuestra App estará cifrada y fuera del alcance de terceros.
Si te ha gustado este post, ¡síguenos en Twitter para estar al día de próximas entregas!