Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
Add support for changing email address
Browse files Browse the repository at this point in the history
  • Loading branch information
M3DZIK committed Jan 13, 2024
1 parent ed189e0 commit 8e12258
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 66 deletions.
10 changes: 10 additions & 0 deletions app/src/main/java/dev/medzik/librepass/android/ui/Navigation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -49,6 +50,10 @@ fun TextInputField(
}
}

if (emptySupportingText) {
supportingText = { Text(text = "") }
}

OutlinedTextField(
value = value ?: "",
onValueChange = onValueChange,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) }
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) },
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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)

Expand Down Expand Up @@ -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
)

Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 =
Expand All @@ -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)

Expand Down Expand Up @@ -108,8 +81,6 @@ fun SettingsAccountDeleteAccountScreen(
value = password,
onValueChange = { password = it },
hidden = true,
isError = passwordInvalid,
errorMessage = stringResource(R.string.Error_InvalidPassword),
keyboardType = KeyboardType.Password
)

Expand Down
6 changes: 4 additions & 2 deletions app/src/main/res/values-pl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@
<string name="Logout">Wyloguj się</string>
<string name="Name">Nazwa</string>
<string name="NewPassword">Nowa hasło</string>
<string name="Notes">Notes</string>
<string name="OldPassword">State hasło</string>
<string name="Notes">Notatka</string>
<string name="OldPassword">Stare hasło</string>
<string name="OtherDetails">Pozostałe informacje</string>
<string name="Password">Hasło</string>
<string name="PasswordGenerator">Generator hasła</string>
Expand Down Expand Up @@ -109,4 +109,6 @@
<string name="Error_InvalidURI">Adres URI jest nieprawidłowy</string>
<string name="CardDetails">Dane karty</string>
<string name="BiometricKeyInvalidated">Klucz biometryczny został unieważniony przez Androida</string>
<string name="NewEmail">Nowy adres e-mail</string>
<string name="ChangeEmail">Zmień adres e-mail</string>
</resources>
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@
<string name="Error_InvalidURI">URI address is invalid</string>
<string name="CardDetails">Card details</string>
<string name="BiometricKeyInvalidated">Biometric key has been invalidated by Android</string>
<string name="NewEmail">New e-mail address</string>
<string name="ChangeEmail">Change e-mail address</string>
</resources>

0 comments on commit 8e12258

Please sign in to comment.