Хранение чувствительной информации в незащищённой базе данных
Критичность: НИЗКИЙ | |
Способ обнаружения: DAST, DATA BASES |
Описание
Приложение хранит чувствительную информацию в незащищенной базе данных, что может повлечь компрометацию данных.
Несмотря на то, что файл хранится внутри директории приложения, не стоит хранить в нем чувствительную информацию. Получить эту информацию можно разными способами, начиная локальным или облачным бекапом и заканчивая различными уязвимостями чтения файлов и инъекций в Content Provider.
Чтобы понять, какие именно данные необходимо защищать, прежде всего необходимо определить, какие данные обрабатывает и хранит приложение, и какая часть из этой информации считается конфиденциальной. Как правило, в таких случаях полагаются на законодательство и здравый смысл. Нет смысла защищать шифрованием абсолютно всю информацию, которую хранит приложение — это может повлиять на скорость и стабильность работы. Вместо этого следует однозначно определить, какие именно данные являются конфиденциальными для вашего приложения или компании и сосредоточить свое внимание именно на них.
Рекомендации
В случае необходимости хранения чувствительной информации в базе данных нужно дополнительно шифровать итоговую базу данных или данные, которые в ней хранятся. В качестве примера с шифрованием базы данных можно воспользоваться библиотекой sqlcipher.
Пример использования SQLCipher (Java):
package com.demo.sqlcipher;
import java.io.File;
import net.sqlcipher.database.SQLiteDatabase;
import android.app.Activity;
import android.os.Bundle;
public class HelloSQLCipherActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
InitializeSQLCipher();
}
private void InitializeSQLCipher() {
SQLiteDatabase.loadLibs(this);
File databaseFile = getDatabasePath("demo.db");
databaseFile.mkdirs();
databaseFile.delete();
SQLiteDatabase database = SQLiteDatabase.openOrCreateDatabase(databaseFile, "test123", null);
database.execSQL("create table t1(a, b)");
database.execSQL("insert into t1(a, b) values(?, ?)", new Object[]{"one for the money",
"two for the show"});
}
}
Пример использования SQLCipher (Kotlin):
package com.demo.sqlcipher
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import net.sqlcipher.database.SQLiteDatabase
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
SQLiteDatabase.loadLibs(this)
val databaseFile = getDatabasePath("demo.db")
if(databaseFile.exists()) databaseFile.delete()
databaseFile.mkdirs()
databaseFile.delete()
val database = SQLiteDatabase.openOrCreateDatabase(databaseFile, "test123", null)
database.execSQL("create table t1(a, b)")
database.execSQL("insert into t1(a, b) values(?, ?)",
arrayOf<Any>("one for the money", "two for the show")
)
}
}
В данном примере использован «захардкоженный» пароль для базы данных «test123». В реальном приложении не стоит использовать настолько ненадежный пароль и хранить его в исходном коде или в открытом виде.
В качестве способа без хранения пароля — вычисление его «на лету» с использованием пароля пользователя при помощи процедуры усиления ключа.
Правила:
- Явно определяйте режим шифрования и дополнения блоков.
- Используйте криптостойкие технологии шифрования, включающие алгоритм, режим блочного шифрования и режим дополнения блоков.
- В процессе генерации ключа на основе пароля используйте «соль» (salt).
- В процессе генерации ключа на основе пароля используйте достаточное количество итераций хеширования.
- Используйте ключ с длиной, которая обеспечит криптостойкость шифрования.
import android.os.Build package com.appsec.android.cryptsymmetricpasswordbasedkey; import javax.crypto.SecretKeyFactory import javax.crypto.spec.PBEKeySpec @Deprecated("Use Argon2 instead") internal object Pbkdf2Factory { private const val DEFAULT_ITERATIONS = 10_000 private val systemAlgorithm by lazy { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { "PBKDF2withHmacSHA1" } else { "PBKDF2withHmacSHA256" } } fun createKey( passphraseOrPin: CharArray, salt: ByteArray, algorithm: String = systemAlgorithm, iterations: Int = DEFAULT_ITERATIONS ): Pbkdf2Key { @Suppress("MagicNumber") val keySpec = PBEKeySpec(passphraseOrPin, salt, iterations, 256) val secretKey = SecretKeyFactory.getInstance(algorithm).generateSecret(keySpec) return Pbkdf2Key( secretKey.algorithm, iterations, salt, secretKey.encoded ) } }
В дальнейшем получившееся значение ключа можно использовать в качестве пароля для шифрования базы данных и нет необходимости в его хранении, так как каждый раз при вводе пароля он буде вычисляться на лету и передаваться в функцию открытия БД.
Важно!
При таком подходе при изменении секрета (пароля пользователя) данные необходимо перешифровать с новым секретом, если есть необходимость в их сохранении. Также если пароль пользователя представляет из себя пин-код, то лучше использовать подход KEK(Key Encryption Key) + DEK(Data Encryption Key), при котором создается ключ для шифрования данных и он дополнительно шифруется на пароле пользователя. При таком подходе в случае изменения секрета необходимо будет только перешифровать ключ и не трогать зашифрованные данные пользователя.
Примечание
При подключении библиотеки SQLCipher не забудьте добавить правила в Proguard для коррекной работы приложения.
Правила для proguard:
-keep,includedescriptorclasses class net.sqlcipher.** { *; }
-keep,includedescriptorclasses interface net.sqlcipher.** { *; }