diff --git a/CHANGES.md b/CHANGES.md index a5b9e43142..92cef39707 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ Improvements 🙌: - Wording differentiation for direct rooms (#2176) - PIN code: request PIN code if phone has been locked - Small optimisation of scrolling experience in timeline (#2114) + - Allow user to reset cross signing if he has no way to recover (#2052) Bugfix 🐛: - Improve support for image/audio/video/file selection with intent changes (#1376) diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 3ced7de7e2..1d3aaa9f69 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt -enum class===78 +enum class===80 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/build.gradle b/vector/build.gradle index 222c0c5cb3..87c8696b02 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -453,6 +453,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" + androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version" androidTestImplementation "org.amshove.kluent:kluent-android:$kluent_version" androidTestImplementation "androidx.arch.core:core-testing:$arch_version" // Plant Timber tree for test diff --git a/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt new file mode 100644 index 0000000000..3ab8fe7dd9 --- /dev/null +++ b/vector/src/androidTest/java/im/vector/app/SecurityBootstrapTest.kt @@ -0,0 +1,176 @@ +/* + * 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.app + +import android.app.Activity +import android.app.Instrumentation.ActivityResult +import android.content.Intent +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.pressBack +import androidx.test.espresso.action.ViewActions.replaceText +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.espresso.intent.matcher.IntentMatchers.isInternal +import androidx.test.espresso.matcher.RootMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.LargeTest +import androidx.test.platform.app.InstrumentationRegistry +import im.vector.app.features.MainActivity +import im.vector.app.features.crypto.recover.SetupMode +import im.vector.app.features.home.HomeActivity +import org.hamcrest.CoreMatchers.not +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.matrix.android.sdk.api.Matrix +import org.matrix.android.sdk.api.session.Session + +@RunWith(AndroidJUnit4::class) +@LargeTest +class SecurityBootstrapTest : VerificationTestBase() { + + var existingSession: Session? = null + + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + @Before + fun createSessionWithCrossSigning() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val matrix = Matrix.getInstance(context) + val userName = "foobar_${System.currentTimeMillis()}" + existingSession = createAccountAndSync(matrix, userName, password, true) + stubAllExternalIntents() + } + + private fun stubAllExternalIntents() { + // By default Espresso Intents does not stub any Intents. Stubbing needs to be setup before + // every test run. In this case all external Intents will be blocked. + Intents.init() + intending(not(isInternal())).respondWith(ActivityResult(Activity.RESULT_OK, null)) + } + + @Test + fun testBasicBootstrap() { + val userId: String = existingSession!!.myUserId + + doLogin(homeServerUrl, userId, password) + + // Thread.sleep(6000) + withIdlingResource(activityIdlingResource(HomeActivity::class.java)) { + onView(withId(R.id.roomListContainer)) + .check(matches(isDisplayed())) + .perform(closeSoftKeyboard()) + } + + val activity = EspressoHelper.getCurrentActivity()!! + val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession() + + withIdlingResource(initialSyncIdlingResource(uiSession)) { + onView(withId(R.id.roomListContainer)) + .check(matches(isDisplayed())) + } + + activity.navigator.open4SSetup(activity, SetupMode.NORMAL) + + Thread.sleep(1000) + + onView(withId(R.id.bootstrapSetupSecureUseSecurityKey)) + .check(matches(isDisplayed())) + + onView(withId(R.id.bootstrapSetupSecureUseSecurityPassphrase)) + .check(matches(isDisplayed())) + .perform(click()) + + onView(isRoot()) + .perform(waitForView(withText(R.string.bootstrap_info_text_2))) + + // test back + onView(isRoot()).perform(pressBack()) + + Thread.sleep(1000) + + onView(withId(R.id.bootstrapSetupSecureUseSecurityKey)) + .check(matches(isDisplayed())) + + onView(withId(R.id.bootstrapSetupSecureUseSecurityPassphrase)) + .check(matches(isDisplayed())) + .perform(click()) + + onView(isRoot()) + .perform(waitForView(withText(R.string.bootstrap_info_text_2))) + + onView(withId(R.id.ssss_passphrase_enter_edittext)) + .perform(typeText("person woman man camera tv")) + + onView(withId(R.id.bootstrapSubmit)) + .perform(closeSoftKeyboard(), click()) + + // test bad pass + onView(withId(R.id.ssss_passphrase_enter_edittext)) + .perform(typeText("person woman man cmera tv")) + + onView(withId(R.id.bootstrapSubmit)) + .perform(closeSoftKeyboard(), click()) + + onView(withText(R.string.passphrase_passphrase_does_not_match)).check(matches(isDisplayed())) + + onView(withId(R.id.ssss_passphrase_enter_edittext)) + .perform(replaceText("person woman man camera tv")) + + onView(withId(R.id.bootstrapSubmit)) + .perform(closeSoftKeyboard(), click()) + + onView(withId(R.id.bottomSheetScrollView)) + .perform(waitForView(withText(R.string.bottom_sheet_save_your_recovery_key_content))) + + intending(hasAction(Intent.ACTION_SEND)).respondWith(ActivityResult(Activity.RESULT_OK, null)) + + onView(withId(R.id.recoveryCopy)) + .perform(click()) + + Thread.sleep(1000) + + // Dismiss dialog + onView(withText(R.string.ok)).inRoot(RootMatchers.isDialog()).perform(click()) + + onView(withId(R.id.bottomSheetScrollView)) + .perform(waitForView(withText(R.string.bottom_sheet_save_your_recovery_key_content))) + + onView(withText(R.string._continue)).perform(click()) + + // Assert that all is configured + assert(uiSession.cryptoService().crossSigningService().isCrossSigningInitialized()) + assert(uiSession.cryptoService().crossSigningService().canCrossSign()) + assert(uiSession.cryptoService().crossSigningService().allPrivateKeysKnown()) + assert(uiSession.cryptoService().keysBackupService().isEnabled) + assert(uiSession.cryptoService().keysBackupService().currentBackupVersion != null) + assert(uiSession.sharedSecretStorageService.isRecoverySetup()) + assert(uiSession.sharedSecretStorageService.isMegolmKeyInBackup()) + } +} diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt index 5405c086eb..f8c2a89ea8 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt @@ -38,6 +38,7 @@ import im.vector.app.features.MainActivity import im.vector.app.features.crypto.quads.SharedSecureStorageActivity import im.vector.app.features.crypto.recover.BootstrapCrossSigningTask import im.vector.app.features.crypto.recover.Params +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.home.HomeActivity import kotlinx.coroutines.runBlocking import org.junit.Before @@ -77,7 +78,8 @@ class VerifySessionPassphraseTest : VerificationTestBase() { runBlocking { task.execute(Params( userPasswordAuth = UserPasswordAuth(password = password), - passphrase = passphrase + passphrase = passphrase, + setupMode = SetupMode.NORMAL )) } } diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index 75f61a7b01..86d59b630b 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -27,6 +27,7 @@ import im.vector.app.features.contactsbook.ContactsBookFragment import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragment import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment +import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment import im.vector.app.features.crypto.recover.BootstrapConclusionFragment import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment @@ -530,6 +531,11 @@ interface FragmentModule { @FragmentKey(SharedSecuredStorageKeyFragment::class) fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment + @Binds + @IntoMap + @FragmentKey(SharedSecuredStorageResetAllFragment::class) + fun bindSharedSecuredStorageResetAllFragment(fragment: SharedSecuredStorageResetAllFragment): Fragment + @Binds @IntoMap @FragmentKey(SetIdentityServerFragment::class) diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt index 9ed5c5c455..9f4924ebb2 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseBottomSheetDialogFragment.kt @@ -17,6 +17,7 @@ package im.vector.app.core.platform import android.app.Dialog import android.content.Context +import android.content.DialogInterface import android.os.Bundle import android.os.Parcelable import android.view.LayoutInflater @@ -86,6 +87,24 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment() open val showExpanded = false + interface ResultListener { + fun onBottomSheetResult(resultCode: Int, data: Any?) + + companion object { + const val RESULT_OK = 1 + const val RESULT_CANCEL = 0 + } + } + + var resultListener : ResultListener? = null + var bottomSheetResult: Int = ResultListener.RESULT_CANCEL + var bottomSheetResultData: Any? = null + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + resultListener?.onBottomSheetResult(bottomSheetResult, bottomSheetResultData) + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val view = inflater.inflate(getLayoutResId(), container, false) unBinder = ButterKnife.bind(this, view) diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index 0259898ee3..d418822b7f 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -24,6 +24,7 @@ import android.view.View import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.content.withStyledAttributes import androidx.core.view.isGone import androidx.core.view.isInvisible @@ -107,6 +108,12 @@ class BottomSheetActionButton @JvmOverloads constructor( leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) } } + var titleTextColor: Int? = null + set(value) { + field = value + value?.let { actionTextView.setTextColor(it) } + } + init { inflate(context, R.layout.item_verification_action, this) ButterKnife.bind(this) @@ -120,6 +127,7 @@ class BottomSheetActionButton @JvmOverloads constructor( rightIcon = getDrawable(R.styleable.BottomSheetActionButton_rightIcon) tint = getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor)) + titleTextColor = getColor(R.styleable.BottomSheetActionButton_titleTextColor, ContextCompat.getColor(context, R.color.riotx_accent)) } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageAction.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageAction.kt index b47b7dc3a9..30a7ab3cc0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageAction.kt @@ -28,6 +28,8 @@ sealed class SharedSecureStorageAction : VectorViewModelAction { object Cancel : SharedSecureStorageAction() data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction() data class SubmitKey(val recoveryKey: String) : SharedSecureStorageAction() + object ForgotResetAll : SharedSecureStorageAction() + object DoResetAll : SharedSecureStorageAction() } sealed class SharedSecureStorageViewEvent : VectorViewEvents { @@ -40,4 +42,5 @@ sealed class SharedSecureStorageViewEvent : VectorViewEvents { object ShowModalLoading : SharedSecureStorageViewEvent() object HideModalLoading : SharedSecureStorageViewEvent() data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent() + object ShowResetBottomSheet : SharedSecureStorageViewEvent() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt index bca7a63470..42d000ecc3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageActivity.kt @@ -31,12 +31,14 @@ import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment +import im.vector.app.features.crypto.recover.SetupMode import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.activity.* import javax.inject.Inject import kotlin.reflect.KClass -class SharedSecureStorageActivity : SimpleFragmentActivity() { +class SharedSecureStorageActivity : SimpleFragmentActivity(), VectorBaseBottomSheetDialogFragment.ResultListener { @Parcelize data class Args( @@ -69,18 +71,22 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() { private fun renderState(state: SharedSecureStorageViewState) { if (!state.ready) return - val fragment = if (state.hasPassphrase) { - if (state.useKey) SharedSecuredStorageKeyFragment::class else SharedSecuredStoragePassphraseFragment::class - } else SharedSecuredStorageKeyFragment::class + val fragment = + when (state.step) { + SharedSecureStorageViewState.Step.EnterPassphrase -> SharedSecuredStoragePassphraseFragment::class + SharedSecureStorageViewState.Step.EnterKey -> SharedSecuredStorageKeyFragment::class + SharedSecureStorageViewState.Step.ResetAll -> SharedSecuredStorageResetAllFragment::class + } + showFragment(fragment, Bundle()) } private fun observeViewEvents(it: SharedSecureStorageViewEvent?) { when (it) { - is SharedSecureStorageViewEvent.Dismiss -> { + is SharedSecureStorageViewEvent.Dismiss -> { finish() } - is SharedSecureStorageViewEvent.Error -> { + is SharedSecureStorageViewEvent.Error -> { AlertDialog.Builder(this) .setTitle(getString(R.string.dialog_title_error)) .setMessage(it.message) @@ -92,21 +98,31 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() { } .show() } - is SharedSecureStorageViewEvent.ShowModalLoading -> { + is SharedSecureStorageViewEvent.ShowModalLoading -> { showWaitingView() } - is SharedSecureStorageViewEvent.HideModalLoading -> { + is SharedSecureStorageViewEvent.HideModalLoading -> { hideWaitingView() } - is SharedSecureStorageViewEvent.UpdateLoadingState -> { + is SharedSecureStorageViewEvent.UpdateLoadingState -> { updateWaitingView(it.waitingData) } - is SharedSecureStorageViewEvent.FinishSuccess -> { + is SharedSecureStorageViewEvent.FinishSuccess -> { val dataResult = Intent() dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult) setResult(Activity.RESULT_OK, dataResult) finish() } + is SharedSecureStorageViewEvent.ShowResetBottomSheet -> { + navigator.open4SSetup(this, SetupMode.HARD_RESET) + } + } + } + + override fun onAttachFragment(fragment: Fragment) { + super.onAttachFragment(fragment) + if (fragment is VectorBaseBottomSheetDialogFragment) { + fragment.resultListener = this } } @@ -124,6 +140,7 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() { companion object { const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT" + const val EXTRA_DATA_RESET = "EXTRA_DATA_RESET" const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity" fun newIntent(context: Context, @@ -140,4 +157,12 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() { } } } + + override fun onBottomSheetResult(resultCode: Int, data: Any?) { + if (resultCode == VectorBaseBottomSheetDialogFragment.ResultListener.RESULT_OK) { + // the 4S has been reset + setResult(Activity.RESULT_OK, Intent().apply { putExtra(EXTRA_DATA_RESET, true) }) + finish() + } + } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt index 5bc87dfce7..951ff4ede6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecureStorageViewModel.kt @@ -33,6 +33,9 @@ import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.IntegrityResult @@ -40,19 +43,26 @@ import org.matrix.android.sdk.api.session.securestorage.KeyInfoResult import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding import org.matrix.android.sdk.internal.util.awaitCallback -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import org.matrix.android.sdk.rx.rx import timber.log.Timber import java.io.ByteArrayOutputStream data class SharedSecureStorageViewState( val ready: Boolean = false, val hasPassphrase: Boolean = true, - val useKey: Boolean = false, val passphraseVisible: Boolean = false, - val checkingSSSSAction: Async = Uninitialized -) : MvRxState + val checkingSSSSAction: Async = Uninitialized, + val step: Step = Step.EnterPassphrase, + val activeDeviceCount: Int = 0, + val showResetAllAction: Boolean = false, + val userId: String = "" +) : MvRxState { + enum class Step { + EnterPassphrase, + EnterKey, + ResetAll + } +} class SharedSecureStorageViewModel @AssistedInject constructor( @Assisted initialState: SharedSecureStorageViewState, @@ -67,6 +77,10 @@ class SharedSecureStorageViewModel @AssistedInject constructor( } init { + + setState { + copy(userId = session.myUserId) + } val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success if (!isValid) { _viewEvents.post( @@ -86,20 +100,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor( if (info.content.passphrase != null) { setState { copy( - ready = true, hasPassphrase = true, - useKey = false + ready = true, + step = SharedSecureStorageViewState.Step.EnterPassphrase ) } } else { setState { copy( + hasPassphrase = false, ready = true, - hasPassphrase = false + step = SharedSecureStorageViewState.Step.EnterKey ) } } } + + session.rx() + .liveUserCryptoDevices(session.myUserId) + .distinctUntilChanged() + .execute { + copy( + activeDeviceCount = it.invoke()?.size ?: 0 + ) + } } override fun handle(action: SharedSecureStorageAction) = withState { @@ -110,27 +134,52 @@ class SharedSecureStorageViewModel @AssistedInject constructor( SharedSecureStorageAction.UseKey -> handleUseKey() is SharedSecureStorageAction.SubmitKey -> handleSubmitKey(action) SharedSecureStorageAction.Back -> handleBack() + SharedSecureStorageAction.ForgotResetAll -> handleResetAll() + SharedSecureStorageAction.DoResetAll -> handleDoResetAll() }.exhaustive } + private fun handleDoResetAll() { + _viewEvents.post(SharedSecureStorageViewEvent.ShowResetBottomSheet) + } + + private fun handleResetAll() { + setState { + copy( + step = SharedSecureStorageViewState.Step.ResetAll + ) + } + } + private fun handleUseKey() { setState { copy( - useKey = true + step = SharedSecureStorageViewState.Step.EnterKey ) } } private fun handleBack() = withState { state -> if (state.checkingSSSSAction is Loading) return@withState // ignore - if (state.hasPassphrase && state.useKey) { - setState { - copy( - useKey = false - ) + when (state.step) { + SharedSecureStorageViewState.Step.EnterKey -> { + setState { + copy( + step = SharedSecureStorageViewState.Step.EnterPassphrase + ) + } + } + SharedSecureStorageViewState.Step.ResetAll -> { + setState { + copy( + step = if (state.hasPassphrase) SharedSecureStorageViewState.Step.EnterPassphrase + else SharedSecureStorageViewState.Step.EnterKey + ) + } + } + else -> { + _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) } - } else { - _viewEvents.post(SharedSecureStorageViewEvent.Dismiss) } } @@ -158,6 +207,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor( val keySpec = RawBytesKeySpec.fromRecoveryKey(recoveryKey) ?: return@launch Unit.also { _viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.bootstrap_invalid_recovery_key))) _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) + setState { copy(checkingSSSSAction = Fail(IllegalArgumentException(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))) } } withContext(Dispatchers.IO) { diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt index ee47a4d8e9..366c979155 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageKeyFragment.kt @@ -27,9 +27,9 @@ import com.jakewharton.rxbinding3.widget.textChanges import im.vector.app.R import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.utils.startImportTextFromFileIntent -import org.matrix.android.sdk.api.extensions.tryOrNull import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.* +import org.matrix.android.sdk.api.extensions.tryOrNull import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -63,6 +63,10 @@ class SharedSecuredStorageKeyFragment @Inject constructor() : VectorBaseFragment ssss_key_use_file.debouncedClicks { startImportTextFromFileIntent(this, IMPORT_FILE_REQ) } + ssss_key_reset.clickableView.debouncedClicks { + sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll) + } + sharedViewModel.observeViewEvents { when (it) { is SharedSecureStorageViewEvent.KeyInlineError -> { diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt index 09e67948d0..97047fbc65 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStoragePassphraseFragment.kt @@ -74,6 +74,10 @@ class SharedSecuredStoragePassphraseFragment @Inject constructor( } .disposeOnDestroyView() + ssss_passphrase_reset.clickableView.debouncedClicks { + sharedViewModel.handle(SharedSecureStorageAction.ForgotResetAll) + } + sharedViewModel.observeViewEvents { when (it) { is SharedSecureStorageViewEvent.InlineError -> { diff --git a/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt new file mode 100644 index 0000000000..d7db779230 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/quads/SharedSecuredStorageResetAllFragment.kt @@ -0,0 +1,61 @@ +/* + * 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.app.features.crypto.quads + +import android.os.Bundle +import android.view.View +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.extensions.setTextOrHide +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.features.roommemberprofile.devices.DeviceListBottomSheet +import kotlinx.android.synthetic.main.fragment_ssss_reset_all.* +import javax.inject.Inject + +class SharedSecuredStorageResetAllFragment @Inject constructor() : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_ssss_reset_all + + val sharedViewModel: SharedSecureStorageViewModel by activityViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + ssss_reset_button_reset.debouncedClicks { + sharedViewModel.handle(SharedSecureStorageAction.DoResetAll) + } + + ssss_reset_button_cancel.debouncedClicks { + sharedViewModel.handle(SharedSecureStorageAction.Back) + } + + ssss_reset_other_devices.debouncedClicks { + withState(sharedViewModel) { + DeviceListBottomSheet.newInstance(it.userId, false).show(childFragmentManager, "DEV_LIST") + } + } + + sharedViewModel.subscribe(this) { state -> + ssss_reset_other_devices.setTextOrHide( + state.activeDeviceCount + .takeIf { it > 0 } + ?.let { resources.getQuantityString(R.plurals.secure_backup_reset_devices_you_can_verify, it, it) } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index 1b9beabe9c..e6260b6e7e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -45,8 +45,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { @Parcelize data class Args( - val initCrossSigningOnly: Boolean, - val forceReset4S: Boolean + val setUpMode: SetupMode = SetupMode.NORMAL ) : Parcelable override val showExpanded = true @@ -66,7 +65,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { super.onViewCreated(view, savedInstanceState) viewModel.observeViewEvents { event -> when (event) { - is BootstrapViewEvents.Dismiss -> dismiss() + is BootstrapViewEvents.Dismiss -> { + bottomSheetResult = if (event.success) ResultListener.RESULT_OK else ResultListener.RESULT_CANCEL + dismiss() + } is BootstrapViewEvents.ModalError -> { AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) @@ -90,6 +92,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { .setMessage(R.string.bootstrap_cancel_text) .setPositiveButton(R.string._continue, null) .setNegativeButton(R.string.skip) { _, _ -> + bottomSheetResult = ResultListener.RESULT_CANCEL dismiss() } .show() @@ -181,16 +184,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { const val EXTRA_ARGS = "EXTRA_ARGS" - fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) { - BootstrapBottomSheet().apply { + fun show(fragmentManager: FragmentManager, mode: SetupMode): BootstrapBottomSheet { + return BootstrapBottomSheet().apply { isCancelable = false arguments = Bundle().apply { - this.putParcelable(EXTRA_ARGS, Args( - initCrossSigningOnly, - forceReset4S - )) + this.putParcelable(EXTRA_ARGS, Args(setUpMode = mode)) } - }.show(fragmentManager, "BootstrapBottomSheet") + }.also { + it.show(fragmentManager, "BootstrapBottomSheet") + } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index 5da788583e..4dc2b92751 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -69,10 +69,10 @@ interface BootstrapProgressListener { data class Params( val userPasswordAuth: UserPasswordAuth? = null, - val initOnlyCrossSigning: Boolean = false, val progressListener: BootstrapProgressListener? = null, val passphrase: String?, - val keySpec: SsssKeySpec? = null + val keySpec: SsssKeySpec? = null, + val setupMode: SetupMode ) // TODO Rename to CreateServerRecovery @@ -84,9 +84,13 @@ class BootstrapCrossSigningTask @Inject constructor( override suspend fun execute(params: Params): BootstrapResult { val crossSigningService = session.cryptoService().crossSigningService() - Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Starting...") + Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Starting...") // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized - if (!crossSigningService.isCrossSigningInitialized()) { + + val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() + || (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) + || (params.setupMode == SetupMode.HARD_RESET) + if (shouldSetCrossSigning) { Timber.d("## BootstrapCrossSigningTask: Cross signing not enabled, so initialize") params.progressListener?.onProgress( WaitingViewData( @@ -99,7 +103,7 @@ class BootstrapCrossSigningTask @Inject constructor( awaitCallback { crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) } - if (params.initOnlyCrossSigning) { + if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { return BootstrapResult.SuccessCrossSigningOnly } } catch (failure: Throwable) { @@ -107,7 +111,7 @@ class BootstrapCrossSigningTask @Inject constructor( } } else { Timber.d("## BootstrapCrossSigningTask: Cross signing already setup, go to 4S setup") - if (params.initOnlyCrossSigning) { + if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { // not sure how this can happen?? return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup")) } @@ -236,7 +240,13 @@ class BootstrapCrossSigningTask @Inject constructor( val serverVersion = awaitCallback { session.cryptoService().keysBackupService().getCurrentVersion(it) } - if (serverVersion == null) { + + val knownMegolmSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() + val isMegolmBackupSecretKnown = knownMegolmSecret != null && knownMegolmSecret.version == serverVersion?.version + val shouldCreateKeyBackup = serverVersion == null + || (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !isMegolmBackupSecretKnown) + || (params.setupMode == SetupMode.HARD_RESET) + if (shouldCreateKeyBackup) { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup") val creationInfo = awaitCallback { session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) @@ -260,16 +270,15 @@ class BootstrapCrossSigningTask @Inject constructor( } else { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found") // ensure we store existing backup secret if we have it! - val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo() - if (knownSecret != null && knownSecret.version == serverVersion.version) { + if (isMegolmBackupSecretKnown) { // check it matches val isValid = awaitCallback { - session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it) + session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownMegolmSecret!!.recoveryKey, it) } if (isValid) { Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known") awaitCallback { - extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret -> + extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret -> ssssService.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, secret, @@ -286,7 +295,7 @@ class BootstrapCrossSigningTask @Inject constructor( Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") } - Timber.d("## BootstrapCrossSigningTask: initXSOnly:${params.initOnlyCrossSigning} Finished") + Timber.d("## BootstrapCrossSigningTask: mode:${params.setupMode} Finished") return BootstrapResult.Success(keyInfo) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index 32b4771286..a3955d561e 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -34,6 +34,8 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.resources.StringProvider import im.vector.app.features.login.ReAuthHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec @@ -41,8 +43,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionR import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.util.awaitCallback -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import java.io.OutputStream class BootstrapSharedViewModel @AssistedInject constructor( @@ -69,46 +69,52 @@ class BootstrapSharedViewModel @AssistedInject constructor( init { - if (args.forceReset4S) { - setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true)) - } - } else if (args.initCrossSigningOnly) { - // Go straight to account password - setState { - copy(step = BootstrapStep.AccountPassword(false)) - } - } else { - // need to check if user have an existing keybackup - setState { - copy(step = BootstrapStep.CheckingMigration) - } - - // We need to check if there is an existing backup - viewModelScope.launch(Dispatchers.IO) { - val version = awaitCallback { - session.cryptoService().keysBackupService().getCurrentVersion(it) + when (args.setUpMode) { + SetupMode.PASSPHRASE_RESET, + SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET, + SetupMode.HARD_RESET -> { + setState { + copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true)) } - if (version == null) { - // we just resume plain bootstrap - doesKeyBackupExist = false - setState { - copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) + } + SetupMode.CROSS_SIGNING_ONLY -> { + // Go straight to account password + setState { + copy(step = BootstrapStep.AccountPassword(false)) + } + } + SetupMode.NORMAL -> { + // need to check if user have an existing keybackup + setState { + copy(step = BootstrapStep.CheckingMigration) + } + + // We need to check if there is an existing backup + viewModelScope.launch(Dispatchers.IO) { + val version = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) } - } else { - // we need to get existing backup passphrase/key and convert to SSSS - val keyVersion = awaitCallback { - session.cryptoService().keysBackupService().getVersion(version.version ?: "", it) - } - if (keyVersion == null) { - // strange case... just finish? - _viewEvents.post(BootstrapViewEvents.Dismiss) - } else { - doesKeyBackupExist = true - isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null + if (version == null) { + // we just resume plain bootstrap + doesKeyBackupExist = false setState { copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) } + } else { + // we need to get existing backup passphrase/key and convert to SSSS + val keyVersion = awaitCallback { + session.cryptoService().keysBackupService().getVersion(version.version ?: "", it) + } + if (keyVersion == null) { + // strange case... just finish? + _viewEvents.post(BootstrapViewEvents.Dismiss(false)) + } else { + doesKeyBackupExist = true + isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null + setState { + copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) + } + } } } } @@ -234,7 +240,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } BootstrapActions.Completed -> { - _viewEvents.post(BootstrapViewEvents.Dismiss) + _viewEvents.post(BootstrapViewEvents.Dismiss(true)) } BootstrapActions.GoToCompleted -> { setState { @@ -395,16 +401,16 @@ class BootstrapSharedViewModel @AssistedInject constructor( bootstrapTask.invoke(this, Params( userPasswordAuth = userPasswordAuth, - initOnlyCrossSigning = args.initCrossSigningOnly, progressListener = progressListener, passphrase = state.passphrase, - keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } } + keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }, + setupMode = args.setUpMode ) ) { bootstrapResult -> when (bootstrapResult) { - is BootstrapResult.SuccessCrossSigningOnly -> { + is BootstrapResult.SuccessCrossSigningOnly -> { // TPD - _viewEvents.post(BootstrapViewEvents.Dismiss) + _viewEvents.post(BootstrapViewEvents.Dismiss(true)) } is BootstrapResult.Success -> { setState { @@ -428,7 +434,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( } is BootstrapResult.UnsupportedAuthFlow -> { _viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported))) - _viewEvents.post(BootstrapViewEvents.Dismiss) + _viewEvents.post(BootstrapViewEvents.Dismiss(false)) } is BootstrapResult.InvalidPasswordError -> { // it's a bad password @@ -522,7 +528,13 @@ class BootstrapSharedViewModel @AssistedInject constructor( } BootstrapStep.CheckingMigration -> Unit is BootstrapStep.FirstForm -> { - _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + _viewEvents.post( + when (args.setUpMode) { + SetupMode.CROSS_SIGNING_ONLY, + SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap() + else -> BootstrapViewEvents.Dismiss(success = false) + } + ) } is BootstrapStep.GetBackupSecretForMigration -> { setState { @@ -558,7 +570,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS) - ?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false) + ?: BootstrapBottomSheet.Args(SetupMode.CROSS_SIGNING_ONLY) return fragment.bootstrapViewModelFactory.create(state, args) } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt index 58bc64a9ad..be523a0ce3 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt @@ -19,7 +19,7 @@ package im.vector.app.features.crypto.recover import im.vector.app.core.platform.VectorViewEvents sealed class BootstrapViewEvents : VectorViewEvents { - object Dismiss : BootstrapViewEvents() + data class Dismiss(val success: Boolean) : BootstrapViewEvents() data class ModalError(val error: String) : BootstrapViewEvents() object RecoveryKeySaved: BootstrapViewEvents() data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents() diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/SetupMode.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/SetupMode.kt new file mode 100644 index 0000000000..0879490e79 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/SetupMode.kt @@ -0,0 +1,50 @@ +/* + * 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.app.features.crypto.recover + +enum class SetupMode { + + /** + * Only setup cross signing, no 4S or megolm backup + */ + CROSS_SIGNING_ONLY, + + /** + * Normal setup mode. + */ + NORMAL, + + /** + * Only reset the 4S passphrase/key, but do not touch + * to existing cross-signing or megolm backup + * It take the local known secrets and put them in 4S + */ + PASSPHRASE_RESET, + + /** + * Resets the passphrase/key, and all missing secrets + * are re-created. Meaning that if cross signing is setup and the secrets + * keys are not known, cross signing will be reset (if secret is known we just keep same cross signing) + * Same apply to megolm + */ + PASSPHRASE_AND_NEEDED_SECRETS_RESET, + + /** + * Resets the passphrase/key, cross signing and megolm backup + */ + HARD_RESET +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt index 1c6ea413cb..a32a9de97f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationAction.kt @@ -31,4 +31,5 @@ sealed class VerificationAction : VectorViewModelAction { object SkipVerification : VerificationAction() object VerifyFromPassphrase : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() + object SecuredStorageHasBeenReset : VerificationAction() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt index f979539f2e..c7ce4d6f01 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheet.kt @@ -48,6 +48,7 @@ import im.vector.app.features.crypto.verification.qrconfirmation.VerificationQrS import im.vector.app.features.crypto.verification.request.VerificationRequestFragment import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.settings.VectorSettingsActivity +import kotlinx.android.parcel.Parcelize import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME @@ -55,7 +56,6 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.SELF_SIGNING_KEY_S import org.matrix.android.sdk.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.verification.CancelCode import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState -import kotlinx.android.parcel.Parcelize import timber.log.Timber import javax.inject.Inject import kotlin.reflect.KClass @@ -76,6 +76,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { @Inject lateinit var verificationViewModelFactory: VerificationBottomSheetViewModel.Factory + @Inject lateinit var avatarRenderer: AvatarRenderer @@ -146,8 +147,13 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) { - data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let { - viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)) + val result = data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT) + val reset = data?.getBooleanExtra(SharedSecureStorageActivity.EXTRA_DATA_RESET, false) ?: false + if (result != null) { + viewModel.handle(VerificationAction.GotResultFromSsss(result, SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)) + } else if (reset) { + // all have been reset, so we are verified? + viewModel.handle(VerificationAction.SecuredStorageHasBeenReset) } } super.onActivityResult(requestCode, resultCode, data) @@ -182,6 +188,17 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } + if (state.quadSHasBeenReset) { + showFragment(VerificationConclusionFragment::class, Bundle().apply { + putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args( + isSuccessFull = true, + isMe = true, + cancelReason = null + )) + }) + return@withState + } + if (state.userThinkItsNotHim) { otherUserNameText.text = getString(R.string.dialog_title_warning) showFragment(VerificationNotMeFragment::class, Bundle()) @@ -356,6 +373,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() { } } } + fun forSelfVerification(session: Session, outgoingRequest: String): VerificationBottomSheet { return VerificationBottomSheet().apply { arguments = Bundle().apply { diff --git a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt index a4ce9bd38d..3c00478ab0 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -31,6 +31,7 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider +import kotlinx.coroutines.Dispatchers import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME @@ -74,7 +75,8 @@ data class VerificationBottomSheetViewState( val currentDeviceCanCrossSign: Boolean = false, val userWantsToCancel: Boolean = false, val userThinkItsNotHim: Boolean = false, - val quadSContainsSecrets: Boolean = true + val quadSContainsSecrets: Boolean = true, + val quadSHasBeenReset: Boolean = false ) : MvRxState class VerificationBottomSheetViewModel @AssistedInject constructor( @@ -349,6 +351,14 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( is VerificationAction.GotResultFromSsss -> { handleSecretBackFromSSSS(action) } + VerificationAction.SecuredStorageHasBeenReset -> { + if (session.cryptoService().crossSigningService().allPrivateKeysKnown()) { + setState { + copy(quadSHasBeenReset = true) + } + } + Unit + } }.exhaustive } @@ -393,7 +403,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor( } private fun tentativeRestoreBackup(res: Map?) { - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { try { val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also { Timber.v("## Keybackup secret not restored from SSSS") diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 5de8796c06..3a8d302fc7 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -37,6 +37,7 @@ import im.vector.app.features.createdirect.CreateDirectRoomActivity import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.debug.DebugMenuActivity @@ -153,7 +154,10 @@ class DefaultNavigator @Inject constructor( override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) { if (context is VectorBaseActivity) { - BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false) + BootstrapBottomSheet.show( + context.supportFragmentManager, + if (initCrossSigningOnly) SetupMode.CROSS_SIGNING_ONLY else SetupMode.NORMAL + ) } } @@ -226,13 +230,19 @@ class DefaultNavigator @Inject constructor( // if cross signing is enabled we should propose full 4S sessionHolder.getSafeActiveSession()?.let { session -> if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) { - BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly = false, forceReset4S = false) + BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL) } else { context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) } } } + override fun open4SSetup(context: Context, setupMode: SetupMode) { + if (context is VectorBaseActivity) { + BootstrapBottomSheet.show(context.supportFragmentManager, setupMode) + } + } + override fun openKeysBackupManager(context: Context) { context.startActivity(KeysBackupManageActivity.intent(context)) } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index ed710a124f..e92fea267f 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -21,6 +21,7 @@ import android.content.Context import android.view.View import androidx.core.util.Pair import androidx.fragment.app.Fragment +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.home.room.detail.widget.WidgetRequestCodes import im.vector.app.features.media.AttachmentData import im.vector.app.features.pin.PinActivity @@ -71,6 +72,8 @@ interface Navigator { fun openKeysBackupSetup(context: Context, showManualExport: Boolean) + fun open4SSetup(context: Context, setupMode: SetupMode) + fun openKeysBackupManager(context: Context) fun openGroupDetail(groupId: String, context: Context, buildTask: Boolean = false) diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt index a8c6842c08..9ba1c59983 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheet.kt @@ -18,6 +18,7 @@ package im.vector.app.features.roommemberprofile.devices import android.content.DialogInterface import android.os.Bundle +import android.os.Parcelable import android.view.KeyEvent import androidx.fragment.app.Fragment import com.airbnb.mvrx.MvRx @@ -29,6 +30,7 @@ import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.features.crypto.verification.VerificationBottomSheet +import kotlinx.android.parcel.Parcelize import javax.inject.Inject import kotlin.reflect.KClass @@ -104,10 +106,16 @@ class DeviceListBottomSheet : VectorBaseBottomSheetDialogFragment() { } } + @Parcelize + data class Args( + val userId: String, + val allowDeviceAction: Boolean + ) : Parcelable + companion object { - fun newInstance(userId: String): DeviceListBottomSheet { + fun newInstance(userId: String, allowDeviceAction: Boolean = true): DeviceListBottomSheet { val args = Bundle() - args.putString(MvRx.KEY_ARG, userId) + args.putParcelable(MvRx.KEY_ARG, Args(userId, allowDeviceAction)) return DeviceListBottomSheet().apply { arguments = args } } } diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt index db97399f1b..28af45797e 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/devices/DeviceListBottomSheetViewModel.kt @@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.args import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.app.core.di.HasScreenInjector @@ -44,24 +45,24 @@ data class DeviceListViewState( ) : MvRxState class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted private val initialState: DeviceListViewState, - @Assisted private val userId: String, + @Assisted private val args: DeviceListBottomSheet.Args, private val session: Session) : VectorViewModel(initialState) { @AssistedInject.Factory interface Factory { - fun create(initialState: DeviceListViewState, userId: String): DeviceListBottomSheetViewModel + fun create(initialState: DeviceListViewState, args: DeviceListBottomSheet.Args): DeviceListBottomSheetViewModel } init { - session.rx().liveUserCryptoDevices(userId) + session.rx().liveUserCryptoDevices(args.userId) .execute { copy(cryptoDevices = it).also { refreshSelectedId() } } - session.rx().liveCrossSigningInfo(userId) + session.rx().liveCrossSigningInfo(args.userId) .execute { copy(memberCrossSigningKey = it.invoke()?.getOrNull()) } @@ -88,6 +89,7 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva } private fun selectDevice(action: DeviceListAction.SelectDevice) { + if (!args.allowDeviceAction) return setState { copy(selectedDevice = action.device) } @@ -100,8 +102,9 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva } private fun manuallyVerify(action: DeviceListAction.ManuallyVerify) { - session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, userId, action.deviceId, null)?.let { txID -> - _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(userId, txID)) + if (!args.allowDeviceAction) return + session.cryptoService().verificationService().beginKeyVerification(VerificationMethod.SAS, args.userId, action.deviceId, null)?.let { txID -> + _viewEvents.post(DeviceListBottomSheetViewEvents.Verify(args.userId, txID)) } } @@ -109,12 +112,12 @@ class DeviceListBottomSheetViewModel @AssistedInject constructor(@Assisted priva @JvmStatic override fun create(viewModelContext: ViewModelContext, state: DeviceListViewState): DeviceListBottomSheetViewModel? { val fragment: DeviceListBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - val userId = viewModelContext.args() - return fragment.viewModelFactory.create(state, userId) + val args = viewModelContext.args() + return fragment.viewModelFactory.create(state, args) } override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? { - val userId = viewModelContext.args() + val userId = viewModelContext.args().userId val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession() return session.getUser(userId)?.toMatrixItem()?.let { DeviceListViewState( diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 44cef8f974..b0ec7426a7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -53,6 +53,7 @@ import im.vector.app.features.crypto.keys.KeysExporter import im.vector.app.features.crypto.keys.KeysImporter import im.vector.app.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet +import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.navigation.Navigator import im.vector.app.features.pin.PinActivity import im.vector.app.features.pin.PinCodeStore @@ -193,7 +194,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( secureBackupCategory.isVisible = true secureBackupPreference.title = getString(R.string.settings_secure_backup_setup) secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false) + BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL) true } } else { @@ -212,7 +213,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( secureBackupCategory.isVisible = true secureBackupPreference.title = getString(R.string.settings_secure_backup_reset) secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true) + BootstrapBottomSheet.show(parentFragmentManager, SetupMode.PASSPHRASE_RESET) true } } else if (!info.megolmSecretKnown) { diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt index c17c1a1cf8..ac4d495a8c 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetDialogFragment.kt @@ -44,6 +44,7 @@ import im.vector.app.core.extensions.queryExportKeys import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.features.crypto.keysbackup.setup.KeysBackupSetupActivity import im.vector.app.features.crypto.recover.BootstrapBottomSheet +import im.vector.app.features.crypto.recover.SetupMode import timber.log.Timber import javax.inject.Inject @@ -121,7 +122,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(), super.onActivityCreated(savedInstanceState) setupRecoveryButton.action = { - BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false) + BootstrapBottomSheet.show(parentFragmentManager, SetupMode.NORMAL) } exitAnywayButton.action = { diff --git a/vector/src/main/res/layout/fragment_ssss_access_from_key.xml b/vector/src/main/res/layout/fragment_ssss_access_from_key.xml index b6bdb2586a..bbe9282cc1 100644 --- a/vector/src/main/res/layout/fragment_ssss_access_from_key.xml +++ b/vector/src/main/res/layout/fragment_ssss_access_from_key.xml @@ -15,11 +15,11 @@ android:layout_width="24dp" android:layout_height="24dp" android:layout_marginStart="16dp" + android:src="@drawable/ic_security_key_24dp" android:tint="?riotx_text_primary" app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key" - android:src="@drawable/ic_security_key_24dp" /> + app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key" /> + + + + app:layout_constraintTop_toBottomOf="@id/ssss_key_flow" + app:leftIcon="@drawable/ic_alert_triangle" + app:tint="@color/vector_error_color" + app:titleTextColor="?riotx_text_secondary" /> + \ No newline at end of file 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 index e5482f0ec7..09bd823257 100644 --- a/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml +++ b/vector/src/main/res/layout/fragment_ssss_access_from_passphrase.xml @@ -109,16 +109,33 @@ tools:ignore="MissingConstraints" /> + + + app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_flow" + app:leftIcon="@drawable/ic_alert_triangle" + app:tint="@color/vector_error_color" + app:titleTextColor="?riotx_text_secondary" /> + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_ssss_reset_all.xml b/vector/src/main/res/layout/fragment_ssss_reset_all.xml new file mode 100644 index 0000000000..a3b2984bce --- /dev/null +++ b/vector/src/main/res/layout/fragment_ssss_reset_all.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index 8c71fb26b2..bf393b779a 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -52,6 +52,7 @@ + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index a29959e90e..0edd930498 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -78,6 +78,7 @@ Play Pause Dismiss + Reset @@ -2446,6 +2447,15 @@ Select your Recovery Key, or input it manually by typing it or pasting from your clipboard Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key. Failed to access secure storage + Forgot or lost all recovery options? Reset everything + Reset everything + Only do this if you have no other device you can verify this device with. + If you reset everything + You will restart with no history, no messages, trusted devices or trusted users + + Show the device you can verify with now + Show %d devices you can verify with now + Unencrypted Encrypted by an unverified device