From a40dd31543839b3e1c30a99e9ae9038de6529a2c Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 27 Mar 2020 15:17:21 +0100 Subject: [PATCH 01/11] Bootstrap bottomsheet --- .../im/vector/riotx/core/di/FragmentModule.kt | 14 +- .../vector/riotx/core/di/ScreenComponent.kt | 2 + .../vector/riotx/core/di/VectorComponent.kt | 3 + .../im/vector/riotx/features/MainActivity.kt | 3 +- .../crypto/recover/BootstrapActions.kt | 29 +++ .../crypto/recover/BootstrapBottomSheet.kt | 107 +++++++++++ .../BootstrapConfirmPassphraseFragment.kt | 120 +++++++++++++ .../BootstrapEnterPassphraseFragment.kt | 168 ++++++++++++++++++ .../recover/BootstrapSharedViewModel.kt | 138 ++++++++++++++ .../crypto/recover/BootstrapViewEvents.kt | 34 ++++ .../verification/VerificationBottomSheet.kt | 2 +- .../riotx/features/home/HomeActivity.kt | 13 +- .../features/home/HomeActivitySharedAction.kt | 1 + .../riotx/features/login/LoginActivity.kt | 5 +- .../riotx/features/login/LoginViewModel.kt | 5 +- .../riotx/features/login/ReAuthHelper.kt | 47 +++++ .../res/layout/bottom_sheet_bootstrap.xml | 115 ++++++++++++ .../fragment_bootstrap_enter_passphrase.xml | 93 ++++++++++ vector/src/main/res/values/strings_riotX.xml | 7 + 19 files changed, 900 insertions(+), 6 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt 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 create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt create mode 100644 vector/src/main/res/layout/bottom_sheet_bootstrap.xml create mode 100644 vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml 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 4deaef32ab..8ec7352677 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 @@ -26,6 +26,8 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment +import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment +import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment @@ -78,8 +80,8 @@ import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment import im.vector.riotx.features.settings.devtools.AccountDataFragment import im.vector.riotx.features.settings.devtools.GossipingEventsPaperTrailFragment import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment -import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.riotx.features.settings.devtools.KeyRequestsFragment +import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.share.IncomingShareFragment @@ -402,4 +404,14 @@ interface FragmentModule { @IntoMap @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 } diff --git a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt index 5cd54c6c2b..af49b00b59 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/ScreenComponent.kt @@ -27,6 +27,7 @@ import im.vector.riotx.features.MainActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.debug.DebugMenuActivity import im.vector.riotx.features.home.HomeActivity @@ -128,6 +129,7 @@ interface ScreenComponent { fun inject(bottomSheet: VerificationBottomSheet) fun inject(bottomSheet: DeviceVerificationInfoBottomSheet) fun inject(bottomSheet: DeviceListBottomSheet) + fun inject(bottomSheet: BootstrapBottomSheet) /* ========================================================================================== * Others diff --git a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt index 2652f58b04..6f864c7f5b 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/VectorComponent.kt @@ -39,6 +39,7 @@ import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.HomeRoomListDataSource import im.vector.riotx.features.html.EventHtmlRenderer import im.vector.riotx.features.html.VectorHtmlCompressor +import im.vector.riotx.features.login.ReAuthHelper import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.notifications.NotifiableEventResolver import im.vector.riotx.features.notifications.NotificationBroadcastReceiver @@ -131,6 +132,8 @@ interface VectorComponent { fun alertManager() : PopupAlertManager + fun reAuthHelper() : ReAuthHelper + @Component.Factory interface Factory { fun create(@BindsInstance context: Context): VectorComponent diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index bc5a1aff95..c894e0739c 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -195,7 +195,8 @@ class MainActivity : VectorBaseActivity() { // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { - HomeActivity.newIntent(this) + // DO NOT COMMIT + HomeActivity.newIntent(this, accountCreation = true) } else { // The token is still invalid SoftLogoutActivity.newIntent(this) 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 new file mode 100644 index 0000000000..177a72dff1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt @@ -0,0 +1,29 @@ +/* + * 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 im.vector.riotx.core.platform.VectorViewModelAction + +sealed class BootstrapActions : VectorViewModelAction { + + object GoBack : BootstrapActions() + data class GoToConfirmPassphrase(val passphrase: String) : BootstrapActions() + object TogglePasswordVisibility : BootstrapActions() + data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() + data class UpdateConfirmCandidatePassphrase(val pass: String) : 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 new file mode 100644 index 0000000000..27b6d79d7a --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt @@ -0,0 +1,107 @@ +/* + * 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.app.Dialog +import android.os.Bundle +import android.view.KeyEvent +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* +import javax.inject.Inject +import kotlin.reflect.KClass + +class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { + + override val showExpanded = true + + @Inject + lateinit var bootstrapViewModelFactory: BootstrapSharedViewModel.Factory + + private val viewModel by fragmentViewModel(BootstrapSharedViewModel::class) + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + override fun getLayoutResId() = R.layout.bottom_sheet_bootstrap + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.observeViewEvents { + when (it) { + is BootstrapViewEvents.Dismiss -> dismiss() + } + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + val rootView = super.onCreateView(inflater, container, savedInstanceState) + dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + return rootView + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + return super.onCreateDialog(savedInstanceState).apply { + setOnKeyListener { _, keyCode, keyEvent -> + if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) { + viewModel.handle(BootstrapActions.GoBack) + true + } else { + false + } + } + } + } + + override fun invalidate() = withState(viewModel) { state -> + + when (state.step) { + is BootstrapStep.SetupPassphrase -> { + bootstrapTitleText.text = getString(R.string.recovery_passphrase) + showFragment(BootstrapEnterPassphraseFragment::class, Bundle()) + } + is BootstrapStep.ConfirmPassphrase -> { + bootstrapTitleText.text = getString(R.string.passphrase_confirm_passphrase) + showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) + } + is BootstrapStep.Initializing -> TODO() + } + super.invalidate() + } + + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { + if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { + childFragmentManager.commitTransaction { + replace(R.id.bottomSheetFragmentContainer, + fragmentClass.java, + bundle, + fragmentClass.simpleName + ) + } + } + } +} 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..df793f56b1 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConfirmPassphraseFragment.kt @@ -0,0 +1,120 @@ +/* + * 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.view.clicks +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 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) + + ssss_passphrase_enter_edittext.editorActionEvents() + .debounce(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() ?: "")) +// 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.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.ConfirmPassphrase) { + 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 -> + super.invalidate() + + if (state.step is BootstrapStep.ConfirmPassphrase) { + val isPasswordVisible = state.step.isPasswordVisible + ssss_passphrase_enter_edittext.showPassword(isPasswordVisible) + 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/BootstrapEnterPassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt new file mode 100644 index 0000000000..2d2370d997 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapEnterPassphraseFragment.kt @@ -0,0 +1,168 @@ +/* + * 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.view.clicks +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 kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_passphrase_enter_edittext +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_passphrase_enter_til +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password +import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.* +import timber.log.Timber +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 (usefull when coming back) + ssss_passphrase_enter_edittext.setText(it.passphrase ?: "") + } + ssss_passphrase_enter_edittext.editorActionEvents() + .debounce(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_passphrase_submit.clicks() +// .debounce(300, TimeUnit.MILLISECONDS) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe { +// submit() +// } +// .disposeOnDestroyView() + +// ssss_passphrase_cancel.clicks() +// .debounce(300, TimeUnit.MILLISECONDS) +// .observeOn(AndroidSchedulers.mainThread()) +// .subscribe { +// sharedViewModel.handle(SharedSecureStorageAction.Cancel) +// } +// .disposeOnDestroyView() + + ssss_view_show_password.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + } + + 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 -> + super.invalidate() + + if (state.step is BootstrapStep.SetupPassphrase) { + val isPasswordVisible = state.step.isPasswordVisible + ssss_passphrase_enter_edittext.showPassword(isPasswordVisible) + 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 + + Timber.e("## Strength info: $strength") + Timber.e("## Strength info score: $score") + Timber.e("## Strength info getWarning: ${strength.feedback?.getWarning(VectorLocale.applicationLocale)}") + Timber.e("## Strength info getSuggestions: ${strength.feedback?.getSuggestions(VectorLocale.applicationLocale)}") + Timber.e("## Strength info getFirstSuggestions: ${strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull()}") + if (score in 1..3) { + val hint = + strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } + ?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull() + Timber.e("## Strength info: $hint") + Timber.e("## Strength currentValue : ${ssss_passphrase_enter_til.error}") + 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/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt new file mode 100644 index 0000000000..4b363242ab --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -0,0 +1,138 @@ +/* + * 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 com.airbnb.mvrx.Async +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.Uninitialized +import com.airbnb.mvrx.ViewModelContext +import com.nulabinc.zxcvbn.Strength +import com.nulabinc.zxcvbn.Zxcvbn +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.session.Session +import im.vector.riotx.core.extensions.exhaustive +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.features.login.ReAuthHelper + +data class BootstrapViewState( + val step: BootstrapStep = BootstrapStep.SetupPassphrase(false), + val passphrase: String? = null, + val crossSigningInitialization: Async = Uninitialized, + val passphraseStrength: Async = Uninitialized +) : MvRxState + +sealed class BootstrapStep { + data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + object Initializing : BootstrapStep() +} + +class BootstrapSharedViewModel @AssistedInject constructor( + @Assisted initialState: BootstrapViewState, + private val session: Session, + private val reAuthHelper: ReAuthHelper +) : VectorViewModel(initialState) { + + private val zxcvbn = Zxcvbn() + + @AssistedInject.Factory + interface Factory { + fun create(initialState: BootstrapViewState): BootstrapSharedViewModel + } + + override fun handle(action: BootstrapActions) = withState { state -> + when (action) { + 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)) + } + } + else -> { + } + } + } + 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 -> { + + } + }.exhaustive + } + + // ======================================= + // Fragment interaction + // ======================================= + + private fun queryBack() = withState { state -> + when (state.step) { + is BootstrapStep.SetupPassphrase -> { + + } + is BootstrapStep.ConfirmPassphrase -> { + setState { + copy( + step = BootstrapStep.SetupPassphrase( + isPasswordVisible = (state.step as? BootstrapStep.ConfirmPassphrase)?.isPasswordVisible ?: false + ) + ) + } + } + + } + } + + // ====================================== + // Companion, view model assisted creation + // ====================================== + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { + val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() + return fragment.bootstrapViewModelFactory.create(state) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt new file mode 100644 index 0000000000..56cb9456af --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt @@ -0,0 +1,34 @@ +/* + * 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 im.vector.riotx.core.platform.VectorViewEvents + +sealed class BootstrapViewEvents : VectorViewEvents { + object Dismiss : BootstrapViewEvents() +// data class Failure(val throwable: Throwable) : DevicesViewEvents() +// +// object RequestPassword : DevicesViewEvents() +// +// data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents() +// +// data class ShowVerifyDevice( +// val userId: String, +// val transactionId: String? +// ) : DevicesViewEvents() +} + diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index 5b7adbdb91..2c29357113 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -130,7 +130,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return super.onCreateDialog(savedInstanceState).apply { setOnKeyListener { _, keyCode, keyEvent -> if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index b814fd9410..9cdb40e0d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -40,7 +40,9 @@ import im.vector.riotx.core.extensions.replaceFragment import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.pushers.PushersManager +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.disclaimer.showDisclaimerDialog +import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert @@ -95,6 +97,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) } + is HomeActivitySharedAction.PromptForSecurityBootstrap -> { + BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet") + } } } .disposeOnDestroy() @@ -103,6 +108,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { notificationDrawerManager.clearAllEvents() intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) } + if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) { + sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap) + intent.removeExtra(EXTRA_ACCOUNT_CREATION) + } activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status -> if (status == null) { @@ -246,11 +255,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { companion object { private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION" + private const val EXTRA_ACCOUNT_CREATION = "EXTRA_ACCOUNT_CREATION" - fun newIntent(context: Context, clearNotification: Boolean = false): Intent { + fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent { return Intent(context, HomeActivity::class.java) .apply { putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification) + putExtra(EXTRA_ACCOUNT_CREATION, accountCreation) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt index 493a14512d..902ea93588 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivitySharedAction.kt @@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorSharedAction sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() + object PromptForSecurityBootstrap : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 908a9c6370..9146e6a1eb 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -217,7 +217,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { private fun updateWithState(loginViewState: LoginViewState) { if (loginViewState.isUserLogged()) { - val intent = HomeActivity.newIntent(this) + val intent = HomeActivity.newIntent( + this, + accountCreation = true //loginViewState.signMode == SignMode.SignUp + ) startActivity(intent) finish() return diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt index b38b1d3ee2..80b04fe062 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginViewModel.kt @@ -38,6 +38,7 @@ import im.vector.matrix.android.api.auth.registration.Stage import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.extensions.configureAndStart import im.vector.riotx.core.platform.VectorViewModel @@ -56,7 +57,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private val activeSessionHolder: ActiveSessionHolder, private val pushRuleTriggerListener: PushRuleTriggerListener, private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory, - private val sessionListener: SessionListener) + private val sessionListener: SessionListener, + private val reAuthHelper: ReAuthHelper) : VectorViewModel(initialState) { @AssistedInject.Factory @@ -240,6 +242,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi private fun handleRegisterWith(action: LoginAction.LoginOrRegister) { setState { copy(asyncRegistration = Loading()) } + reAuthHelper.rememberAuth(UserPasswordAuth(user = action.username, password = action.password)) currentTask = registrationWizard?.createAccount( action.username, action.password, diff --git a/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt b/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt new file mode 100644 index 0000000000..a08ea051e2 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt @@ -0,0 +1,47 @@ +/* + * 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.login + +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import java.util.Timer +import java.util.TimerTask +import javax.inject.Inject +import javax.inject.Singleton + +const val THREE_MINUTES = 3 * 60_000L + +@Singleton +class ReAuthHelper @Inject constructor() { + + private var timer: Timer? = null + + private var rememberedInfo: UserPasswordAuth? = null + + private var clearTask = object : TimerTask() { + override fun run() { + rememberedInfo = null + } + } + + fun rememberAuth(password: UserPasswordAuth?) { + timer?.cancel() + rememberedInfo = password + timer = Timer().also { + it.schedule(clearTask, THREE_MINUTES) + } + } +} diff --git a/vector/src/main/res/layout/bottom_sheet_bootstrap.xml b/vector/src/main/res/layout/bottom_sheet_bootstrap.xml new file mode 100644 index 0000000000..dd194bcb86 --- /dev/null +++ b/vector/src/main/res/layout/bottom_sheet_bootstrap.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml b/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml new file mode 100644 index 0000000000..804325cbe0 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index ccb4bfa9be..b9fd1fa018 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -32,6 +32,13 @@ Verify your devices from Settings. Verification Cancelled + Recovery Passphrase + + + Secure & unlock information with a %s so only you can access encrypted messages and secure information. + + Enter your %s again to confirm it. + Don’t re-use your account password. From 8ecdac7c31f5e817627a5dc44e3d42b8d8e85e52 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 31 Mar 2020 17:08:11 +0200 Subject: [PATCH 02/11] Fixes #1191 --- .../android/internal/crypto/tasks/RoomVerificationUpdateTask.kt | 2 +- .../internal/session/room/EventRelationsAggregationTask.kt | 2 +- .../room/detail/timeline/factory/VerificationItemFactory.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index 014234885a..7a58ecb931 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -75,7 +75,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor( // TODO use a global event decryptor? attache to session and that listen to new sessionId? // for now decrypt sync try { - val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString()) + val result = cryptoService.decryptEvent(event, "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index d466ecc321..dcc1de5e4f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -230,7 +230,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( private fun decryptIfNeeded(event: Event) { if (event.mxDecryptionResult == null) { try { - val result = cryptoService.decryptEvent(event, event.roomId ?: "") + val result = cryptoService.decryptEvent(event, "") event.mxDecryptionResult = OlmDecryptionResult( payload = result.clearEvent, senderKey = result.senderCurve25519Key, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 890612c04c..419435c579 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -71,7 +71,7 @@ class VerificationItemFactory @Inject constructor( ?: return ignoredConclusion(event, highlight, callback) // If it's not a request ignore this event - if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) + // if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) val referenceInformationData = messageInformationDataFactory.create(refEvent, null) From bf5ba996539b29075bc3104d472541499187cb54 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2020 16:51:03 +0200 Subject: [PATCH 03/11] Full bootstrap flow initial commit --- .../crosssigning/CrossSigningService.kt | 3 + .../api/session/securestorage/KeySigner.kt | 4 + .../securestorage/SsssKeyCreationInfo.kt | 3 +- .../android/internal/crypto/MXOlmDevice.kt | 2 +- .../DefaultCrossSigningService.kt | 5 + .../crypto/model/rest/KeysQueryResponse.kt | 2 +- .../DefaultSharedSecretStorageService.kt | 6 +- .../im/vector/riotx/core/di/FragmentModule.kt | 24 ++ .../core/ui/views/BottomSheetActionButton.kt | 133 ++++++++++++ .../im/vector/riotx/core/utils/SystemUtils.kt | 8 +- .../im/vector/riotx/features/MainActivity.kt | 3 +- .../BootstrapAccountPasswordFragment.kt | 116 ++++++++++ .../crypto/recover/BootstrapActions.kt | 16 +- .../crypto/recover/BootstrapBottomSheet.kt | 47 +++- .../recover/BootstrapConclusionFragment.kt | 63 ++++++ .../BootstrapConfirmPassphraseFragment.kt | 31 +-- .../recover/BootstrapCrossSigningTask.kt | 177 +++++++++++++++ .../BootstrapEnterPassphraseFragment.kt | 35 +-- .../BootstrapSaveRecoveryKeyFragment.kt | 147 +++++++++++++ .../recover/BootstrapSharedViewModel.kt | 205 +++++++++++++++++- .../crypto/recover/BootstrapViewEvents.kt | 3 + .../recover/BootstrapWaitingFragment.kt | 42 ++++ .../crypto/recover/KeepItSafeDialog.kt | 68 ++++++ .../features/crypto/recover/RecoveryKeyExt.kt | 27 +++ .../verification/VerificationBottomSheet.kt | 19 +- .../riotx/features/login/LoginActivity.kt | 2 +- .../riotx/features/login/ReAuthHelper.kt | 21 +- .../features/rageshake/VectorFileLogger.kt | 4 +- vector/src/main/res/drawable/ic_clipboard.xml | 21 ++ vector/src/main/res/drawable/ic_download.xml | 27 +++ .../src/main/res/drawable/ic_message_key.xml | 22 ++ .../main/res/drawable/ic_message_password.xml | 22 ++ vector/src/main/res/drawable/ic_user.xml | 21 +- .../res/layout/bottom_sheet_bootstrap.xml | 71 +----- .../res/layout/bottom_sheet_verification.xml | 5 +- .../layout/dialog_recovery_key_saved_info.xml | 57 +++++ .../layout/fragment_bootstrap_conclusion.xml | 50 +++++ ...gment_bootstrap_enter_account_password.xml | 70 ++++++ .../fragment_bootstrap_enter_passphrase.xml | 2 +- .../layout/fragment_bootstrap_save_key.xml | 88 ++++++++ .../res/layout/fragment_bootstrap_waiting.xml | 41 ++++ .../res/layout/item_verification_action.xml | 21 +- vector/src/main/res/values/attrs.xml | 9 + vector/src/main/res/values/strings_riotX.xml | 47 +++- 44 files changed, 1616 insertions(+), 174 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.kt create mode 100644 vector/src/main/res/drawable/ic_clipboard.xml create mode 100644 vector/src/main/res/drawable/ic_download.xml create mode 100644 vector/src/main/res/drawable/ic_message_key.xml create mode 100644 vector/src/main/res/drawable/ic_message_password.xml create mode 100644 vector/src/main/res/layout/dialog_recovery_key_saved_info.xml create mode 100644 vector/src/main/res/layout/fragment_bootstrap_conclusion.xml create mode 100644 vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml create mode 100644 vector/src/main/res/layout/fragment_bootstrap_save_key.xml create mode 100644 vector/src/main/res/layout/fragment_bootstrap_waiting.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt index fe3f643124..4085e1233d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/crosssigning/CrossSigningService.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo interface CrossSigningService { @@ -52,6 +53,8 @@ interface CrossSigningService { fun getMyCrossSigningKeys(): MXCrossSigningInfo? + fun getCrossSigningPrivateKeys(): PrivateKeysInfo? + fun canCrossSign(): Boolean fun trustUser(otherUserId: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt index 2cd7a74f31..242f9a8945 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/KeySigner.kt @@ -19,3 +19,7 @@ package im.vector.matrix.android.api.session.securestorage interface KeySigner { fun sign(canonicalJson: String): Map>? } + +class EmptyKeySigner : KeySigner { + override fun sign(canonicalJson: String): Map>? = null +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt index 1d5522b8bf..85823d9dbf 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SsssKeyCreationInfo.kt @@ -19,5 +19,6 @@ package im.vector.matrix.android.api.session.securestorage data class SsssKeyCreationInfo( val keyId: String = "", var content: SecretStorageKeyContent?, - val recoveryKey: String = "" + val recoveryKey: String = "", + val keySpec: SsssKeySpec ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt index 0f48b5ecfb..86f0768a7d 100755 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/MXOlmDevice.kt @@ -641,7 +641,7 @@ internal class MXOlmDevice @Inject constructor( throw MXCryptoError.OlmError(e) } - if (null != timeline) { + if (timeline?.isNotBlank() == true) { val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() } val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 389fa1ea50..54392ad44c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.KeyUsage import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore +import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask import im.vector.matrix.android.internal.di.UserId @@ -595,6 +596,10 @@ internal class DefaultCrossSigningService @Inject constructor( return cryptoStore.getMyCrossSigningInfo() } + override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? { + return cryptoStore.getCrossSigningPrivateKeys() + } + override fun canCrossSign(): Boolean { return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null && cryptoStore.getCrossSigningPrivateKeys()?.user != null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt index dd3cb049dd..aea8eeb301 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/KeysQueryResponse.kt @@ -28,7 +28,7 @@ import com.squareup.moshi.JsonClass * The user_signing_keys property will only be included when a user requests their own keys. */ @JsonClass(generateAdapter = true) -internal data class KeysQueryResponse( +internal data class KeysQueryResponse( /** * The device keys per devices per users. * Map from userId to map from deviceId to MXDeviceInfo diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 42f72a0a33..62bc4774c6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -102,7 +102,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( callback.onSuccess(SsssKeyCreationInfo( keyId = keyId, content = storageKeyContent, - recoveryKey = computeRecoveryKey(key) + recoveryKey = computeRecoveryKey(key), + keySpec = RawBytesKeySpec(key) )) } } @@ -142,7 +143,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( callback.onSuccess(SsssKeyCreationInfo( keyId = keyId, content = storageKeyContent, - recoveryKey = computeRecoveryKey(privatePart.privateKey) + recoveryKey = computeRecoveryKey(privatePart.privateKey), + keySpec = RawBytesKeySpec(privatePart.privateKey) )) } } 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 8ec7352677..9f3cdba683 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 @@ -26,8 +26,12 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment +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.BootstrapSaveRecoveryKeyFragment +import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment @@ -414,4 +418,24 @@ interface FragmentModule { @IntoMap @FragmentKey(BootstrapConfirmPassphraseFragment::class) fun bindBootstrapConfirmPassphraseFragment(fragment: BootstrapConfirmPassphraseFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapWaitingFragment::class) + fun bindBootstrapWaitingFragment(fragment: BootstrapWaitingFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapSaveRecoveryKeyFragment::class) + fun bindBootstrapSaveRecoveryKeyFragment(fragment: BootstrapSaveRecoveryKeyFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapConclusionFragment::class) + fun bindBootstrapConclusionFragment(fragment: BootstrapConclusionFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(BootstrapAccountPasswordFragment::class) + fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt new file mode 100644 index 0000000000..07cb7bb38d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt @@ -0,0 +1,133 @@ +/* + * 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.core.ui.views + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.TextView +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import butterknife.BindView +import butterknife.ButterKnife +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.themes.ThemeUtils + +class BottomSheetActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr) { + + @BindView(R.id.itemVerificationActionTitle) + lateinit var actionTextView: TextView + + @BindView(R.id.itemVerificationActionSubTitle) + lateinit var descriptionTextView: TextView + + @BindView(R.id.itemVerificationLeftIcon) + lateinit var leftIconImageView: ImageView + + @BindView(R.id.itemVerificationActionIcon) + lateinit var rightIconImageView: ImageView + + @BindView(R.id.itemVerificationClickableZone) + lateinit var clickableView: View + + + var title: String? = null + set(value) { + field = value + actionTextView.setTextOrHide(value) + } + + var subTitle: String? = null + set(value) { + field = value + descriptionTextView.setTextOrHide(value) + } + + var forceStartPadding: Boolean? = null + set(value) { + field = value + if (leftIcon == null) { + if (forceStartPadding == true) { + leftIconImageView.isInvisible = true + } else { + leftIconImageView.isGone = true + } + } + } + + var leftIcon: Drawable? = null + set(value) { + field = value + if (value == null) { + if (forceStartPadding == true) { + leftIconImageView.isInvisible = true + } else { + leftIconImageView.isGone = true + } + leftIconImageView.setImageDrawable(null) + } else { + leftIconImageView.isVisible = true + leftIconImageView.setImageDrawable(value) + } + } + + var rightIcon: Drawable? = null + set(value) { + field = value + rightIconImageView.setImageDrawable(value) + } + + var tint: Int? = null + set(value) { + field = value + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) } + } else { + leftIcon?.let { + leftIcon = ThemeUtils.tintDrawable(context, it, value ?: ThemeUtils.getColor(context, android.R.attr.textColor)) + } + } + } + + init { + inflate(context, R.layout.item_verification_action, this) + ButterKnife.bind(this) + + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0) + title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" + subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: "" + forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false) + leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon) + + rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon) + + tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) + + typedArray.recycle() + } +} diff --git a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt index 1e005c777b..b77553a3d6 100644 --- a/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt +++ b/vector/src/main/java/im/vector/riotx/core/utils/SystemUtils.kt @@ -153,7 +153,7 @@ fun startAddGoogleAccountIntent(context: AppCompatActivity, requestCode: Int) { } } -fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null) { +fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null, requestCode: Int? = null) { val share = Intent(Intent.ACTION_SEND) share.type = "text/plain" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -165,7 +165,11 @@ fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: S share.putExtra(Intent.EXTRA_SUBJECT, subject) share.putExtra(Intent.EXTRA_TEXT, text) try { - fragment.startActivity(Intent.createChooser(share, chooserTitle)) + if (requestCode != null) { + fragment.startActivityForResult(Intent.createChooser(share, chooserTitle), requestCode) + } else { + fragment.startActivity(Intent.createChooser(share, chooserTitle)) + } } catch (activityNotFoundException: ActivityNotFoundException) { fragment.activity?.toast(R.string.error_no_external_application_found) } diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index c894e0739c..bc5a1aff95 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -195,8 +195,7 @@ class MainActivity : VectorBaseActivity() { // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { - // DO NOT COMMIT - HomeActivity.newIntent(this, accountCreation = true) + HomeActivity.newIntent(this) } else { // The token is still invalid SoftLogoutActivity.newIntent(this) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt new file mode 100644 index 0000000000..ba9a4e8460 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -0,0 +1,116 @@ +/* + * 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.view.clicks +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_account_password.* +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapAccountPasswordFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_account_password + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val recPassPhrase = getString(R.string.account_password) + bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + bootstrapAccountPasswordEditText.hint = getString(R.string.account_password) + + bootstrapAccountPasswordEditText.editorActionEvents() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + bootstrapAccountPasswordEditText.textChanges() + .distinct() + .subscribe { + if (!it.isNullOrBlank()) { + bootstrapAccountPasswordTil.error = null + } + } + .disposeOnDestroyView() + + ssss_view_show_password.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + + withState(sharedViewModel) {state -> + (state.step as? BootstrapStep.AccountPassword)?.failure?.let { + bootstrapAccountPasswordTil.error = it + } + } + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.AccountPassword) { + return@withState + } + val accountPassword = bootstrapAccountPasswordEditText.text?.toString() + if (accountPassword.isNullOrBlank()) { + bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password) + } else { + view?.hideKeyboard() + sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword)) + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + super.invalidate() + + if (state.step is BootstrapStep.AccountPassword) { + val isPasswordVisible = state.step.isPasswordVisible + bootstrapAccountPasswordEditText.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/BootstrapActions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt index 177a72dff1..6029c526f8 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 @@ -16,14 +16,28 @@ package im.vector.riotx.features.crypto.recover +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.riotx.core.platform.VectorViewModelAction +import java.io.OutputStream sealed class BootstrapActions : VectorViewModelAction { + // Navigation + object GoBack : BootstrapActions() data class GoToConfirmPassphrase(val passphrase: String) : BootstrapActions() + object GoToCompleted : BootstrapActions() + object GoToEnterAccountPassword : BootstrapActions() + + + data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : 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() + object SaveReqQueryStarted : BootstrapActions() + data class SaveKeyToUri(val os: OutputStream) : BootstrapActions() + object SaveReqFailed : 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 27b6d79d7a..8c3086b318 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 @@ -23,11 +23,15 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.WindowManager +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent +import im.vector.riotx.core.dialogs.PromptPasswordDialog import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* @@ -51,9 +55,19 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - viewModel.observeViewEvents { - when (it) { - is BootstrapViewEvents.Dismiss -> dismiss() + viewModel.observeViewEvents { event -> + when (event) { + is BootstrapViewEvents.Dismiss -> dismiss() + is BootstrapViewEvents.ModalError -> { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_title_error) + .setMessage(event.error) + .setPositiveButton(R.string.ok, null) + .show() + } + BootstrapViewEvents.RecoveryKeySaved -> { + KeepItSafeDialog().show(requireActivity()) + } } } } @@ -81,14 +95,35 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { when (state.step) { is BootstrapStep.SetupPassphrase -> { - bootstrapTitleText.text = getString(R.string.recovery_passphrase) + 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 -> { - bootstrapTitleText.text = getString(R.string.passphrase_confirm_passphrase) + 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.Initializing -> TODO() + is BootstrapStep.AccountPassword -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) + bootstrapTitleText.text = getString(R.string.account_password) + showFragment(BootstrapAccountPasswordFragment::class, Bundle()) + } + is BootstrapStep.Initializing -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) + bootstrapTitleText.text = getString(R.string.bootstrap_loading_title) + showFragment(BootstrapWaitingFragment::class, Bundle()) + } + is BootstrapStep.SaveRecoveryKey -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) + bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy) + showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle()) + } + is BootstrapStep.DoneSuccess -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) + bootstrapTitleText.text = getString(R.string.bootstrap_finish_title) + showFragment(BootstrapConclusionFragment::class, Bundle()) + } } super.invalidate() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt new file mode 100644 index 0000000000..b7b656be74 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -0,0 +1,63 @@ +/* + * 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 androidx.core.text.toSpannable +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import im.vector.riotx.R +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_conclusion.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapConclusionFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_conclusion + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + bootstrapConclusionContinue.clickableView.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.Completed) + } + .disposeOnDestroyView() + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.DoneSuccess) return@withState + + bootstrapConclusionText.text = getString(R.string.bootstrap_cross_signing_success, getString(R.string.recovery_passphrase), getString(R.string.message_key)) + .toSpannable() + .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + } +} 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 index df793f56b1..a7ebd673b6 100644 --- 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 @@ -27,6 +27,7 @@ import com.jakewharton.rxbinding3.view.clicks 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 @@ -56,6 +57,12 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( 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() .debounce(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -68,9 +75,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( ssss_passphrase_enter_edittext.textChanges() .subscribe { - // ssss_passphrase_enter_til.error = null + ssss_passphrase_enter_til.error = null sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it?.toString() ?: "")) -// ssss_passphrase_submit.isEnabled = it.isNotBlank() } .disposeOnDestroyView() @@ -95,16 +101,15 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( if (state.step !is BootstrapStep.ConfirmPassphrase) { 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)) -// } + 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 (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 -> @@ -112,7 +117,7 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( if (state.step is BootstrapStep.ConfirmPassphrase) { val isPasswordVisible = state.step.isPasswordVisible - ssss_passphrase_enter_edittext.showPassword(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 new file mode 100644 index 0000000000..09f962dd3f --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -0,0 +1,177 @@ +/* + * 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 im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.failure.MatrixError +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME +import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.auth.data.LoginFlowTypes +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.R +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch +import java.util.UUID +import javax.inject.Inject + +sealed class BootstrapResult { + + data class Success(val keyInfo: SsssKeyCreationInfo) : BootstrapResult() + + abstract class Failure(val error: String?) : BootstrapResult() + + class UnsupportedAuthFlow : Failure(null) + + data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage) + data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null) + class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) + class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) + class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage) + object MissingPrivateKey : Failure(null) + + data class PasswordAuthFlowMissing(val sessionId: String, val userId: String) : Failure(null) +} + +interface BootstrapProgressListener { + fun onProgress(data: WaitingViewData) +} + +data class Params( + val userPasswordAuth: UserPasswordAuth? = null, + val progressListener: BootstrapProgressListener? = null, + val passphrase: String +) + +class BootstrapCrossSigningTask @Inject constructor( + private val session: Session, + private val stringProvider: StringProvider +) { + + operator fun invoke( + scope: CoroutineScope, + params: Params, + onResult: (BootstrapResult) -> Unit = {} + ) { + val backgroundJob = scope.async { execute(params) } + scope.launch { onResult(backgroundJob.await()) } + } + + suspend fun execute(params: Params): BootstrapResult { + + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), isIndeterminate = true)) + val crossSigningService = session.cryptoService().crossSigningService() + + try { + awaitCallback { + crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) + } + } catch (failure: Throwable) { + return handleInitializeXSigningError(failure) + } + + val keyInfo: SsssKeyCreationInfo + + val ssssService = session.sharedSecretStorageService + + + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true)) + try { + keyInfo = awaitCallback { + ssssService.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + params.passphrase, + EmptyKeySigner(), + null, + it + ) + } + } catch (failure: Failure) { + return BootstrapResult.FailedToCreateSSSSKey(failure) + } + + + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true)) + try { + awaitCallback { + ssssService.setDefaultKey(keyInfo.keyId, it) + } + } catch (failure: Failure) { + // Maybe we could just ignore this error? + return BootstrapResult.FailedToSetDefaultSSSSKey(failure) + } + + val xKeys = crossSigningService.getCrossSigningPrivateKeys() + val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey + val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey + val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey + + try { + + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk), isIndeterminate = true)) + awaitCallback { + ssssService.storeSecret(MASTER_KEY_SSSS_NAME, mskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) + } + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), isIndeterminate = true)) + awaitCallback { + ssssService.storeSecret(USER_SIGNING_KEY_SSSS_NAME, uskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) + } + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true)) + awaitCallback { + ssssService.storeSecret(SELF_SIGNING_KEY_SSSS_NAME, sskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) + } + } catch (failure: Failure) { + // Maybe we could just ignore this error? + return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure) + } + + // TODO configure key backup? + + return BootstrapResult.Success(keyInfo) + } + + private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult { + if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) { + return BootstrapResult.InvalidPasswordError(failure.error) + } else if (failure is Failure.OtherServerError && failure.httpCode == 401) { + try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(failure.errorBody) + } catch (e: Exception) { + null + }?.let { flowResponse -> + if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) { + // can't do this from here + return BootstrapResult.UnsupportedAuthFlow() + } + } + } + return BootstrapResult.GenericError(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 index 2d2370d997..3995a7a6dc 100644 --- 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 @@ -33,11 +33,6 @@ 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 kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_passphrase_enter_edittext -import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_passphrase_enter_til -import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password -import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.* -import timber.log.Timber import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -52,7 +47,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor( 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() @@ -89,22 +83,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor( // } } -// ssss_passphrase_submit.clicks() -// .debounce(300, TimeUnit.MILLISECONDS) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe { -// submit() -// } -// .disposeOnDestroyView() - -// ssss_passphrase_cancel.clicks() -// .debounce(300, TimeUnit.MILLISECONDS) -// .observeOn(AndroidSchedulers.mainThread()) -// .subscribe { -// sharedViewModel.handle(SharedSecureStorageAction.Cancel) -// } -// .disposeOnDestroyView() - ssss_view_show_password.clicks() .debounce(300, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -118,7 +96,6 @@ class BootstrapEnterPassphraseFragment @Inject constructor( 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()) { @@ -135,24 +112,16 @@ class BootstrapEnterPassphraseFragment @Inject constructor( if (state.step is BootstrapStep.SetupPassphrase) { val isPasswordVisible = state.step.isPasswordVisible - ssss_passphrase_enter_edittext.showPassword(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 - - Timber.e("## Strength info: $strength") - Timber.e("## Strength info score: $score") - Timber.e("## Strength info getWarning: ${strength.feedback?.getWarning(VectorLocale.applicationLocale)}") - Timber.e("## Strength info getSuggestions: ${strength.feedback?.getSuggestions(VectorLocale.applicationLocale)}") - Timber.e("## Strength info getFirstSuggestions: ${strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull()}") if (score in 1..3) { val hint = strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() } ?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull() - Timber.e("## Strength info: $hint") - Timber.e("## Strength currentValue : ${ssss_passphrase_enter_til.error}") if (hint != null && hint != ssss_passphrase_enter_til.error.toString()) { ssss_passphrase_enter_til.error = hint } @@ -160,9 +129,7 @@ class BootstrapEnterPassphraseFragment @Inject constructor( ssss_passphrase_enter_til.error = null } } - } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt new file mode 100644 index 0000000000..186e24a90d --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -0,0 +1,147 @@ +/* + * 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.app.Activity.RESULT_OK +import android.content.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.View +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import im.vector.riotx.R +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.core.utils.startSharePlainTextIntent +import im.vector.riotx.core.utils.toast +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapSaveRecoveryKeyFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_save_key + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase)) + .toSpannable() + .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + recoverySave.clickableView.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + downloadRecoveryKey() + } + .disposeOnDestroyView() + + recoveryCopy.clickableView.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + shareRecoveryKey() + } + .disposeOnDestroyView() + + recoveryContinue.clickableView.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.GoToCompleted) + } + .disposeOnDestroyView() + } + + private fun downloadRecoveryKey() = withState(sharedViewModel) { _ -> + + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "text/plain" + intent.putExtra(Intent.EXTRA_TITLE, "riot-recovery-key.txt") + + try { + sharedViewModel.handle(BootstrapActions.SaveReqQueryStarted) + startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step3_please_make_copy)), REQUEST_CODE_SAVE) + } catch (activityNotFoundException: ActivityNotFoundException) { + requireActivity().toast(R.string.error_no_external_application_found) + sharedViewModel.handle(BootstrapActions.SaveReqFailed) + } + + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE_SAVE) { + val uri = data?.data + if (resultCode == RESULT_OK && uri != null) { + GlobalScope.launch(Dispatchers.IO) { + try { + sharedViewModel.handle(BootstrapActions.SaveKeyToUri(context!!.contentResolver!!.openOutputStream(uri)!!)) + } catch (failure: Throwable) { + sharedViewModel.handle(BootstrapActions.SaveReqFailed) + } + } + } else { + // result code seems to be always cancelled here.. so act as if it was saved + sharedViewModel.handle(BootstrapActions.SaveReqFailed) + } + return + } else if (requestCode == REQUEST_CODE_COPY) { + sharedViewModel.handle(BootstrapActions.RecoveryKeySaved) + } + super.onActivityResult(requestCode, resultCode, data) + } + + private fun shareRecoveryKey() = withState(sharedViewModel) { state -> + val recoveryKey = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() + ?: return@withState + + startSharePlainTextIntent(this, + context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title), + recoveryKey, + context?.getString(R.string.recovery_key) + , REQUEST_CODE_COPY) + + } + + override fun invalidate() = withState(sharedViewModel) { state -> + val step = state.step + if (step !is BootstrapStep.SaveRecoveryKey) return@withState + + recoveryContinue.isVisible = step.isSaved + bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() + } + + companion object { + const val REQUEST_CODE_SAVE = 123 + const val REQUEST_CODE_COPY = 124 + } +} 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 4b363242ab..fcb03c05c1 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 @@ -16,8 +16,11 @@ package im.vector.riotx.features.crypto.recover +import androidx.lifecycle.viewModelScope import com.airbnb.mvrx.Async +import com.airbnb.mvrx.Fail import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.Success @@ -28,26 +31,44 @@ import com.nulabinc.zxcvbn.Zxcvbn import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.riotx.R import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.login.ReAuthHelper +import kotlinx.coroutines.launch +import java.io.OutputStream data class BootstrapViewState( val step: BootstrapStep = BootstrapStep.SetupPassphrase(false), val passphrase: String? = null, + val passphraseRepeat: String? = null, val crossSigningInitialization: Async = Uninitialized, - val passphraseStrength: Async = Uninitialized + val passphraseStrength: Async = Uninitialized, + val passphraseConfirmMatch: Async = Uninitialized, + val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null, + val initializationWaitingViewData: WaitingViewData? = null, + val currentReAuth: UserPasswordAuth? = null, + val recoverySaveFileProcess: Async = Uninitialized ) : MvRxState sealed class 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 Initializing : BootstrapStep() + data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() + object DoneSuccess : BootstrapStep() } class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, + private val stringProvider: StringProvider, private val session: Session, + private val bootstrapTask: BootstrapCrossSigningTask, private val reAuthHelper: ReAuthHelper ) : VectorViewModel(initialState) { @@ -60,8 +81,8 @@ 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 { @@ -73,11 +94,18 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } + + is BootstrapStep.AccountPassword -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } else -> { } } } - is BootstrapActions.UpdateCandidatePassphrase -> { + + is BootstrapActions.UpdateCandidatePassphrase -> { val strength = zxcvbn.measure(action.pass) setState { copy( @@ -86,7 +114,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapActions.GoToConfirmPassphrase -> { + is BootstrapActions.GoToConfirmPassphrase -> { setState { copy( passphrase = action.passphrase, @@ -97,18 +125,181 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } is BootstrapActions.UpdateConfirmCandidatePassphrase -> { - + setState { + copy( + passphraseRepeat = action.pass + ) + } + } + is BootstrapActions.DoInitialize -> { + if (state.passphrase == state.passphraseRepeat) { + val auth = action.auth ?: reAuthHelper.rememberedAuth() + if (auth == null) { + setState { + copy( + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(action.auth) + } + } else { + setState { + copy( + passphraseConfirmMatch = Fail(Throwable(stringProvider.getString(R.string.passphrase_passphrase_does_not_match))) + ) + } + } + } + BootstrapActions.RecoveryKeySaved -> { + _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) + setState { + copy(step = BootstrapStep.SaveRecoveryKey(true)) + } + } + BootstrapActions.Completed -> { + _viewEvents.post(BootstrapViewEvents.Dismiss) + } + BootstrapActions.GoToCompleted -> { + setState { + copy(step = BootstrapStep.DoneSuccess) + } + } + BootstrapActions.SaveReqQueryStarted -> { + setState { + copy(recoverySaveFileProcess = Loading()) + } + } + is BootstrapActions.SaveKeyToUri -> { + saveRecoveryKeyToUri(action.os) + } + BootstrapActions.SaveReqFailed -> { + setState { + copy(recoverySaveFileProcess = Uninitialized) + } + } + BootstrapActions.GoToEnterAccountPassword -> { + setState { + copy(step = BootstrapStep.AccountPassword(false)) + } + } + is BootstrapActions.ReAuth -> { + startInitializeFlow( + state.currentReAuth?.copy(password = action.pass) + ?: UserPasswordAuth(user = session.myUserId, password = action.pass) + ) } }.exhaustive } + // ======================================= + // Business Logic + // ======================================= + private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state -> + viewModelScope.launch { + kotlin.runCatching { + os.use { + os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray()) + } + }.fold({ + setState { + _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) + copy( + recoverySaveFileProcess = Success(Unit), + step = BootstrapStep.SaveRecoveryKey(isSaved = true) + ) + } + }, { + setState { + copy(recoverySaveFileProcess = Fail(it)) + } + }) + + } + } + + private fun startInitializeFlow(auth: UserPasswordAuth?) { + + setState { + copy(step = BootstrapStep.Initializing) + } + + val progressListener = object : BootstrapProgressListener { + override fun onProgress(data: WaitingViewData) { + setState { + copy( + initializationWaitingViewData = data + ) + } + } + } + + + withState { state -> + viewModelScope.launch { + bootstrapTask.invoke(this, Params( + userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(), + progressListener = progressListener, + passphrase = state.passphrase!! + )) { + when (it) { + is BootstrapResult.Success -> { + setState { + copy( + recoveryKeyCreationInfo = it.keyInfo, + step = BootstrapStep.SaveRecoveryKey(false) + ) + } + } + is BootstrapResult.PasswordAuthFlowMissing -> { + setState { + copy( + currentReAuth = UserPasswordAuth(session = it.sessionId, user = it.userId), + step = BootstrapStep.AccountPassword(false) + ) + } + } + is BootstrapResult.UnsupportedAuthFlow -> { + _viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported))) + _viewEvents.post(BootstrapViewEvents.Dismiss) + } + is BootstrapResult.InvalidPasswordError -> { + // it's a bad password + setState { + copy( + // We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error + currentReAuth = UserPasswordAuth(session = null, user = session.myUserId), + step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param)) + ) + } + } + is BootstrapResult.Failure -> { + if (it is BootstrapResult.GenericError + && it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError + && it.failure.httpCode == 401) { + + } else { + _viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error))) + setState { + copy( + step = BootstrapStep.ConfirmPassphrase(false) + ) + } + } + } + } + } + } + } + } + // ======================================= // Fragment interaction // ======================================= private fun queryBack() = withState { state -> when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.SetupPassphrase -> { } is BootstrapStep.ConfirmPassphrase -> { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt index 56cb9456af..ae6c00d656 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt @@ -20,6 +20,9 @@ import im.vector.riotx.core.platform.VectorViewEvents sealed class BootstrapViewEvents : VectorViewEvents { object Dismiss : BootstrapViewEvents() +// data class RequestPassword(val sessionId: String, val userId: String) : BootstrapViewEvents() + data class ModalError(val error: String) : BootstrapViewEvents() + object RecoveryKeySaved: BootstrapViewEvents() // data class Failure(val throwable: Throwable) : DevicesViewEvents() // // object RequestPassword : DevicesViewEvents() diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt new file mode 100644 index 0000000000..ff79fa6a4b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt @@ -0,0 +1,42 @@ +/* + * 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 com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorBaseFragment +import kotlinx.android.synthetic.main.fragment_bootstrap_waiting.* +import javax.inject.Inject + +class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_waiting + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.Initializing) return@withState + bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt new file mode 100644 index 0000000000..c8348ea7e9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/KeepItSafeDialog.kt @@ -0,0 +1,68 @@ +/* + * 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.app.Activity +import android.content.DialogInterface +import android.view.KeyEvent +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.content.ContextCompat +import im.vector.riotx.R +import me.gujun.android.span.image +import me.gujun.android.span.span + +class KeepItSafeDialog { + + fun show(activity: Activity) { + val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_recovery_key_saved_info, null) + + val descriptionText = dialogLayout.findViewById(R.id.keepItSafeText) + + descriptionText.text = span { + span { + image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!) + +" " + +activity.getString(R.string.bootstrap_crosssigning_print_it) + +"\n\n" + image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!) + +" " + +activity.getString(R.string.bootstrap_crosssigning_save_usb) + +"\n\n" + image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!) + +" " + +activity.getString(R.string.bootstrap_crosssigning_save_cloud) + +"\n\n" + } + } + + AlertDialog.Builder(activity) +// .setIcon(android.R.drawable.ic_dialog_alert) +// .setTitle(R.string.devices_delete_dialog_title) + .setView(dialogLayout) + .setPositiveButton(R.string.ok, null) + .setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + dialog.cancel() + return@OnKeyListener true + } + false + }) + .create() + .show() + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.kt new file mode 100644 index 0000000000..29453af932 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/RecoveryKeyExt.kt @@ -0,0 +1,27 @@ +/* + * 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 + +fun String.formatRecoveryKey(): String { + return this.replace(" ", "") + .chunked(16) + .joinToString("\n") { + it + .chunked(4) + .joinToString(" ") + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index 2c29357113..9097724a35 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -25,7 +25,6 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible import androidx.fragment.app.Fragment import butterknife.BindView @@ -130,7 +129,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { return super.onCreateDialog(savedInstanceState).apply { setOnKeyListener { _, keyCode, keyEvent -> if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) { @@ -352,11 +351,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } -fun View.getParentCoordinatorLayout(): CoordinatorLayout? { - var current = this as? View - while (current != null) { - if (current is CoordinatorLayout) return current - current = current.parent as? View - } - return null -} +//fun View.getParentCoordinatorLayout(): CoordinatorLayout? { +// var current = this as? View +// while (current != null) { +// if (current is CoordinatorLayout) return current +// current = current.parent as? View +// } +// return null +//} diff --git a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt index 9146e6a1eb..c67e45d9e7 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/LoginActivity.kt @@ -219,7 +219,7 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable { if (loginViewState.isUserLogged()) { val intent = HomeActivity.newIntent( this, - accountCreation = true //loginViewState.signMode == SignMode.SignUp + accountCreation = loginViewState.signMode == SignMode.SignUp ) startActivity(intent) finish() diff --git a/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt b/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt index a08ea051e2..3a6142bc08 100644 --- a/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt +++ b/vector/src/main/java/im/vector/riotx/features/login/ReAuthHelper.kt @@ -31,17 +31,18 @@ class ReAuthHelper @Inject constructor() { private var rememberedInfo: UserPasswordAuth? = null - private var clearTask = object : TimerTask() { - override fun run() { - rememberedInfo = null + fun rememberAuth(password: UserPasswordAuth?) { + timer?.cancel() + timer = null + rememberedInfo = password + timer = Timer().apply { + schedule(object : TimerTask() { + override fun run() { + rememberedInfo = null + } + }, THREE_MINUTES) } } - fun rememberAuth(password: UserPasswordAuth?) { - timer?.cancel() - rememberedInfo = password - timer = Timer().also { - it.schedule(clearTask, THREE_MINUTES) - } - } + fun rememberedAuth() = rememberedInfo } diff --git a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt index 16e231491a..3beb2882fb 100644 --- a/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt +++ b/vector/src/main/java/im/vector/riotx/features/rageshake/VectorFileLogger.kt @@ -162,7 +162,9 @@ class VectorFileLogger @Inject constructor(val context: Context, private val vec companion object { private val LINE_SEPARATOR = System.getProperty("line.separator") ?: "\n" - private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) +// private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US) + private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss*SSSZZZZ", Locale.US) + private var mIsTimeZoneSet = false } } diff --git a/vector/src/main/res/drawable/ic_clipboard.xml b/vector/src/main/res/drawable/ic_clipboard.xml new file mode 100644 index 0000000000..a94b845bed --- /dev/null +++ b/vector/src/main/res/drawable/ic_clipboard.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_download.xml b/vector/src/main/res/drawable/ic_download.xml new file mode 100644 index 0000000000..58c4a76a79 --- /dev/null +++ b/vector/src/main/res/drawable/ic_download.xml @@ -0,0 +1,27 @@ + + + + + diff --git a/vector/src/main/res/drawable/ic_message_key.xml b/vector/src/main/res/drawable/ic_message_key.xml new file mode 100644 index 0000000000..c4a415477b --- /dev/null +++ b/vector/src/main/res/drawable/ic_message_key.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_message_password.xml b/vector/src/main/res/drawable/ic_message_password.xml new file mode 100644 index 0000000000..311be14cc6 --- /dev/null +++ b/vector/src/main/res/drawable/ic_message_password.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/vector/src/main/res/drawable/ic_user.xml b/vector/src/main/res/drawable/ic_user.xml index cc811c6073..119c0a5170 100644 --- a/vector/src/main/res/drawable/ic_user.xml +++ b/vector/src/main/res/drawable/ic_user.xml @@ -1,22 +1,21 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/vector/src/main/res/layout/bottom_sheet_bootstrap.xml b/vector/src/main/res/layout/bottom_sheet_bootstrap.xml index dd194bcb86..5f2ce368ff 100644 --- a/vector/src/main/res/layout/bottom_sheet_bootstrap.xml +++ b/vector/src/main/res/layout/bottom_sheet_bootstrap.xml @@ -12,17 +12,19 @@ + + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:layout_height="wrap_content"> @@ -32,6 +34,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" + android:layout_marginEnd="16dp" android:layout_weight="1" android:ellipsize="end" android:maxLines="2" @@ -52,64 +55,6 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/bootstrapTitleText" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/layout/bottom_sheet_verification.xml b/vector/src/main/res/layout/bottom_sheet_verification.xml index 6e4a952e69..5f1461594d 100644 --- a/vector/src/main/res/layout/bottom_sheet_verification.xml +++ b/vector/src/main/res/layout/bottom_sheet_verification.xml @@ -58,11 +58,10 @@ diff --git a/vector/src/main/res/layout/dialog_recovery_key_saved_info.xml b/vector/src/main/res/layout/dialog_recovery_key_saved_info.xml new file mode 100644 index 0000000000..3e5b31bbb0 --- /dev/null +++ b/vector/src/main/res/layout/dialog_recovery_key_saved_info.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml b/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml new file mode 100644 index 0000000000..f78289da7f --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml b/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml new file mode 100644 index 0000000000..c91110402f --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_enter_account_password.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml b/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml index 804325cbe0..96618e519f 100644 --- a/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml +++ b/vector/src/main/res/layout/fragment_bootstrap_enter_passphrase.xml @@ -5,7 +5,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - tools:padding="16dp"> + android:padding="16dp"> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_bootstrap_waiting.xml b/vector/src/main/res/layout/fragment_bootstrap_waiting.xml new file mode 100644 index 0000000000..afd8faf4dc --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_waiting.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/item_verification_action.xml b/vector/src/main/res/layout/item_verification_action.xml index aad83d1b80..c6f1152afe 100644 --- a/vector/src/main/res/layout/item_verification_action.xml +++ b/vector/src/main/res/layout/item_verification_action.xml @@ -2,6 +2,7 @@ + + + diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 73b741d74b..27d53fe90e 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -105,4 +105,13 @@ + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index b9fd1fa018..6a33492e8b 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -32,13 +32,56 @@ Verify your devices from Settings. Verification Cancelled - Recovery Passphrase + Message Password + Message Key + Account Password + - Secure & unlock information with a %s so only you can access encrypted messages and secure information. + Set a %s + + + Confirm %s + + + + Enter your %s to continue. + + + Secure & unlock encrypted messages and trust with a %s. Enter your %s again to confirm it. Don’t re-use your account password. + + + This might take several seconds, please be patient. + Setting up recovery. + Your recovery key + You‘re done! + Keep it safe + + + Use this %1$s as a safety net in case you forget your %2$s. + + Publishing created identity keys + Generating secure key from passphrase + Defining SSSS default Key + Synchronizing Master key + Synchronizing User key + Synchronizing Self Signing key + + + + Your %2$s & %1$s are now set.\n\nKeep them safe! You’ll need them to unlock encrypted messages and secure information if you lose all of your active sessions. + + + Print it and store it somewhere safe + Save it on a USB key or backup drive + Copy it to your personal cloud storage + + You cannot do that from mobile + + From 2f237cf17b2f2469b61db7700ae7366995c7b33f Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2020 16:51:40 +0200 Subject: [PATCH 04/11] klint --- .../internal/crypto/tasks/RoomVerificationUpdateTask.kt | 1 - .../vector/riotx/core/ui/views/BottomSheetActionButton.kt | 1 - .../crypto/recover/BootstrapAccountPasswordFragment.kt | 3 +-- .../riotx/features/crypto/recover/BootstrapActions.kt | 1 - .../riotx/features/crypto/recover/BootstrapBottomSheet.kt | 2 -- .../features/crypto/recover/BootstrapConclusionFragment.kt | 1 - .../crypto/recover/BootstrapConfirmPassphraseFragment.kt | 1 - .../features/crypto/recover/BootstrapCrossSigningTask.kt | 4 ---- .../crypto/recover/BootstrapEnterPassphraseFragment.kt | 1 - .../crypto/recover/BootstrapSaveRecoveryKeyFragment.kt | 5 +---- .../features/crypto/recover/BootstrapSharedViewModel.kt | 6 ------ .../riotx/features/crypto/recover/BootstrapViewEvents.kt | 1 - .../features/crypto/verification/VerificationBottomSheet.kt | 4 ++-- .../main/java/im/vector/riotx/features/home/HomeActivity.kt | 1 - .../room/detail/timeline/factory/VerificationItemFactory.kt | 1 - 15 files changed, 4 insertions(+), 29 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt index 7a58ecb931..400febc15f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/tasks/RoomVerificationUpdateTask.kt @@ -35,7 +35,6 @@ import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.task.Task import timber.log.Timber import java.util.ArrayList -import java.util.UUID import javax.inject.Inject internal interface RoomVerificationUpdateTask : Task { diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt index 07cb7bb38d..d29982c9e4 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/BottomSheetActionButton.kt @@ -54,7 +54,6 @@ class BottomSheetActionButton @JvmOverloads constructor( @BindView(R.id.itemVerificationClickableZone) lateinit var clickableView: View - var title: String? = null set(value) { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt index ba9a4e8460..553bff75c4 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -83,7 +83,7 @@ class BootstrapAccountPasswordFragment @Inject constructor( } .disposeOnDestroyView() - withState(sharedViewModel) {state -> + withState(sharedViewModel) { state -> (state.step as? BootstrapStep.AccountPassword)?.failure?.let { bootstrapAccountPasswordTil.error = it } @@ -111,6 +111,5 @@ class BootstrapAccountPasswordFragment @Inject constructor( bootstrapAccountPasswordEditText.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/BootstrapActions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt index 6029c526f8..4286c9e571 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 @@ -29,7 +29,6 @@ sealed class BootstrapActions : VectorViewModelAction { object GoToCompleted : BootstrapActions() object GoToEnterAccountPassword : BootstrapActions() - data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions() object TogglePasswordVisibility : BootstrapActions() data class UpdateCandidatePassphrase(val pass: String) : 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 8c3086b318..d9ca97a830 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 @@ -28,10 +28,8 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState -import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent -import im.vector.riotx.core.dialogs.PromptPasswordDialog import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt index b7b656be74..b04f6951c9 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -58,6 +58,5 @@ class BootstrapConclusionFragment @Inject constructor( .toSpannable() .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) - } } 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 index a7ebd673b6..34a49e852e 100644 --- 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 @@ -120,6 +120,5 @@ class BootstrapConfirmPassphraseFragment @Inject constructor( 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 09f962dd3f..7c579d8d74 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 @@ -82,7 +82,6 @@ class BootstrapCrossSigningTask @Inject constructor( } suspend fun execute(params: Params): BootstrapResult { - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), isIndeterminate = true)) val crossSigningService = session.cryptoService().crossSigningService() @@ -98,7 +97,6 @@ class BootstrapCrossSigningTask @Inject constructor( val ssssService = session.sharedSecretStorageService - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true)) try { keyInfo = awaitCallback { @@ -115,7 +113,6 @@ class BootstrapCrossSigningTask @Inject constructor( return BootstrapResult.FailedToCreateSSSSKey(failure) } - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true)) try { awaitCallback { @@ -132,7 +129,6 @@ class BootstrapCrossSigningTask @Inject constructor( val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey try { - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk), isIndeterminate = true)) awaitCallback { ssssService.storeSecret(MASTER_KEY_SSSS_NAME, mskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) 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 index 3995a7a6dc..35e2e1373c 100644 --- 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 @@ -130,6 +130,5 @@ class BootstrapEnterPassphraseFragment @Inject constructor( } } } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index 186e24a90d..ccd6017613 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -95,7 +95,6 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( requireActivity().toast(R.string.error_no_external_application_found) sharedViewModel.handle(BootstrapActions.SaveReqFailed) } - } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -127,9 +126,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( startSharePlainTextIntent(this, context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title), recoveryKey, - context?.getString(R.string.recovery_key) - , REQUEST_CODE_COPY) - + context?.getString(R.string.recovery_key), REQUEST_CODE_COPY) } override fun invalidate() = withState(sharedViewModel) { state -> 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 fcb03c05c1..db714ba2f0 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 @@ -214,12 +214,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(recoverySaveFileProcess = Fail(it)) } }) - } } private fun startInitializeFlow(auth: UserPasswordAuth?) { - setState { copy(step = BootstrapStep.Initializing) } @@ -234,7 +232,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } - withState { state -> viewModelScope.launch { bootstrapTask.invoke(this, Params( @@ -277,7 +274,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( if (it is BootstrapResult.GenericError && it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError && it.failure.httpCode == 401) { - } else { _viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error))) setState { @@ -300,7 +296,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( private fun queryBack() = withState { state -> when (state.step) { is BootstrapStep.SetupPassphrase -> { - } is BootstrapStep.ConfirmPassphrase -> { setState { @@ -311,7 +306,6 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt index ae6c00d656..524112f8bb 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt @@ -34,4 +34,3 @@ sealed class BootstrapViewEvents : VectorViewEvents { // val transactionId: String? // ) : DevicesViewEvents() } - diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt index 9097724a35..cd91cc3712 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheet.kt @@ -351,11 +351,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } -//fun View.getParentCoordinatorLayout(): CoordinatorLayout? { +// fun View.getParentCoordinatorLayout(): CoordinatorLayout? { // var current = this as? View // while (current != null) { // if (current is CoordinatorLayout) return current // current = current.parent as? View // } // return null -//} +// } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index 9cdb40e0d9..e286c82532 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -42,7 +42,6 @@ import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.disclaimer.showDisclaimerDialog -import im.vector.riotx.features.login.LoginAction import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.popup.PopupAlertManager import im.vector.riotx.features.popup.VerificationVectorAlert diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 419435c579..ee529282f9 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent -import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.session.room.VerificationState import im.vector.riotx.core.epoxy.VectorEpoxyModel From c6abfa14ea19c2d7fb69a6cb800f5f063b38fc07 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2020 16:54:30 +0200 Subject: [PATCH 05/11] Fix / Bind continue button --- .../crypto/recover/BootstrapAccountPasswordFragment.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt index 553bff75c4..abe6e54092 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapAccountPasswordFragment.kt @@ -83,6 +83,14 @@ class BootstrapAccountPasswordFragment @Inject constructor( } .disposeOnDestroyView() + bootstrapPasswordButton.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + submit() + } + .disposeOnDestroyView() + withState(sharedViewModel) { state -> (state.step as? BootstrapStep.AccountPassword)?.failure?.let { bootstrapAccountPasswordTil.error = it From c27264761dbb7d905f9768ae483f9815322eff83 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2020 18:11:22 +0200 Subject: [PATCH 06/11] Back /Skip navigation --- .../crypto/recover/BootstrapBottomSheet.kt | 16 ++++++++++++++++ .../recover/BootstrapSaveRecoveryKeyFragment.kt | 4 ++-- .../crypto/recover/BootstrapSharedViewModel.kt | 13 +++++++++++++ .../crypto/recover/BootstrapViewEvents.kt | 12 +----------- .../res/layout/fragment_bootstrap_conclusion.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 4 +++- 6 files changed, 36 insertions(+), 15 deletions(-) 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 d9ca97a830..6812997ffa 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 @@ -66,10 +66,26 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { BootstrapViewEvents.RecoveryKeySaved -> { KeepItSafeDialog().show(requireActivity()) } + BootstrapViewEvents.SkipBootstrap -> { + promptSkip() + } } } } + private fun promptSkip() { + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.are_you_sure) + .setMessage(R.string.bootstrap_skip_text) + .setPositiveButton(R.string._continue, null) + .setNeutralButton(R.string.generate_message_key) { _, _ -> + } + .setNegativeButton(R.string.skip) { _, _ -> + dismiss() + } + .show() + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = super.onCreateView(inflater, container, savedInstanceState) dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt index ccd6017613..05c6f7a53f 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSaveRecoveryKeyFragment.kt @@ -57,7 +57,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) recoverySave.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) + .debounce(600, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { downloadRecoveryKey() @@ -65,7 +65,7 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor( .disposeOnDestroyView() recoveryCopy.clickableView.clicks() - .debounce(300, TimeUnit.MILLISECONDS) + .debounce(600, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) .subscribe { shareRecoveryKey() 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 db714ba2f0..0e9395468f 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 @@ -296,6 +296,8 @@ class BootstrapSharedViewModel @AssistedInject constructor( private fun queryBack() = withState { state -> when (state.step) { is BootstrapStep.SetupPassphrase -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap) } is BootstrapStep.ConfirmPassphrase -> { setState { @@ -306,6 +308,17 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } + is BootstrapStep.AccountPassword -> { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap) + } + BootstrapStep.Initializing -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap) + } + is BootstrapStep.SaveRecoveryKey, + BootstrapStep.DoneSuccess -> { + // nop + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt index 524112f8bb..efac9c9e34 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt @@ -20,17 +20,7 @@ import im.vector.riotx.core.platform.VectorViewEvents sealed class BootstrapViewEvents : VectorViewEvents { object Dismiss : BootstrapViewEvents() -// data class RequestPassword(val sessionId: String, val userId: String) : BootstrapViewEvents() data class ModalError(val error: String) : BootstrapViewEvents() object RecoveryKeySaved: BootstrapViewEvents() -// data class Failure(val throwable: Throwable) : DevicesViewEvents() -// -// object RequestPassword : DevicesViewEvents() -// -// data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents() -// -// data class ShowVerifyDevice( -// val userId: String, -// val transactionId: String? -// ) : DevicesViewEvents() + object SkipBootstrap: BootstrapViewEvents() } diff --git a/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml b/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml index f78289da7f..37e7292366 100644 --- a/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml +++ b/vector/src/main/res/layout/fragment_bootstrap_conclusion.xml @@ -37,7 +37,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="50dp" - app:actionTitle="@string/_continue" + app:actionTitle="@string/finish" app:forceStartPadding="false" app:rightIcon="@drawable/ic_arrow_right" app:tint="?colorAccent" /> diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 6a33492e8b..c875cb9ea9 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -39,6 +39,7 @@ Set a %s + Generate a Message Key Confirm %s @@ -59,6 +60,7 @@ Your recovery key You‘re done! Keep it safe + Finish Use this %1$s as a safety net in case you forget your %2$s. @@ -81,7 +83,7 @@ You cannot do that from mobile - + Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead. From 45c562626787e41e8483e532b9927f06b8d0c1d9 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 2 Apr 2020 18:30:43 +0200 Subject: [PATCH 07/11] Add generate key option --- .../im/vector/riotx/features/MainActivity.kt | 3 ++- .../crypto/recover/BootstrapActions.kt | 1 + .../crypto/recover/BootstrapBottomSheet.kt | 25 ++++++++++------- .../recover/BootstrapCrossSigningTask.kt | 27 ++++++++++++------- .../recover/BootstrapSharedViewModel.kt | 22 ++++++++++++--- .../crypto/recover/BootstrapViewEvents.kt | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 ++ 7 files changed, 57 insertions(+), 25 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index bc5a1aff95..c894e0739c 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -195,7 +195,8 @@ class MainActivity : VectorBaseActivity() { // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { - HomeActivity.newIntent(this) + // DO NOT COMMIT + HomeActivity.newIntent(this, accountCreation = true) } else { // The token is still invalid SoftLogoutActivity.newIntent(this) 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 4286c9e571..7c0f2c1c46 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 @@ -30,6 +30,7 @@ sealed class BootstrapActions : VectorViewModelAction { object GoToEnterAccountPassword : BootstrapActions() data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions() + data class DoInitializeGeneratedKey(val auth: UserPasswordAuth? = null) : BootstrapActions() object TogglePasswordVisibility : BootstrapActions() data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateConfirmCandidatePassphrase(val pass: String) : 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 6812997ffa..6305f161e3 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 @@ -55,30 +55,35 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { super.onViewCreated(view, savedInstanceState) viewModel.observeViewEvents { event -> when (event) { - is BootstrapViewEvents.Dismiss -> dismiss() - is BootstrapViewEvents.ModalError -> { + is BootstrapViewEvents.Dismiss -> dismiss() + is BootstrapViewEvents.ModalError -> { AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) .setMessage(event.error) .setPositiveButton(R.string.ok, null) .show() } - BootstrapViewEvents.RecoveryKeySaved -> { + BootstrapViewEvents.RecoveryKeySaved -> { KeepItSafeDialog().show(requireActivity()) } - BootstrapViewEvents.SkipBootstrap -> { - promptSkip() + is BootstrapViewEvents.SkipBootstrap -> { + promptSkip(event.genKeyOption) } } } } - private fun promptSkip() { - AlertDialog.Builder(requireActivity()) + private fun promptSkip(genKeyOption: Boolean) { + AlertDialog.Builder(requireContext()) .setTitle(R.string.are_you_sure) - .setMessage(R.string.bootstrap_skip_text) + .setMessage(if (genKeyOption) R.string.bootstrap_skip_text else R.string.bootstrap_skip_text_no_gen_key) .setPositiveButton(R.string._continue, null) - .setNeutralButton(R.string.generate_message_key) { _, _ -> + .apply { + if (genKeyOption) { + setNeutralButton(R.string.generate_message_key) { _, _ -> + viewModel.handle(BootstrapActions.DoInitializeGeneratedKey()) + } + } } .setNegativeButton(R.string.skip) { _, _ -> dismiss() @@ -118,7 +123,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase)) showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) bootstrapTitleText.text = getString(R.string.account_password) showFragment(BootstrapAccountPasswordFragment::class, Bundle()) 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 7c579d8d74..9e9a579bde 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 @@ -64,7 +64,7 @@ interface BootstrapProgressListener { data class Params( val userPasswordAuth: UserPasswordAuth? = null, val progressListener: BootstrapProgressListener? = null, - val passphrase: String + val passphrase: String? ) class BootstrapCrossSigningTask @Inject constructor( @@ -100,14 +100,23 @@ class BootstrapCrossSigningTask @Inject constructor( params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true)) try { keyInfo = awaitCallback { - ssssService.generateKeyWithPassphrase( - UUID.randomUUID().toString(), - "ssss_key", - params.passphrase, - EmptyKeySigner(), - null, - it - ) + params.passphrase?.let { passphrase -> + ssssService.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + passphrase, + EmptyKeySigner(), + null, + it + ) + } ?: kotlin.run { + ssssService.generateKey( + UUID.randomUUID().toString(), + "ssss_key", + EmptyKeySigner(), + it + ) + } } } catch (failure: Failure) { return BootstrapResult.FailedToCreateSSSSKey(failure) 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 0e9395468f..998899374c 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 @@ -151,6 +151,20 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } } + is BootstrapActions.DoInitializeGeneratedKey -> { + val auth = action.auth ?: reAuthHelper.rememberedAuth() + if (auth == null) { + setState { + copy( + passphrase = null, + passphraseRepeat = null, + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(action.auth) + } + } BootstrapActions.RecoveryKeySaved -> { _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) setState { @@ -237,7 +251,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( bootstrapTask.invoke(this, Params( userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(), progressListener = progressListener, - passphrase = state.passphrase!! + passphrase = state.passphrase )) { when (it) { is BootstrapResult.Success -> { @@ -297,7 +311,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( when (state.step) { is BootstrapStep.SetupPassphrase -> { // do we let you cancel from here? - _viewEvents.post(BootstrapViewEvents.SkipBootstrap) + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) } is BootstrapStep.ConfirmPassphrase -> { setState { @@ -309,11 +323,11 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } is BootstrapStep.AccountPassword -> { - _viewEvents.post(BootstrapViewEvents.SkipBootstrap) + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } BootstrapStep.Initializing -> { // do we let you cancel from here? - _viewEvents.post(BootstrapViewEvents.SkipBootstrap) + _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } is BootstrapStep.SaveRecoveryKey, BootstrapStep.DoneSuccess -> { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt index efac9c9e34..679eabd561 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapViewEvents.kt @@ -22,5 +22,5 @@ sealed class BootstrapViewEvents : VectorViewEvents { object Dismiss : BootstrapViewEvents() data class ModalError(val error: String) : BootstrapViewEvents() object RecoveryKeySaved: BootstrapViewEvents() - object SkipBootstrap: BootstrapViewEvents() + data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents() } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index c875cb9ea9..c850316912 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -84,6 +84,8 @@ You cannot do that from mobile Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead. + Setting a Message Password lets you secure & unlock encrypted messages and trust. + From 64747356626001dee74dbaeb5ede2189bf053516 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2020 10:15:00 +0200 Subject: [PATCH 08/11] Fix / devtools was not showing all json numbers --- vector/build.gradle | 2 +- vector/src/main/java/im/vector/riotx/features/MainActivity.kt | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/vector/build.gradle b/vector/build.gradle index eea99d418e..ee437eabee 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -368,7 +368,7 @@ dependencies { implementation "androidx.emoji:emoji-appcompat:1.0.0" - implementation 'com.github.BillCarsonFr:JsonViewer:0.4' + implementation 'com.github.BillCarsonFr:JsonViewer:0.5' // QR-code // Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170 diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index c894e0739c..bc5a1aff95 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -195,8 +195,7 @@ class MainActivity : VectorBaseActivity() { // We have a session. // Check it can be opened if (sessionHolder.getActiveSession().isOpenable) { - // DO NOT COMMIT - HomeActivity.newIntent(this, accountCreation = true) + HomeActivity.newIntent(this) } else { // The token is still invalid SoftLogoutActivity.newIntent(this) From 92bf3f1349980c79a2e7be785838284a1441a39c Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2020 11:14:14 +0200 Subject: [PATCH 09/11] Update change log + code quality --- CHANGES.md | 6 ++++-- .../features/crypto/recover/BootstrapConclusionFragment.kt | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0b9d8d5991..416c55d239 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,16 +4,18 @@ Changes in RiotX 0.19.0 (2020-XX-XX) Features ✨: - Cross-Signing | Support SSSS secret sharing (#944) - Cross-Signing | Verify new session from existing session (#1134) + - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) Improvements 🙌: - Verification DM / Handle concurrent .start after .ready (#794) - - CrossSigning / Update Shield Logic for DM (#963) - - Xsigning | Complete security new session design update (#1135) + - Cross-Signing | Update Shield Logic for DM (#963) + - Cross-Signing | Complete security new session design update (#1135) Bugfix 🐛: - Missing avatar/displayname after verification request message (#841) - Crypto | RiotX sometimes rotate the current device keys (#1170) - RiotX can't restore cross signing keys saved by web in SSSS (#1174) + - Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191) Translations 🗣: - diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt index b04f6951c9..d84283b14c 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapConclusionFragment.kt @@ -54,7 +54,11 @@ class BootstrapConclusionFragment @Inject constructor( override fun invalidate() = withState(sharedViewModel) { state -> if (state.step !is BootstrapStep.DoneSuccess) return@withState - bootstrapConclusionText.text = getString(R.string.bootstrap_cross_signing_success, getString(R.string.recovery_passphrase), getString(R.string.message_key)) + bootstrapConclusionText.text = getString( + R.string.bootstrap_cross_signing_success, + getString(R.string.recovery_passphrase), + getString(R.string.message_key) + ) .toSpannable() .colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) .colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) From 1dfd6f232ad09b0ca7d0c9dd295cff156651d654 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 3 Apr 2020 18:59:21 +0200 Subject: [PATCH 10/11] Code quality / line too long --- .../recover/BootstrapCrossSigningTask.kt | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) 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 9e9a579bde..7e9d054242 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 @@ -82,7 +82,12 @@ class BootstrapCrossSigningTask @Inject constructor( } suspend fun execute(params: Params): BootstrapResult { - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), isIndeterminate = true)) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), + isIndeterminate = true + ) + ) val crossSigningService = session.cryptoService().crossSigningService() try { @@ -97,7 +102,11 @@ class BootstrapCrossSigningTask @Inject constructor( val ssssService = session.sharedSecretStorageService - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), isIndeterminate = true)) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2), + isIndeterminate = true) + ) try { keyInfo = awaitCallback { params.passphrase?.let { passphrase -> @@ -122,7 +131,11 @@ class BootstrapCrossSigningTask @Inject constructor( return BootstrapResult.FailedToCreateSSSSKey(failure) } - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), isIndeterminate = true)) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key), + isIndeterminate = true) + ) try { awaitCallback { ssssService.setDefaultKey(keyInfo.keyId, it) @@ -138,17 +151,47 @@ class BootstrapCrossSigningTask @Inject constructor( val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey try { - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk), isIndeterminate = true)) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk), + isIndeterminate = true + ) + ) awaitCallback { - ssssService.storeSecret(MASTER_KEY_SSSS_NAME, mskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) + ssssService.storeSecret( + MASTER_KEY_SSSS_NAME, + mskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + , it + ) } - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), isIndeterminate = true)) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk), + isIndeterminate = true + ) + ) awaitCallback { - ssssService.storeSecret(USER_SIGNING_KEY_SSSS_NAME, uskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) + ssssService.storeSecret( + USER_SIGNING_KEY_SSSS_NAME, + uskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), + it + ) } - params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true)) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk) + , isIndeterminate = true + ) + ) awaitCallback { - ssssService.storeSecret(SELF_SIGNING_KEY_SSSS_NAME, sskPrivateKey, listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it) + ssssService.storeSecret( + SELF_SIGNING_KEY_SSSS_NAME, + sskPrivateKey, + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) + , it + ) } } catch (failure: Failure) { // Maybe we could just ignore this error? From 326f2e99fb46770c30447b8acfd8e4b63f00fa1f Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 6 Apr 2020 10:00:59 +0200 Subject: [PATCH 11/11] klint --- .../features/crypto/recover/BootstrapCrossSigningTask.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) 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 7e9d054242..2571ef674b 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 @@ -161,8 +161,7 @@ class BootstrapCrossSigningTask @Inject constructor( ssssService.storeSecret( MASTER_KEY_SSSS_NAME, mskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) - , it + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it ) } params.progressListener?.onProgress( @@ -181,16 +180,14 @@ class BootstrapCrossSigningTask @Inject constructor( } params.progressListener?.onProgress( WaitingViewData( - stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk) - , isIndeterminate = true + stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true ) ) awaitCallback { ssssService.storeSecret( SELF_SIGNING_KEY_SSSS_NAME, sskPrivateKey, - listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)) - , it + listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it ) } } catch (failure: Failure) {