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:
Stanislav Dmitrenko
2022-09-15 22:59:54 +03:00
committed by GitHub
parent 98ccab394a
commit 568c9201d6
7 changed files with 88 additions and 7 deletions

View File

@@ -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) {

View File

@@ -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"
}

View File

@@ -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 -> {

View File

@@ -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() {

View File

@@ -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) }

View File

@@ -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>

View File

@@ -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>