Add Remember pass key (#1046)

* feat: validate pass key

* feat: validate passkeys

* feat: improve padding in hint screen

* feat: minor changes

* feat: cleanup
This commit is contained in:
Yogesh Choudhary Paliyal
2025-01-25 19:45:22 +05:30
committed by GitHub
parent 2b9452f8dd
commit 5be19d1600
15 changed files with 172 additions and 16 deletions

View File

@@ -53,12 +53,11 @@ fun AuthScreen(state: AuthState) {
}
LaunchedEffect(key1 = userSettings.keyPassPassword, block = {
if (userSettings.isDefault) {
return@LaunchedEffect
}
val mPassword = userSettings.keyPassPassword
if (mPassword == null) {
dispatchAction(NavigationAction(AuthState.CreatePassword, true))
} else {
dispatchAction(NavigationAction(AuthState.Login, true))
}
})

View File

@@ -16,6 +16,7 @@ import com.yogeshpaliyal.common.utils.clearBackupKey
import com.yogeshpaliyal.common.utils.setAutoBackupEnabled
import com.yogeshpaliyal.common.utils.setBackupDirectory
import com.yogeshpaliyal.common.utils.setOverrideAutoBackup
import com.yogeshpaliyal.common.utils.updateLastKeyPhraseEnterTime
import com.yogeshpaliyal.keypass.ui.backup.components.BackSettingOptions
import com.yogeshpaliyal.keypass.ui.backup.components.BackupDialogs
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
@@ -80,6 +81,7 @@ fun BackupScreen(state: BackupScreenState) {
if (it.not()) {
context.clearBackupKey()
}
context.updateLastKeyPhraseEnterTime(System.currentTimeMillis())
}
})

View File

@@ -22,10 +22,14 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.yogeshpaliyal.keypass.ui.home.components.AccountsList
import com.yogeshpaliyal.keypass.ui.home.components.SearchBar
import com.yogeshpaliyal.keypass.ui.nav.LocalUserSettings
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateDialogState
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
import com.yogeshpaliyal.keypass.ui.redux.states.ValidateKeyPhrase
import org.reduxkotlin.compose.rememberDispatcher
import java.util.concurrent.TimeUnit
/*
* @author Yogesh Paliyal
@@ -44,6 +48,8 @@ fun Homepage(
val sortField = homeState.sortField
val sortAscendingOrder = homeState.sortAscending
val userSettings = LocalUserSettings.current
val listOfAccountsLiveData by mViewModel.mediator.observeAsState()
val dispatchAction = rememberDispatcher()
@@ -52,6 +58,20 @@ fun Homepage(
mViewModel.queryUpdated(keyword, tag, sortField, sortAscendingOrder)
})
LaunchedEffect(Unit, {
if (userSettings.backupKey == null) {
return@LaunchedEffect
}
val currentTime = System.currentTimeMillis()
val lastPasswordLoginTime = userSettings.lastKeyPhraseEnterTime ?: -1
val diff = currentTime - lastPasswordLoginTime
val diffInDays = TimeUnit.MILLISECONDS.toDays(diff)
if (diffInDays >= 7) {
// Show the modal
dispatchAction(UpdateDialogState(dialogState = ValidateKeyPhrase))
}
})
Column(modifier = Modifier.fillMaxSize()) {
SearchBar(keyword, {
dispatchAction(StateUpdateAction(homeState.copy(keyword = it)))

View File

@@ -0,0 +1,93 @@
package com.yogeshpaliyal.keypass.ui.home.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.size
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.common.utils.updateLastKeyPhraseEnterTime
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.nav.LocalUserSettings
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateDialogState
import kotlinx.coroutines.launch
import org.reduxkotlin.compose.rememberTypedDispatcher
@Composable
fun ValidateKeyPhraseDialog() {
val (keyphrase, setKeyPhrase) = remember {
mutableStateOf("")
}
val dispatchAction = rememberTypedDispatcher<Action>()
val userSettings = LocalUserSettings.current
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val hideDialog: () -> Unit = {
dispatchAction(UpdateDialogState(null))
}
AlertDialog(
onDismissRequest = {
hideDialog()
},
title = {
Text(text = stringResource(id = R.string.validate_keyphrase))
},
confirmButton = {
TextButton(onClick = {
if (keyphrase.isEmpty()) {
dispatchAction(ToastAction(R.string.alert_blank_keyphrase))
return@TextButton
}
if (userSettings.backupKey != keyphrase) {
dispatchAction(ToastAction(R.string.mismatch_keyphrase))
return@TextButton
}
coroutineScope.launch {
context.updateLastKeyPhraseEnterTime(System.currentTimeMillis())
hideDialog()
dispatchAction(ToastAction(R.string.valid_keyphrase))
}
}) {
Text(text = stringResource(id = R.string.validate))
}
},
dismissButton = {
TextButton(onClick = hideDialog) {
Text(text = stringResource(id = R.string.cancel))
}
},
text = {
Column(modifier = Modifier.fillMaxWidth(1f)) {
Text(text = stringResource(id = R.string.keyphrase_validate_info))
Spacer(modifier = Modifier.size(8.dp))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(1f),
value = keyphrase,
onValueChange = setKeyPhrase,
placeholder = {
Text(text = stringResource(id = R.string.enter_keyphrase))
}
)
}
}
)
}

View File

@@ -18,8 +18,6 @@ import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import com.yogeshpaliyal.common.data.UserSettings
import com.yogeshpaliyal.common.utils.getUserSettings
import com.yogeshpaliyal.common.utils.getUserSettingsFlow
@@ -35,12 +33,12 @@ import com.yogeshpaliyal.keypass.ui.changePassword.ChangePassword
import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage
import com.yogeshpaliyal.keypass.ui.generate.ui.GeneratePasswordScreen
import com.yogeshpaliyal.keypass.ui.home.Homepage
import com.yogeshpaliyal.keypass.ui.home.components.ValidateKeyPhraseDialog
import com.yogeshpaliyal.keypass.ui.nav.components.DashboardBottomSheet
import com.yogeshpaliyal.keypass.ui.nav.components.KeyPassBottomBar
import com.yogeshpaliyal.keypass.ui.passwordHint.PasswordHintScreen
import com.yogeshpaliyal.keypass.ui.redux.KeyPassRedux
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction
import com.yogeshpaliyal.keypass.ui.redux.states.AboutState
import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState
@@ -53,8 +51,8 @@ import com.yogeshpaliyal.keypass.ui.redux.states.ChangeDefaultPasswordLengthStat
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
import com.yogeshpaliyal.keypass.ui.redux.states.PasswordGeneratorState
import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState
import com.yogeshpaliyal.keypass.ui.redux.states.SettingsState
import com.yogeshpaliyal.keypass.ui.redux.states.ValidateKeyPhrase
import com.yogeshpaliyal.keypass.ui.settings.MySettingCompose
import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme
import dagger.hilt.android.AndroidEntryPoint
@@ -77,7 +75,7 @@ class DashboardComposeActivity : AppCompatActivity() {
}
setContent {
val localUserSettings by getUserSettingsFlow().collectAsState(initial = UserSettings(true))
val localUserSettings by getUserSettingsFlow().collectAsState(initial = UserSettings())
CompositionLocalProvider(LocalUserSettings provides localUserSettings) {
KeyPassTheme {
@@ -117,9 +115,9 @@ fun Dashboard() {
}
// Call this like any other SideEffect in your composable
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
dispatch(NavigationAction(AuthState.Login))
}
// LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
// dispatch(NavigationAction(AuthState.Login))
// }
LaunchedEffect(key1 = systemBackPress, block = {
if (systemBackPress) {
@@ -148,9 +146,11 @@ fun Dashboard() {
@Composable
fun CurrentPage() {
val currentScreen by selectState<KeyPassState, ScreenState> { this.currentScreen }
val currentScreen by selectState<KeyPassState, KeyPassState> { this }
currentScreen.let {
// val currentDialog by selectState<KeyPassState, DialogState?> { this.dialog }
currentScreen.currentScreen.let {
when (it) {
is HomeState -> {
Homepage(homeState = it)
@@ -186,4 +186,12 @@ fun CurrentPage() {
is ChangeAppHintState -> PasswordHintScreen()
}
}
currentScreen.dialog?.let {
when (it) {
is ValidateKeyPhrase -> {
ValidateKeyPhraseDialog()
}
}
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.common.utils.setPasswordHint
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
@@ -42,6 +43,7 @@ fun PasswordHintScreen() {
modifier = Modifier
.padding(contentPadding)
.fillMaxWidth()
.padding(16.dp)
) {
Column(modifier = Modifier.fillMaxSize(1f)) {
KeyPassInputField(

View File

@@ -13,6 +13,7 @@ import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastActionStr
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateDialogState
import com.yogeshpaliyal.keypass.ui.redux.middlewares.intentNavigationMiddleware
import com.yogeshpaliyal.keypass.ui.redux.states.BottomSheetState
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
@@ -101,6 +102,9 @@ object KeyPassRedux {
)
)
}
is UpdateDialogState -> {
state.copy(dialog = action.dialogState)
}
else -> state
}

View File

@@ -1,6 +1,7 @@
package com.yogeshpaliyal.keypass.ui.redux.actions
import android.content.Context
import com.yogeshpaliyal.keypass.ui.redux.states.DialogState
import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState
sealed interface Action
@@ -9,3 +10,4 @@ class UpdateContextAction(val context: Context?) : Action
data class NavigationAction(val state: ScreenState, val clearBackStack: Boolean = false) : Action
data class StateUpdateAction(val state: ScreenState) : Action
data class UpdateDialogState(val dialogState: DialogState? = null) : Action

View File

@@ -0,0 +1,5 @@
package com.yogeshpaliyal.keypass.ui.redux.states
sealed class DialogState()
object ValidateKeyPhrase : DialogState()

View File

@@ -7,6 +7,7 @@ data class KeyPassState(
val context: Context? = null,
val currentScreen: ScreenState,
val bottomSheet: BottomSheetState? = null,
val dialog: DialogState? = null,
val systemBackPress: Boolean = false
)

View File

@@ -1,3 +1,3 @@
package com.yogeshpaliyal.keypass.ui.redux.states
sealed class ScreenState(val showMainBottomAppBar: Boolean = false)
sealed class ScreenState(val showMainBottomAppBar: Boolean = false, val dialogState: DialogState? = null)

View File

@@ -48,12 +48,14 @@ import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateDialogState
import com.yogeshpaliyal.keypass.ui.redux.states.AboutState
import com.yogeshpaliyal.keypass.ui.redux.states.BackupImporterState
import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState
import com.yogeshpaliyal.keypass.ui.redux.states.ChangeAppHintState
import com.yogeshpaliyal.keypass.ui.redux.states.ChangeAppPasswordState
import com.yogeshpaliyal.keypass.ui.redux.states.ChangeDefaultPasswordLengthState
import com.yogeshpaliyal.keypass.ui.redux.states.ValidateKeyPhrase
import kotlinx.coroutines.launch
import org.reduxkotlin.compose.rememberTypedDispatcher
@@ -109,6 +111,13 @@ fun MySettingCompose() {
dispatchAction(NavigationAction(ChangeDefaultPasswordLengthState()))
}
PreferenceItem(
title = R.string.validate_keyphrase,
summary = R.string.validate_keyphrase
) {
dispatchAction(UpdateDialogState(ValidateKeyPhrase))
}
BiometricsOption()
Divider(

View File

@@ -74,6 +74,7 @@
<string name="enter_keyphrase">Enter Keyphrase</string>
<string name="restore">Restore</string>
<string name="keyphrase_restore_info">Please enter keyphrase you get when you backed up</string>
<string name="keyphrase_validate_info">Please enter the keyphrase you used to create your backup. We will occasionally show this screen to help you remember it.</string>
<string name="custom_generated_keyphrase_info">Do you want to create your own keyphrase for backups or should I generate for you?</string>
<string name="ascending">Ascending</string>
@@ -123,6 +124,10 @@
<string name="invalid_csv_file">Invalid CSV File</string>
<string name="file_not_found">File not found (please try again)</string>
<string name="enter_password_hint">Enter password hint</string>
<string name="validate_keyphrase">Keyphrase Check</string>
<string name="valid_keyphrase">Valid Keyphrase</string>
<string name="mismatch_keyphrase">Entered Keyphrase does not match with backup keyphrase</string>
<string name="validate">Check</string>
<string name="biometric_disabled_due_to_timeout">Please login via password because you haven\'t used password in last 24 hours</string>
</resources>

View File

@@ -8,7 +8,6 @@ const val DEFAULT_PASSWORD_LENGTH = 10f
@Keep
@Serializable
data class UserSettings(
val isDefault: Boolean = false,
val keyPassPassword: String? = null,
val dbPassword: String? = null,
@Deprecated("Use passwordConfig instead")
@@ -23,7 +22,8 @@ data class UserSettings(
val currentAppVersion: Int? = null,
val passwordConfig: PasswordConfig = PasswordConfig.Initial,
val passwordHint: String? = null,
val lastPasswordLoginTime: Long? = null
val lastPasswordLoginTime: Long? = null,
val lastKeyPhraseEnterTime: Long? = null
) {
fun isKeyPresent() = backupKey != null
}

View File

@@ -145,6 +145,12 @@ suspend fun Context.updateLastPasswordLoginTime(lastPasswordLoginTime: Long?) {
}
}
suspend fun Context.updateLastKeyPhraseEnterTime(lastKeyPhraseEnterTime: Long?) {
getUserSettingsDataStore().updateData {
it.copy(lastKeyPhraseEnterTime = lastKeyPhraseEnterTime)
}
}
private val BACKUP_KEY = stringPreferencesKey("backup_key")
private val BIOMETRIC_ENABLE = booleanPreferencesKey("biometric_enable")
private val KEYPASS_PASSWORD = stringPreferencesKey("keypass_password")