Merge pull request #2187 from vector-im/feature/forgot_pass_reset_all_4S

Feature/forgot pass reset all 4 s
This commit is contained in:
Valere 2020-09-30 18:22:48 +02:00 committed by GitHub
commit b6b73f2361
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 777 additions and 126 deletions

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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())
}
}

View file

@ -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
))
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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))
}
}
}

View file

@ -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()
}

View file

@ -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()
}
}
}

View file

@ -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<Unit> = Uninitialized
) : MvRxState
val checkingSSSSAction: Async<Unit> = 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) {

View file

@ -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 -> {

View file

@ -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 -> {

View file

@ -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) }
)
}
}
}

View file

@ -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")
}
}
}

View file

@ -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<Unit> {
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<KeysVersionResult?> {
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<MegolmBackupCreationInfo> {
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<Boolean> {
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<Unit> {
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)
}

View file

@ -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<KeysVersionResult?> {
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<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
} else {
// we need to get existing backup passphrase/key and convert to SSSS
val keyVersion = awaitCallback<KeysVersionResult?> {
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<KeysVersionResult?> {
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)
}
}

View file

@ -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()

View file

@ -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
}

View file

@ -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()
}

View file

@ -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 {

View file

@ -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<String, String>?) {
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")

View file

@ -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))
}

View file

@ -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)

View file

@ -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 }
}
}

View file

@ -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<DeviceListViewState, DeviceListAction, DeviceListBottomSheetViewEvents>(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<String>()
return fragment.viewModelFactory.create(state, userId)
val args = viewModelContext.args<DeviceListBottomSheet.Args>()
return fragment.viewModelFactory.create(state, args)
}
override fun initialState(viewModelContext: ViewModelContext): DeviceListViewState? {
val userId = viewModelContext.args<String>()
val userId = viewModelContext.args<DeviceListBottomSheet.Args>().userId
val session = (viewModelContext.activity as HasScreenInjector).injector().activeSessionHolder().getActiveSession()
return session.getUser(userId)?.toMatrixItem()?.let {
DeviceListViewState(

View file

@ -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) {

View file

@ -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 = {

View file

@ -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" />
<TextView
android:id="@+id/ssss_restore_with_key"
@ -56,8 +56,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
@ -97,16 +97,32 @@
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_key_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
app:constraint_referenced_ids="ssss_key_use_file,ssss_key_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toTopOf="@+id/ssss_key_reset"
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til"
app:layout_goneMarginBottom="@dimen/layout_vertical_margin_big" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/ssss_key_reset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til" />
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -109,16 +109,33 @@
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_passphrase_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toTopOf="@+id/ssss_passphrase_reset"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til"
app:layout_goneMarginBottom="32dp" />
<im.vector.app.core.ui.views.BottomSheetActionButton
android:id="@+id/ssss_passphrase_reset"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="32dp"
android:clickable="true"
android:focusable="true"
app:actionTitle="@string/bad_passphrase_key_reset_all_action"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ssss__root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/reset_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:drawableStart="@drawable/ic_alert_triangle"
android:drawablePadding="8dp"
android:drawableTint="?riot_primary_text_color"
android:text="@string/secure_backup_reset_all"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
android:tint="?riot_primary_text_color"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ssss_reset_all_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/secure_backup_reset_all_no_other_devices"
android:textColor="?riotx_text_primary"
app:layout_constraintBottom_toTopOf="@id/ssss_reset_other_devices"
app:layout_constraintTop_toBottomOf="@id/reset_title" />
<TextView
android:id="@+id/ssss_reset_other_devices"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:drawableStart="@drawable/ic_smartphone"
android:drawablePadding="8dp"
android:gravity="center_vertical"
android:textSize="17sp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_all_description"
tools:text="Show 2 devices you can verify with now"
tools:visibility="visible" />
<TextView
android:id="@+id/ssss_reset_text3"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:text="@string/secure_backup_reset_if_you_reset_all"
android:textColor="@color/riotx_destructive_accent"
android:textSize="15sp"
android:textStyle="bold"
android:tint="?riot_primary_text_color"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_other_devices" />
<TextView
android:id="@+id/ssss_reset_text4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/secure_backup_reset_no_history"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text3" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_reset_button_cancel"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/cancel"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_reset_button_reset"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/ssss_passphrase_flow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="16dp"
app:constraint_referenced_ids="ssss_reset_button_cancel, ssss_reset_button_reset"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_reset_text4" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -52,6 +52,7 @@
<declare-styleable name="BottomSheetActionButton">
<attr name="tint" format="color" />
<attr name="titleTextColor" format="color" />
<attr name="actionTitle" format="string" />
<attr name="actionDescription" format="string" />
<attr name="leftIcon" format="reference" />

View file

@ -78,6 +78,7 @@
<string name="play_video">Play</string>
<string name="pause_video">Pause</string>
<string name="dismiss">Dismiss</string>
<string name="reset">Reset</string>
<!-- First param will be replace by the value of ongoing_conference_call_voice, and second one by the value of ongoing_conference_call_video -->
@ -2446,6 +2447,15 @@
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
<string name="bad_passphrase_key_reset_all_action">Forgot or lost all recovery options? Reset everything</string>
<string name="secure_backup_reset_all">Reset everything</string>
<string name="secure_backup_reset_all_no_other_devices">Only do this if you have no other device you can verify this device with.</string>
<string name="secure_backup_reset_if_you_reset_all">If you reset everything</string>
<string name="secure_backup_reset_no_history">You will restart with no history, no messages, trusted devices or trusted users</string>
<plurals name="secure_backup_reset_devices_you_can_verify">
<item quantity="one">Show the device you can verify with now</item>
<item quantity="other">Show %d devices you can verify with now</item>
</plurals>
<string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string>