desktop: setup passphrase during onboarding (#2987)

* desktop: setup passphrase during onboarding

* updated logic

* removed unused code

* button and starting chat action

* better

* removed debug code

* fallback

* focusing and moving focus on desktop text fields

* different logic

* removed unused variable

* divided logic in two functions

* enabled keyboard enter

* rollback when db deleted by hand on desktop

* update texts, font size

* stopping chat before other actions

---------

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2023-09-05 13:45:09 +03:00 committed by GitHub
parent 8aed568199
commit aff71c58d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 652 additions and 153 deletions

View File

@ -71,7 +71,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
}
Lifecycle.Event.ON_RESUME -> {
isAppOnForeground = true
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
}
/**
@ -80,7 +80,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
* It can happen when app was started and a user enables battery optimization while app in background
* */
if (chatModel.chatRunning.value != false &&
chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete &&
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
) {
SimplexService.start()
@ -191,7 +191,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
override fun androidChatInitializedAndStarted() {
// Prevents from showing "Enable notifications" alert when onboarding wasn't complete yet
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete) {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
SimplexService.showBackgroundServiceNoticeIfNeeded()
if (appPrefs.notificationsMode.get() == NotificationsMode.SERVICE)
withBGApi {

View File

@ -0,0 +1,106 @@
package chat.simplex.common.views.database
import SectionItemView
import SectionTextFooter
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.SimplexGreen
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SavePassphraseSetting(
useKeychain: Boolean,
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_keychain),
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(MR.strings.save_passphrase_in_keychain),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = useKeychain,
onCheckedChange = onCheckedChange,
enabled = !initialRandomDBPassphrase && !progressIndicator
)
}
}
}
@Composable
actual fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
initialRandomDBPassphrase: MutableState<Boolean>,
) {
if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
} else {
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
}
} else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database),
onConfirm = onConfirm,
destructive = true,
)
}
actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database),
onConfirm = onConfirm,
destructive = false,
)
}
actual fun removePassphraseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase),
onConfirm = onConfirm,
destructive = true,
)
}

View File

@ -32,8 +32,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.*
data class SettingsViewState(
val userPickerState: MutableStateFlow<AnimatedViewState>,
@ -64,7 +63,7 @@ fun MainScreen() {
if (
!chatModel.controller.appPrefs.laNoticeShown.get()
&& showAdvertiseLAAlert
&& chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete
&& chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete
&& chatModel.chats.isNotEmpty()
&& chatModel.activeCallInvitation.value == null
) {
@ -102,7 +101,10 @@ fun MainScreen() {
}
Box {
val onboarding = chatModel.onboardingStage.value
var onboarding by remember { mutableStateOf(chatModel.controller.appPrefs.onboardingStage.get()) }
LaunchedEffect(Unit) {
snapshotFlow { chatModel.controller.appPrefs.onboardingStage.state.value }.distinctUntilChanged().collect { onboarding = it }
}
val userCreated = chatModel.userCreated.value
var showInitializationView by remember { mutableStateOf(false) }
when {
@ -112,7 +114,7 @@ fun MainScreen() {
DatabaseErrorView(chatModel.chatDbStatus, chatModel.controller.appPrefs)
}
}
onboarding == null || userCreated == null -> SplashView()
remember { chatModel.chatDbEncrypted }.value == null || userCreated == null -> SplashView()
onboarding == OnboardingStage.OnboardingComplete && userCreated -> {
Box {
showAdvertiseLAAlert = true
@ -134,6 +136,7 @@ fun MainScreen() {
}
}
onboarding == OnboardingStage.Step2_CreateProfile -> CreateProfile(chatModel) {}
onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel)
onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel)
onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel)
}

View File

@ -38,7 +38,6 @@ import kotlin.time.*
@Stable
object ChatModel {
val controller: ChatController = ChatController
val onboardingStage = mutableStateOf<OnboardingStage?>(null)
val setDeliveryReceipts = mutableStateOf(false)
val currentUser = mutableStateOf<User?>(null)
val users = mutableStateListOf<UserInfo>()

View File

@ -50,17 +50,16 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
if (user == null) {
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo)
chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true)
chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo
chatModel.currentUser.value = null
chatModel.users.clear()
} else {
val savedOnboardingStage = appPreferences.onboardingStage.get()
chatModel.onboardingStage.value = if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
appPreferences.onboardingStage.set(if (listOf(OnboardingStage.Step1_SimpleXInfo, OnboardingStage.Step2_CreateProfile).contains(savedOnboardingStage) && chatModel.users.size == 1) {
OnboardingStage.Step3_CreateSimpleXAddress
} else {
savedOnboardingStage
}
if (chatModel.onboardingStage.value == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
})
if (appPreferences.onboardingStage.get() == OnboardingStage.OnboardingComplete && !chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.get()) {
chatModel.setDeliveryReceipts.value = true
}
chatController.startChat(user)

View File

@ -100,7 +100,7 @@ abstract class NtfManager {
if (chatModel.chatRunning.value == null) {
val step = 50L
for (i in 0..(timeout / step)) {
if (chatModel.chatRunning.value == true || chatModel.onboardingStage.value == OnboardingStage.Step1_SimpleXInfo) {
if (chatModel.chatRunning.value == true || chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.Step1_SimpleXInfo) {
break
}
delay(step)

View File

@ -24,6 +24,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.Profile
import chat.simplex.common.platform.appPlatform
import chat.simplex.common.platform.navigationBarsWithImePadding
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@ -88,14 +89,20 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
icon = painterResource(MR.images.ic_arrow_back_ios_new),
textDecoration = TextDecoration.None,
fontWeight = FontWeight.Medium
) { chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo }
) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) }
}
Spacer(Modifier.fillMaxWidth().weight(1f))
val enabled = displayName.value.isNotEmpty() && isValidDisplayName(displayName.value)
val createModifier: Modifier
val createColor: Color
if (enabled) {
createModifier = Modifier.clickable { createProfile(chatModel, displayName.value, fullName.value, close) }.padding(8.dp)
createModifier = Modifier.clickable {
if (chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) {
createProfileInProfiles(chatModel, displayName.value, fullName.value, close)
} else {
createProfileOnboarding(chatModel, displayName.value, fullName.value, close)
}
}.padding(8.dp)
createColor = MaterialTheme.colors.primary
} else {
createModifier = Modifier.padding(8.dp)
@ -116,7 +123,7 @@ fun CreateProfilePanel(chatModel: ChatModel, close: () -> Unit) {
}
}
fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
fun createProfileInProfiles(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi {
val user = chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
@ -125,16 +132,32 @@ fun createProfile(chatModel: ChatModel, displayName: String, fullName: String, c
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
chatModel.onboardingStage.value = OnboardingStage.Step3_CreateSimpleXAddress
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
close()
}
}
}
fun createProfileOnboarding(chatModel: ChatModel, displayName: String, fullName: String, close: () -> Unit) {
withApi {
chatModel.controller.apiCreateActiveUser(
Profile(displayName, fullName, null)
) ?: return@withApi
val onboardingStage = chatModel.controller.appPrefs.onboardingStage
if (chatModel.users.isEmpty()) {
onboardingStage.set(if (appPlatform.isDesktop && chatModel.controller.appPrefs.initialRandomDBPassphrase.get()) {
OnboardingStage.Step2_5_SetupDatabasePassphrase
} else {
OnboardingStage.Step3_CreateSimpleXAddress
})
} else {
// the next two lines are only needed for failure case when because of the database error the app gets stuck on on-boarding screen,
// this will get it unstuck.
chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
chatModel.onboardingStage.value = OnboardingStage.OnboardingComplete
onboardingStage.set(OnboardingStage.OnboardingComplete)
close()
}
}

View File

@ -30,6 +30,7 @@ import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.model.*
import chat.simplex.common.platform.appPlatform
import chat.simplex.res.MR
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.datetime.Clock
@ -61,46 +62,8 @@ fun DatabaseEncryptionView(m: ChatModel) {
initialRandomDBPassphrase,
progressIndicator,
onConfirmEncrypt = {
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 -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.wrong_passphrase_title),
generalGetString(MR.strings.enter_correct_current_passphrase)
)
}
}
error != null -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
"failed to set storage encryption: ${error.responseType} ${error.details}"
)
}
}
else -> {
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
}
}
}
} catch (e: Exception) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString())
}
}
encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator)
}
}
)
@ -143,17 +106,11 @@ fun DatabaseEncryptionLayout(
if (checked) {
setUseKeychain(true, useKeychain, prefs)
} else if (storedKey.value) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_keychain),
text = generalGetString(MR.strings.notifications_will_be_hidden) + "\n" + storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase),
onConfirm = {
DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs)
storedKey.value = false
},
destructive = true,
)
removePassphraseAlert {
DatabaseUtils.ksDatabasePassword.remove()
setUseKeychain(false, useKeychain, prefs)
storedKey.value = false
}
} else {
setUseKeychain(false, useKeychain, prefs)
}
@ -217,37 +174,13 @@ fun DatabaseEncryptionLayout(
}
Column {
if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.keychain_is_storing_securely))
if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
} else {
SectionTextFooter(generalGetString(MR.strings.keychain_allows_to_receive_ntfs))
}
} else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase)
}
SectionBottomSpacer()
}
}
fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database),
onConfirm = onConfirm,
destructive = true,
)
}
expect fun encryptDatabaseSavedAlert(onConfirm: () -> Unit)
fun encryptDatabaseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
@ -259,15 +192,7 @@ fun encryptDatabaseAlert(onConfirm: () -> Unit) {
)
}
fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database),
onConfirm = onConfirm,
destructive = false,
)
}
expect fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit)
fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
@ -279,37 +204,25 @@ fun changeDatabaseKeyAlert(onConfirm: () -> Unit) {
)
}
expect fun removePassphraseAlert(onConfirm: () -> Unit)
@Composable
fun SavePassphraseSetting(
expect fun SavePassphraseSetting(
useKeychain: Boolean,
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp = TextFieldDefaults.MinHeight,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_keychain),
tint = if (storedKey) SimplexGreen else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(MR.strings.save_passphrase_in_keychain),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = useKeychain,
onCheckedChange = onCheckedChange,
enabled = !initialRandomDBPassphrase && !progressIndicator
)
}
}
}
)
@Composable
expect fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
initialRandomDBPassphrase: MutableState<Boolean>,
)
fun resetFormAfterEncryption(
m: ChatModel,
@ -443,6 +356,62 @@ fun PassphraseField(
}
}
suspend fun encryptDatabase(
currentKey: MutableState<String>,
newKey: MutableState<String>,
confirmNewKey: MutableState<String>,
initialRandomDBPassphrase: MutableState<Boolean>,
useKeychain: MutableState<Boolean>,
storedKey: MutableState<Boolean>,
progressIndicator: MutableState<Boolean>
): Boolean {
val m = ChatModel
val prefs = ChatController.appPrefs
progressIndicator.value = true
return 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 -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.wrong_passphrase_title),
generalGetString(MR.strings.enter_correct_current_passphrase)
)
}
false
}
error != null -> {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database),
"failed to set storage encryption: ${error.responseType} ${error.details}"
)
}
false
}
else -> {
prefs.initialRandomDBPassphrase.set(false)
initialRandomDBPassphrase.value = false
if (useKeychain.value) {
DatabaseUtils.ksDatabasePassword.set(newKey.value)
}
resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value)
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted))
}
true
}
}
} catch (e: Exception) {
operationEnded(m, progressIndicator) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_encrypting_database), e.stackTraceToString())
}
false
}
}
// based on https://generatepasswords.org/how-to-calculate-entropy/
private fun passphraseEntropy(s: String): Double {
var hasDigits = false

View File

@ -12,6 +12,9 @@ import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.AppPreferences
@ -252,6 +255,11 @@ private fun mtrErrorDescription(err: MTRError): String =
@Composable
private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onClick: (() -> Unit)? = null) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
delay(100L)
focusRequester.requestFocus()
}
PassphraseField(
text,
generalGetString(MR.strings.enter_passphrase),
@ -259,7 +267,15 @@ private fun DatabaseKeyField(text: MutableState<String>, enabled: Boolean, onCli
keyboardActions = KeyboardActions(onDone = if (enabled) {
{ onClick?.invoke() }
} else null
)
),
modifier = Modifier.focusRequester(focusRequester).onPreviewKeyEvent {
if (onClick != null && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
onClick()
true
} else {
false
}
}
)
}

View File

@ -73,6 +73,7 @@ fun DatabaseView(
m.chatDbChanged.value,
useKeychain.value,
m.chatDbEncrypted.value,
m.controller.appPrefs.storeDBPassphrase.state.value,
m.controller.appPrefs.initialRandomDBPassphrase,
importArchiveLauncher,
chatArchiveName,
@ -122,6 +123,7 @@ fun DatabaseLayout(
chatDbChanged: Boolean,
useKeyChain: Boolean,
chatDbEncrypted: Boolean?,
passphraseSaved: Boolean,
initialRandomDBPassphrase: SharedPreference<Boolean>,
importArchiveLauncher: FileChooserLauncher,
chatArchiveName: MutableState<String?>,
@ -182,7 +184,7 @@ fun DatabaseLayout(
else painterResource(MR.images.ic_lock),
stringResource(MR.strings.database_passphrase),
click = showSettingsModal() { DatabaseEncryptionView(it) },
iconColor = if (unencrypted) WarningOrange else MaterialTheme.colors.secondary,
iconColor = if (unencrypted || (appPlatform.isDesktop && passphraseSaved)) WarningOrange else MaterialTheme.colors.secondary,
disabled = operationsDisabled
)
SettingsActionItem(
@ -657,6 +659,7 @@ fun PreviewDatabaseLayout() {
chatDbChanged = false,
useKeyChain = false,
chatDbEncrypted = false,
passphraseSaved = false,
initialRandomDBPassphrase = SharedPreference({ true }, {}),
importArchiveLauncher = rememberFileChooserLauncher(true) {},
chatArchiveName = remember { mutableStateOf("dummy_archive") },

View File

@ -101,6 +101,10 @@ class AlertManager {
Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING).padding(bottom = DEFAULT_PADDING_HALF),
horizontalArrangement = Arrangement.SpaceBetween
) {
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
TextButton(onClick = {
onDismiss?.invoke()
hideAlert()
@ -108,7 +112,7 @@ class AlertManager {
TextButton(onClick = {
onConfirm?.invoke()
hideAlert()
}) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}, Modifier.focusRequester(focusRequester)) { Text(confirmText, color = if (destructive) MaterialTheme.colors.error else Color.Unspecified) }
}
},
shape = RoundedCornerShape(corner = CornerSize(25.dp))

View File

@ -54,6 +54,12 @@ object DatabaseUtils {
} else {
dbKey = ksDatabasePassword.get() ?: ""
}
} else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) {
// In case of database was deleted by hand
dbKey = randomDatabasePassword()
ksDatabasePassword.set(dbKey)
appPreferences.initialRandomDBPassphrase.set(true)
appPreferences.storeDBPassphrase.set(true)
}
return dbKey
}

View File

@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
@ -66,11 +67,13 @@ fun SimpleButton(
fun SimpleButtonIconEnded(
text: String,
icon: Painter,
style: TextStyle = MaterialTheme.typography.caption,
color: Color = MaterialTheme.colors.primary,
disabled: Boolean = false,
click: () -> Unit
) {
SimpleButtonFrame(click) {
Text(text, style = MaterialTheme.typography.caption, color = color)
SimpleButtonFrame(click, disabled = disabled) {
Text(text, style = style, color = color)
Icon(
icon, text, tint = color,
modifier = Modifier.padding(start = 8.dp)

View File

@ -66,7 +66,6 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
val createdUser = m.controller.apiCreateActiveUser(profile, pastTimestamp = true)
m.currentUser.value = createdUser
m.controller.appPrefs.onboardingStage.set(OnboardingStage.OnboardingComplete)
m.onboardingStage.value = OnboardingStage.OnboardingComplete
if (createdUser != null) {
m.controller.startChat(createdUser)
}

View File

@ -14,8 +14,7 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.ChatModel
import chat.simplex.common.model.UserContactLinkRec
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
@ -29,6 +28,10 @@ fun CreateSimpleXAddress(m: ChatModel) {
val clipboard = LocalClipboardManager.current
val uriHandler = LocalUriHandler.current
LaunchedEffect(Unit) {
prepareChatBeforeAddressCreation()
}
CreateSimpleXAddressLayout(
userAddress.value,
share = { address: String -> clipboard.shareText(address) },
@ -63,7 +66,6 @@ fun CreateSimpleXAddress(m: ChatModel) {
OnboardingStage.OnboardingComplete
}
m.controller.appPrefs.onboardingStage.set(next)
m.onboardingStage.value = next
},
)
@ -172,3 +174,19 @@ private fun ProgressIndicator() {
)
}
}
private fun prepareChatBeforeAddressCreation() {
if (chatModel.users.isNotEmpty()) return
withApi {
val user = chatModel.controller.apiGetActiveUser() ?: return@withApi
chatModel.currentUser.value = user
if (chatModel.users.isEmpty()) {
chatModel.controller.startChat(user)
} else {
val users = chatModel.controller.listUsers()
chatModel.users.clear()
chatModel.users.addAll(users)
chatModel.controller.getUserChatData()
}
}
}

View File

@ -13,8 +13,7 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import chat.simplex.common.model.ChatController
import chat.simplex.common.model.User
import chat.simplex.common.model.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chat.item.MarkdownText
import chat.simplex.common.views.helpers.*
@ -22,7 +21,7 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
@Composable
fun HowItWorks(user: User?, onboardingStage: MutableState<OnboardingStage?>? = null) {
fun HowItWorks(user: User?, onboardingStage: SharedPreference<OnboardingStage>? = null) {
Column(Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING),

View File

@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
enum class OnboardingStage {
Step1_SimpleXInfo,
Step2_CreateProfile,
Step2_5_SetupDatabasePassphrase,
Step3_CreateSimpleXAddress,
Step4_SetNotificationsMode,
OnboardingComplete

View File

@ -41,7 +41,7 @@ fun SetNotificationsMode(m: ChatModel) {
}
Spacer(Modifier.fillMaxHeight().weight(1f))
Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) {
OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, m.onboardingStage, false) {
OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, false) {
changeNotificationsMode(currentMode.value, m)
}
}

View File

@ -0,0 +1,233 @@
package chat.simplex.common.views.onboarding
import SectionBottomSpacer
import SectionItemView
import SectionItemViewSpaceBetween
import SectionTextFooter
import SectionView
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.key.*
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.database.*
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
@Composable
fun SetupDatabasePassphrase(m: ChatModel) {
val progressIndicator = remember { mutableStateOf(false) }
val prefs = m.controller.appPrefs
val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) }
val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) }
// Do not do rememberSaveable on current key to prevent saving it on disk in clear text
val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") }
val newKey = rememberSaveable { mutableStateOf("") }
val confirmNewKey = rememberSaveable { mutableStateOf("") }
fun nextStep() {
m.controller.appPrefs.onboardingStage.set(OnboardingStage.Step3_CreateSimpleXAddress)
}
SetupDatabasePassphraseLayout(
currentKey,
newKey,
confirmNewKey,
progressIndicator,
onConfirmEncrypt = {
withApi {
if (m.chatRunning.value == true) {
// Stop chat if it's started before doing anything
stopChatAsync(m)
}
prefs.storeDBPassphrase.set(false)
val newKeyValue = newKey.value
val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator)
if (success) {
startChat(newKeyValue)
nextStep()
} else {
// Rollback in case of it is finished with error in order to allow to repeat the process again
prefs.storeDBPassphrase.set(true)
}
}
},
nextStep = ::nextStep,
)
if (progressIndicator.value) {
ProgressIndicator()
}
DisposableEffect(Unit) {
onDispose {
if (m.chatRunning.value != true) {
withBGApi {
val user = chatController.apiGetActiveUser()
if (user != null) {
m.controller.startChat(user)
}
}
}
}
}
}
@Composable
private fun SetupDatabasePassphraseLayout(
currentKey: MutableState<String>,
newKey: MutableState<String>,
confirmNewKey: MutableState<String>,
progressIndicator: MutableState<Boolean>,
onConfirmEncrypt: () -> Unit,
nextStep: () -> Unit,
) {
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(top = DEFAULT_PADDING),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AppBarTitle(stringResource(MR.strings.setup_database_passphrase))
Spacer(Modifier.weight(1f))
Column(Modifier.width(600.dp)) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
LaunchedEffect(Unit) {
delay(100L)
focusRequester.requestFocus()
}
PassphraseField(
newKey,
generalGetString(MR.strings.new_passphrase),
modifier = Modifier
.padding(horizontal = DEFAULT_PADDING)
.focusRequester(focusRequester)
.onPreviewKeyEvent {
if (it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
focusManager.moveFocus(FocusDirection.Down)
true
} else {
false
}
},
showStrength = true,
isValid = ::validKey,
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
)
val onClickUpdate = {
// Don't do things concurrently. Shouldn't be here concurrently, just in case
if (!progressIndicator.value) {
encryptDatabaseAlert(onConfirmEncrypt)
}
}
val disabled = currentKey.value == newKey.value ||
newKey.value != confirmNewKey.value ||
newKey.value.isEmpty() ||
!validKey(currentKey.value) ||
!validKey(newKey.value) ||
progressIndicator.value
PassphraseField(
confirmNewKey,
generalGetString(MR.strings.confirm_new_passphrase),
modifier = Modifier
.padding(horizontal = DEFAULT_PADDING)
.onPreviewKeyEvent {
if (!disabled && it.key == Key.Enter && it.type == KeyEventType.KeyUp) {
onClickUpdate()
true
} else {
false
}
},
isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value },
keyboardActions = KeyboardActions(onDone = {
if (!disabled) onClickUpdate()
defaultKeyboardAction(ImeAction.Done)
}),
)
Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) {
SetPassphraseButton(disabled, onClickUpdate)
}
Column {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
Spacer(Modifier.weight(1f))
SkipButton(progressIndicator.value, nextStep)
SectionBottomSpacer()
}
}
@Composable
private fun SetPassphraseButton(disabled: Boolean, onClick: () -> Unit) {
SimpleButtonIconEnded(
stringResource(MR.strings.set_database_passphrase),
painterResource(MR.images.ic_check),
style = MaterialTheme.typography.h2,
color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
disabled = disabled,
click = onClick
)
}
@Composable
private fun SkipButton(disabled: Boolean, onClick: () -> Unit) {
SimpleButtonIconEnded(stringResource(MR.strings.use_random_passphrase), painterResource(MR.images.ic_chevron_right), color =
if (disabled) MaterialTheme.colors.secondary else WarningOrange, disabled = disabled, click = onClick)
Text(
stringResource(MR.strings.you_can_change_it_later),
Modifier
.fillMaxWidth()
.padding(horizontal = DEFAULT_PADDING * 3),
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.secondary,
textAlign = TextAlign.Center,
)
}
@Composable
private fun ProgressIndicator() {
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
Modifier
.padding(horizontal = 2.dp)
.size(30.dp),
color = MaterialTheme.colors.secondary,
strokeWidth = 3.dp
)
}
}
private suspend fun startChat(key: String?) {
val m = ChatModel
initChatController(key)
m.chatDbChanged.value = false
m.chatRunning.value = true
}

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
@ -25,7 +24,7 @@ import dev.icerock.moko.resources.StringResource
fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
SimpleXInfoLayout(
user = chatModel.currentUser.value,
onboardingStage = if (onboarding) chatModel.onboardingStage else null,
onboardingStage = if (onboarding) chatModel.controller.appPrefs.onboardingStage else null,
showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } },
)
}
@ -33,7 +32,7 @@ fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) {
@Composable
fun SimpleXInfoLayout(
user: User?,
onboardingStage: MutableState<OnboardingStage?>?,
onboardingStage: SharedPreference<OnboardingStage>?,
showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit),
) {
Column(
@ -100,11 +99,11 @@ private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResour
}
@Composable
fun OnboardingActionButton(user: User?, onboardingStage: MutableState<OnboardingStage?>, onclick: (() -> Unit)? = null) {
fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference<OnboardingStage>, onclick: (() -> Unit)? = null) {
if (user == null) {
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onboardingStage, true, onclick)
OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick)
} else {
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onboardingStage, true, onclick)
OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick)
}
}
@ -112,7 +111,6 @@ fun OnboardingActionButton(user: User?, onboardingStage: MutableState<Onboarding
fun OnboardingActionButton(
labelId: StringResource,
onboarding: OnboardingStage?,
onboardingStage: MutableState<OnboardingStage?>,
border: Boolean,
onclick: (() -> Unit)?
) {
@ -129,7 +127,6 @@ fun OnboardingActionButton(
SimpleButtonFrame(click = {
onclick?.invoke()
onboardingStage.value = onboarding
if (onboarding != null) {
ChatController.appPrefs.onboardingStage.set(onboarding)
}

View File

@ -43,6 +43,7 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt
profile = user.profile,
stopped,
chatModel.chatDbEncrypted.value == true,
remember { chatModel.controller.appPrefs.storeDBPassphrase.state }.value,
remember { chatModel.controller.appPrefs.notificationsMode.state },
user.displayName,
setPerformLA = setPerformLA,
@ -115,6 +116,7 @@ fun SettingsLayout(
profile: LocalProfile,
stopped: Boolean,
encrypted: Boolean,
passphraseSaved: Boolean,
notificationsMode: State<NotificationsMode>,
userDisplayName: String,
setPerformLA: (Boolean) -> Unit,
@ -162,7 +164,7 @@ fun SettingsLayout(
SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true)
DatabaseItem(encrypted, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped)
}
SectionDividerSpaced()
@ -207,7 +209,7 @@ expect fun SettingsSectionApp(
withAuth: (title: String, desc: String, block: () -> Unit) -> Unit
)
@Composable private fun DatabaseItem(encrypted: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
@Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) {
SectionItemViewWithIcon(openDatabaseView) {
Row(
Modifier.fillMaxWidth(),
@ -217,7 +219,7 @@ expect fun SettingsSectionApp(
Icon(
painterResource(MR.images.ic_database),
contentDescription = stringResource(MR.strings.database_passphrase_and_export),
tint = if (encrypted) MaterialTheme.colors.secondary else WarningOrange,
tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange,
)
TextIconSpaced(true)
Text(stringResource(MR.strings.database_passphrase_and_export))
@ -473,6 +475,7 @@ fun PreviewSettingsLayout() {
profile = LocalProfile.sampleData,
stopped = false,
encrypted = false,
passphraseSaved = false,
notificationsMode = remember { mutableStateOf(NotificationsMode.OFF) },
userDisplayName = "Alice",
setPerformLA = { _ -> },

View File

@ -769,6 +769,11 @@
<string name="onboarding_notifications_mode_periodic_desc"><![CDATA[<b>Good for battery</b>. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]></string>
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Uses more battery</b>! Background service always runs notifications are shown as soon as messages are available.]]></string>
<!-- SetupDatabasePassphrase.kt -->
<string name="setup_database_passphrase">Setup database passphrase</string>
<string name="you_can_change_it_later">Random passphrase is stored in settings as plaintext.\nYou can change it later.</string>
<string name="use_random_passphrase">Use random passphrase</string>
<!-- MakeConnection -->
<string name="paste_the_link_you_received">Paste received link</string>
@ -984,9 +989,11 @@
<!-- DatabaseEncryptionView.kt -->
<string name="save_passphrase_in_keychain">Save passphrase in Keystore</string>
<string name="save_passphrase_in_settings">Save passphrase in settings</string>
<string name="database_encrypted">Database encrypted!</string>
<string name="error_encrypting_database">Error encrypting database</string>
<string name="remove_passphrase_from_keychain">Remove passphrase from Keystore?</string>
<string name="remove_passphrase_from_settings">Remove passphrase from settings?</string>
<string name="notifications_will_be_hidden">Notifications will be delivered only until the app stops!</string>
<string name="remove_passphrase">Remove</string>
<string name="encrypt_database">Encrypt</string>
@ -995,18 +1002,23 @@
<string name="new_passphrase">New passphrase…</string>
<string name="confirm_new_passphrase">Confirm new passphrase…</string>
<string name="update_database_passphrase">Update database passphrase</string>
<string name="set_database_passphrase">Set database passphrase</string>
<string name="enter_correct_current_passphrase">Please enter correct current passphrase.</string>
<string name="database_is_not_encrypted">Your chat database is not encrypted - set passphrase to protect it.</string>
<string name="keychain_is_storing_securely">Android Keystore is used to securely store passphrase - it allows notification service to work.</string>
<string name="settings_is_storing_in_clear_text">The passphrase is stored in settings as plaintext.</string>
<string name="encrypted_with_random_passphrase">Database is encrypted using a random passphrase, you can change it.</string>
<string name="impossible_to_recover_passphrase"><![CDATA[<b>Please note</b>: you will NOT be able to recover or change passphrase if you lose it.]]></string>
<string name="keychain_allows_to_receive_ntfs">Android Keystore will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving notifications.</string>
<string name="passphrase_will_be_saved_in_settings">The passphrase will be stored in settings as plaintext after you change it or restart the app.</string>
<string name="you_have_to_enter_passphrase_every_time">You have to enter passphrase every time the app starts - it is not stored on the device.</string>
<string name="encrypt_database_question">Encrypt database?</string>
<string name="change_database_passphrase_question">Change database passphrase?</string>
<string name="database_will_be_encrypted">Database will be encrypted.</string>
<string name="database_will_be_encrypted_and_passphrase_stored">Database will be encrypted and the passphrase stored in the Keystore.</string>
<string name="database_will_be_encrypted_and_passphrase_stored_in_settings">Database will be encrypted and the passphrase stored in settings.</string>
<string name="database_encryption_will_be_updated">Database encryption passphrase will be updated and stored in the Keystore.</string>
<string name="database_encryption_will_be_updated_in_settings">Database encryption passphrase will be updated and stored in settings.</string>
<string name="database_passphrase_will_be_updated">Database encryption passphrase will be updated.</string>
<string name="store_passphrase_securely">Please store passphrase securely, you will NOT be able to change it if you lose it.</string>
<string name="store_passphrase_securely_without_recover">Please store passphrase securely, you will NOT be able to access chat if you lose it.</string>

View File

@ -0,0 +1,106 @@
package chat.simplex.common.views.database
import SectionItemView
import SectionTextFooter
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import chat.simplex.common.ui.theme.WarningOrange
import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
actual fun SavePassphraseSetting(
useKeychain: Boolean,
initialRandomDBPassphrase: Boolean,
storedKey: Boolean,
progressIndicator: Boolean,
minHeight: Dp,
onCheckedChange: (Boolean) -> Unit,
) {
SectionItemView(minHeight = minHeight) {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
if (storedKey) painterResource(MR.images.ic_vpn_key_filled) else painterResource(MR.images.ic_vpn_key_off_filled),
stringResource(MR.strings.save_passphrase_in_settings),
tint = if (storedKey) WarningOrange else MaterialTheme.colors.secondary
)
Spacer(Modifier.padding(horizontal = 4.dp))
Text(
stringResource(MR.strings.save_passphrase_in_settings),
Modifier.padding(end = 24.dp),
color = Color.Unspecified
)
Spacer(Modifier.fillMaxWidth().weight(1f))
DefaultSwitch(
checked = useKeychain,
onCheckedChange = onCheckedChange,
enabled = !initialRandomDBPassphrase && !progressIndicator
)
}
}
}
@Composable
actual fun DatabaseEncryptionFooter(
useKeychain: MutableState<Boolean>,
chatDbEncrypted: Boolean?,
storedKey: MutableState<Boolean>,
initialRandomDBPassphrase: MutableState<Boolean>,
) {
if (chatDbEncrypted == false) {
SectionTextFooter(generalGetString(MR.strings.database_is_not_encrypted))
} else if (useKeychain.value) {
if (storedKey.value) {
SectionTextFooter(generalGetString(MR.strings.settings_is_storing_in_clear_text))
if (initialRandomDBPassphrase.value) {
SectionTextFooter(generalGetString(MR.strings.encrypted_with_random_passphrase))
} else {
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
} else {
SectionTextFooter(generalGetString(MR.strings.passphrase_will_be_saved_in_settings))
}
} else {
SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time))
SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase))
}
}
actual fun encryptDatabaseSavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.encrypt_database_question),
text = generalGetString(MR.strings.database_will_be_encrypted_and_passphrase_stored_in_settings) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.encrypt_database),
onConfirm = onConfirm,
destructive = true,
)
}
actual fun changeDatabaseKeySavedAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.change_database_passphrase_question),
text = generalGetString(MR.strings.database_encryption_will_be_updated_in_settings) + "\n" + storeSecurelySaved(),
confirmText = generalGetString(MR.strings.update_database),
onConfirm = onConfirm,
destructive = false,
)
}
actual fun removePassphraseAlert(onConfirm: () -> Unit) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.remove_passphrase_from_settings),
text = storeSecurelyDanger(),
confirmText = generalGetString(MR.strings.remove_passphrase),
onConfirm = onConfirm,
destructive = true,
)
}