From 8e122584bfb6d7ac4a38f7c300135f15e5cce41d Mon Sep 17 00:00:00 2001 From: M3DZIK Date: Sat, 13 Jan 2024 19:11:45 +0100 Subject: [PATCH] Add support for changing email address --- .../medzik/librepass/android/ui/Navigation.kt | 10 ++ .../android/ui/components/TextInputField.kt | 5 + .../android/ui/screens/settings/Settings.kt | 4 +- .../ui/screens/settings/SettingsAccount.kt | 7 ++ .../screens/settings/account/ChangeEmail.kt | 115 ++++++++++++++++++ .../settings/account/ChangePassword.kt | 38 +----- .../screens/settings/account/DeleteAccount.kt | 29 ----- app/src/main/res/values-pl/strings.xml | 6 +- app/src/main/res/values/strings.xml | 2 + 9 files changed, 150 insertions(+), 66 deletions(-) create mode 100644 app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangeEmail.kt diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt b/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt index 6d5d0fb7..4272a608 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt @@ -52,6 +52,7 @@ import dev.medzik.librepass.android.ui.screens.settings.SettingsAccountScreen import dev.medzik.librepass.android.ui.screens.settings.SettingsAppearanceScreen import dev.medzik.librepass.android.ui.screens.settings.SettingsScreen import dev.medzik.librepass.android.ui.screens.settings.SettingsSecurityScreen +import dev.medzik.librepass.android.ui.screens.settings.account.SettingsAccountChangeEmailScreen import dev.medzik.librepass.android.ui.screens.settings.account.SettingsAccountChangePasswordScreen import dev.medzik.librepass.android.ui.screens.settings.account.SettingsAccountDeleteAccountScreen import dev.medzik.librepass.android.ui.screens.vault.CipherAddScreen @@ -255,6 +256,15 @@ enum class Screen( composable = { SettingsAccountScreen(it) }, noHorizontalPadding = true ), + SettingsAccountChangeEmail( + topBar = { + TopBar( + stringResource(R.string.ChangeEmail), + navigationIcon = { TopBarBackIcon(it) } + ) + }, + composable = { SettingsAccountChangeEmailScreen(it) } + ), SettingsAccountChangePassword( topBar = { TopBar( diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/components/TextInputField.kt b/app/src/main/java/dev/medzik/librepass/android/ui/components/TextInputField.kt index 0bc459ef..b89a760a 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/components/TextInputField.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/components/TextInputField.kt @@ -25,6 +25,7 @@ fun TextInputField( hidden: Boolean = false, value: String?, onValueChange: (String) -> Unit, + emptySupportingText: Boolean = false, isError: Boolean = false, errorMessage: String? = null, keyboardType: KeyboardType = KeyboardType.Text @@ -49,6 +50,10 @@ fun TextInputField( } } + if (emptySupportingText) { + supportingText = { Text(text = "") } + } + OutlinedTextField( value = value ?: "", onValueChange = onValueChange, diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/Settings.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/Settings.kt index f3502630..fa3a46e1 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/Settings.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/Settings.kt @@ -2,9 +2,9 @@ package dev.medzik.librepass.android.ui.screens.settings import androidx.compose.foundation.layout.Column import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.ColorLens import androidx.compose.material.icons.filled.Fingerprint +import androidx.compose.material.icons.filled.ManageAccounts import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource @@ -30,7 +30,7 @@ fun SettingsScreen(navController: NavController) { ) PreferenceEntry( - icon = { Icon(Icons.Default.AccountCircle, contentDescription = null) }, + icon = { Icon(Icons.Default.ManageAccounts, contentDescription = null) }, title = stringResource(R.string.Settings_Account), onClick = { navController.navigate(Screen.SettingsAccount) } ) diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsAccount.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsAccount.kt index 5de96623..b313c481 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsAccount.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/SettingsAccount.kt @@ -1,6 +1,7 @@ package dev.medzik.librepass.android.ui.screens.settings import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Email import androidx.compose.material.icons.filled.LockReset import androidx.compose.material.icons.filled.Logout import androidx.compose.material.icons.filled.NoAccounts @@ -21,6 +22,12 @@ fun SettingsAccountScreen( navController: NavController, viewModel: LibrePassViewModel = hiltViewModel() ) { + PreferenceEntry( + title = stringResource(R.string.ChangeEmail), + icon = { Icon(Icons.Default.Email, contentDescription = null) }, + onClick = { navController.navigate(Screen.SettingsAccountChangeEmail) } + ) + PreferenceEntry( title = stringResource(R.string.ChangePassword), icon = { Icon(Icons.Default.LockReset, contentDescription = null) }, diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangeEmail.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangeEmail.kt new file mode 100644 index 00000000..ae0f4433 --- /dev/null +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangeEmail.kt @@ -0,0 +1,115 @@ +package dev.medzik.librepass.android.ui.screens.settings.account + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.navigation.NavController +import dev.medzik.android.components.LoadingButton +import dev.medzik.android.components.navigate +import dev.medzik.android.components.rememberMutableBoolean +import dev.medzik.android.components.rememberMutableString +import dev.medzik.android.utils.runOnUiThread +import dev.medzik.librepass.android.R +import dev.medzik.librepass.android.ui.LibrePassViewModel +import dev.medzik.librepass.android.ui.Screen +import dev.medzik.librepass.android.ui.components.TextInputField +import dev.medzik.librepass.android.utils.showErrorToast +import dev.medzik.librepass.client.Server +import dev.medzik.librepass.client.api.UserClient +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking + +@Composable +fun SettingsAccountChangeEmailScreen( + navController: NavController, + viewModel: LibrePassViewModel = hiltViewModel() +) { + val context = LocalContext.current + val credentials = viewModel.credentialRepository.get() ?: return + + var newEmail by rememberMutableString() + var password by rememberMutableString() + var loading by rememberMutableBoolean() + + val scope = rememberCoroutineScope() + + val userClient = + UserClient( + email = credentials.email, + apiKey = credentials.apiKey, + apiUrl = credentials.apiUrl ?: Server.PRODUCTION + ) + + fun changeEmail( + newEmail: String, + password: String + ) { + loading = true + + // TODO: show error "invalid password" + + scope.launch(Dispatchers.IO) { + try { + userClient.changeEmail(newEmail, password) + + runBlocking { + viewModel.credentialRepository.drop() + viewModel.cipherRepository.drop(credentials.userId) + context + } + + runOnUiThread { + navController.navigate( + screen = Screen.Welcome, + disableBack = true + ) + } + } catch (e: Exception) { + e.showErrorToast(context) + + loading = false + } + } + } + + TextInputField( + label = stringResource(R.string.NewEmail), + value = newEmail, + onValueChange = { newEmail = it }, + isError = newEmail.isNotEmpty() && !newEmail.contains('@'), + errorMessage = stringResource(R.string.Error_InvalidEmail), + keyboardType = KeyboardType.Email + ) + + TextInputField( + label = stringResource(R.string.Password), + value = password, + onValueChange = { password = it }, + emptySupportingText = true, + hidden = true, + keyboardType = KeyboardType.Password, + ) + + LoadingButton( + loading = loading, + onClick = { changeEmail(newEmail, password) }, + enabled = newEmail.isNotEmpty() && newEmail.contains('@') && password.isNotEmpty(), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 40.dp, vertical = 8.dp) + ) { + Text(stringResource(R.string.ChangeEmail)) + } +} diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangePassword.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangePassword.kt index dcd275b4..da66d60b 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangePassword.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/ChangePassword.kt @@ -19,17 +19,13 @@ import dev.medzik.android.components.navigate import dev.medzik.android.components.rememberMutableBoolean import dev.medzik.android.components.rememberMutableString import dev.medzik.android.utils.runOnUiThread -import dev.medzik.libcrypto.Argon2 import dev.medzik.librepass.android.R import dev.medzik.librepass.android.ui.LibrePassViewModel import dev.medzik.librepass.android.ui.Screen import dev.medzik.librepass.android.ui.components.TextInputField -import dev.medzik.librepass.android.utils.SecretStore.getUserSecrets import dev.medzik.librepass.android.utils.showErrorToast import dev.medzik.librepass.client.Server import dev.medzik.librepass.client.api.UserClient -import dev.medzik.librepass.utils.Cryptography.computeAesKey -import dev.medzik.librepass.utils.Cryptography.computePasswordHash import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -41,10 +37,8 @@ fun SettingsAccountChangePasswordScreen( ) { val context = LocalContext.current val credentials = viewModel.credentialRepository.get() ?: return - val userSecrets = context.getUserSecrets() ?: return var oldPassword by rememberMutableString() - var oldPasswordInvalid by rememberMutableBoolean() var newPassword by rememberMutableString() var newPasswordConfirm by rememberMutableString() var newPasswordHint by rememberMutableString() @@ -59,36 +53,14 @@ fun SettingsAccountChangePasswordScreen( apiUrl = credentials.apiUrl ?: Server.PRODUCTION ) - fun resetPassword( + fun changePassword( oldPassword: String, newPassword: String, newPasswordHint: String ) { loading = true - oldPasswordInvalid = false scope.launch(Dispatchers.IO) { - // check old password - val oldPasswordHash = - computePasswordHash( - password = oldPassword, - email = credentials.email, - argon2Function = - Argon2( - 32, - credentials.parallelism, - credentials.memory, - credentials.iterations - ) - ) - val oldAesKey = computeAesKey(oldPasswordHash.hash) - - if (!oldAesKey.contentEquals(userSecrets.secretKey)) { - oldPasswordInvalid = true - loading = false - return@launch - } - try { userClient.changePassword(oldPassword, newPassword, newPasswordHint) @@ -117,8 +89,7 @@ fun SettingsAccountChangePasswordScreen( value = oldPassword, onValueChange = { oldPassword = it }, hidden = true, - isError = oldPasswordInvalid, - errorMessage = stringResource(R.string.Error_InvalidPassword), + emptySupportingText = true, keyboardType = KeyboardType.Password ) @@ -145,12 +116,13 @@ fun SettingsAccountChangePasswordScreen( TextInputField( label = stringResource(R.string.PasswordHint), value = newPasswordHint, - onValueChange = { newPasswordHint = it } + onValueChange = { newPasswordHint = it }, + emptySupportingText = true ) LoadingButton( loading = loading, - onClick = { resetPassword(oldPassword, newPassword, newPasswordHint) }, + onClick = { changePassword(oldPassword, newPassword, newPasswordHint) }, enabled = oldPassword.isNotEmpty() && newPassword.isNotEmpty() && newPasswordConfirm == newPassword, modifier = Modifier diff --git a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/DeleteAccount.kt b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/DeleteAccount.kt index bb8e2330..1cc96410 100644 --- a/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/DeleteAccount.kt +++ b/app/src/main/java/dev/medzik/librepass/android/ui/screens/settings/account/DeleteAccount.kt @@ -19,16 +19,13 @@ import dev.medzik.android.components.navigate import dev.medzik.android.components.rememberMutableBoolean import dev.medzik.android.components.rememberMutableString import dev.medzik.android.utils.runOnUiThread -import dev.medzik.libcrypto.Argon2 import dev.medzik.librepass.android.R import dev.medzik.librepass.android.ui.LibrePassViewModel import dev.medzik.librepass.android.ui.Screen import dev.medzik.librepass.android.ui.components.TextInputField -import dev.medzik.librepass.android.utils.SecretStore.getUserSecrets import dev.medzik.librepass.android.utils.showErrorToast import dev.medzik.librepass.client.Server import dev.medzik.librepass.client.api.UserClient -import dev.medzik.librepass.utils.Cryptography import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -40,11 +37,9 @@ fun SettingsAccountDeleteAccountScreen( ) { val context = LocalContext.current val credentials = viewModel.credentialRepository.get() ?: return - val userSecrets = context.getUserSecrets() ?: return var loading by rememberMutableBoolean() var password by rememberMutableString() - var passwordInvalid by rememberMutableBoolean() val scope = rememberCoroutineScope() val userClient = @@ -56,30 +51,8 @@ fun SettingsAccountDeleteAccountScreen( fun deleteAccount(password: String) { loading = true - passwordInvalid = false scope.launch(Dispatchers.IO) { - // check password - val passwordHash = - Cryptography.computePasswordHash( - password = password, - email = credentials.email, - argon2Function = - Argon2( - 32, - credentials.parallelism, - credentials.memory, - credentials.iterations - ) - ) - val aesKey = Cryptography.computeAesKey(passwordHash.hash) - - if (!aesKey.contentEquals(userSecrets.secretKey)) { - passwordInvalid = true - loading = false - return@launch - } - try { userClient.deleteAccount(password) @@ -108,8 +81,6 @@ fun SettingsAccountDeleteAccountScreen( value = password, onValueChange = { password = it }, hidden = true, - isError = passwordInvalid, - errorMessage = stringResource(R.string.Error_InvalidPassword), keyboardType = KeyboardType.Password ) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2d1f172d..55008420 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -58,8 +58,8 @@ Wyloguj się Nazwa Nowa hasło - Notes - State hasło + Notatka + Stare hasło Pozostałe informacje Hasło Generator hasła @@ -109,4 +109,6 @@ Adres URI jest nieprawidłowy Dane karty Klucz biometryczny został unieważniony przez Androida + Nowy adres e-mail + Zmień adres e-mail \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 141001e2..dcb43b52 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,4 +108,6 @@ URI address is invalid Card details Biometric key has been invalidated by Android + New e-mail address + Change e-mail address \ No newline at end of file