android: Restore database from a backup when encryption fails for some reason (#1058)
* Restore database from a backup when encryption fails for some reason * Removed unused code * Safer way of doing some things * Ordering * Increased possible diff in time to 10 seconds * update strings * Alert confirmation * update strings Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
98ccab394a
commit
568c9201d6
@@ -53,6 +53,8 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
if (res.second != DBMigrationResult.OK) {
|
||||
Log.d(TAG, "Unable to migrate successfully: ${res.second}")
|
||||
} else if (startChat) {
|
||||
// If we migrated successfully means previous re-encryption process on database level finished successfully too
|
||||
if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null)
|
||||
withApi {
|
||||
val user = chatController.apiGetActiveUser()
|
||||
if (user == null) {
|
||||
|
||||
@@ -110,6 +110,7 @@ class AppPreferences(val context: Context) {
|
||||
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
|
||||
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
|
||||
val initializationVectorDBPassphrase = mkStrPreference(SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE, null)
|
||||
val encryptionStartedAt = mkDatePreference(SHARED_PREFS_ENCRYPTION_STARTED_AT, null, true)
|
||||
|
||||
val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name)
|
||||
val primaryColor = mkIntPreference(SHARED_PREFS_PRIMARY_COLOR, LightColorPalette.primary.toArgb())
|
||||
@@ -146,13 +147,19 @@ class AppPreferences(val context: Context) {
|
||||
set = fun(value) = sharedPreferences.edit().putString(prefName, value).apply()
|
||||
)
|
||||
|
||||
private fun mkDatePreference(prefName: String, default: Instant?): Preference<Instant?> =
|
||||
/**
|
||||
* Provide `[commit] = true` to save preferences right now, not after some unknown period of time.
|
||||
* So in case of a crash this value will be saved 100%
|
||||
* */
|
||||
private fun mkDatePreference(prefName: String, default: Instant?, commit: Boolean = false): Preference<Instant?> =
|
||||
Preference(
|
||||
get = {
|
||||
val pref = sharedPreferences.getString(prefName, default?.toEpochMilliseconds()?.toString())
|
||||
pref?.let { Instant.fromEpochMilliseconds(pref.toLong()) }
|
||||
},
|
||||
set = fun(value) = sharedPreferences.edit().putString(prefName, value?.toEpochMilliseconds()?.toString()).apply()
|
||||
set = fun(value) = sharedPreferences.edit().putString(prefName, value?.toEpochMilliseconds()?.toString()).let {
|
||||
if (commit) it.commit() else it.apply()
|
||||
}
|
||||
)
|
||||
|
||||
companion object {
|
||||
@@ -189,6 +196,7 @@ class AppPreferences(val context: Context) {
|
||||
private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE = "EncryptedDBPassphrase"
|
||||
private const val SHARED_PREFS_INITIALIZATION_VECTOR_DB_PASSPHRASE = "InitializationVectorDBPassphrase"
|
||||
private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt"
|
||||
private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme"
|
||||
private const val SHARED_PREFS_PRIMARY_COLOR = "PrimaryColor"
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import chat.simplex.app.SimplexApp
|
||||
import chat.simplex.app.model.*
|
||||
import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlin.math.log2
|
||||
|
||||
@Composable
|
||||
@@ -63,7 +64,9 @@ fun DatabaseEncryptionView(m: ChatModel) {
|
||||
progressIndicator.value = true
|
||||
withApi {
|
||||
try {
|
||||
prefs.encryptionStartedAt.set(Clock.System.now())
|
||||
val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value)
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError
|
||||
when {
|
||||
sqliteError is SQLiteError.ErrorNotADatabase -> {
|
||||
|
||||
@@ -2,6 +2,7 @@ package chat.simplex.app.views.database
|
||||
|
||||
import SectionSpacer
|
||||
import SectionView
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -11,6 +12,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import chat.simplex.app.*
|
||||
@@ -20,6 +22,11 @@ import chat.simplex.app.ui.theme.*
|
||||
import chat.simplex.app.views.helpers.*
|
||||
import chat.simplex.app.views.usersettings.NotificationsMode
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.datetime.Clock
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.StandardCopyOption
|
||||
import kotlin.io.path.Path
|
||||
|
||||
@Composable
|
||||
fun DatabaseErrorView(
|
||||
@@ -30,6 +37,8 @@ fun DatabaseErrorView(
|
||||
val dbKey = remember { mutableStateOf("") }
|
||||
var storedDBKey by remember { mutableStateOf(DatabaseUtils.getDatabaseKey()) }
|
||||
var useKeychain by remember { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
val context = LocalContext.current
|
||||
val restoreDbFromBackup = remember { mutableStateOf(shouldShowRestoreDbButton(appPreferences, context)) }
|
||||
val saveAndRunChatOnClick: () -> Unit = {
|
||||
DatabaseUtils.setDatabaseKey(dbKey.value)
|
||||
storedDBKey = dbKey.value
|
||||
@@ -102,6 +111,20 @@ fun DatabaseErrorView(
|
||||
null -> {
|
||||
}
|
||||
}
|
||||
if (restoreDbFromBackup.value) {
|
||||
SectionSpacer()
|
||||
Text(generalGetString(R.string.database_backup_can_be_restored))
|
||||
Spacer(Modifier.size(16.dp))
|
||||
RestoreDbButton {
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(R.string.restore_database_alert_title),
|
||||
text = generalGetString(R.string.restore_database_alert_desc),
|
||||
confirmText = generalGetString(R.string.restore_database_alert_confirm),
|
||||
onConfirm = { restoreDb(restoreDbFromBackup, appPreferences, context) },
|
||||
destructive = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,6 +183,31 @@ private fun runChat(
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldShowRestoreDbButton(prefs: AppPreferences, context: Context): Boolean {
|
||||
val startedAt = prefs.encryptionStartedAt.get() ?: return false
|
||||
/** Just in case there is any small difference between reported Java's [Clock.System.now] and Linux's time on a file */
|
||||
val safeDiffInTime = 10_000L
|
||||
val filesChat = File(context.dataDir.absolutePath + File.separator + "files_chat.db.bak")
|
||||
val filesAgent = File(context.dataDir.absolutePath + File.separator + "files_agent.db.bak")
|
||||
return filesChat.exists() &&
|
||||
filesAgent.exists() &&
|
||||
startedAt.toEpochMilliseconds() - safeDiffInTime <= filesChat.lastModified() &&
|
||||
startedAt.toEpochMilliseconds() - safeDiffInTime <= filesAgent.lastModified()
|
||||
}
|
||||
|
||||
private fun restoreDb(restoreDbFromBackup: MutableState<Boolean>, prefs: AppPreferences, context: Context) {
|
||||
val filesChatBase = context.dataDir.absolutePath + File.separator + "files_chat.db"
|
||||
val filesAgentBase = context.dataDir.absolutePath + File.separator + "files_agent.db"
|
||||
try {
|
||||
Files.move(Path("$filesChatBase.bak"), Path(filesChatBase), StandardCopyOption.REPLACE_EXISTING)
|
||||
Files.move(Path("$filesAgentBase.bak"), Path(filesAgentBase), StandardCopyOption.REPLACE_EXISTING)
|
||||
restoreDbFromBackup.value = false
|
||||
prefs.encryptionStartedAt.set(null)
|
||||
} catch (e: Exception) {
|
||||
AlertManager.shared.showAlertMsg(generalGetString(R.string.database_restore_error), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
|
||||
DatabaseKeyField(
|
||||
@@ -187,6 +235,13 @@ private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColumnScope.RestoreDbButton(onClick: () -> Unit) {
|
||||
TextButton(onClick, Modifier.align(Alignment.CenterHorizontally)) {
|
||||
Text(generalGetString(R.string.restore_database), color = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun PreviewChatInfoLayout() {
|
||||
|
||||
@@ -18,7 +18,8 @@ object DatabaseUtils {
|
||||
|
||||
private const val DATABASE_PASSWORD_ALIAS: String = "databasePassword"
|
||||
|
||||
fun hasDatabase(filesDirectory: String): Boolean = File(filesDirectory + File.separator + "files_chat.db").exists()
|
||||
private fun hasDatabase(rootDir: String): Boolean =
|
||||
File(rootDir + File.separator + "files_chat.db").exists() && File(rootDir + File.separator + "files_agent.db").exists()
|
||||
|
||||
fun getDatabaseKey(): String? {
|
||||
return cryptor.decryptData(
|
||||
@@ -42,21 +43,21 @@ object DatabaseUtils {
|
||||
|
||||
fun migrateChatDatabase(useKey: String? = null): Pair<Boolean, DBMigrationResult> {
|
||||
Log.d(TAG, "migrateChatDatabase ${appPreferences.storeDBPassphrase.get()}")
|
||||
val dbPath = getFilesDirectory(SimplexApp.context)
|
||||
val dbAbsolutePathPrefix = getFilesDirectory(SimplexApp.context)
|
||||
var dbKey = ""
|
||||
val useKeychain = appPreferences.storeDBPassphrase.get()
|
||||
if (useKey != null) {
|
||||
dbKey = useKey
|
||||
} else if (useKeychain) {
|
||||
if (!hasDatabase(dbPath)) {
|
||||
if (!hasDatabase(SimplexApp.context.dataDir.absolutePath)) {
|
||||
dbKey = randomDatabasePassword()
|
||||
appPreferences.initialRandomDBPassphrase.set(true)
|
||||
} else {
|
||||
dbKey = getDatabaseKey() ?: ""
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "migrateChatDatabase DB path: $dbPath")
|
||||
val migrated = chatMigrateDB(dbPath, dbKey)
|
||||
Log.d(TAG, "migrateChatDatabase DB path: $dbAbsolutePathPrefix")
|
||||
val migrated = chatMigrateDB(dbAbsolutePathPrefix, dbKey)
|
||||
val res: DBMigrationResult = kotlin.runCatching {
|
||||
json.decodeFromString<DBMigrationResult>(migrated)
|
||||
}.getOrElse { DBMigrationResult.Unknown(migrated) }
|
||||
|
||||
@@ -603,6 +603,12 @@
|
||||
<string name="enter_passphrase">Введите пароль…</string>
|
||||
<string name="save_passphrase_and_open_chat">Сохранить пароль и открыть чат</string>
|
||||
<string name="open_chat">Открыть чат</string>
|
||||
<string name="database_backup_can_be_restored">Попытка поменять пароль базы данных не была завершена.</string>
|
||||
<string name="restore_database">Восстановить резервную копию</string>
|
||||
<string name="restore_database_alert_title">Восстановить резервную копию?</string>
|
||||
<string name="restore_database_alert_desc">Введите предыдущий пароль после восстановления резервной копии. Это действие нельзя отменить.</string>
|
||||
<string name="restore_database_alert_confirm">Восстановить</string>
|
||||
<string name="database_restore_error">Ошибка при восстановлении базы данных</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Чат остановлен</string>
|
||||
|
||||
@@ -604,6 +604,12 @@
|
||||
<string name="enter_passphrase">Enter passphrase…</string>
|
||||
<string name="save_passphrase_and_open_chat">Save passphrase and open chat</string>
|
||||
<string name="open_chat">Open chat</string>
|
||||
<string name="database_backup_can_be_restored">The attempt to change database passphrase was not completed.</string>
|
||||
<string name="restore_database">Restore database backup</string>
|
||||
<string name="restore_database_alert_title">Restore database backup?</string>
|
||||
<string name="restore_database_alert_desc">Please enter the previous password after restoring database backup. This action can not be undone.</string>
|
||||
<string name="restore_database_alert_confirm">Restore</string>
|
||||
<string name="database_restore_error">Restore database error</string>
|
||||
|
||||
<!-- ChatModel.chatRunning interactions -->
|
||||
<string name="chat_is_stopped_indication">Chat is stopped</string>
|
||||
|
||||
Reference in New Issue
Block a user