Add support for mandatory backup or passphrase from .well-known configuration

This commit is contained in:
Jorge Martín 2022-05-23 16:51:42 +02:00
parent 483b1ab503
commit 130ed63b03
19 changed files with 435 additions and 142 deletions

1
changelog.d/6133.feature Normal file
View File

@ -0,0 +1 @@
Added support for mandatory backup or passphrase from .well-known configuration.

View File

@ -67,6 +67,7 @@ data class Params(
val progressListener: BootstrapProgressListener? = null, val progressListener: BootstrapProgressListener? = null,
val passphrase: String?, val passphrase: String?,
val keySpec: SsssKeySpec? = null, val keySpec: SsssKeySpec? = null,
val forceResetIfSomeSecretsAreMissing: Boolean = false,
val setupMode: SetupMode val setupMode: SetupMode
) )
@ -83,6 +84,7 @@ class BootstrapCrossSigningTask @Inject constructor(
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized // Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() || val shouldSetCrossSigning = !crossSigningService.isCrossSigningInitialized() ||
(params.forceResetIfSomeSecretsAreMissing && !crossSigningService.allPrivateKeysKnown()) ||
(params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) || (params.setupMode == SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET && !crossSigningService.allPrivateKeysKnown()) ||
(params.setupMode == SetupMode.HARD_RESET) (params.setupMode == SetupMode.HARD_RESET)
if (shouldSetCrossSigning) { if (shouldSetCrossSigning) {

View File

@ -26,6 +26,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentBootstrapSetupRecoveryBinding import im.vector.app.databinding.FragmentBootstrapSetupRecoveryBinding
import im.vector.app.features.raw.wellknown.SecureBackupMethod
import javax.inject.Inject import javax.inject.Inject
class BootstrapSetupRecoveryKeyFragment @Inject constructor() : class BootstrapSetupRecoveryKeyFragment @Inject constructor() :
@ -55,27 +56,40 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() :
} }
override fun invalidate() = withState(sharedViewModel) { state -> override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step is BootstrapStep.FirstForm) { val firstFormStep = state.step as? BootstrapStep.FirstForm ?: return@withState
if (state.step.keyBackUpExist) {
// Display the set up action if (firstFormStep.keyBackUpExist) {
views.bootstrapSetupSecureSubmit.isVisible = true renderStateWithExistingKeyBackup()
views.bootstrapSetupSecureUseSecurityKey.isVisible = false } else {
views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = false renderSetupHeader(needsReset = firstFormStep.reset)
views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false views.bootstrapSetupSecureSubmit.isVisible = false
} else {
if (state.step.reset) { // Choose between create a passphrase or use a recovery key
views.bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title) renderBackupMethodActions(firstFormStep.methods)
views.bootstrapSetupWarningTextView.isVisible = true
} else {
views.bootstrapSetupSecureText.text = getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
views.bootstrapSetupWarningTextView.isVisible = false
}
// Choose between create a passphrase or use a recovery key
views.bootstrapSetupSecureSubmit.isVisible = false
views.bootstrapSetupSecureUseSecurityKey.isVisible = true
views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = true
views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = true
}
} }
} }
private fun renderStateWithExistingKeyBackup() = with(views) {
// Display the set up action
bootstrapSetupSecureSubmit.isVisible = true
// Disable creating backup / passphrase options
bootstrapSetupSecureUseSecurityKey.isVisible = false
bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
}
private fun renderSetupHeader(needsReset: Boolean) = with(views) {
bootstrapSetupSecureText.text = if (needsReset) {
getString(R.string.reset_secure_backup_title)
} else {
getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
}
bootstrapSetupWarningTextView.isVisible = needsReset
}
private fun renderBackupMethodActions(method: SecureBackupMethod) = with(views) {
bootstrapSetupSecureUseSecurityKey.isVisible = method.isKeyAvailable
bootstrapSetupSecureUseSecurityPassphrase.isVisible = method.isPassphraseAvailable
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = method.isPassphraseAvailable
}
} }

View File

@ -33,6 +33,10 @@ import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.raw.wellknown.SecureBackupMethod
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import im.vector.app.features.raw.wellknown.secureBackupMethod
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
@ -41,7 +45,9 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupLastVersionResult
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysVersionResult
@ -61,6 +67,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter, private val errorFormatter: ErrorFormatter,
private val session: Session, private val session: Session,
private val rawService: RawService,
private val bootstrapTask: BootstrapCrossSigningTask, private val bootstrapTask: BootstrapCrossSigningTask,
private val migrationTask: BackupToQuadSMigrationTask, private val migrationTask: BackupToQuadSMigrationTask,
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) { ) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
@ -83,12 +90,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
init { init {
setState {
copy(step = BootstrapStep.CheckingMigration, isRecoverySetup = session.sharedSecretStorageService().isRecoverySetup())
}
// Refresh the well-known configuration
viewModelScope.launch(Dispatchers.IO) {
val wellKnown = rawService.getElementWellknown(session.sessionParams)
setState {
copy(
isSecureBackupRequired = wellKnown?.isSecureBackupRequired().orFalse(),
secureBackupMethod = wellKnown?.secureBackupMethod() ?: SecureBackupMethod.KEY_OR_PASSPHRASE,
)
}
}
when (initialState.setupMode) { when (initialState.setupMode) {
SetupMode.PASSPHRASE_RESET, SetupMode.PASSPHRASE_RESET,
SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET, SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET,
SetupMode.HARD_RESET -> { SetupMode.HARD_RESET -> {
setState { setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true)) copy(
step = BootstrapStep.FirstForm(
keyBackUpExist = false,
reset = session.sharedSecretStorageService().isRecoverySetup(),
methods = this.secureBackupMethod
)
)
} }
} }
SetupMode.CROSS_SIGNING_ONLY -> { SetupMode.CROSS_SIGNING_ONLY -> {
@ -112,7 +140,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// we just resume plain bootstrap // we just resume plain bootstrap
doesKeyBackupExist = false doesKeyBackupExist = false
setState { setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod))
} }
} else { } else {
// we need to get existing backup passphrase/key and convert to SSSS // we need to get existing backup passphrase/key and convert to SSSS
@ -126,7 +154,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
doesKeyBackupExist = true doesKeyBackupExist = true
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
setState { setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist)) copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod))
} }
} }
} }
@ -411,6 +439,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
progressListener = progressListener, progressListener = progressListener,
passphrase = state.passphrase, passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }, keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
forceResetIfSomeSecretsAreMissing = state.isSecureBackupRequired,
setupMode = state.setupMode setupMode = state.setupMode
) )
) { bootstrapResult -> ) { bootstrapResult ->
@ -419,14 +448,22 @@ class BootstrapSharedViewModel @AssistedInject constructor(
_viewEvents.post(BootstrapViewEvents.Dismiss(true)) _viewEvents.post(BootstrapViewEvents.Dismiss(true))
} }
is BootstrapResult.Success -> { is BootstrapResult.Success -> {
setState { val isSecureBackupRequired = state.isSecureBackupRequired
copy( val secureBackupMethod = state.secureBackupMethod
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
step = BootstrapStep.SaveRecoveryKey( if (state.passphrase != null && isSecureBackupRequired && secureBackupMethod == SecureBackupMethod.PASSPHRASE) {
// If a passphrase was used, saving key is optional // Go straight to conclusion, skip the save key step
state.passphrase != null _viewEvents.post(BootstrapViewEvents.Dismiss(success = true))
) } else {
) setState {
copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
step = BootstrapStep.SaveRecoveryKey(
// If a passphrase was used, saving key is optional
state.passphrase != null
)
)
}
} }
} }
is BootstrapResult.InvalidPasswordError -> { is BootstrapResult.InvalidPasswordError -> {
@ -476,7 +513,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} else { } else {
setState { setState {
copy( copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase // Also reset the passphrase
passphrase = null, passphrase = null,
passphraseRepeat = null, passphraseRepeat = null,
@ -489,7 +526,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
is BootstrapStep.SetupPassphrase -> { is BootstrapStep.SetupPassphrase -> {
setState { setState {
copy( copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase // Also reset the passphrase
passphrase = null, passphrase = null,
passphraseRepeat = null passphraseRepeat = null
@ -504,11 +541,25 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
} }
is BootstrapStep.AccountReAuth -> { is BootstrapStep.AccountReAuth -> {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) if (state.canLeave) {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
} else {
// Go back to the first step
setState {
copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null
)
}
}
} }
BootstrapStep.Initializing -> { BootstrapStep.Initializing -> {
// do we let you cancel from here? // do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) if (state.canLeave) {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
}
} }
is BootstrapStep.SaveRecoveryKey, is BootstrapStep.SaveRecoveryKey,
BootstrapStep.DoneSuccess -> { BootstrapStep.DoneSuccess -> {
@ -516,18 +567,20 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
BootstrapStep.CheckingMigration -> Unit BootstrapStep.CheckingMigration -> Unit
is BootstrapStep.FirstForm -> { is BootstrapStep.FirstForm -> {
_viewEvents.post( if (state.canLeave) {
when (state.setupMode) { _viewEvents.post(
SetupMode.CROSS_SIGNING_ONLY, when (state.setupMode) {
SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap() SetupMode.CROSS_SIGNING_ONLY,
else -> BootstrapViewEvents.Dismiss(success = false) SetupMode.NORMAL -> BootstrapViewEvents.SkipBootstrap()
} else -> BootstrapViewEvents.Dismiss(success = false)
) }
)
}
} }
is BootstrapStep.GetBackupSecretForMigration -> { is BootstrapStep.GetBackupSecretForMigration -> {
setState { setState {
copy( copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist), step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist, methods = this.secureBackupMethod),
// Also reset the passphrase // Also reset the passphrase
passphrase = null, passphrase = null,
passphraseRepeat = null, passphraseRepeat = null,
@ -549,3 +602,5 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
} }
} }
private val BootstrapViewState.canLeave: Boolean get() = !isSecureBackupRequired || isRecoverySetup

View File

@ -16,6 +16,8 @@
package im.vector.app.features.crypto.recover package im.vector.app.features.crypto.recover
import im.vector.app.features.raw.wellknown.SecureBackupMethod
/** /**
* TODO The schema is not up to date * TODO The schema is not up to date
* *
@ -89,7 +91,7 @@ sealed class BootstrapStep {
object CheckingMigration : BootstrapStep() object CheckingMigration : BootstrapStep()
// Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists // Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep() data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false, val methods: SecureBackupMethod) : BootstrapStep()
object SetupPassphrase : BootstrapStep() object SetupPassphrase : BootstrapStep()
object ConfirmPassphrase : BootstrapStep() object ConfirmPassphrase : BootstrapStep()

View File

@ -21,6 +21,7 @@ import com.airbnb.mvrx.MavericksState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.nulabinc.zxcvbn.Strength import com.nulabinc.zxcvbn.Strength
import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.platform.WaitingViewData
import im.vector.app.features.raw.wellknown.SecureBackupMethod
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
data class BootstrapViewState( data class BootstrapViewState(
@ -34,7 +35,10 @@ data class BootstrapViewState(
val passphraseConfirmMatch: Async<Unit> = Uninitialized, val passphraseConfirmMatch: Async<Unit> = Uninitialized,
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null, val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
val initializationWaitingViewData: WaitingViewData? = null, val initializationWaitingViewData: WaitingViewData? = null,
val recoverySaveFileProcess: Async<Unit> = Uninitialized val recoverySaveFileProcess: Async<Unit> = Uninitialized,
val isSecureBackupRequired: Boolean = false,
val secureBackupMethod: SecureBackupMethod = SecureBackupMethod.KEY_OR_PASSPHRASE,
val isRecoverySetup: Boolean = true
) : MavericksState { ) : MavericksState {
constructor(args: BootstrapBottomSheet.Args) : this(setupMode = args.setUpMode) constructor(args: BootstrapBottomSheet.Args) : this(setupMode = args.setUpMode)

View File

@ -27,7 +27,7 @@ sealed class VerificationAction : VectorViewModelAction {
object OtherUserDidNotScanned : VerificationAction() object OtherUserDidNotScanned : VerificationAction()
data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() data class SASMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction() data class SASDoNotMatchAction(val otherUserId: String, val sasTransactionId: String) : VerificationAction()
object GotItConclusion : VerificationAction() data class GotItConclusion(val verified: Boolean) : VerificationAction()
object SkipVerification : VerificationAction() object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction() object VerifyFromPassphrase : VerificationAction()
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()

View File

@ -30,9 +30,13 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session 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.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -78,6 +82,7 @@ data class VerificationBottomSheetViewState(
val userWantsToCancel: Boolean = false, val userWantsToCancel: Boolean = false,
val userThinkItsNotHim: Boolean = false, val userThinkItsNotHim: Boolean = false,
val quadSContainsSecrets: Boolean = true, val quadSContainsSecrets: Boolean = true,
val isVerificationRequired: Boolean = false,
val quadSHasBeenReset: Boolean = false, val quadSHasBeenReset: Boolean = false,
val hasAnyOtherSession: Boolean = false val hasAnyOtherSession: Boolean = false
) : MavericksState { ) : MavericksState {
@ -92,6 +97,7 @@ data class VerificationBottomSheetViewState(
class VerificationBottomSheetViewModel @AssistedInject constructor( class VerificationBottomSheetViewModel @AssistedInject constructor(
@Assisted initialState: VerificationBottomSheetViewState, @Assisted initialState: VerificationBottomSheetViewState,
private val rawService: RawService,
private val session: Session, private val session: Session,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider, private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stringProvider: StringProvider) : private val stringProvider: StringProvider) :
@ -108,6 +114,15 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
init { init {
session.cryptoService().verificationService().addListener(this) session.cryptoService().verificationService().addListener(this)
// This is async, but at this point should be in cache
// so it's ok to not wait until result
viewModelScope.launch(Dispatchers.IO) {
val wellKnown = rawService.getElementWellknown(session.sessionParams)
setState {
copy(isVerificationRequired = wellKnown?.isSecureBackupRequired().orFalse())
}
}
val userItem = session.getUser(initialState.otherUserId) val userItem = session.getUser(initialState.otherUserId)
var autoReady = false var autoReady = false
@ -182,8 +197,10 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
state.verifyingFrom4S) { state.verifyingFrom4S) {
// you cannot cancel anymore // you cannot cancel anymore
} else { } else {
setState { if (!state.isVerificationRequired) {
copy(userWantsToCancel = true) setState {
copy(userWantsToCancel = true)
}
} }
} }
} }
@ -341,7 +358,18 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
?.shortCodeDoesNotMatch() ?.shortCodeDoesNotMatch()
} }
is VerificationAction.GotItConclusion -> { is VerificationAction.GotItConclusion -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) if (state.isVerificationRequired && !action.verified) {
// we should go back to first screen
setState {
copy(
pendingRequest = Uninitialized,
sasTransactionState = null,
qrTransactionState = null
)
}
} else {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
} }
is VerificationAction.SkipVerification -> { is VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)

View File

@ -84,7 +84,7 @@ class VerificationConclusionController @Inject constructor(
notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised)).toEpoxyCharSequence()) notice(host.eventHtmlRenderer.render(host.stringProvider.getString(R.string.verification_conclusion_compromised)).toEpoxyCharSequence())
} }
bottomDone() bottomGotIt()
} }
ConclusionState.CANCELLED -> { ConclusionState.CANCELLED -> {
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
@ -92,18 +92,7 @@ class VerificationConclusionController @Inject constructor(
notice(host.stringProvider.getString(R.string.verify_cancelled_notice).toEpoxyCharSequence()) notice(host.stringProvider.getString(R.string.verify_cancelled_notice).toEpoxyCharSequence())
} }
bottomSheetDividerItem { bottomGotIt()
id("sep0")
}
bottomSheetVerificationActionItem {
id("got_it")
title(host.stringProvider.getString(R.string.sas_got_it))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
listener { host.listener?.onButtonTapped() }
}
} }
} }
} }
@ -120,11 +109,27 @@ class VerificationConclusionController @Inject constructor(
titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
listener { host.listener?.onButtonTapped() } listener { host.listener?.onButtonTapped(true) }
}
}
private fun bottomGotIt() {
val host = this
bottomSheetDividerItem {
id("sep0")
}
bottomSheetVerificationActionItem {
id("got_it")
title(host.stringProvider.getString(R.string.sas_got_it))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
listener { host.listener?.onButtonTapped(false) }
} }
} }
interface Listener { interface Listener {
fun onButtonTapped() fun onButtonTapped(success: Boolean)
} }
} }

View File

@ -73,7 +73,7 @@ class VerificationConclusionFragment @Inject constructor(
controller.update(state) controller.update(state)
} }
override fun onButtonTapped() { override fun onButtonTapped(success: Boolean) {
sharedViewModel.handle(VerificationAction.GotItConclusion) sharedViewModel.handle(VerificationAction.GotItConclusion(success))
} }
} }

View File

@ -88,17 +88,19 @@ class VerificationRequestController @Inject constructor(
} }
} }
bottomSheetDividerItem { if (!state.isVerificationRequired) {
id("sep1") bottomSheetDividerItem {
} id("sep1")
}
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {
id("skip") id("skip")
title(host.stringProvider.getString(R.string.action_skip)) title(host.stringProvider.getString(R.string.action_skip))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
listener { host.listener?.onClickSkip() } listener { host.listener?.onClickSkip() }
}
} }
} else { } else {
val styledText = val styledText =

View File

@ -50,6 +50,7 @@ import im.vector.app.features.MainActivityArgs
import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel import im.vector.app.features.analytics.accountdata.AnalyticsAccountDataViewModel
import im.vector.app.features.analytics.plan.MobileScreen import im.vector.app.features.analytics.plan.MobileScreen
import im.vector.app.features.analytics.plan.ViewRoom import im.vector.app.features.analytics.plan.ViewRoom
import im.vector.app.features.crypto.recover.SetupMode
import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
import im.vector.app.features.matrixto.OriginOfMatrixTo import im.vector.app.features.matrixto.OriginOfMatrixTo
@ -226,6 +227,14 @@ class HomeActivity :
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it) is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it) is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush() HomeActivityViewEvents.PromptToEnableSessionPush -> handlePromptToEnablePush()
HomeActivityViewEvents.StartRecoverySetupFlow -> handleStartRecoverySetup()
is HomeActivityViewEvents.ForceVerification -> {
if (it.sendRequest) {
navigator.requestSelfSessionVerification(this)
} else {
navigator.waitSessionVerification(this)
}
}
is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it) is HomeActivityViewEvents.OnCrossSignedInvalidated -> handleCrossSigningInvalidated(it)
HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn() HomeActivityViewEvents.ShowAnalyticsOptIn -> handleShowAnalyticsOptIn()
HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration() HomeActivityViewEvents.NotifyUserForThreadsMigration -> handleNotifyUserForThreadsMigration()
@ -355,6 +364,13 @@ class HomeActivity :
} }
} }
private fun handleStartRecoverySetup() {
// To avoid IllegalStateException in case the transaction was executed after onSaveInstanceState
lifecycleScope.launchWhenResumed {
navigator.open4SSetup(this@HomeActivity, SetupMode.NORMAL)
}
}
private fun renderState(state: HomeActivityViewState) { private fun renderState(state: HomeActivityViewState) {
when (val status = state.syncStatusServiceStatus) { when (val status = state.syncStatusServiceStatus) {
is SyncStatusService.Status.InitialSyncProgressing -> { is SyncStatusService.Status.InitialSyncProgressing -> {

View File

@ -27,4 +27,6 @@ sealed interface HomeActivityViewEvents : VectorViewEvents {
object ShowAnalyticsOptIn : HomeActivityViewEvents object ShowAnalyticsOptIn : HomeActivityViewEvents
object NotifyUserForThreadsMigration : HomeActivityViewEvents object NotifyUserForThreadsMigration : HomeActivityViewEvents
data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents data class MigrateThreads(val checkSession: Boolean) : HomeActivityViewEvents
object StartRecoverySetupFlow : HomeActivityViewEvents
data class ForceVerification(val sendRequest: Boolean) : HomeActivityViewEvents
} }

View File

@ -17,7 +17,9 @@
package im.vector.app.features.home package im.vector.app.features.home
import androidx.lifecycle.asFlow import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
@ -28,6 +30,9 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.analytics.store.AnalyticsStore import im.vector.app.features.analytics.store.AnalyticsStore
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.raw.wellknown.ElementWellKnown
import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isSecureBackupRequired
import im.vector.app.features.session.coroutineScope import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -42,6 +47,8 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.api.session.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.api.session.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.api.session.getUser import org.matrix.android.sdk.api.session.getUser
@ -59,8 +66,9 @@ import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
class HomeActivityViewModel @AssistedInject constructor( class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState, @Assisted private val initialState: HomeActivityViewState,
private val activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val rawService: RawService,
private val reAuthHelper: ReAuthHelper, private val reAuthHelper: ReAuthHelper,
private val analyticsStore: AnalyticsStore, private val analyticsStore: AnalyticsStore,
private val lightweightSettingsStorage: LightweightSettingsStorage, private val lightweightSettingsStorage: LightweightSettingsStorage,
@ -72,10 +80,17 @@ class HomeActivityViewModel @AssistedInject constructor(
override fun create(initialState: HomeActivityViewState): HomeActivityViewModel override fun create(initialState: HomeActivityViewState): HomeActivityViewModel
} }
companion object : MavericksViewModelFactory<HomeActivityViewModel, HomeActivityViewState> by hiltMavericksViewModelFactory() companion object : MavericksViewModelFactory<HomeActivityViewModel, HomeActivityViewState> by hiltMavericksViewModelFactory() {
override fun initialState(viewModelContext: ViewModelContext): HomeActivityViewState? {
val activity: HomeActivity = viewModelContext.activity()
val args: HomeActivityArgs? = activity.intent.getParcelableExtra(Mavericks.KEY_ARG)
return args?.let { HomeActivityViewState(accountCreation = it.accountCreation) }
?: super.initialState(viewModelContext)
}
}
private var isInitialized = false private var isInitialized = false
private var checkBootstrap = false private var hasCheckedBootstrap = false
private var onceTrusted = false private var onceTrusted = false
private fun initialize() { private fun initialize() {
@ -116,17 +131,13 @@ class HomeActivityViewModel @AssistedInject constructor(
safeActiveSession safeActiveSession
.flow() .flow()
.liveCrossSigningInfo(safeActiveSession.myUserId) .liveCrossSigningInfo(safeActiveSession.myUserId)
.onEach { .onEach { info ->
val isVerified = it.getOrNull()?.isTrusted() ?: false val isVerified = info.getOrNull()?.isTrusted() ?: false
if (!isVerified && onceTrusted) { if (!isVerified && onceTrusted) {
// cross signing keys have been reset viewModelScope.launch(Dispatchers.IO) {
// Trigger a popup to re-verify val elementWellKnown = rawService.getElementWellknown(safeActiveSession.sessionParams)
// Note: user can be null in case of logout sessionHasBeenUnverified(elementWellKnown)
safeActiveSession.getUser(safeActiveSession.myUserId) }
?.toMatrixItem()
?.let { user ->
_viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user))
}
} }
onceTrusted = isVerified onceTrusted = isVerified
} }
@ -180,15 +191,8 @@ class HomeActivityViewModel @AssistedInject constructor(
.asFlow() .asFlow()
.onEach { status -> .onEach { status ->
when (status) { when (status) {
is SyncStatusService.Status.InitialSyncProgressing -> {
// Schedule a check of the bootstrap when the init sync will be finished
checkBootstrap = true
}
is SyncStatusService.Status.Idle -> { is SyncStatusService.Status.Idle -> {
if (checkBootstrap) { maybeVerifyOrBootstrapCrossSigning()
checkBootstrap = false
maybeBootstrapCrossSigningAfterInitialSync()
}
} }
else -> Unit else -> Unit
} }
@ -200,6 +204,10 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
if (session.hasAlreadySynced()) {
maybeVerifyOrBootstrapCrossSigning()
}
} }
/** /**
@ -240,12 +248,72 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
private fun maybeBootstrapCrossSigningAfterInitialSync() { private fun sessionHasBeenUnverified(elementWellKnown: ElementWellKnown?) {
val session = activeSessionHolder.getSafeActiveSession() ?: return
val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false
if (isSecureBackupRequired) {
// If 4S is forced, force verification
// for stability cancel all pending verifications?
session.cryptoService().verificationService().getExistingVerificationRequests(session.myUserId).forEach {
session.cryptoService().verificationService().cancelVerificationRequest(it)
}
_viewEvents.post(HomeActivityViewEvents.ForceVerification(false))
} else {
// cross signing keys have been reset
// Trigger a popup to re-verify
// Note: user can be null in case of logout
session.getUser(session.myUserId)
?.toMatrixItem()
?.let { user ->
_viewEvents.post(HomeActivityViewEvents.OnCrossSignedInvalidated(user))
}
}
}
private fun maybeVerifyOrBootstrapCrossSigning() {
// The contents of this method should only run once
if (hasCheckedBootstrap) return
hasCheckedBootstrap = true
// We do not use the viewModel context because we do not want to tie this action to activity view model // We do not use the viewModel context because we do not want to tie this action to activity view model
activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch(Dispatchers.IO) { activeSessionHolder.getSafeActiveSession()?.coroutineScope?.launch(Dispatchers.IO) {
val session = activeSessionHolder.getSafeActiveSession() ?: return@launch val session = activeSessionHolder.getSafeActiveSession() ?: return@launch Unit.also {
Timber.w("## No session to init cross signing or bootstrap")
}
tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") { val elementWellKnown = rawService.getElementWellknown(session.sessionParams)
val isSecureBackupRequired = elementWellKnown?.isSecureBackupRequired() ?: false
// In case of account creation, it is already done before
if (initialState.accountCreation) {
if (isSecureBackupRequired) {
_viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
} else {
val password = reAuthHelper.data ?: return@launch Unit.also {
Timber.w("No password to init cross signing")
}
// Silently initialize cross signing without 4S
// We do not use the viewModel context because we do not want to cancel this action
Timber.d("Initialize cross signing")
try {
session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, _ ->
resume(
UserPasswordAuth(
session = response.session,
user = session.myUserId,
password = password
)
)
}
} catch (failure: Throwable) {
Timber.e(failure, "Failed to initialize cross signing")
}
}
return@launch
}
tryOrNull("## MaybeVerifyOrBootstrapCrossSigning: Failed to download keys") {
awaitCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { awaitCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
session.cryptoService().downloadKeys(listOf(session.myUserId), true, it) session.cryptoService().downloadKeys(listOf(session.myUserId), true, it)
} }
@ -255,47 +323,68 @@ class HomeActivityViewModel @AssistedInject constructor(
// Is there already cross signing keys here? // Is there already cross signing keys here?
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
if (mxCrossSigningInfo != null) { if (mxCrossSigningInfo != null) {
// Cross-signing is already set up for this user, is it trusted? if (isSecureBackupRequired && !session.sharedSecretStorageService().isRecoverySetup()) {
if (!mxCrossSigningInfo.isTrusted()) { // If 4S is forced, start the full interactive setup flow
// New session _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
_viewEvents.post( } else {
HomeActivityViewEvents.OnNewSession( // Cross-signing is already set up for this user, is it trusted?
session.getUser(session.myUserId)?.toMatrixItem(), if (!mxCrossSigningInfo.isTrusted()) {
// Always send request instead of waiting for an incoming as per recent EW changes if (isSecureBackupRequired) {
false // If 4S is forced, force verification
_viewEvents.post(HomeActivityViewEvents.ForceVerification(true))
} else {
// New session
_viewEvents.post(
HomeActivityViewEvents.OnNewSession(
session.getUser(session.myUserId)?.toMatrixItem(),
// Always send request instead of waiting for an incoming as per recent EW changes
false
)
) )
) }
}
} }
} else { } else {
// Try to initialize cross signing in background if possible // Cross signing is not initialized
Timber.d("Initialize cross signing...") if (isSecureBackupRequired) {
try { // If 4S is forced, start the full interactive setup flow
awaitCallback<Unit> { _viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
session.cryptoService().crossSigningService().initializeCrossSigning( } else {
object : UserInteractiveAuthInterceptor { // Initialize cross-signing silently
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) { val password = reAuthHelper.data
// We missed server grace period or it's not setup, see if we remember locally password
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && if (password == null) {
errCode == null && // Check this is not an SSO account
reAuthHelper.data != null) { if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) {
promise.resume( // Ask password to the user: Upgrade security
UserPasswordAuth( _viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
session = flowResponse.session, }
user = session.myUserId, // Else (SSO) just ignore for the moment
password = reAuthHelper.data } else {
) // Try to initialize cross signing in background if possible
Timber.d("Initialize cross signing...")
try {
session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, errCode ->
// We missed server grace period or it's not setup, see if we remember locally password
if (response.nextUncompletedStage() == LoginFlowTypes.PASSWORD &&
errCode == null &&
reAuthHelper.data != null) {
resume(
UserPasswordAuth(
session = response.session,
user = session.myUserId,
password = reAuthHelper.data
) )
} else { )
promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing")) Timber.d("Initialize cross signing SUCCESS")
} } else {
} resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing"))
}, }
callback = it }
) } catch (failure: Throwable) {
Timber.d("Initialize cross signing SUCCESS") Timber.e(failure, "Failed to initialize cross signing")
}
} }
} catch (failure: Throwable) {
Timber.e(failure, "Failed to initialize cross signing")
} }
} }
} }
@ -312,3 +401,18 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
} }
private suspend fun CrossSigningService.awaitCrossSigninInitialization(
block: Continuation<UIABaseAuth>.(response: RegistrationFlowResponse, errCode: String?) -> Unit
) {
awaitCallback<Unit> {
initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.block(flowResponse, errCode)
}
},
callback = it
)
}
}

View File

@ -20,5 +20,6 @@ import com.airbnb.mvrx.MavericksState
import org.matrix.android.sdk.api.session.initsync.SyncStatusService import org.matrix.android.sdk.api.session.initsync.SyncStatusService
data class HomeActivityViewState( data class HomeActivityViewState(
val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle val syncStatusServiceStatus: SyncStatusService.Status = SyncStatusService.Status.Idle,
val accountCreation: Boolean = false
) : MavericksState ) : MavericksState

View File

@ -53,7 +53,19 @@ data class E2EWellKnownConfig(
* (as it was before) for various environments where this is desired. * (as it was before) for various environments where this is desired.
*/ */
@Json(name = "default") @Json(name = "default")
val e2eDefault: Boolean? = null val e2eDefault: Boolean? = null,
@Json(name = "secure_backup_required")
val secureBackupRequired: Boolean? = null,
/**
* The new field secure_backup_setup_methods is an array listing the methods the client should display.
* Supported values currently include key and passphrase.
* If the secure_backup_setup_methods field is not present or exists but does not contain any supported methods,
* clients should fallback to the default value of: ["key", "passphrase"].
*/
@Json(name = "secure_backup_setup_methods")
val secureBackupSetupMethods: List<String>? = null
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)

View File

@ -29,3 +29,22 @@ suspend fun RawService.getElementWellknown(sessionParams: SessionParams): Elemen
} }
fun ElementWellKnown.isE2EByDefault() = elementE2E?.e2eDefault ?: riotE2E?.e2eDefault ?: true fun ElementWellKnown.isE2EByDefault() = elementE2E?.e2eDefault ?: riotE2E?.e2eDefault ?: true
fun ElementWellKnown.isSecureBackupRequired() = elementE2E?.secureBackupRequired
?: riotE2E?.secureBackupRequired
?: false
fun ElementWellKnown?.secureBackupMethod(): SecureBackupMethod {
val methodList = this?.elementE2E?.secureBackupSetupMethods
?: this?.riotE2E?.secureBackupSetupMethods
?: listOf("key", "passphrase")
return if (methodList.contains("key") && methodList.contains("passphrase")) {
SecureBackupMethod.KEY_OR_PASSPHRASE
} else if (methodList.contains("key")) {
SecureBackupMethod.KEY
} else if (methodList.contains("passphrase")) {
SecureBackupMethod.PASSPHRASE
} else {
SecureBackupMethod.KEY_OR_PASSPHRASE
}
}

View File

@ -0,0 +1,26 @@
/*
* Copyright (c) 2022 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.raw.wellknown
enum class SecureBackupMethod {
KEY,
PASSPHRASE,
KEY_OR_PASSPHRASE;
val isKeyAvailable: Boolean get() = this == KEY || this == KEY_OR_PASSPHRASE
val isPassphraseAvailable: Boolean get() = this == PASSPHRASE || this == KEY_OR_PASSPHRASE
}

View File

@ -25,7 +25,8 @@
android:scaleType="fitCenter" android:scaleType="fitCenter"
android:src="@drawable/ic_security_key_24dp" android:src="@drawable/ic_security_key_24dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="@id/bootstrapTitleText"
app:layout_constraintBottom_toBottomOf="@id/bootstrapTitleText"
app:tint="?vctr_content_primary" app:tint="?vctr_content_primary"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix" />
@ -39,10 +40,9 @@
android:ellipsize="end" android:ellipsize="end"
android:textColor="?vctr_content_primary" android:textColor="?vctr_content_primary"
android:textStyle="bold" android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/bootstrapIcon"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/bootstrapIcon" app:layout_constraintStart_toEndOf="@id/bootstrapIcon"
app:layout_constraintTop_toTopOf="@id/bootstrapIcon" app:layout_constraintTop_toTopOf="parent"
tools:text="@string/bottom_sheet_setup_secure_backup_title" /> tools:text="@string/bottom_sheet_setup_secure_backup_title" />
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView