Added support for Encrypted database, migration from non-encrypted from encrypted DB (#626)

* security in db

* feat: add sqlite cipher

* feat: added support for db encryption

* feat: run spotless
This commit is contained in:
Yogesh Choudhary Paliyal
2023-07-23 11:16:57 +05:30
committed by GitHub
parent d5cccf2843
commit 555ddace30
12 changed files with 100 additions and 69 deletions

View File

@@ -11,7 +11,7 @@ android {
compileSdk = 33
defaultConfig {
minSdk = 21
minSdk = 23
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -72,4 +72,7 @@ dependencies {
implementation("androidx.test.ext:junit-ktx:1.1.5")
androidTestApi("androidx.test:rules:1.5.0")
implementation("net.zetetic:android-database-sqlcipher:4.5.4")
implementation("androidx.sqlite:sqlite:2.3.1")
}

View File

@@ -18,4 +18,7 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-keep,includedescriptorclasses class net.sqlcipher.** { *; }
-keep,includedescriptorclasses interface net.sqlcipher.** { *; }

View File

@@ -15,10 +15,11 @@ import com.yogeshpaliyal.common.db.DbDao
const val DB_VERSION_3 = 3
const val DB_VERSION_4 = 4
const val DB_VERSION_5 = 5
const val DB_VERSION_6 = 6
@Database(
entities = [AccountModel::class],
version = 5,
version = DB_VERSION_6,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {

View File

@@ -9,6 +9,7 @@ const val DEFAULT_PASSWORD_LENGTH = 10f
@Serializable
data class UserSettings(
val keyPassPassword: String? = null,
val dbPassword: String? = null,
val defaultPasswordLength: Float = DEFAULT_PASSWORD_LENGTH,
val backupKey: String? = null,
val isBiometricEnable: Boolean = false,

View File

@@ -1,6 +1,7 @@
package com.yogeshpaliyal.common.di.module
import android.content.Context
import android.database.sqlite.SQLiteException
import androidx.room.Room
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
@@ -10,11 +11,16 @@ import com.yogeshpaliyal.common.DB_VERSION_4
import com.yogeshpaliyal.common.DB_VERSION_5
import com.yogeshpaliyal.common.R
import com.yogeshpaliyal.common.utils.getRandomString
import com.yogeshpaliyal.common.utils.getUserSettingsOrNull
import com.yogeshpaliyal.common.utils.setDatabasePassword
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.runBlocking
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SupportFactory
import javax.inject.Singleton
@Module
@@ -24,27 +30,74 @@ object AppModule {
@Provides
@Singleton
fun getDb(@ApplicationContext context: Context): AppDatabase {
return Room.databaseBuilder(
val dbName = context.getString(R.string.app_name)
val dbNameEncrypted = "$dbName.encrypted"
val builder = Room.databaseBuilder(
context,
AppDatabase::class.java,
context.getString(R.string.app_name)
).addMigrations(object : Migration(DB_VERSION_3, DB_VERSION_4) {
dbNameEncrypted
)
var userEnteredPassphrase: String
var isMigratedFromNonEncryption = false
runBlocking {
val userSettings = context.getUserSettingsOrNull()
if (userSettings?.dbPassword == null) {
userEnteredPassphrase = getRandomString()
isMigratedFromNonEncryption = true
context.setDatabasePassword(userEnteredPassphrase)
} else {
userEnteredPassphrase = userSettings.dbPassword
}
}
SQLiteDatabase.loadLibs(context)
if (isMigratedFromNonEncryption) {
context.migrateNonEncryptedToEncryptedDb(dbName, dbNameEncrypted, userEnteredPassphrase)
}
val passphrase: ByteArray = SQLiteDatabase.getBytes(userEnteredPassphrase.toCharArray())
val factory = SupportFactory(passphrase)
builder.openHelperFactory(factory)
builder.addMigrations(object : Migration(DB_VERSION_3, DB_VERSION_4) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `account` ADD COLUMN `unique_id` TEXT")
database.query("select id,unique_id from `account` where unique_id IS NULL")
?.use {
.use {
while (it.moveToNext()) {
val id = it.getInt(0)
val query = "update `account` set `unique_id` = '${getRandomString()}' where `id` = '$id'"
val query =
"update `account` set `unique_id` = '${getRandomString()}' where `id` = '$id'"
database.execSQL(query)
}
}
}
}).addMigrations(object : Migration(DB_VERSION_4, DB_VERSION_5) {
})
builder.addMigrations(object : Migration(DB_VERSION_4, DB_VERSION_5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `account` ADD COLUMN `type` INT DEFAULT 0")
}
})
.build()
return builder.build()
}
private fun Context.migrateNonEncryptedToEncryptedDb(nonEncryptedDbName: String, encryptedDbName: String, userEnteredPassphrase: String) {
try {
val oldDb = getDatabasePath(nonEncryptedDbName)
val database = SQLiteDatabase.openOrCreateDatabase(oldDb, "", null)
val encryptedDbPath = getDatabasePath(encryptedDbName).path
database.rawExecSQL(
"ATTACH DATABASE '$encryptedDbPath' AS encrypted KEY '$userEnteredPassphrase'"
)
database.rawExecSQL("select sqlcipher_export('encrypted')")
database.rawExecSQL("DETACH DATABASE encrypted")
database.close()
oldDb.delete()
} catch (e: SQLiteException) {
e.printStackTrace()
}
}
}

View File

@@ -74,6 +74,12 @@ suspend fun Context.setBackupKey(backupKey: String?) {
}
}
suspend fun Context.setDatabasePassword(databasePassword: String) {
getUserSettingsDataStore().updateData {
it.copy(dbPassword = databasePassword)
}
}
suspend fun Context.setBackupTime(backupTime: Long?) {
getUserSettingsDataStore().updateData {
it.copy(backupTime = backupTime)

View File

@@ -1,7 +1,6 @@
package com.yogeshpaliyal.common.utils
import android.content.Context
import android.os.Build
import androidx.datastore.core.Serializer
import androidx.datastore.dataStore
import com.yogeshpaliyal.common.data.UserSettings
@@ -9,11 +8,7 @@ import com.yogeshpaliyal.common.data.UserSettings
class UserSettingsDataStore(val context: Context) {
private fun getSerializer(context: Context): Serializer<UserSettings> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
UserSettingsSerializer(CryptoManager())
} else {
UserSettingsSerializerLegacy(context)
}
return UserSettingsSerializer(CryptoManager())
}
private val Context.dataStoreSerializer by dataStore(

View File

@@ -1,7 +1,5 @@
package com.yogeshpaliyal.common.utils
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.datastore.core.Serializer
import com.yogeshpaliyal.common.data.UserSettings
import kotlinx.serialization.SerializationException
@@ -9,7 +7,6 @@ import kotlinx.serialization.json.Json
import java.io.InputStream
import java.io.OutputStream
@RequiresApi(Build.VERSION_CODES.M)
class UserSettingsSerializer(
private val cryptoManager: CryptoManager
) : Serializer<UserSettings> {

View File

@@ -1,40 +0,0 @@
package com.yogeshpaliyal.common.utils
import android.content.Context
import androidx.datastore.core.Serializer
import com.yogeshpaliyal.common.data.DEFAULT_PASSWORD_LENGTH
import com.yogeshpaliyal.common.data.UserSettings
import java.io.InputStream
import java.io.OutputStream
class UserSettingsSerializerLegacy constructor(
private val applicationContext: Context
) : Serializer<UserSettings> {
override val defaultValue: UserSettings
get() = UserSettings()
override suspend fun readFrom(input: InputStream): UserSettings {
return UserSettings(
autoBackupEnable = applicationContext.isAutoBackupEnabledLegacy(),
backupDirectory = applicationContext.getBackupDirectoryLegacy(),
backupTime = applicationContext.getBackupTimeLegacy(),
defaultPasswordLength = applicationContext.getKeyPassPasswordLengthLegacy() ?: DEFAULT_PASSWORD_LENGTH,
isBiometricEnable = applicationContext.isBiometricEnableLegacy(),
keyPassPassword = applicationContext.getKeyPassPasswordLegacy(),
overrideAutoBackup = applicationContext.overrideAutoBackupLegacy(),
backupKey = applicationContext.getOrCreateBackupKey(false).second
)
}
override suspend fun writeTo(t: UserSettings, output: OutputStream) {
applicationContext.setBiometricEnableLegacy(t.isBiometricEnable)
applicationContext.setAutoBackupEnabledLegacy(t.autoBackupEnable)
applicationContext.setBackupDirectoryLegacy(t.backupDirectory ?: "")
applicationContext.setBackupTimeLegacy(t.backupTime ?: 0L)
applicationContext.setKeyPassPasswordLengthLegacy(t.defaultPasswordLength)
applicationContext.setKeyPassPasswordLegacy(t.keyPassPassword)
applicationContext.setOverrideAutoBackupLegacy(t.overrideAutoBackup)
applicationContext.saveKeyphraseLegacy(t.backupKey ?: "")
}
}

View File

@@ -24,7 +24,7 @@ class AutoBackupWorker @AssistedInject constructor(
override suspend fun doWork(): Result {
return withContext(Dispatchers.IO) {
if (appContext.canUserAccessBackupDirectory()) {
val userSettings = appContext.getUserSettings() ?: return@withContext Result.failure()
val userSettings = appContext.getUserSettings()
val selectedDirectory = Uri.parse(userSettings.backupDirectory)
appContext.backupAccounts(
appDatabase,