При разработке Android-приложений мы часто используем Room для локального хранения данных. Но задумывались ли вы когда-нибудь, насколько легко кто-то может извлечь эти данные, если ваше приложение будет подвергнуто реверс-инжинирингу?
Локальные базы данных могут стать настоящей золотой жилой для злоумышленников. Именно поэтому шифрование базы данных Room — это важный шаг к защите пользовательских данных. В этой статье я расскажу вам о готовом к использованию и безопасном подходе к шифрованию базы данных Room с помощью SQLCipher, Android Keystore и некоторых правил криптографической гигиены.
Зачем шифровать Room
Room не поддерживает шифрование по умолчанию. Но вы можете использовать SQLCipher для Android — готовую замену SQLite со встроенными функциями шифрования. Он надёжен, активно поддерживается и пользуется широким доверием.
Шаг 1: Добавляем зависимости
Начнем с добавления необходимых зависимостей в файл build.gradle:
// SQLCipher
implementation "net.zetetic:sqlcipher-android:4.9.0" // or latest
// Room
implementation("androidx.room:room-runtime:$room_version")
ksp("androidx.room:room-compiler:$room_version")
Шаг 2: Безопасно генерируем ключ шифрования
Не прописывайте пароли жёстко! Вместо этого сгенерируйте случайный 256-битный ключ в рантайме и защитите его с помощью хранилища ключей Android.
Вот неправильный способ:
val key = SQLiteDatabase.getBytes("your-password".toCharArray())
Это небезопасно — пароль точно так же можно извлечь из вашего APK.
Лучший способ:
val sqlCipherKey = ByteArray(32) SecureRandom().nextBytes(sqlCipherKey)
Но нам всё равно нужно хранить этот ключ безопасно. Одного SharedPreferences недостаточно.
Шаг 3: Используем Android Keystore для защиты ключа
Мы используем хранилище ключей Android для шифрования сгенерированного ключа перед его сохранением.
Создайте SqlCipherKeyManager:
class SqlCipherKeyManager constructor(
private val sharedPreferences: SharedPreferences
) {
private val keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
init {
initialize()
}
private fun initialize() {
generateKeystoreKeyIfNeeded()
if (!sharedPreferences.contains("encrypted_key")) {
generateAndEncryptSqlCipherKey()
}
}
private fun generateKeystoreKeyIfNeeded() {
if (!keyStore.containsAlias("sqlcipher_keystore_key")) {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenSpec = KeyGenParameterSpec.Builder(
"sqlcipher_keystore_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
keyGenerator.init(keyGenSpec)
keyGenerator.generateKey()
}
}
private fun generateAndEncryptSqlCipherKey() {
val secretKey = getSecretKey("sqlcipher_keystore_key")
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val sqlCipherKey = ByteArray(32)
SecureRandom().nextBytes(sqlCipherKey)
val encryptedKey = cipher.doFinal(sqlCipherKey)
val iv = cipher.iv
sharedPreferences.edit {
putString("encrypted_key", Base64.encodeToString(encryptedKey, Base64.NO_WRAP))
putString("encryption_iv", Base64.encodeToString(iv, Base64.NO_WRAP))
}
// Zero out the key in memory
sqlCipherKey.fill(0)
}
private fun getDecryptedSqlCipherKey(keyAlias: String, key: String, iv: String): ByteArray {
val encryptedKey = Base64.decode(key, Base64.NO_WRAP)
val ivBytes = Base64.decode(iv, Base64.NO_WRAP)
val secretKey = getSecretKey(keyAlias)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, secretKey, GCMParameterSpec(128, ivBytes))
return cipher.doFinal(encryptedKey)
}
private fun getSecretKey(keyAlias: String): SecretKey =
(keyStore.getEntry(keyAlias, null) as KeyStore.SecretKeyEntry).secretKey
fun getSupportFactory(): SupportOpenHelperFactory {
val encryptedKey = sharedPreferences.getString("encrypted_key", null).orEmpty()
val iv = sharedPreferences.getString("encryption_iv", null).orEmpty()
val decryptedKey = getDecryptedSqlCipherKey("sqlcipher_keystore_key", encryptedKey, iv)
return WipeAfterUseSupportFactory(decryptedKey)
}
}
Шаг 4: Очищаем конфиденциальные данные из оперативной памяти
Чтобы ключ не хранился в памяти дольше, чем необходимо, мы создаём специальную фабрику SupportOpenHelperFactory:
class DisposableKeySupportFactory(private val decryptedKey: ByteArray) :
SupportOpenHelperFactory(decryptedKey) {
override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper {
val helper = super.create(configuration)
decryptedKey.fill(0)
return helper
}
}
Это сотрёт ключ из памяти сразу после того, как Room его использует.
Шаг 5: Подключаем всё к Room
Теперь инициализируйте Room с помощью зашифрованного SupportFactory:
System.loadLibrary("sqlcipher")
val sqlCipherKeyManager = SqlCipherKeyManager(sharedPreferences)
val dbFile = context.getDatabasePath("your-db-name")
val db = Room.databaseBuilder(
context,
AppDatabase::class.java,
dbFile.absolutePath
)
.openHelperFactory(sqlCipherKeyManager.getSupportFactory())
.build()
Заключение
- Этот подход шифрует всю вашу базу данных Room с помощью алгоритма AES-256 с помощью SQLCipher.
- Ключ шифрования генерируется случайным образом и защищается с помощью хранилища ключей Android.
- Расшифрованный ключ немедленно стирается из памяти после использования.
⚠️ Внимание: даже с учетом этих мер защиты, хранения конфиденциальных данных на устройстве по возможности следует избегать.
Резюме
С помощью всего нескольких классов и осторожного обращения с ключами шифрования вы можете значительно повысить безопасность локального хранилища вашего приложения.
Это решение промышленного уровня использует проверенные криптографические примитивы и защищает данные ваших пользователей от реверс-инжиниринга и локального взлома.

