From 487a28ad320473ce06324b0e94835d80fdccadc4 Mon Sep 17 00:00:00 2001 From: SAUL Date: Mon, 2 Dec 2024 13:03:45 +0400 Subject: [PATCH] Password Decryption Support --- src/main/kotlin/core/AppState.kt | 28 ++++++ .../core/security/AuthenticationManager.kt | 3 +- src/main/kotlin/di/AppModule.kt | 2 +- .../ui/components/FormScreenComponents.kt | 7 +- .../components/dialog/MasterPasswordDialog.kt | 90 +++++++++++++++++++ .../secvault/passwordinfo/CredentialForms.kt | 6 +- src/main/kotlin/ui/screens/SecVaultScreen.kt | 13 +++ src/main/kotlin/viewmodel/LoginScreenModel.kt | 8 +- .../viewmodel/PasswordMgntScreenModel.kt | 4 +- .../kotlin/viewmodel/SecVaultScreenModel.kt | 20 +++++ 10 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/ui/components/dialog/MasterPasswordDialog.kt diff --git a/src/main/kotlin/core/AppState.kt b/src/main/kotlin/core/AppState.kt index 5193cbe..dd7de5f 100644 --- a/src/main/kotlin/core/AppState.kt +++ b/src/main/kotlin/core/AppState.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import cafe.adriel.voyager.core.screen.Screen +import core.security.MasterPasswordManager import repository.user.User import ui.screens.LoginScreen import ui.screens.LoginSplashScreen @@ -53,6 +54,33 @@ class AppState { masterPassword = null } + fun isMasterPasswordPresent(): Boolean { + return masterPassword != null + } + + fun encryptString(text: String?): String { + return text?.let { + MasterPasswordManager.encryptString( + it, + MasterPasswordManager.getKey( + MasterPasswordManager.convertToUnsecureString(this.fetchMasterPassword()!!) + ) + ) + } ?: "" + } + + fun decryptPassword(text: String?): String { + return text?.let { + MasterPasswordManager.decryptString( + it, + MasterPasswordManager.getKey( + MasterPasswordManager.convertToUnsecureString(this.fetchMasterPassword()!!) + ) + ) + } ?: "" + } + + private val isAuthenticated: Boolean get() = currentUser != null diff --git a/src/main/kotlin/core/security/AuthenticationManager.kt b/src/main/kotlin/core/security/AuthenticationManager.kt index 6cc4da9..5ceccde 100644 --- a/src/main/kotlin/core/security/AuthenticationManager.kt +++ b/src/main/kotlin/core/security/AuthenticationManager.kt @@ -21,12 +21,13 @@ class AuthenticationManager( loadUserFromToken() } - suspend fun login(username: String, password: String): Result { + suspend fun login(username: String, password: String, masterPassword: String): Result { delay(200) return userRepository.findByUsername(username).let { result -> if (result is Result.Success && BCrypt.checkpw(password, result.data.password)) { result.data.also { appState.updateCurrentUser(it) + appState.initializeMasterPassword(masterPassword.toCharArray()) TokenManager.saveToken(jwtService.generateToken(it)) }.let { user -> Result.Success(user) diff --git a/src/main/kotlin/di/AppModule.kt b/src/main/kotlin/di/AppModule.kt index 1ef42cd..c10ad6a 100644 --- a/src/main/kotlin/di/AppModule.kt +++ b/src/main/kotlin/di/AppModule.kt @@ -69,7 +69,7 @@ val viewModelModule = module { factory { ForgotPasswordScreenModel(get()) } - factory { SecVaultScreenModel(get(), get(), get()) } + factory { SecVaultScreenModel(get(), get(), get(), get { parametersOf(SecVaultScreenModel::class.java) }) } factory { PasswordMgntScreenModel(get(), get(), get(), get()) } diff --git a/src/main/kotlin/ui/components/FormScreenComponents.kt b/src/main/kotlin/ui/components/FormScreenComponents.kt index 963d077..26e9810 100644 --- a/src/main/kotlin/ui/components/FormScreenComponents.kt +++ b/src/main/kotlin/ui/components/FormScreenComponents.kt @@ -27,16 +27,17 @@ fun LoginScreenContent( var username by remember { mutableStateOf("") } var password by remember { mutableStateOf("") } + var masterPassword by remember { mutableStateOf("") } LoginForm( username = username, onUsernameChange = { username = it }, password = password, onPasswordChange = { password = it }, - masterPassword = password, - onMasterPassword = { password = it }, + masterPassword = masterPassword, + onMasterPassword = { masterPassword = it }, loginState = loginState, - onLoginClick = { screenModel.login(username, password) }, + onLoginClick = { screenModel.login(username, password, masterPassword) }, onForgotPasswordClick = { navigator?.push(ForgotPasswordScreen()) }, onRegisterClick = { if (navigator?.canPop == true) navigator.pop() else navigator?.push(RegisterScreen()) diff --git a/src/main/kotlin/ui/components/dialog/MasterPasswordDialog.kt b/src/main/kotlin/ui/components/dialog/MasterPasswordDialog.kt new file mode 100644 index 0000000..a6084ee --- /dev/null +++ b/src/main/kotlin/ui/components/dialog/MasterPasswordDialog.kt @@ -0,0 +1,90 @@ +package ui.components.dialog + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import ui.components.PasswordTextField +import ui.components.SecVaultDialog +import ui.theme.Font +import ui.theme.secondary +import ui.theme.tertiary +import viewmodel.SecVaultScreenModel + +@Composable +fun MasterPasswordDialog( + viewModel: SecVaultScreenModel, + dialogState: MutableState +) { + var masterPassword by remember { mutableStateOf("") } + + SecVaultDialog( + onDismissRequest = { dialogState.value = false }, + modifier = Modifier.fillMaxWidth() + .width(50.dp) + .height(400.dp), + roundedSize = 20.dp, + backgroundColor = tertiary + ) { + + Column( + modifier = Modifier.fillMaxSize() + .fillMaxHeight() + .background(color = tertiary), + verticalArrangement = Arrangement.spacedBy(15.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Enter your master password.", + color = Color.White, + fontFamily = Font.RussoOne, + fontWeight = FontWeight.Normal, + fontSize = 20.sp, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(10.dp)) + PasswordTextField( + value = masterPassword, + onValueChange = { masterPassword = it }, + label = "Master Password", + modifier = Modifier.height(40.dp).width(360.dp) + ) + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Button( + enabled = true, + onClick = { + viewModel.setMasterPassword(masterPassword) + dialogState.value = false + }, + modifier = Modifier.width(175.dp), + colors = ButtonColors( + containerColor = secondary, + contentColor = Color.White, + disabledContentColor = secondary, + disabledContainerColor = secondary + ) + ) + { + Text( + text = "Apply", + fontStyle = FontStyle.Normal, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + fontFamily = Font.RussoOne + ) + } + } + } + + } +} \ No newline at end of file diff --git a/src/main/kotlin/ui/components/secvault/passwordinfo/CredentialForms.kt b/src/main/kotlin/ui/components/secvault/passwordinfo/CredentialForms.kt index 1507d83..bf1b40c 100644 --- a/src/main/kotlin/ui/components/secvault/passwordinfo/CredentialForms.kt +++ b/src/main/kotlin/ui/components/secvault/passwordinfo/CredentialForms.kt @@ -17,7 +17,11 @@ fun PasswordCredentialForm(screenModel: SecVaultScreenModel) { } var password by remember(selectedCredential) { - mutableStateOf(selectedCredential.password?.password ?: "") + mutableStateOf( + selectedCredential.password?.password?.let { + screenModel.decryptPassword(it) + } ?: "" + ) } var email by remember(selectedCredential) { diff --git a/src/main/kotlin/ui/screens/SecVaultScreen.kt b/src/main/kotlin/ui/screens/SecVaultScreen.kt index 52e5e8f..81a1358 100644 --- a/src/main/kotlin/ui/screens/SecVaultScreen.kt +++ b/src/main/kotlin/ui/screens/SecVaultScreen.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel @@ -12,6 +14,7 @@ import com.dokar.sonner.Toaster import com.dokar.sonner.rememberToasterState import core.models.UiState import ui.components.LoadingScreen +import ui.components.dialog.MasterPasswordDialog import ui.components.secvault.SecVaultContentScreen import ui.theme.tertiary import viewmodel.SecVaultScreenModel @@ -24,6 +27,7 @@ class SecVaultScreen : Screen { val screenModel = koinScreenModel() val secVaultState by screenModel.secVaultState.collectAsState() + val masterPasswordDialogState = remember { mutableStateOf(!screenModel.isMasterPasswordPresent()) } val toaster = rememberToasterState() LaunchedEffect(Unit) { @@ -59,5 +63,14 @@ class SecVaultScreen : Screen { is UiState.Idle -> {} } + + when { + masterPasswordDialogState.value -> { + MasterPasswordDialog( + dialogState = masterPasswordDialogState, + viewModel = screenModel + ) + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/viewmodel/LoginScreenModel.kt b/src/main/kotlin/viewmodel/LoginScreenModel.kt index aaa85c0..5672d69 100644 --- a/src/main/kotlin/viewmodel/LoginScreenModel.kt +++ b/src/main/kotlin/viewmodel/LoginScreenModel.kt @@ -21,11 +21,13 @@ class LoginScreenModel( private val _loginState = MutableStateFlow>(UiState.Idle) val loginState: StateFlow> = _loginState.asStateFlow() - fun login(username: String, password: String) { + fun login(username: String, password: String, masterPassword: String) { screenModelScope.launch(dispatcher) { _loginState.value = UiState.Loading - when (val result = authenticationManager.login(username, password)) { - is Result.Success -> _loginState.value = UiState.Success(result.data) + when (val result = authenticationManager.login(username, password, masterPassword)) { + is Result.Success -> { + _loginState.value = UiState.Success(result.data) + } is Result.Error -> _loginState.value = UiState.Error(result.message) } } diff --git a/src/main/kotlin/viewmodel/PasswordMgntScreenModel.kt b/src/main/kotlin/viewmodel/PasswordMgntScreenModel.kt index 8257964..89a8638 100644 --- a/src/main/kotlin/viewmodel/PasswordMgntScreenModel.kt +++ b/src/main/kotlin/viewmodel/PasswordMgntScreenModel.kt @@ -34,7 +34,9 @@ class PasswordMgntScreenModel( fun savePassword(id: UUID?, password: PasswordDto, formType: FormType) { saveOrUpdate( id = id, - dto = password, + dto = password.apply { + password.password = appState.encryptString(password.password) + }, formType = formType, saveAction = passwordRepository::save, updateAction = passwordRepository::update, diff --git a/src/main/kotlin/viewmodel/SecVaultScreenModel.kt b/src/main/kotlin/viewmodel/SecVaultScreenModel.kt index 512f3ea..d5ee44c 100644 --- a/src/main/kotlin/viewmodel/SecVaultScreenModel.kt +++ b/src/main/kotlin/viewmodel/SecVaultScreenModel.kt @@ -5,6 +5,7 @@ import cafe.adriel.voyager.core.model.screenModelScope import core.AppState import core.models.* import core.models.criteria.CredentialSearchCriteria +import core.security.MasterPasswordManager import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -13,6 +14,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.launch +import org.slf4j.Logger import repository.creditcard.CreditCard import repository.creditcard.CreditCardRepository import repository.creditcard.projection.CreditCardSummary @@ -25,6 +27,7 @@ class SecVaultScreenModel( private val passwordRepository: PasswordRepository, private val creditCardRepository: CreditCardRepository, private val appState: AppState, + private val logger: Logger, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ScreenModel { @@ -133,6 +136,23 @@ class SecVaultScreenModel( } } + fun decryptPassword(text: String?): String { + try { + return appState.decryptPassword(text) + } catch (e: Exception) { + logger.error(e.message, e) + return "FUCK U" + } + } + + fun setMasterPassword(masterPassword: String) { + appState.initializeMasterPassword(MasterPasswordManager.convertToSecureString(masterPassword)) + } + + fun isMasterPasswordPresent(): Boolean { + return appState.isMasterPasswordPresent() + } + private suspend fun loadPasswords(sort: CredentialSort) { val criteria = CredentialSearchCriteria(appState.getAuthenticatedUser?.id?.value, sort) _passwordItems.value = when (val passwords = passwordRepository.findSummaries(criteria)) {