From e758ede7062ef30c817a4e655e27eca0a9f442b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 25 Jun 2020 14:38:22 +0200 Subject: [PATCH] Revert "Remove BootstrapStep.SetupPassphrase and BootstrapStep.ConfirmPassphrase" This reverts commit 23fa44b6a6a6d34b425e2c1adef4fd2beb9800a7. --- .../im/vector/riotx/core/di/FragmentModule.kt | 12 ++ .../crypto/recover/BootstrapActions.kt | 4 + .../crypto/recover/BootstrapBottomSheet.kt | 10 ++ .../BootstrapConfirmPassphraseFragment.kt | 118 ++++++++++++++++ .../recover/BootstrapCrossSigningTask.kt | 26 +++- .../BootstrapEnterPassphraseFragment.kt | 126 ++++++++++++++++++ .../recover/BootstrapMigrateBackupFragment.kt | 6 +- .../recover/BootstrapSharedViewModel.kt | 126 +++++++++++++++--- .../features/crypto/recover/BootstrapStep.kt | 39 +++--- .../crypto/recover/BootstrapViewState.kt | 7 +- 10 files changed, 427 insertions(+), 47 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c797588449..21cff188d0 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -28,6 +28,8 @@ import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment +import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment +import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment import im.vector.riotx.features.crypto.recover.BootstrapMigrateBackupFragment import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment import im.vector.riotx.features.crypto.recover.BootstrapSetupRecoveryKeyFragment @@ -452,6 +454,16 @@ interface FragmentModule { @FragmentKey(GossipingEventsPaperTrailFragment::class) fun bindGossipingEventsPaperTrailFragment(fragment: GossipingEventsPaperTrailFragment): Fragment + @Binds + @IntoMap + @FragmentKey(BootstrapEnterPassphraseFragment::class) + fun bindBootstrapEnterPassphraseFragment(fragment: BootstrapEnterPassphraseFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapConfirmPassphraseFragment::class) + fun bindBootstrapConfirmPassphraseFragment(fragment: BootstrapConfirmPassphraseFragment): Fragment + @Binds @IntoMap @FragmentKey(BootstrapWaitingFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt index 596dcaba73..2f6cfcf799 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt @@ -24,13 +24,17 @@ sealed class BootstrapActions : VectorViewModelAction { // Navigation object GoBack : BootstrapActions() + data class GoToConfirmPassphrase(val passphrase: String) : BootstrapActions() object GoToCompleted : BootstrapActions() object GoToEnterAccountPassword : BootstrapActions() object SetupRecoveryKey : BootstrapActions() + data class DoInitialize(val passphrase: String) : BootstrapActions() object DoInitializeGeneratedKey : BootstrapActions() object TogglePasswordVisibility : BootstrapActions() + data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() + data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions() data class ReAuth(val pass: String) : BootstrapActions() object RecoveryKeySaved : BootstrapActions() object Completed : BootstrapActions() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt index c7d7ad99f3..4234fd38ee 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt @@ -132,6 +132,16 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { bootstrapTitleText.text = getString(R.string.upgrade_security) showFragment(BootstrapWaitingFragment::class, Bundle()) } + is BootstrapStep.SetupPassphrase -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) + bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase)) + showFragment(BootstrapEnterPassphraseFragment::class, Bundle()) + } + is BootstrapStep.ConfirmPassphrase -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) + bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase)) + showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) + } is BootstrapStep.AccountPassword -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) bootstrapTitleText.text = getString(R.string.account_password) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt new file mode 100644 index 0000000000..ebb6416317 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.crypto.recover + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import androidx.core.view.isGone +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapConfirmPassphraseFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + ssss_passphrase_security_progress.isGone = true + + val recPassPhrase = getString(R.string.recovery_passphrase) + bootstrapDescriptionText.text = getString(R.string.bootstrap_info_confirm_text, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_confirm_passphrase) + + withState(sharedViewModel) { + // set initial value (useful when coming back) + ssss_passphrase_enter_edittext.setText(it.passphraseRepeat ?: "") + ssss_passphrase_enter_edittext.requestFocus() + } + + ssss_passphrase_enter_edittext.editorActionEvents() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + ssss_passphrase_enter_edittext.textChanges() + .subscribe { + ssss_passphrase_enter_til.error = null + sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it?.toString() ?: "")) + } + .disposeOnDestroyView() + + sharedViewModel.observeViewEvents { + // when (it) { +// is SharedSecureStorageViewEvent.InlineError -> { +// ssss_passphrase_enter_til.error = it.message +// } +// } + } + + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.ConfirmPassphrase) { + return@withState + } + val passphrase = ssss_passphrase_enter_edittext.text?.toString() + when { + passphrase.isNullOrBlank() -> + ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message) + passphrase != state.passphrase -> + ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_does_not_match) + else -> { + view?.hideKeyboard() + sharedViewModel.handle(BootstrapActions.DoInitialize(passphrase)) + } + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step is BootstrapStep.ConfirmPassphrase) { + val isPasswordVisible = state.step.isPasswordVisible + ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false) + ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index db25e093ce..7a610d8c85 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -66,6 +66,7 @@ interface BootstrapProgressListener { data class Params( val userPasswordAuth: UserPasswordAuth? = null, val progressListener: BootstrapProgressListener? = null, + val passphrase: String?, val keySpec: SsssKeySpec? = null ) @@ -107,13 +108,24 @@ class BootstrapCrossSigningTask @Inject constructor( ) try { keyInfo = awaitCallback { - ssssService.generateKey( - UUID.randomUUID().toString(), - params.keySpec, - "ssss_key", - EmptyKeySigner(), - it - ) + params.passphrase?.let { passphrase -> + ssssService.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + passphrase, + EmptyKeySigner(), + null, + it + ) + } ?: kotlin.run { + ssssService.generateKey( + UUID.randomUUID().toString(), + params.keySpec, + "ssss_key", + EmptyKeySigner(), + it + ) + } } } catch (failure: Failure) { return BootstrapResult.FailedToCreateSSSSKey(failure) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt new file mode 100644 index 0000000000..982f72c14e --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.crypto.recover + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.riotx.R +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.features.settings.VectorLocale +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapEnterPassphraseFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val recPassPhrase = getString(R.string.recovery_passphrase) + bootstrapDescriptionText.text = getString(R.string.bootstrap_info_text, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_enter_passphrase) + withState(sharedViewModel) { + // set initial value (useful when coming back) + ssss_passphrase_enter_edittext.setText(it.passphrase ?: "") + } + ssss_passphrase_enter_edittext.editorActionEvents() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + ssss_passphrase_enter_edittext.textChanges() + .subscribe { + // ssss_passphrase_enter_til.error = null + sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: "")) +// ssss_passphrase_submit.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + + sharedViewModel.observeViewEvents { + // when (it) { +// is SharedSecureStorageViewEvent.InlineError -> { +// ssss_passphrase_enter_til.error = it.message +// } +// } + } + + ssss_view_show_password.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) } + bootstrapSubmit.debouncedClicks { submit() } + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.SetupPassphrase) { + return@withState + } + val score = state.passphraseStrength.invoke()?.score + val passphrase = ssss_passphrase_enter_edittext.text?.toString() + if (passphrase.isNullOrBlank()) { + ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message) + } else if (score != 4) { + ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_too_weak) + } else { + sharedViewModel.handle(BootstrapActions.GoToConfirmPassphrase(passphrase)) + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step is BootstrapStep.SetupPassphrase) { + val isPasswordVisible = state.step.isPasswordVisible + ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false) + ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + + state.passphraseStrength.invoke()?.let { strength -> + val score = strength.score + ssss_passphrase_security_progress.strength = score + if (score in 1..3) { + val hint = + strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } + ?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull() + if (hint != null && hint != ssss_passphrase_enter_til.error.toString()) { + ssss_passphrase_enter_til.error = hint + } + } else { + ssss_passphrase_enter_til.error = null + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt index ee583e0381..0b8e201edd 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -51,11 +51,15 @@ class BootstrapMigrateBackupFragment @Inject constructor( override fun getLayoutResId() = R.layout.fragment_bootstrap_migrate_backup - private val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + withState(sharedViewModel) { + // set initial value (useful when coming back) + bootstrapMigrateEditText.setText(it.passphrase ?: "") + } bootstrapMigrateEditText.editorActionEvents() .throttleFirst(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index f2609bdf77..ebbe502a65 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -24,6 +24,7 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.ViewModelContext +import com.nulabinc.zxcvbn.Zxcvbn import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.failure.Failure @@ -43,6 +44,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.OutputStream + class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, @Assisted val args: BootstrapBottomSheet.Args, @@ -53,6 +55,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( private val reAuthHelper: ReAuthHelper ) : VectorViewModel(initialState) { + private val zxcvbn = Zxcvbn() + @AssistedInject.Factory interface Factory { fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel @@ -64,7 +68,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( // need to check if user have an existing keybackup if (args.isNewAccount) { setState { - copy(step = BootstrapStep.AccountPassword(false)) + copy(step = BootstrapStep.SetupPassphrase(false)) } } else { setState { @@ -79,7 +83,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( if (version == null) { // we just resume plain bootstrap setState { - copy(step = BootstrapStep.AccountPassword(false)) + copy(step = BootstrapStep.SetupPassphrase(false)) } } else { // we need to get existing backup passphrase/key and convert to SSSS @@ -108,9 +112,19 @@ class BootstrapSharedViewModel @AssistedInject constructor( override fun handle(action: BootstrapActions) = withState { state -> when (action) { - is BootstrapActions.GoBack -> queryBack() - BootstrapActions.TogglePasswordVisibility -> { + is BootstrapActions.GoBack -> queryBack() + BootstrapActions.TogglePasswordVisibility -> { when (state.step) { + is BootstrapStep.SetupPassphrase -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } + is BootstrapStep.ConfirmPassphrase -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } is BootstrapStep.AccountPassword -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) @@ -128,64 +142,118 @@ class BootstrapSharedViewModel @AssistedInject constructor( BootstrapActions.SetupRecoveryKey -> { startProcess() } - is BootstrapActions.DoInitializeGeneratedKey -> { + is BootstrapActions.UpdateCandidatePassphrase -> { + val strength = zxcvbn.measure(action.pass) + setState { + copy( + passphrase = action.pass, + passphraseStrength = Success(strength) + ) + } + } + is BootstrapActions.GoToConfirmPassphrase -> { + setState { + copy( + passphrase = action.passphrase, + step = BootstrapStep.ConfirmPassphrase( + isPasswordVisible = (state.step as? BootstrapStep.SetupPassphrase)?.isPasswordVisible ?: false + ) + ) + } + } + is BootstrapActions.UpdateConfirmCandidatePassphrase -> { + setState { + copy( + passphraseRepeat = action.pass + ) + } + } + is BootstrapActions.DoInitialize -> { + if (state.passphrase == state.passphraseRepeat) { + val userPassword = reAuthHelper.data + if (userPassword == null) { + setState { + copy( + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(userPassword) + } + } else { + setState { + copy( + passphraseConfirmMatch = Fail(Throwable(stringProvider.getString(R.string.passphrase_passphrase_does_not_match))) + ) + } + } + } + is BootstrapActions.DoInitializeGeneratedKey -> { val userPassword = reAuthHelper.data if (userPassword == null) { setState { copy( + passphrase = null, + passphraseRepeat = null, step = BootstrapStep.AccountPassword(false) ) } } else { + setState { + copy( + passphrase = null, + passphraseRepeat = null + ) + } startInitializeFlow(userPassword) } } - BootstrapActions.RecoveryKeySaved -> { + BootstrapActions.RecoveryKeySaved -> { _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) setState { copy(step = BootstrapStep.SaveRecoveryKey(true)) } } - BootstrapActions.Completed -> { + BootstrapActions.Completed -> { _viewEvents.post(BootstrapViewEvents.Dismiss) } - BootstrapActions.GoToCompleted -> { + BootstrapActions.GoToCompleted -> { setState { copy(step = BootstrapStep.DoneSuccess) } } - BootstrapActions.SaveReqQueryStarted -> { + BootstrapActions.SaveReqQueryStarted -> { setState { copy(recoverySaveFileProcess = Loading()) } } - is BootstrapActions.SaveKeyToUri -> { + is BootstrapActions.SaveKeyToUri -> { saveRecoveryKeyToUri(action.os) } - BootstrapActions.SaveReqFailed -> { + BootstrapActions.SaveReqFailed -> { setState { copy(recoverySaveFileProcess = Uninitialized) } } - BootstrapActions.GoToEnterAccountPassword -> { + BootstrapActions.GoToEnterAccountPassword -> { setState { copy(step = BootstrapStep.AccountPassword(false)) } } - BootstrapActions.HandleForgotBackupPassphrase -> { + BootstrapActions.HandleForgotBackupPassphrase -> { if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { setState { copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true)) } } else return@withState } - is BootstrapActions.ReAuth -> { + is BootstrapActions.ReAuth -> { startInitializeFlow(action.pass) } - is BootstrapActions.DoMigrateWithPassphrase -> { + is BootstrapActions.DoMigrateWithPassphrase -> { startMigrationFlow(state.step, action.passphrase, null) } - is BootstrapActions.DoMigrateWithRecoveryKey -> { + is BootstrapActions.DoMigrateWithRecoveryKey -> { startMigrationFlow(state.step, null, action.recoveryKey) } }.exhaustive @@ -234,6 +302,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( if (it is BackupToQuadSMigrationTask.Result.Success) { setState { copy( + passphrase = passphrase, + passphraseRepeat = passphrase, migrationRecoveryKey = recoveryKey ) } @@ -281,7 +351,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( } withState { state -> - val previousStep = state.step viewModelScope.launch(Dispatchers.IO) { val userPasswordAuth = userPassword?.let { UserPasswordAuth( @@ -296,6 +365,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( Params( userPasswordAuth = userPasswordAuth, progressListener = progressListener, + passphrase = state.passphrase, keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } } ) ) { bootstrapResult -> @@ -339,7 +409,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( _viewEvents.post(BootstrapViewEvents.ModalError(bootstrapResult.error ?: stringProvider.getString(R.string.matrix_error))) setState { copy( - step = previousStep + step = BootstrapStep.ConfirmPassphrase(false) ) } } @@ -375,12 +445,25 @@ class BootstrapSharedViewModel @AssistedInject constructor( // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) } + is BootstrapStep.SetupPassphrase -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + } + is BootstrapStep.ConfirmPassphrase -> { + setState { + copy( + step = BootstrapStep.SetupPassphrase( + isPasswordVisible = (state.step as? BootstrapStep.ConfirmPassphrase)?.isPasswordVisible ?: false + ) + ) + } + } is BootstrapStep.AccountPassword -> { - _viewEvents.post(BootstrapViewEvents.SkipBootstrap(false)) + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } BootstrapStep.Initializing -> { // do we let you cancel from here? - _viewEvents.post(BootstrapViewEvents.SkipBootstrap(false)) + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } is BootstrapStep.SaveRecoveryKey, BootstrapStep.DoneSuccess -> { @@ -393,8 +476,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( // Companion, view model assisted creation // ====================================== - companion object - : MvRxViewModelFactory { + companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt index 9dc8710d2d..f480402b6c 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt @@ -36,25 +36,29 @@ package im.vector.riotx.features.crypto.recover * └───────────────────────────────────┘ │ * │ │ * │ │ - * Existing ├─────────No ──────────────────┐ - * ┌────Keybackup───────┘ KeyBackup │ - * │ │ - * │ │ - * ▼ │ - * ┌─────────────────────────────────────────┐ │ - * │BootstrapStep.GetBackupSecretForMigration│ │ - * └─────────────────────────────────────────┘ │ - * │ │ - * │ │ - * │ is password needed? ─────────────┐ - * │ │ │ - * │ ▼ │ + * Existing ├─────────No ───────┐ │ + * ┌────Keybackup───────┘ KeyBackup │ │ + * │ │ │ + * │ ▼ ▼ + * ▼ ┌────────────────────────────────────┐ + * ┌─────────────────────────────────────────┐ │ BootstrapStep.SetupPassphrase │◀─┐ + * │BootstrapStep.GetBackupSecretForMigration│ └────────────────────────────────────┘ │ + * └─────────────────────────────────────────┘ │ │ + * │ │ ┌Back + * │ ▼ │ + * │ ┌────────────────────────────────────┤ + * │ │ BootstrapStep.ConfirmPassphrase │──┐ + * │ └────────────────────────────────────┘ │ + * │ │ │ + * │ is password needed? │ + * │ │ │ + * │ ▼ │ * │ ┌────────────────────────────────────┐ │ * │ │ BootstrapStep.AccountPassword │ │ * │ └────────────────────────────────────┘ │ - * │ │ │ - * │ │ │ - * │ ┌──────────────┘ password not needed (in + * │ │ │ + * │ │ │ + * │ ┌──────────────────┘ password not needed (in * │ │ memory) * │ │ │ * │ ▼ │ @@ -81,6 +85,9 @@ package im.vector.riotx.features.crypto.recover sealed class BootstrapStep { object SetupSecureBackup : BootstrapStep() + data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep() object CheckingMigration : BootstrapStep() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewState.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewState.kt index 4a53872ae5..8607bf1399 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewState.kt @@ -19,13 +19,18 @@ package im.vector.riotx.features.crypto.recover import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import com.nulabinc.zxcvbn.Strength import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo import im.vector.riotx.core.platform.WaitingViewData data class BootstrapViewState( - val step: BootstrapStep = BootstrapStep.SetupSecureBackup, + val step: BootstrapStep = BootstrapStep.SetupPassphrase(false), + val passphrase: String? = null, val migrationRecoveryKey: String? = null, + val passphraseRepeat: String? = null, val crossSigningInitialization: Async = Uninitialized, + val passphraseStrength: Async = Uninitialized, + val passphraseConfirmMatch: Async = Uninitialized, val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null, val initializationWaitingViewData: WaitingViewData? = null, val recoverySaveFileProcess: Async = Uninitialized