Site icon AppTractor

Шифруем базу данных Room в Android с помощью SQLCipher

При разработке 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()

Заключение

⚠️ Внимание: даже с учетом этих мер защиты, хранения конфиденциальных данных на устройстве по возможности следует избегать.

Резюме

С помощью всего нескольких классов и осторожного обращения с ключами шифрования вы можете значительно повысить безопасность локального хранилища вашего приложения.

Это решение промышленного уровня использует проверенные криптографические примитивы и защищает данные ваших пользователей от реверс-инжиниринга и локального взлома.

Источник

Exit mobile version