From cb669ad8812eb686c1ecf076f9d397e13cdfb8f5 Mon Sep 17 00:00:00 2001 From: Valere Date: Fri, 14 Feb 2020 09:36:23 +0100 Subject: [PATCH] 4S Activity WIP --- vector/src/main/AndroidManifest.xml | 23 +-- .../vector/riotx/core/di/ScreenComponent.kt | 3 + .../crypto/quads/SharedSecureStorageAction.kt | 38 +++++ .../quads/SharedSecureStorageActivity.kt | 120 +++++++++++++++ .../quads/SharedSecureStorageViewModel.kt | 134 +++++++++++++++++ .../SharedSecuredStoragePassphraseFragment.kt | 142 ++++++++++++++++++ .../crypto/verification/VerificationAction.kt | 1 + .../verification/VerificationBottomSheet.kt | 4 + .../VerificationBottomSheetViewEvents.kt | 1 + .../VerificationBottomSheetViewModel.kt | 3 + .../request/VerificationRequestFragment.kt | 1 + .../riotx/features/home/HomeActivity.kt | 19 ++- .../fragment_ssss_access_from_passphrase.xml | 131 ++++++++++++++++ vector/src/main/res/values/dimens.xml | 1 + vector/src/main/res/values/strings.xml | 1 + vector/src/main/res/values/strings_riotX.xml | 9 ++ vector/src/main/res/values/styles.xml | 11 ++ 17 files changed, 622 insertions(+), 20 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageAction.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageActivity.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt create mode 100644 vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml create mode 100644 vector/src/main/res/values/styles.xml diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 9d7495ef23..31a70a22e0 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -19,7 +19,6 @@ android:supportsRtl="true" android:theme="@style/AppTheme.Light" tools:replace="android:allowBackup"> - @@ -57,7 +56,6 @@ - @@ -73,7 +71,7 @@ android:value=".features.home.HomeActivity" /> - + @@ -97,6 +95,7 @@ + @@ -104,15 +103,14 @@ + - - - @@ -137,9 +134,9 @@ android:name="android.support.PARENT_ACTIVITY" android:value=".features.home.HomeActivity" /> - + @@ -149,25 +146,17 @@ android:theme="@style/AppTheme.AttachmentsPreview" /> - - - - - + android:exported="false" /> - - - + android:exported="false" /> , + val resultKeyStoreAlias: String + ) : Parcelable + + private val uiDisposables = CompositeDisposable() + + private fun Disposable.disposeOnDestroyView(): Disposable { + uiDisposables.add(this) + return this + } + + private val viewModel: SharedSecureStorageViewModel by viewModel() + @Inject lateinit var viewModelFactory: SharedSecureStorageViewModel.Factory + @Inject lateinit var errorFormatter: ErrorFormatter + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + toolbar.visibility = View.GONE + if (isFirstCreation()) { + addFragment(R.id.container, SharedSecuredStoragePassphraseFragment::class.java) + } + + + viewModel.viewEvents + .observe() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + observeViewEvents(it) + } + .disposeOnDestroyView() + + viewModel.subscribe(this) { +// renderState(it) + } + } + + private fun observeViewEvents(it: SharedSecureStorageViewEvent?) { + when (it) { + is SharedSecureStorageViewEvent.Dismiss -> { + setResult(Activity.RESULT_CANCELED) + finish() + } + is SharedSecureStorageViewEvent.ShowModalLoading -> { + showWaitingView() + } + is SharedSecureStorageViewEvent.HideModalLoading -> { + hideWaitingView() + } + is SharedSecureStorageViewEvent.UpdateLoadingState -> { + updateWaitingView(it.waitingData) + } + } + } + +// fun renderState(state: SharedSecureStorageViewState) { +// } + + companion object { + + const val RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity" + fun newIntent(context: Context, keyId: String? = null, requestedSecrets: List, resultKeyStoreAlias: String = RESULT_KEYSTORE_ALIAS): Intent { + require(requestedSecrets.isNotEmpty()) + return Intent(context, SharedSecureStorageActivity::class.java).also { + it.putExtra(MvRx.KEY_ARG, Args( + keyId, + requestedSecrets, + resultKeyStoreAlias + )) + } + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt new file mode 100644 index 0000000000..44ba52a8d3 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -0,0 +1,134 @@ +/* + * 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.quads + +import com.airbnb.mvrx.MvRx +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.ViewModelContext +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec +import im.vector.matrix.android.api.session.securestorage.KeyInfoResult +import im.vector.riotx.R +import im.vector.riotx.core.platform.VectorViewModel +import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider + +data class SharedSecureStorageViewState( + val requestedSecrets: List = emptyList(), + val passphraseVisible: Boolean = false +) : MvRxState + +class SharedSecureStorageViewModel @AssistedInject constructor( + @Assisted initialState: SharedSecureStorageViewState, + private val stringProvider: StringProvider, + private val session: Session) + : VectorViewModel(initialState) { + + @AssistedInject.Factory + interface Factory { + fun create(initialState: SharedSecureStorageViewState): SharedSecureStorageViewModel + } + + override fun handle(action: SharedSecureStorageAction) = withState { state -> + when (action) { + is SharedSecureStorageAction.TogglePasswordVisibility -> { + setState { + copy( + passphraseVisible = !passphraseVisible + ) + } + } + is SharedSecureStorageAction.Cancel -> { + _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) + } + is SharedSecureStorageAction.SubmitPassphrase -> { + _viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading) + val passphrase = action.passphrase + val keyInfoResult = session.sharedSecretStorageService.getDefaultKey() + if (!keyInfoResult.isSuccess()) { + _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) + _viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key")) + return@withState + } + val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo + + // TODO +// val decryptedSecretMap = HashMap() +// val errors = ArrayList() + _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState( + WaitingViewData( + message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), + isIndeterminate = true + ) + )) + state.requestedSecrets.forEach { + val keySpec = Curve25519AesSha2KeySpec.fromPassphrase( + passphrase, + keyInfo.content.passphrase?.salt ?: "", + keyInfo.content.passphrase?.iterations ?: 0, + // TODO + object : ProgressListener { + override fun onProgress(progress: Int, total: Int) { + _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState( + WaitingViewData( + message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), + isIndeterminate = false, + progress = progress, + progressTotal = total + ) + )) + } + } + ) + session.sharedSecretStorageService.getSecret( + name = it, + keyId = keyInfo.id, + secretKey = keySpec, + callback = object : MatrixCallback { + override fun onSuccess(data: String) { + _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) + _viewEvents.post(SharedSecureStorageViewEvent.InlineError(failure.localizedMessage)) + } + }) + } + } + } + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? { + val activity: SharedSecureStorageActivity = viewModelContext.activity() + val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG) + return activity.viewModelFactory.create( + SharedSecureStorageViewState( + requestedSecrets = args.requestedSecrets + ) + ) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt new file mode 100644 index 0000000000..58c24cf2e9 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -0,0 +1,142 @@ +/* + * 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.quads + +import android.os.Bundle +import android.view.View +import android.view.inputmethod.EditorInfo +import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import android.widget.TextView +import butterknife.BindView +import butterknife.OnClick +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import com.google.android.material.textfield.TextInputLayout +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.utils.DebouncedClickListener +import me.gujun.android.span.span +import java.util.concurrent.TimeUnit + +class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase + + val sharedViewModel: SharedSecureStorageViewModel by activityViewModel() + + @BindView(R.id.ssss_restore_with_passphrase_warning_text) + lateinit var warningText: TextView + + @BindView(R.id.ssss_restore_with_passphrase_warning_reason) + lateinit var reasonText: TextView + + @BindView(R.id.ssss_passphrase_enter_til) + lateinit var mPassphraseInputLayout: TextInputLayout + + @BindView(R.id.ssss_passphrase_enter_edittext) + lateinit var mPassphraseTextEdit: EditText + + @BindView(R.id.ssss_view_show_password) + lateinit var mPassphraseReveal: ImageView + + @BindView(R.id.ssss_passphrase_submit) + lateinit var submitButton: Button + + @BindView(R.id.ssss_passphrase_cancel) + lateinit var cancelButton: Button + + @OnClick(R.id.ssss_view_show_password) + fun toggleVisibilityMode() { + sharedViewModel.handle(SharedSecureStorageAction.TogglePasswordVisibility) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + warningText.text = span { + span(getString(R.string.enter_secret_storage_passphrase_warning)) { + textStyle = "bold" + } + +" " + +getString(R.string.enter_secret_storage_passphrase_warning_text) + } + + reasonText.text = getString(R.string.enter_secret_storage_passphrase_reason_verify) + + mPassphraseTextEdit.editorActionEvents() + .throttleFirst(300, TimeUnit.MILLISECONDS) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + mPassphraseTextEdit.setOnEditorActionListener { _, actionId, _ -> + if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) { + submit() + return@setOnEditorActionListener true + } + return@setOnEditorActionListener false + } + + mPassphraseTextEdit.textChanges() + .subscribe { + mPassphraseInputLayout.error = null + submitButton.isEnabled = it.isNotBlank() + } + .disposeOnDestroyView() + + sharedViewModel.observeViewEvents { + when (it) { + is SharedSecureStorageViewEvent.InlineError -> { + mPassphraseInputLayout.error = it.message + } + } + } + + submitButton.setOnClickListener(DebouncedClickListener( + View.OnClickListener { + submit() + } + )) + + cancelButton.setOnClickListener(DebouncedClickListener( + View.OnClickListener { + sharedViewModel.handle(SharedSecureStorageAction.Cancel) + } + )) + } + + fun submit() { + val text = mPassphraseTextEdit.text.toString() + if (text.isBlank()) return // Should not reach this point as button disabled + submitButton.isEnabled = false + sharedViewModel.handle(SharedSecureStorageAction.SubmitPassphrase(text)) + } + + override fun invalidate() = withState(sharedViewModel) { state -> + val shouldBeVisible = state.passphraseVisible + mPassphraseTextEdit.showPassword(shouldBeVisible) + mPassphraseReveal.setImageResource(if (shouldBeVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt index 111328b0b1..15eba2d138 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationAction.kt @@ -29,4 +29,5 @@ sealed class VerificationAction : VectorViewModelAction { data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() object GotItConclusion : VerificationAction() object SkipVerification : VerificationAction() + object VerifyFromPassphrase : VerificationAction() } 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 554344b06f..0efa2a8480 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 @@ -36,6 +36,7 @@ import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment import im.vector.riotx.features.crypto.verification.conclusion.VerificationConclusionFragment import im.vector.riotx.features.crypto.verification.emoji.VerificationEmojiCodeFragment @@ -87,6 +88,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { viewModel.observeViewEvents { when (it) { is VerificationBottomSheetViewEvents.Dismiss -> dismiss() + is VerificationBottomSheetViewEvents.AccessSecretStore -> { + startActivity(SharedSecureStorageActivity.newIntent(requireContext(),null, listOf("m.cross_signing.user_signing"))) + } }.exhaustive } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt index 5509ecbe16..3a3bf54883 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewEvents.kt @@ -23,4 +23,5 @@ import im.vector.riotx.core.platform.VectorViewEvents */ sealed class VerificationBottomSheetViewEvents : VectorViewEvents { object Dismiss : VerificationBottomSheetViewEvents() + object AccessSecretStore : VerificationBottomSheetViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index 7ae3cf29eb..981387abd7 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -253,6 +253,9 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini is VerificationAction.SkipVerification -> { _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) } + is VerificationAction.VerifyFromPassphrase -> { + _viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore) + } }.exhaustive } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt index 0cc7a2beab..64000d07a1 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/request/VerificationRequestFragment.kt @@ -63,6 +63,7 @@ class VerificationRequestFragment @Inject constructor( } override fun onClickRecoverFromPassphrase() { + viewModel.handle(VerificationAction.VerifyFromPassphrase) } override fun onClickDismiss() { 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 55e217670e..95cea89702 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 @@ -148,9 +148,22 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { // We need to ask sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true - navigator.waitSessionVerification(this) - } else { - // TODO upgrade security -> bootstrap cross signing + PopupAlertManager.postVectorAlert( + PopupAlertManager.VectorAlert( + uid = "completeSecurity", + title = getString(R.string.new_signin), + description = getString(R.string.complete_security), + iconId = R.drawable.ic_shield_warning + ).apply { + colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_destructive_accent) + contentAction = Runnable { + (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { + it.navigator.waitSessionVerification(it) + } + } + dismissedAction = Runnable {} + } + ) } } diff --git a/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml new file mode 100644 index 0000000000..db50cfd34d --- /dev/null +++ b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/dimens.xml b/vector/src/main/res/values/dimens.xml index 665d1819f7..411945c8ed 100644 --- a/vector/src/main/res/values/dimens.xml +++ b/vector/src/main/res/values/dimens.xml @@ -32,5 +32,6 @@ 280dp + 16dp \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 6c2caedd01..31cd015051 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2151,4 +2151,5 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming No Connectivity to the server has been lost + ShareSecureStorageActivity diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 082ce235bb..e6a2701e33 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -21,6 +21,15 @@ Can‘t access an existing session? Use your recovery key or passphrase + + + New Sign In + + Enter secret storage passphrase + Warning: + You should only access secret storage from a trusted device + Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase + diff --git a/vector/src/main/res/values/styles.xml b/vector/src/main/res/values/styles.xml new file mode 100644 index 0000000000..0bb90ea4d0 --- /dev/null +++ b/vector/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + +