mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 01:45:52 +03:00
Merge pull request #6134 from vector-im/feature/jorgem/mandatory-backup-passphrase
Add support for mandatory backup or passphrase from .well-known home server configuration
This commit is contained in:
commit
2e1a11e10e
19 changed files with 435 additions and 142 deletions
1
changelog.d/6133.feature
Normal file
1
changelog.d/6133.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Added support for mandatory backup or passphrase from .well-known configuration.
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
|
||||||
views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
|
|
||||||
views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
|
|
||||||
} else {
|
} else {
|
||||||
if (state.step.reset) {
|
renderSetupHeader(needsReset = firstFormStep.reset)
|
||||||
views.bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title)
|
|
||||||
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.bootstrapSetupSecureSubmit.isVisible = false
|
||||||
views.bootstrapSetupSecureUseSecurityKey.isVisible = true
|
|
||||||
views.bootstrapSetupSecureUseSecurityPassphrase.isVisible = true
|
// Choose between create a passphrase or use a recovery key
|
||||||
views.bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = true
|
renderBackupMethodActions(firstFormStep.methods)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,6 +448,13 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
|
||||||
}
|
}
|
||||||
is BootstrapResult.Success -> {
|
is BootstrapResult.Success -> {
|
||||||
|
val isSecureBackupRequired = state.isSecureBackupRequired
|
||||||
|
val secureBackupMethod = state.secureBackupMethod
|
||||||
|
|
||||||
|
if (state.passphrase != null && isSecureBackupRequired && secureBackupMethod == SecureBackupMethod.PASSPHRASE) {
|
||||||
|
// Go straight to conclusion, skip the save key step
|
||||||
|
_viewEvents.post(BootstrapViewEvents.Dismiss(success = true))
|
||||||
|
} else {
|
||||||
setState {
|
setState {
|
||||||
copy(
|
copy(
|
||||||
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
|
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
|
||||||
|
@ -429,6 +465,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is BootstrapResult.InvalidPasswordError -> {
|
is BootstrapResult.InvalidPasswordError -> {
|
||||||
// it's a bad password / auth
|
// it's a bad password / auth
|
||||||
setState {
|
setState {
|
||||||
|
@ -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,18 +541,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is BootstrapStep.AccountReAuth -> {
|
is BootstrapStep.AccountReAuth -> {
|
||||||
|
if (state.canLeave) {
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
_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?
|
||||||
|
if (state.canLeave) {
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is BootstrapStep.SaveRecoveryKey,
|
is BootstrapStep.SaveRecoveryKey,
|
||||||
BootstrapStep.DoneSuccess -> {
|
BootstrapStep.DoneSuccess -> {
|
||||||
// nop
|
// nop
|
||||||
}
|
}
|
||||||
BootstrapStep.CheckingMigration -> Unit
|
BootstrapStep.CheckingMigration -> Unit
|
||||||
is BootstrapStep.FirstForm -> {
|
is BootstrapStep.FirstForm -> {
|
||||||
|
if (state.canLeave) {
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
when (state.setupMode) {
|
when (state.setupMode) {
|
||||||
SetupMode.CROSS_SIGNING_ONLY,
|
SetupMode.CROSS_SIGNING_ONLY,
|
||||||
|
@ -524,10 +576,11 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,12 +197,14 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
state.verifyingFrom4S) {
|
state.verifyingFrom4S) {
|
||||||
// you cannot cancel anymore
|
// you cannot cancel anymore
|
||||||
} else {
|
} else {
|
||||||
|
if (!state.isVerificationRequired) {
|
||||||
setState {
|
setState {
|
||||||
copy(userWantsToCancel = true)
|
copy(userWantsToCancel = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun confirmCancel() = withState { state ->
|
fun confirmCancel() = withState { state ->
|
||||||
cancelAllPendingVerifications(state)
|
cancelAllPendingVerifications(state)
|
||||||
|
@ -341,8 +358,19 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
|
||||||
?.shortCodeDoesNotMatch()
|
?.shortCodeDoesNotMatch()
|
||||||
}
|
}
|
||||||
is VerificationAction.GotItConclusion -> {
|
is VerificationAction.GotItConclusion -> {
|
||||||
|
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)
|
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
is VerificationAction.SkipVerification -> {
|
is VerificationAction.SkipVerification -> {
|
||||||
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
|
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ class VerificationRequestController @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.isVerificationRequired) {
|
||||||
bottomSheetDividerItem {
|
bottomSheetDividerItem {
|
||||||
id("sep1")
|
id("sep1")
|
||||||
}
|
}
|
||||||
|
@ -100,6 +101,7 @@ class VerificationRequestController @Inject constructor(
|
||||||
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 =
|
||||||
if (state.isMe) {
|
if (state.isMe) {
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,16 +131,12 @@ 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,8 +323,16 @@ 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) {
|
||||||
|
if (isSecureBackupRequired && !session.sharedSecretStorageService().isRecoverySetup()) {
|
||||||
|
// If 4S is forced, start the full interactive setup flow
|
||||||
|
_viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
|
||||||
|
} else {
|
||||||
// Cross-signing is already set up for this user, is it trusted?
|
// Cross-signing is already set up for this user, is it trusted?
|
||||||
if (!mxCrossSigningInfo.isTrusted()) {
|
if (!mxCrossSigningInfo.isTrusted()) {
|
||||||
|
if (isSecureBackupRequired) {
|
||||||
|
// If 4S is forced, force verification
|
||||||
|
_viewEvents.post(HomeActivityViewEvents.ForceVerification(true))
|
||||||
|
} else {
|
||||||
// New session
|
// New session
|
||||||
_viewEvents.post(
|
_viewEvents.post(
|
||||||
HomeActivityViewEvents.OnNewSession(
|
HomeActivityViewEvents.OnNewSession(
|
||||||
|
@ -266,33 +342,44 @@ class HomeActivityViewModel @AssistedInject constructor(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Cross signing is not initialized
|
||||||
|
if (isSecureBackupRequired) {
|
||||||
|
// If 4S is forced, start the full interactive setup flow
|
||||||
|
_viewEvents.post(HomeActivityViewEvents.StartRecoverySetupFlow)
|
||||||
|
} else {
|
||||||
|
// Initialize cross-signing silently
|
||||||
|
val password = reAuthHelper.data
|
||||||
|
|
||||||
|
if (password == null) {
|
||||||
|
// Check this is not an SSO account
|
||||||
|
if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) {
|
||||||
|
// Ask password to the user: Upgrade security
|
||||||
|
_viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
|
||||||
|
}
|
||||||
|
// Else (SSO) just ignore for the moment
|
||||||
} else {
|
} else {
|
||||||
// Try to initialize cross signing in background if possible
|
// Try to initialize cross signing in background if possible
|
||||||
Timber.d("Initialize cross signing...")
|
Timber.d("Initialize cross signing...")
|
||||||
try {
|
try {
|
||||||
awaitCallback<Unit> {
|
session.cryptoService().crossSigningService().awaitCrossSigninInitialization { response, errCode ->
|
||||||
session.cryptoService().crossSigningService().initializeCrossSigning(
|
|
||||||
object : UserInteractiveAuthInterceptor {
|
|
||||||
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
|
|
||||||
// We missed server grace period or it's not setup, see if we remember locally password
|
// We missed server grace period or it's not setup, see if we remember locally password
|
||||||
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD &&
|
if (response.nextUncompletedStage() == LoginFlowTypes.PASSWORD &&
|
||||||
errCode == null &&
|
errCode == null &&
|
||||||
reAuthHelper.data != null) {
|
reAuthHelper.data != null) {
|
||||||
promise.resume(
|
resume(
|
||||||
UserPasswordAuth(
|
UserPasswordAuth(
|
||||||
session = flowResponse.session,
|
session = response.session,
|
||||||
user = session.myUserId,
|
user = session.myUserId,
|
||||||
password = reAuthHelper.data
|
password = reAuthHelper.data
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
promise.resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
callback = it
|
|
||||||
)
|
|
||||||
Timber.d("Initialize cross signing SUCCESS")
|
Timber.d("Initialize cross signing SUCCESS")
|
||||||
|
} else {
|
||||||
|
resumeWithException(Exception("Cannot silently initialize cross signing, UIA missing"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e(failure, "Failed to initialize cross signing")
|
Timber.e(failure, "Failed to initialize cross signing")
|
||||||
|
@ -300,6 +387,8 @@ class HomeActivityViewModel @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handle(action: HomeActivityViewActions) {
|
override fun handle(action: HomeActivityViewActions) {
|
||||||
when (action) {
|
when (action) {
|
||||||
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue