mirror of
https://github.com/ProtonMail/protoncore_android.git
synced 2026-06-14 09:54:49 +00:00
fix(auth): Added BackupPassword common checks.
This commit is contained in:
+13
-8
@@ -46,6 +46,9 @@ import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.auth.presentation.compose.sso.PasswordFormError.PasswordTooCommon
|
||||
import me.proton.core.auth.presentation.compose.sso.PasswordFormError.PasswordTooShort
|
||||
import me.proton.core.auth.presentation.compose.sso.PasswordFormError.PasswordsDoNotMatch
|
||||
import me.proton.core.compose.component.ProtonPasswordOutlinedTextFieldWithError
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.appbar.ProtonTopAppBar
|
||||
@@ -97,8 +100,7 @@ public fun BackupPasswordChangeScreen(
|
||||
modifier = modifier,
|
||||
onCloseClicked = onCloseClicked,
|
||||
onContinueClicked = onContinueClicked,
|
||||
isPasswordTooShort = state.isPasswordTooShort(),
|
||||
arePasswordsNotMatching = state.arePasswordsNotMatching(),
|
||||
formError = (state as? BackupPasswordChangeState.FormError)?.cause,
|
||||
isLoading = state is BackupPasswordChangeState.Loading
|
||||
)
|
||||
}
|
||||
@@ -108,8 +110,7 @@ public fun BackupPasswordChangeScaffold(
|
||||
modifier: Modifier = Modifier,
|
||||
onCloseClicked: () -> Unit = {},
|
||||
onContinueClicked: (BackupPasswordChangeAction.ChangePassword) -> Unit = {},
|
||||
isPasswordTooShort: Boolean = false,
|
||||
arePasswordsNotMatching: Boolean = false,
|
||||
formError: PasswordFormError? = null,
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -138,12 +139,16 @@ public fun BackupPasswordChangeScaffold(
|
||||
.padding(ProtonDimens.DefaultSpacing),
|
||||
) {
|
||||
|
||||
val errorTooShort = stringResource(R.string.backup_password_setup_password_too_short)
|
||||
val errorNotMatch = stringResource(R.string.backup_password_setup_password_not_matching)
|
||||
val error = when (formError) {
|
||||
null -> null
|
||||
PasswordTooShort -> stringResource(R.string.backup_password_setup_password_too_short)
|
||||
PasswordTooCommon -> stringResource(R.string.backup_password_setup_password_too_common)
|
||||
PasswordsDoNotMatch -> stringResource(R.string.backup_password_setup_password_not_matching)
|
||||
}
|
||||
|
||||
BackupPasswordChangeForm(
|
||||
backupPasswordError = errorTooShort.takeIf { isPasswordTooShort },
|
||||
backupPasswordRepeatedError = errorNotMatch.takeIf { arePasswordsNotMatching },
|
||||
backupPasswordError = error,
|
||||
backupPasswordRepeatedError = error?.takeIf { formError is PasswordsDoNotMatch },
|
||||
onContinueClicked = onContinueClicked,
|
||||
isLoading = isLoading,
|
||||
)
|
||||
|
||||
-9
@@ -29,12 +29,3 @@ public sealed interface BackupPasswordChangeState {
|
||||
|
||||
public data object Success : BackupPasswordChangeState
|
||||
}
|
||||
|
||||
internal fun BackupPasswordChangeState.formErrorOrNull(): PasswordFormError? =
|
||||
(this as? BackupPasswordChangeState.FormError)?.cause
|
||||
|
||||
internal fun BackupPasswordChangeState.isPasswordTooShort(): Boolean =
|
||||
formErrorOrNull() == PasswordFormError.PasswordTooShort
|
||||
|
||||
internal fun BackupPasswordChangeState.arePasswordsNotMatching(): Boolean =
|
||||
formErrorOrNull() == PasswordFormError.PasswordsDoNotMatch
|
||||
|
||||
+18
-4
@@ -50,6 +50,7 @@ import me.proton.core.observability.domain.ObservabilityContext
|
||||
import me.proton.core.observability.domain.ObservabilityManager
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoChangePasswordTotal
|
||||
import me.proton.core.presentation.utils.InputValidationResult
|
||||
import me.proton.core.presentation.utils.InvalidPasswordProvider
|
||||
import me.proton.core.presentation.utils.ValidationType
|
||||
import me.proton.core.presentation.utils.onFailure
|
||||
import me.proton.core.presentation.utils.onSuccess
|
||||
@@ -62,10 +63,15 @@ import javax.inject.Inject
|
||||
public class BackupPasswordChangeViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val context: CryptoContext,
|
||||
private val invalidPasswordProvider: InvalidPasswordProvider,
|
||||
private val changeBackupPassword: ChangeBackupPassword,
|
||||
override val observabilityManager: ObservabilityManager
|
||||
) : ViewModel(), ObservabilityContext {
|
||||
|
||||
init {
|
||||
viewModelScope.launch { invalidPasswordProvider.init() }
|
||||
}
|
||||
|
||||
private val userId: UserId by lazy { savedStateHandle.getUserId() }
|
||||
|
||||
private val mutableAction = MutableStateFlow<BackupPasswordChangeAction?>(null)
|
||||
@@ -91,12 +97,20 @@ public class BackupPasswordChangeViewModel @Inject constructor(
|
||||
}.onSuccess {
|
||||
InputValidationResult(
|
||||
text = action.backupPassword,
|
||||
validationType = ValidationType.PasswordMatch,
|
||||
additionalText = action.repeatBackupPassword
|
||||
validationType = ValidationType.InvalidPassword,
|
||||
provider = invalidPasswordProvider
|
||||
).onFailure {
|
||||
emit(FormError(PasswordFormError.PasswordsDoNotMatch))
|
||||
emit(FormError(PasswordFormError.PasswordTooCommon))
|
||||
}.onSuccess {
|
||||
emitAll(onChangeBackupPassword(action.backupPassword))
|
||||
InputValidationResult(
|
||||
text = action.backupPassword,
|
||||
validationType = ValidationType.PasswordMatch,
|
||||
additionalText = action.repeatBackupPassword
|
||||
).onFailure {
|
||||
emit(FormError(PasswordFormError.PasswordsDoNotMatch))
|
||||
}.onSuccess {
|
||||
emitAll(onChangeBackupPassword(action.backupPassword))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-9
@@ -55,6 +55,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import coil.compose.AsyncImage
|
||||
import me.proton.core.auth.presentation.compose.R
|
||||
import me.proton.core.auth.presentation.compose.sso.PasswordFormError.PasswordTooCommon
|
||||
import me.proton.core.auth.presentation.compose.sso.PasswordFormError.PasswordTooShort
|
||||
import me.proton.core.auth.presentation.compose.sso.PasswordFormError.PasswordsDoNotMatch
|
||||
import me.proton.core.compose.component.ProtonPasswordOutlinedTextFieldWithError
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.appbar.ProtonTopAppBar
|
||||
@@ -110,8 +113,7 @@ public fun BackupPasswordSetupScreen(
|
||||
organizationAdminEmail = state.data.organizationAdminEmail,
|
||||
organizationIcon = state.data.organizationIcon,
|
||||
organizationName = state.data.organizationName,
|
||||
isPasswordTooShort = state.isPasswordTooShort(),
|
||||
arePasswordsNotMatching = state.arePasswordsNotMatching(),
|
||||
formError = (state as? BackupPasswordSetupState.FormError)?.cause,
|
||||
isLoading = state is BackupPasswordSetupState.Loading
|
||||
)
|
||||
}
|
||||
@@ -124,8 +126,7 @@ public fun BackupPasswordSetupScaffold(
|
||||
organizationAdminEmail: String? = null,
|
||||
organizationIcon: Any? = null,
|
||||
organizationName: String? = null,
|
||||
isPasswordTooShort: Boolean = false,
|
||||
arePasswordsNotMatching: Boolean = false,
|
||||
formError: PasswordFormError? = null,
|
||||
isLoading: Boolean = false,
|
||||
) {
|
||||
Scaffold(
|
||||
@@ -163,12 +164,16 @@ public fun BackupPasswordSetupScaffold(
|
||||
color = LocalColors.current.separatorNorm
|
||||
)
|
||||
|
||||
val errorTooShort = stringResource(R.string.backup_password_setup_password_too_short)
|
||||
val errorNotMatch = stringResource(R.string.backup_password_setup_password_not_matching)
|
||||
val error = when (formError) {
|
||||
null -> null
|
||||
PasswordTooShort -> stringResource(R.string.backup_password_setup_password_too_short)
|
||||
PasswordTooCommon -> stringResource(R.string.backup_password_setup_password_too_common)
|
||||
PasswordsDoNotMatch -> stringResource(R.string.backup_password_setup_password_not_matching)
|
||||
}
|
||||
|
||||
BackupPasswordSetupForm(
|
||||
backupPasswordError = errorTooShort.takeIf { isPasswordTooShort },
|
||||
backupPasswordRepeatedError = errorNotMatch.takeIf { arePasswordsNotMatching },
|
||||
backupPasswordError = error,
|
||||
backupPasswordRepeatedError = error?.takeIf { formError is PasswordsDoNotMatch },
|
||||
onContinueClicked = onContinueClicked,
|
||||
isLoading = isLoading,
|
||||
modifier = Modifier
|
||||
@@ -289,7 +294,6 @@ private fun BackupPasswordSetupForm(
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Preview(device = Devices.TABLET)
|
||||
@Composable
|
||||
private fun BackupPasswordSetupScreenPreview() {
|
||||
ProtonTheme {
|
||||
@@ -303,3 +307,20 @@ private fun BackupPasswordSetupScreenPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun BackupPasswordSetupScreenFormErrorPreview() {
|
||||
ProtonTheme {
|
||||
BackupPasswordSetupScreen(
|
||||
state = BackupPasswordSetupState.FormError(
|
||||
data = BackupPasswordSetupData(
|
||||
organizationAdminEmail = "admin@company.test",
|
||||
organizationName = "The Company",
|
||||
),
|
||||
cause = PasswordsDoNotMatch
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
-9
@@ -43,12 +43,3 @@ public sealed class BackupPasswordSetupState(
|
||||
override val data: BackupPasswordSetupData
|
||||
) : BackupPasswordSetupState(data)
|
||||
}
|
||||
|
||||
internal fun BackupPasswordSetupState.formErrorOrNull(): PasswordFormError? =
|
||||
(this as? BackupPasswordSetupState.FormError)?.cause
|
||||
|
||||
internal fun BackupPasswordSetupState.isPasswordTooShort(): Boolean =
|
||||
formErrorOrNull() == PasswordFormError.PasswordTooShort
|
||||
|
||||
internal fun BackupPasswordSetupState.arePasswordsNotMatching(): Boolean =
|
||||
formErrorOrNull() == PasswordFormError.PasswordsDoNotMatch
|
||||
|
||||
+20
-7
@@ -56,11 +56,11 @@ import me.proton.core.observability.domain.metrics.LoginSsoLoadOrganizationTotal
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoSetupPrimaryKeysTotal
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoVerifyUnprivatizationTotal
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoVerifyUnprivatizationTotal.VerifyStatus
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoVerifyUnprivatizationTotal.VerifyStatus.failure
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoVerifyUnprivatizationTotal.VerifyStatus.failurePublicAddressKeysError
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoVerifyUnprivatizationTotal.VerifyStatus.failureUnprivatizeStateError
|
||||
import me.proton.core.observability.domain.metrics.LoginSsoVerifyUnprivatizationTotal.VerifyStatus.failureVerificationError
|
||||
import me.proton.core.presentation.utils.InputValidationResult
|
||||
import me.proton.core.presentation.utils.InvalidPasswordProvider
|
||||
import me.proton.core.presentation.utils.ValidationType
|
||||
import me.proton.core.presentation.utils.onFailure
|
||||
import me.proton.core.presentation.utils.onSuccess
|
||||
@@ -73,6 +73,7 @@ import javax.inject.Inject
|
||||
public class BackupPasswordSetupViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val context: CryptoContext,
|
||||
private val invalidPasswordProvider: InvalidPasswordProvider,
|
||||
private val deviceSecretRepository: DeviceSecretRepository,
|
||||
private val verifyUnprivatization: VerifyUnprivatization,
|
||||
private val setupPrimaryKeys: SetupPrimaryKeys,
|
||||
@@ -80,6 +81,10 @@ public class BackupPasswordSetupViewModel @Inject constructor(
|
||||
override val observabilityManager: ObservabilityManager,
|
||||
) : ViewModel(), ObservabilityContext {
|
||||
|
||||
init {
|
||||
viewModelScope.launch { invalidPasswordProvider.init() }
|
||||
}
|
||||
|
||||
private val userId: UserId by lazy { savedStateHandle.getUserId() }
|
||||
|
||||
private val mutableAction = MutableStateFlow<BackupPasswordSetupAction>(Load())
|
||||
@@ -155,14 +160,22 @@ public class BackupPasswordSetupViewModel @Inject constructor(
|
||||
}.onSuccess {
|
||||
InputValidationResult(
|
||||
text = action.backupPassword,
|
||||
validationType = ValidationType.PasswordMatch,
|
||||
additionalText = action.repeatBackupPassword
|
||||
validationType = ValidationType.InvalidPassword,
|
||||
provider = invalidPasswordProvider
|
||||
).onFailure {
|
||||
emit(FormError(state.value.data, PasswordFormError.PasswordsDoNotMatch))
|
||||
emit(FormError(state.value.data, PasswordFormError.PasswordTooCommon))
|
||||
}.onSuccess {
|
||||
when (val organizationPublicKey = state.value.data.organizationPublicKey) {
|
||||
null -> emit(Error(state.value.data, null))
|
||||
else -> emitAll(onSetupPrimaryKeys(action.backupPassword, organizationPublicKey))
|
||||
InputValidationResult(
|
||||
text = action.backupPassword,
|
||||
validationType = ValidationType.PasswordMatch,
|
||||
additionalText = action.repeatBackupPassword
|
||||
).onFailure {
|
||||
emit(FormError(state.value.data, PasswordFormError.PasswordsDoNotMatch))
|
||||
}.onSuccess {
|
||||
when (val organizationPublicKey = state.value.data.organizationPublicKey) {
|
||||
null -> emit(Error(state.value.data, null))
|
||||
else -> emitAll(onSetupPrimaryKeys(action.backupPassword, organizationPublicKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -2,5 +2,6 @@ package me.proton.core.auth.presentation.compose.sso
|
||||
|
||||
public sealed interface PasswordFormError {
|
||||
public data object PasswordTooShort : PasswordFormError
|
||||
public data object PasswordTooCommon : PasswordFormError
|
||||
public data object PasswordsDoNotMatch : PasswordFormError
|
||||
}
|
||||
|
||||
@@ -74,6 +74,8 @@
|
||||
<string name="backup_password_setup_repeat_password_label">Repeat backup password</string>
|
||||
<string name="backup_password_setup_continue_action">Continue</string>
|
||||
<string name="backup_password_setup_password_too_short">Password should be at least 8 characters long.</string>
|
||||
<string name="backup_password_setup_password_too_common">Password too common. Use a different password.</string>
|
||||
|
||||
<string name="backup_password_setup_password_not_matching">Passwords do not match.</string>
|
||||
|
||||
<string name="backup_password_change_title">Set backup password</string>
|
||||
|
||||
+18
@@ -80,6 +80,24 @@ class BackupPasswordSetupScreenTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwords too common`() {
|
||||
paparazzi.snapshot {
|
||||
ProtonTheme {
|
||||
BackupPasswordSetupScreen(
|
||||
state = FormError(
|
||||
data = BackupPasswordSetupData(
|
||||
organizationAdminEmail = "admin@example.test",
|
||||
organizationIcon = null,
|
||||
organizationName = "Example Organization",
|
||||
),
|
||||
cause = PasswordFormError.PasswordTooCommon
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwords do not match`() {
|
||||
paparazzi.snapshot {
|
||||
|
||||
+33
@@ -21,6 +21,8 @@ package me.proton.core.auth.presentation.compose.sso
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import io.mockk.MockKAnnotations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.impl.annotations.MockK
|
||||
import me.proton.core.auth.domain.repository.DeviceSecretRepository
|
||||
import me.proton.core.auth.domain.usecase.SetupPrimaryKeys
|
||||
@@ -28,6 +30,7 @@ import me.proton.core.auth.domain.usecase.sso.VerifyUnprivatization
|
||||
import me.proton.core.auth.presentation.compose.DeviceSecretRoutes
|
||||
import me.proton.core.crypto.common.context.CryptoContext
|
||||
import me.proton.core.observability.domain.ObservabilityManager
|
||||
import me.proton.core.presentation.utils.InvalidPasswordProvider
|
||||
import me.proton.core.test.kotlin.CoroutinesTest
|
||||
import me.proton.core.usersettings.domain.repository.OrganizationRepository
|
||||
import kotlin.test.BeforeTest
|
||||
@@ -39,6 +42,9 @@ class BackupPasswordSetupViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
@MockK
|
||||
private lateinit var context: CryptoContext
|
||||
|
||||
@MockK
|
||||
private lateinit var invalidPasswordProvider: InvalidPasswordProvider
|
||||
|
||||
@MockK
|
||||
private lateinit var observabilityManager: ObservabilityManager
|
||||
|
||||
@@ -59,9 +65,14 @@ class BackupPasswordSetupViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
@BeforeTest
|
||||
fun setUp() {
|
||||
MockKAnnotations.init(this)
|
||||
|
||||
coEvery { invalidPasswordProvider.init() } returns Unit
|
||||
every { invalidPasswordProvider.isPasswordCommon(any()) } returns false
|
||||
|
||||
tested = BackupPasswordSetupViewModel(
|
||||
savedStateHandle = SavedStateHandle(mapOf(DeviceSecretRoutes.Arg.KEY_USER_ID to "user-id")),
|
||||
context = context,
|
||||
invalidPasswordProvider = invalidPasswordProvider,
|
||||
deviceSecretRepository = deviceSecretRepository,
|
||||
verifyUnprivatization = verifyUnprivatization,
|
||||
setupPrimaryKeys = setupPrimaryKeys,
|
||||
@@ -90,6 +101,28 @@ class BackupPasswordSetupViewModelTest : CoroutinesTest by CoroutinesTest() {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `password too common`() = coroutinesTest {
|
||||
every { invalidPasswordProvider.isPasswordCommon(any()) } returns true
|
||||
|
||||
tested.state.test {
|
||||
assertEquals(BackupPasswordSetupState.Idle(BackupPasswordSetupData()), awaitItem())
|
||||
|
||||
// WHEN
|
||||
tested.submit(BackupPasswordSetupAction.SetPassword("12345678", "12345678")).join()
|
||||
|
||||
// THEN
|
||||
assertEquals(BackupPasswordSetupState.Loading(BackupPasswordSetupData()), awaitItem())
|
||||
assertEquals(
|
||||
expected = BackupPasswordSetupState.FormError(
|
||||
data = BackupPasswordSetupData(),
|
||||
cause = PasswordFormError.PasswordTooCommon
|
||||
),
|
||||
actual = awaitItem()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `passwords not matching`() = coroutinesTest {
|
||||
tested.state.test {
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e400d8764979a996f762ff8c2829e8a47f50ba0fb5aea5f5f1ab4f2174ae50ed
|
||||
size 38018
|
||||
oid sha256:0c9d4ed4b884d93a155bc51dd76a2adeeafc9169331c0f67eb37fbe2012dd942
|
||||
size 40981
|
||||
|
||||
+2
-2
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:163b9f72151331bac4c16025ed05ed14b2af21dcbcfc2f04299742cbcd7dcb5f
|
||||
size 39101
|
||||
oid sha256:6986e400d6a54a65afabacc21bb9e379411f6848d17962fde3d4e84da4397d43
|
||||
size 42090
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d53cfb80b3fbaabb9f680d95075527ca38732d94ad1357b5b89cce366e6f898e
|
||||
size 41287
|
||||
+7
-1
@@ -19,10 +19,16 @@
|
||||
package me.proton.core.presentation.utils
|
||||
|
||||
import android.content.Context
|
||||
import dagger.Reusable
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
class InvalidPasswordProvider(private val context: Context) {
|
||||
@Reusable
|
||||
class InvalidPasswordProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
|
||||
private var commonPasswords: Set<String>? = null
|
||||
|
||||
|
||||
Reference in New Issue
Block a user