mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2026-01-06 00:59:34 -06:00
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:
committed by
GitHub
parent
d5cccf2843
commit
555ddace30
@@ -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")
|
||||
|
||||
}
|
||||
|
||||
5
common/proguard-rules.pro
vendored
5
common/proguard-rules.pro
vendored
@@ -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.** { *; }
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 ?: "")
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user