From aa496e6efbbed89c619f71de33e3b3ecc085d8ad Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 15 Apr 2020 23:59:22 +0200 Subject: [PATCH 01/13] Add migration state to bootstrap --- .../crypto/keysbackup/KeysBackupService.kt | 2 + .../SharedSecretStorageService.kt | 2 + .../keysbackup/DefaultKeysBackupService.kt | 10 + .../DefaultSharedSecretStorageService.kt | 14 +- .../im/vector/riotx/core/di/FragmentModule.kt | 5 + .../riotx/core/platform/ViewModelTask.kt | 34 +++ .../recover/BackupToQuadSMigrationTask.kt | 157 +++++++++++++ .../crypto/recover/BootstrapActions.kt | 4 + .../crypto/recover/BootstrapBottomSheet.kt | 45 +++- .../recover/BootstrapCrossSigningTask.kt | 38 ++-- .../recover/BootstrapMigrateBackupFragment.kt | 213 ++++++++++++++++++ .../recover/BootstrapSharedViewModel.kt | 179 +++++++++++++-- .../features/crypto/recover/BootstrapStep.kt | 91 ++++++++ .../recover/BootstrapWaitingFragment.kt | 25 +- .../riotx/features/home/HomeActivity.kt | 63 ++++-- .../features/navigation/DefaultNavigator.kt | 7 + .../riotx/features/navigation/Navigator.kt | 2 + vector/src/main/res/drawable/ic_file.xml | 21 ++ .../fragment_bootstrap_migrate_backup.xml | 89 ++++++++ vector/src/main/res/values/strings_riotX.xml | 14 ++ 20 files changed, 926 insertions(+), 89 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt create mode 100644 vector/src/main/res/drawable/ic_file.xml create mode 100644 vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt index e9ed36ba23..9ad39d45fd 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/keysbackup/KeysBackupService.kt @@ -217,4 +217,6 @@ interface KeysBackupService { // For gossiping fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? + + fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt index d32e459dd6..5fd0975f1a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/securestorage/SharedSecretStorageService.kt @@ -35,12 +35,14 @@ interface SharedSecretStorageService { * Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...) * * @param keyId the ID of the key + * @param key keep null if you want to generate a random key * @param keyName a human readable name * @param keySigner Used to add a signature to the key (client should check key signature before storing secret) * * @param callback Get key creation info */ fun generateKey(keyId: String, + key: SsssKeySpec?, keyName: String, keySigner: KeySigner?, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 75e37d27f6..946bd4ace1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor( return true } + override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) { + val keysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion).let { + callback.onSuccess(it) + } + } + } + /** * Enable backing up of keys. * This method will update the state and will start sending keys in nominal case diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt index 62bc4774c6..649a5a118f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/secrets/DefaultSharedSecretStorageService.kt @@ -65,14 +65,16 @@ internal class DefaultSharedSecretStorageService @Inject constructor( ) : SharedSecretStorageService { override fun generateKey(keyId: String, + key: SsssKeySpec?, keyName: String, keySigner: KeySigner?, callback: MatrixCallback) { cryptoCoroutineScope.launch(coroutineDispatchers.main) { - val key = try { - ByteArray(32).also { - SecureRandom().nextBytes(it) - } + val bytes = try { + (key as? RawBytesKeySpec)?.privateKey + ?: ByteArray(32).also { + SecureRandom().nextBytes(it) + } } catch (failure: Throwable) { callback.onFailure(failure) return@launch @@ -102,8 +104,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor( callback.onSuccess(SsssKeyCreationInfo( keyId = keyId, content = storageKeyContent, - recoveryKey = computeRecoveryKey(key), - keySpec = RawBytesKeySpec(key) + recoveryKey = computeRecoveryKey(bytes), + keySpec = RawBytesKeySpec(bytes) )) } } diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c68972cdd4..c2f2959bd7 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -30,6 +30,7 @@ import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment +import im.vector.riotx.features.crypto.recover.BootstrapMigrateBackupFragment import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment @@ -444,4 +445,8 @@ interface FragmentModule { @IntoMap @FragmentKey(BootstrapAccountPasswordFragment::class) fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment + @Binds + @IntoMap + @FragmentKey(BootstrapMigrateBackupFragment::class) + fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment } diff --git a/vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt b/vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt new file mode 100644 index 0000000000..abe5cc9095 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/core/platform/ViewModelTask.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.core.platform + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +interface ViewModelTask { + operator fun invoke( + scope: CoroutineScope, + params: Params, + onResult: (Result) -> Unit = {} + ) { + val backgroundJob = scope.async { execute(params) } + scope.launch { onResult(backgroundJob.await()) } + } + + suspend fun execute(params: Params): Result +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt new file mode 100644 index 0000000000..33d726d9b5 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.crypto.recover + +import im.vector.matrix.android.api.NoOpMatrixCallback +import im.vector.matrix.android.api.listeners.ProgressListener +import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME +import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner +import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec +import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService +import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding +import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey +import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey +import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey +import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.core.platform.ViewModelTask +import im.vector.riotx.core.platform.WaitingViewData +import timber.log.Timber +import java.util.UUID +import javax.inject.Inject + +class BackupToQuadSMigrationTask @Inject constructor( + val session: Session +) : ViewModelTask { + + sealed class Result { + object Success : Result() + abstract class Failure(val error: String?) : Result() + object InvalidRecoverySecret : Failure(null) + object NoKeyBackupVersion : Failure(null) + object IllegalParams : Failure(null) + class ErrorFailure(throwable: Throwable) : Failure(throwable.localizedMessage) + } + + data class Params( + val passphrase: String?, + val recoveryKey: String?, + val progressListener: BootstrapProgressListener? = null + ) + + override suspend fun execute(params: Params): Result { + try { + // We need to use the current secret for keybackup and use it as the new master key for SSSS + // Then we need to put back the backup key in sss + val keysBackupService = session.cryptoService().keysBackupService() + val quadS = session.sharedSecretStorageService + + val version = keysBackupService.keysBackupVersion ?: return Result.NoKeyBackupVersion + + params.progressListener?.onProgress(WaitingViewData("Checking backup Key")) + val curveKey = + (if (params.recoveryKey != null) { + extractCurveKeyFromRecoveryKey(params.recoveryKey) + } else if (!params.passphrase.isNullOrEmpty() && version.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null) { + version.getAuthDataAsMegolmBackupAuthData()?.let { authData -> + deriveKey(params.passphrase, authData.privateKeySalt!!, authData.privateKeyIterations!!, object: ProgressListener { + override fun onProgress(progress: Int, total: Int) { + params.progressListener?.onProgress(WaitingViewData("Checking backup Key $progress/$total")) + } + }) + } + } else null) + ?: return Result.IllegalParams + + params.progressListener?.onProgress(WaitingViewData("Getting curvekey")) + val recoveryKey = computeRecoveryKey(curveKey) + + val isValid = awaitCallback { + keysBackupService.isValidRecoveryKeyForCurrentVersion(recoveryKey, it) + } + + if (!isValid) return Result.InvalidRecoverySecret + + val info : SsssKeyCreationInfo = + when { + params.passphrase?.isNotEmpty() == true -> { + params.progressListener?.onProgress(WaitingViewData("Generating SSSS key from passphrase")) + awaitCallback { + quadS.generateKeyWithPassphrase( + UUID.randomUUID().toString(), + "ssss_key", + params.passphrase, + EmptyKeySigner(), + object: ProgressListener { + override fun onProgress(progress: Int, total: Int) { + params.progressListener?.onProgress(WaitingViewData("Generating SSSS key from passphrase $progress/$total")) + } + }, + it + ) + } + } + params.recoveryKey != null -> { + params.progressListener?.onProgress(WaitingViewData("Generating SSSS key from recovery key")) + awaitCallback { + quadS.generateKey( + UUID.randomUUID().toString(), + extractCurveKeyFromRecoveryKey(params.recoveryKey)?.let { RawBytesKeySpec(it) }, + "ssss_key", + EmptyKeySigner(), + it + ) + } + } + else -> { + return Result.IllegalParams + } + } + + // Ok, so now we have migrated the old keybackup secret as the quadS key + // Now we need to store the keybackup key in SSSS in a compatible way + params.progressListener?.onProgress(WaitingViewData("Storing keybackup secret in SSSS")) + awaitCallback { + quadS.storeSecret( + KEYBACKUP_SECRET_SSSS_NAME, + curveKey.toBase64NoPadding(), + listOf(SharedSecretStorageService.KeyRef(info.keyId, info.keySpec)), + it + ) + } + + // save for gossiping + keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version) + + // while we are there let's restore, but do not block + session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey( + version, + recoveryKey, + null, + null, + null, + NoOpMatrixCallback() + ) + + return Result.Success + } catch (failure: Throwable) { + Timber.e(failure, "## BackupToQuadSMigrationTask - Failed to migrate backup") + return Result.ErrorFailure(failure) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt index 7c0f2c1c46..2d9440a77d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapActions.kt @@ -40,4 +40,8 @@ sealed class BootstrapActions : VectorViewModelAction { object SaveReqQueryStarted : BootstrapActions() data class SaveKeyToUri(val os: OutputStream) : BootstrapActions() object SaveReqFailed : BootstrapActions() + + object HandleForgotBackupPassphrase : BootstrapActions() + data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions() + data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt index 6305f161e3..60490fc812 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapBottomSheet.kt @@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover import android.app.Dialog import android.os.Bundle +import android.os.Parcelable import android.view.KeyEvent import android.view.LayoutInflater import android.view.View @@ -26,18 +27,26 @@ import android.view.WindowManager import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.extensions.commitTransaction +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment +import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* import javax.inject.Inject import kotlin.reflect.KClass class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { + @Parcelize + data class Args( + val isNewAccount: Boolean + ) : Parcelable + override val showExpanded = true @Inject @@ -113,40 +122,62 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { override fun invalidate() = withState(viewModel) { state -> when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.CheckingMigration -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) + bootstrapTitleText.text = getString(R.string.upgrade_security) + showFragment(BootstrapWaitingFragment::class, Bundle()) + } + is BootstrapStep.SetupPassphrase -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase)) showFragment(BootstrapEnterPassphraseFragment::class, Bundle()) } - is BootstrapStep.ConfirmPassphrase -> { + is BootstrapStep.ConfirmPassphrase -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase)) showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) bootstrapTitleText.text = getString(R.string.account_password) showFragment(BootstrapAccountPasswordFragment::class, Bundle()) } - is BootstrapStep.Initializing -> { + is BootstrapStep.Initializing -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) bootstrapTitleText.text = getString(R.string.bootstrap_loading_title) showFragment(BootstrapWaitingFragment::class, Bundle()) } - is BootstrapStep.SaveRecoveryKey -> { + is BootstrapStep.SaveRecoveryKey -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy) showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle()) } - is BootstrapStep.DoneSuccess -> { + is BootstrapStep.DoneSuccess -> { bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key)) bootstrapTitleText.text = getString(R.string.bootstrap_finish_title) showFragment(BootstrapConclusionFragment::class, Bundle()) } - } + is BootstrapStep.GetBackupSecretForMigration -> { + bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.key_small)) + bootstrapTitleText.text = getString(R.string.upgrade_security) + showFragment(BootstrapMigrateBackupFragment::class, Bundle()) + } + }.exhaustive super.invalidate() } + companion object { + + const val EXTRA_ARGS = "EXTRA_ARGS" + + fun show(fragmentManager: FragmentManager, isAccountCreation: Boolean) { + BootstrapBottomSheet().apply { + isCancelable = false + arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(isAccountCreation)) } + }.show(fragmentManager, "BootstrapBottomSheet") + } + } + private fun showFragment(fragmentClass: KClass, bundle: Bundle) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { childFragmentManager.commitTransaction { diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt index a19604d78e..c2e0afbe3b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo @@ -33,11 +34,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.util.awaitCallback import im.vector.riotx.R +import im.vector.riotx.core.platform.ViewModelTask import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.resources.StringProvider -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch import timber.log.Timber import java.util.UUID import javax.inject.Inject @@ -67,24 +66,16 @@ interface BootstrapProgressListener { data class Params( val userPasswordAuth: UserPasswordAuth? = null, val progressListener: BootstrapProgressListener? = null, - val passphrase: String? + val passphrase: String?, + val keySpec: SsssKeySpec? = null ) class BootstrapCrossSigningTask @Inject constructor( private val session: Session, private val stringProvider: StringProvider -) { +) : ViewModelTask { - operator fun invoke( - scope: CoroutineScope, - params: Params, - onResult: (BootstrapResult) -> Unit = {} - ) { - val backgroundJob = scope.async { execute(params) } - scope.launch { onResult(backgroundJob.await()) } - } - - suspend fun execute(params: Params): BootstrapResult { + override suspend fun execute(params: Params): BootstrapResult { params.progressListener?.onProgress( WaitingViewData( stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), @@ -124,6 +115,7 @@ class BootstrapCrossSigningTask @Inject constructor( } ?: kotlin.run { ssssService.generateKey( UUID.randomUUID().toString(), + params.keySpec, "ssss_key", EmptyKeySigner(), it @@ -205,14 +197,16 @@ class BootstrapCrossSigningTask @Inject constructor( ) ) try { - val creationInfo = awaitCallback { - session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + if (session.cryptoService().keysBackupService().keysBackupVersion == null) { + val creationInfo = awaitCallback { + session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) + } + val version = awaitCallback { + session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) + } + // Save it for gossiping + session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) } - val version = awaitCallback { - session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it) - } - // Save it for gossiping - session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) } catch (failure: Throwable) { Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt new file mode 100644 index 0000000000..844a16977c --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapMigrateBackupFragment.kt @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.crypto.recover + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.text.InputType.TYPE_CLASS_TEXT +import android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE +import android.text.InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD +import android.view.View +import android.view.inputmethod.EditorInfo +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.view.clicks +import com.jakewharton.rxbinding3.widget.editorActionEvents +import com.jakewharton.rxbinding3.widget.textChanges +import im.vector.matrix.android.api.extensions.tryThis +import im.vector.matrix.android.internal.crypto.keysbackup.util.isValidRecoveryKey +import im.vector.riotx.R +import im.vector.riotx.core.extensions.hideKeyboard +import im.vector.riotx.core.extensions.showPassword +import im.vector.riotx.core.platform.VectorBaseFragment +import im.vector.riotx.core.resources.ColorProvider +import im.vector.riotx.core.utils.colorizeMatchingText +import im.vector.riotx.core.utils.startImportTextFromFileIntent +import io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText +import kotlinx.android.synthetic.main.fragment_bootstrap_migrate_backup.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class BootstrapMigrateBackupFragment @Inject constructor( + private val colorProvider: ColorProvider +) : VectorBaseFragment() { + + override fun getLayoutResId() = R.layout.fragment_bootstrap_migrate_backup + + val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + withState(sharedViewModel) { + // set initial value (usefull when coming back) + bootstrapMigrateEditText.setText(it.passphrase ?: "") + } + bootstrapMigrateEditText.editorActionEvents() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.actionId == EditorInfo.IME_ACTION_DONE) { + submit() + } + } + .disposeOnDestroyView() + + bootstrapMigrateEditText.textChanges() + .skipInitialValue() + .subscribe { + bootstrapRecoveryKeyEnterTil.error = null + // sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: "")) + } + .disposeOnDestroyView() + + // sharedViewModel.observeViewEvents {} + bootstrapMigrateContinueButton.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + submit() + } + .disposeOnDestroyView() + + bootstrapMigrateShowPassword.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) + } + .disposeOnDestroyView() + + bootstrapMigrateForgotPassphrase.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + sharedViewModel.handle(BootstrapActions.HandleForgotBackupPassphrase) + } + .disposeOnDestroyView() + + bootstrapMigrateUseFile.clicks() + .debounce(300, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + startImportTextFromFileIntent(this, IMPORT_FILE_REQ) + } + .disposeOnDestroyView() + } + + private fun submit() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.GetBackupSecretForMigration) { + return@withState + } + val isEnteringKey = + when (state.step) { + is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey + else -> true + } + + val secret = bootstrapMigrateEditText.text?.toString() + if (secret.isNullOrBlank()) { + bootstrapRecoveryKeyEnterTil.error = getString(R.string.passphrase_empty_error_message) + } else if (isEnteringKey && !isValidRecoveryKey(secret)) { + bootstrapRecoveryKeyEnterTil.error = getString(R.string.bootstrap_invalid_recovery_key) + } else { + view?.hideKeyboard() + if (isEnteringKey) { + sharedViewModel.handle(BootstrapActions.DoMigrateWithRecoveryKey(secret)) + } else { + sharedViewModel.handle(BootstrapActions.DoMigrateWithPassphrase(secret)) + } + } + } + + override fun invalidate() = withState(sharedViewModel) { state -> + if (state.step !is BootstrapStep.GetBackupSecretForMigration) { + return@withState + } + + val isEnteringKey = + when (state.step) { + is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey + else -> true + } + + if (isEnteringKey) { + bootstrapMigrateShowPassword.isVisible = false + bootstrapMigrateEditText.inputType = TYPE_CLASS_TEXT or TYPE_TEXT_VARIATION_VISIBLE_PASSWORD or TYPE_TEXT_FLAG_MULTI_LINE + + val recKey = getString(R.string.recovery_key) + bootstrapDescriptionText.text = getString(R.string.enter_account_password, recKey) + .toSpannable() + .colorizeMatchingText(recKey, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + bootstrapMigrateEditText.hint = recKey + + bootstrapMigrateEditText.hint = getString(R.string.keys_backup_restore_key_enter_hint) + bootstrapMigrateForgotPassphrase.isVisible = false + bootstrapMigrateUseFile.isVisible = true + } else { + bootstrapMigrateShowPassword.isVisible = true + + if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { + val isPasswordVisible = state.step.isPasswordVisible + bootstrapMigrateEditText.showPassword(isPasswordVisible, updateCursor = false) + bootstrapMigrateShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black) + } + + val recPassPhrase = getString(R.string.backup_recovery_passphrase) + bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase) + .toSpannable() + .colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + bootstrapMigrateEditText.hint = getString(R.string.passphrase_enter_passphrase) + + bootstrapMigrateForgotPassphrase.isVisible = true + + val recKeye = getString(R.string.keys_backup_restore_use_recovery_key) + bootstrapMigrateForgotPassphrase.text = getString(R.string.keys_backup_restore_with_passphrase_helper_with_link, recKeye) + .toSpannable() + .colorizeMatchingText(recKeye, colorProvider.getColorFromAttribute(android.R.attr.textColorLink)) + + bootstrapMigrateUseFile.isVisible = false + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) { + data?.data?.let { dataURI -> + tryThis { + activity?.contentResolver?.openInputStream(dataURI) + ?.bufferedReader() + ?.use { it.readText() } + ?.let { + bootstrapMigrateEditText.setText(it) + } + } + } + return + } + super.onActivityResult(requestCode, resultCode, data) + } + + companion object { + private const val IMPORT_FILE_REQ = 0 + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index 998899374c..58aee5ecb0 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -31,20 +31,26 @@ import com.nulabinc.zxcvbn.Zxcvbn import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo +import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult +import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.util.awaitCallback import im.vector.riotx.R import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.features.login.ReAuthHelper +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.io.OutputStream data class BootstrapViewState( val step: BootstrapStep = BootstrapStep.SetupPassphrase(false), val passphrase: String? = null, + val migrationRecoveryKey: String? = null, val passphraseRepeat: String? = null, val crossSigningInitialization: Async = Uninitialized, val passphraseStrength: Async = Uninitialized, @@ -55,20 +61,13 @@ data class BootstrapViewState( val recoverySaveFileProcess: Async = Uninitialized ) : MvRxState -sealed class BootstrapStep { - data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() - data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() - data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep() - object Initializing : BootstrapStep() - data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() - object DoneSuccess : BootstrapStep() -} - class BootstrapSharedViewModel @AssistedInject constructor( @Assisted initialState: BootstrapViewState, + @Assisted val args: BootstrapBottomSheet.Args, private val stringProvider: StringProvider, private val session: Session, private val bootstrapTask: BootstrapCrossSigningTask, + private val migrationTask: BackupToQuadSMigrationTask, private val reAuthHelper: ReAuthHelper ) : VectorViewModel(initialState) { @@ -76,7 +75,53 @@ class BootstrapSharedViewModel @AssistedInject constructor( @AssistedInject.Factory interface Factory { - fun create(initialState: BootstrapViewState): BootstrapSharedViewModel + fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel + } + + init { + // need to check if user have an existing keybackup + if (args.isNewAccount) { + setState { + copy(step = BootstrapStep.SetupPassphrase(false)) + } + } else { + setState { + copy(step = BootstrapStep.CheckingMigration) + } + + // We need to check if there is an existing backup + viewModelScope.launch(Dispatchers.IO) { + val version = awaitCallback { + session.cryptoService().keysBackupService().getCurrentVersion(it) + } + if (version == null) { + // we just resume plain bootstrap + setState { + copy(step = BootstrapStep.SetupPassphrase(false)) + } + } else { + // we need to get existing backup passphrase/key and convert to SSSS + val keyVersion = awaitCallback { + session.cryptoService().keysBackupService().getVersion(version.version ?: "", it) + } + if (keyVersion == null) { + // strange case... just finish? + _viewEvents.post(BootstrapViewEvents.Dismiss) + } else { + val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null + if (isBackupCreatedFromPassphrase) { + setState { + copy(step = BootstrapStep.GetBackupSecretPassForMigration(isBackupCreatedFromPassphrase, false)) + } + } else { + setState { + copy(step = BootstrapStep.GetBackupSecretKeyForMigration) + } + } + } + } + } + } } override fun handle(action: BootstrapActions) = withState { state -> @@ -84,23 +129,27 @@ class BootstrapSharedViewModel @AssistedInject constructor( is BootstrapActions.GoBack -> queryBack() BootstrapActions.TogglePasswordVisibility -> { when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.SetupPassphrase -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - is BootstrapStep.ConfirmPassphrase -> { + is BootstrapStep.ConfirmPassphrase -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { setState { copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) } } - else -> { + is BootstrapStep.GetBackupSecretPassForMigration -> { + setState { + copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) + } + } + else -> { } } } @@ -197,12 +246,25 @@ class BootstrapSharedViewModel @AssistedInject constructor( copy(step = BootstrapStep.AccountPassword(false)) } } + BootstrapActions.HandleForgotBackupPassphrase -> { + if (state.step is BootstrapStep.GetBackupSecretPassForMigration) { + setState { + copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true)) + } + } else return@withState + } is BootstrapActions.ReAuth -> { startInitializeFlow( state.currentReAuth?.copy(password = action.pass) ?: UserPasswordAuth(user = session.myUserId, password = action.pass) ) } + is BootstrapActions.DoMigrateWithPassphrase -> { + startMigrationFlow(state.step, action.passphrase, null) + } + is BootstrapActions.DoMigrateWithRecoveryKey -> { + startMigrationFlow(state.step, null, action.recoveryKey) + } }.exhaustive } @@ -210,7 +272,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( // Business Logic // ======================================= private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state -> - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { kotlin.runCatching { os.use { os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray()) @@ -231,6 +293,57 @@ class BootstrapSharedViewModel @AssistedInject constructor( } } + private fun startMigrationFlow(prevState: BootstrapStep, passphrase: String?, recoveryKey: String?) { + setState { + copy(step = BootstrapStep.Initializing) + } + viewModelScope.launch(Dispatchers.IO) { + val progressListener = object : BootstrapProgressListener { + override fun onProgress(data: WaitingViewData) { + setState { + copy( + initializationWaitingViewData = data + ) + } + } + } + migrationTask.invoke(this, BackupToQuadSMigrationTask.Params(passphrase, recoveryKey, progressListener)) { + if (it is BackupToQuadSMigrationTask.Result.Success) { + setState { + copy( + passphrase = passphrase, + passphraseRepeat = passphrase, + migrationRecoveryKey = recoveryKey + ) + } + val auth = reAuthHelper.rememberedAuth() + if (auth == null) { + setState { + copy( + step = BootstrapStep.AccountPassword(false) + ) + } + } else { + startInitializeFlow(auth) + } + } else { + _viewEvents.post( + BootstrapViewEvents.ModalError( + (it as? BackupToQuadSMigrationTask.Result.Failure)?.error + ?: stringProvider.getString(R.string.matrix_error + ) + ) + ) + setState { + copy( + step = prevState + ) + } + } + } + } + } + private fun startInitializeFlow(auth: UserPasswordAuth?) { setState { copy(step = BootstrapStep.Initializing) @@ -247,11 +360,12 @@ class BootstrapSharedViewModel @AssistedInject constructor( } withState { state -> - viewModelScope.launch { + viewModelScope.launch(Dispatchers.IO) { bootstrapTask.invoke(this, Params( userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(), progressListener = progressListener, - passphrase = state.passphrase + passphrase = state.passphrase, + keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } } )) { when (it) { is BootstrapResult.Success -> { @@ -309,11 +423,26 @@ class BootstrapSharedViewModel @AssistedInject constructor( private fun queryBack() = withState { state -> when (state.step) { - is BootstrapStep.SetupPassphrase -> { + is BootstrapStep.GetBackupSecretPassForMigration -> { + if (state.step.useKey) { + // go back to passphrase + setState { + copy( + step = BootstrapStep.GetBackupSecretPassForMigration( + isPasswordVisible = state.step.isPasswordVisible, + useKey = false + ) + ) + } + } else { + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + } + } + is BootstrapStep.SetupPassphrase -> { // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) } - is BootstrapStep.ConfirmPassphrase -> { + is BootstrapStep.ConfirmPassphrase -> { setState { copy( step = BootstrapStep.SetupPassphrase( @@ -322,15 +451,15 @@ class BootstrapSharedViewModel @AssistedInject constructor( ) } } - is BootstrapStep.AccountPassword -> { + is BootstrapStep.AccountPassword -> { _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } - BootstrapStep.Initializing -> { + BootstrapStep.Initializing -> { // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) } is BootstrapStep.SaveRecoveryKey, - BootstrapStep.DoneSuccess -> { + BootstrapStep.DoneSuccess -> { // nop } } @@ -344,7 +473,9 @@ class BootstrapSharedViewModel @AssistedInject constructor( override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.bootstrapViewModelFactory.create(state) + val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS) + ?: BootstrapBottomSheet.Args(true) + return fragment.bootstrapViewModelFactory.create(state, args) } } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt new file mode 100644 index 0000000000..42ed65f326 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.riotx.features.crypto.recover + +/** + * ┌─────────────────────────┐ + * │ User has signing keys? │──────────── Account + * └─────────────────────────┘ Creation ? + * │ │ + * No │ + * │ │ + * │ │ + * ▼ │ + * ┌───────────────────────────────────┐ │ + * │ BootstrapStep.CheckingMigration │ │ + * └───────────────────────────────────┘ │ + * │ │ + * │ │ + * Existing ├─────────No ───────┐ │ + * ┌────Keybackup───────┘ KeyBackup │ │ + * │ │ │ + * │ ▼ ▼ + * ▼ ┌────────────────────────────────────┐ + * ┌─────────────────────────────────────────┐ │ BootstrapStep.SetupPassphrase │◀─┐ + * │BootstrapStep.GetBackupSecretForMigration│ └────────────────────────────────────┘ │ + * └─────────────────────────────────────────┘ │ │ + * │ │ ┌Back + * │ ▼ │ + * │ ┌────────────────────────────────────┤ + * │ │ BootstrapStep.ConfirmPassphrase │──┐ + * │ └────────────────────────────────────┘ │ + * │ │ │ + * │ is password needed? │ + * │ │ │ + * │ ▼ │ + * │ ┌────────────────────────────────────┐ │ + * │ │ BootstrapStep.AccountPassword │ │ + * │ └────────────────────────────────────┘ │ + * │ │ │ + * │ │ │ + * │ ┌──────────────────┘ password not needed (in + * │ │ memory) + * │ │ │ + * │ ▼ │ + * │ ┌────────────────────────────────────┐ │ + * └────────▶│ BootstrapStep.Initializing │◀────────────────────┘ + * └────────────────────────────────────┘ + * │ + * │ + * │ + * ▼ + * ┌────────────────────────────────────┐ + * │ BootstrapStep.SaveRecoveryKey │ + * └────────────────────────────────────┘ + * │ + * │ + * │ + * ▼ + * ┌────────────────────────────────────────┐ + * │ BootstrapStep.DoneSuccess │ + * └────────────────────────────────────────┘ + */ + +sealed class BootstrapStep { + data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() + data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep() + object CheckingMigration : BootstrapStep() + + abstract class GetBackupSecretForMigration : BootstrapStep() + data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration() + object GetBackupSecretKeyForMigration : GetBackupSecretForMigration() + + object Initializing : BootstrapStep() + data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep() + object DoneSuccess : BootstrapStep() +} diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt index ff79fa6a4b..1c6c9e6d9b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapWaitingFragment.kt @@ -16,8 +16,7 @@ package im.vector.riotx.features.crypto.recover -import android.os.Bundle -import android.view.View +import androidx.core.view.isVisible import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.withState import im.vector.riotx.R @@ -31,12 +30,22 @@ class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() { val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - } - override fun invalidate() = withState(sharedViewModel) { state -> - if (state.step !is BootstrapStep.Initializing) return@withState - bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message + when (state.step) { + is BootstrapStep.Initializing -> { + bootstrapLoadingStatusText.isVisible = true + bootstrapDescriptionText.isVisible = true + bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message + } +// is BootstrapStep.CheckingMigration -> { +// bootstrapLoadingStatusText.isVisible = false +// bootstrapDescriptionText.isVisible = false +// } + else -> { + // just show the spinner + bootstrapLoadingStatusText.isVisible = false + bootstrapDescriptionText.isVisible = false + } + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index e286c82532..d090e5e639 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -91,13 +91,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { .observe() .subscribe { sharedAction -> when (sharedAction) { - is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) - is HomeActivitySharedAction.OpenGroup -> { + is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START) + is HomeActivitySharedAction.OpenGroup -> { drawerLayout.closeDrawer(GravityCompat.START) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) } is HomeActivitySharedAction.PromptForSecurityBootstrap -> { - BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet") + BootstrapBottomSheet.show(supportFragmentManager, true) } } } @@ -163,29 +163,48 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { .getMyCrossSigningKeys() val crossSigningEnabledOnAccount = myCrossSigningKeys != null - if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) { + if (!crossSigningEnabledOnAccount) { // We need to ask - sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true - popupAlertManager.postVectorAlert( - VerificationVectorAlert( - uid = "completeSecurity", - title = getString(R.string.complete_security), - description = getString(R.string.crosssigning_verify_this_session), - iconId = R.drawable.ic_shield_warning - ).apply { - matrixItem = session.getUser(session.myUserId)?.toMatrixItem() - colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) - contentAction = Runnable { - (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { - it.navigator.waitSessionVerification(it) - } - } - dismissedAction = Runnable {} - } - ) + promptSecurityEvent( + session, + R.string.upgrade_security, + R.string.security_prompt_text + ) { + it.navigator.upgradeSessionSecurity(it) + } + } else if (myCrossSigningKeys?.isTrusted() == false) { + // We need to ask + promptSecurityEvent( + session, + R.string.complete_security, + R.string.crosssigning_verify_this_session + ) { + it.navigator.waitSessionVerification(it) + } } } + private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) { + sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true + popupAlertManager.postVectorAlert( + VerificationVectorAlert( + uid = "upgradeSecurity", + title = getString(titleRes), + description = getString(descRes), + iconId = R.drawable.ic_shield_warning + ).apply { + matrixItem = session.getUser(session.myUserId)?.toMatrixItem() + colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) + contentAction = Runnable { + (weakCurrentActivity?.get() as? VectorBaseActivity)?.let { + action(it) + } + } + dismissedAction = Runnable {} + } + ) + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) { diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt index 2e91090ec4..0f19a1292a 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/DefaultNavigator.kt @@ -34,6 +34,7 @@ import im.vector.riotx.core.utils.toast import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity +import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.debug.DebugMenuActivity @@ -107,6 +108,12 @@ class DefaultNavigator @Inject constructor( } } + override fun upgradeSessionSecurity(context: Context) { + if (context is VectorBaseActivity) { + BootstrapBottomSheet.show(context.supportFragmentManager, false) + } + } + override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) { if (context is VectorBaseActivity) { context.notImplemented("Open not joined room") diff --git a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt index 65ef08dd05..bf99643912 100644 --- a/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/riotx/features/navigation/Navigator.kt @@ -34,6 +34,8 @@ interface Navigator { fun waitSessionVerification(context: Context) + fun upgradeSessionSecurity(context: Context) + fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false) diff --git a/vector/src/main/res/drawable/ic_file.xml b/vector/src/main/res/drawable/ic_file.xml new file mode 100644 index 0000000000..2b74a19a94 --- /dev/null +++ b/vector/src/main/res/drawable/ic_file.xml @@ -0,0 +1,21 @@ + + + + diff --git a/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml b/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml new file mode 100644 index 0000000000..68417044f9 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_migrate_backup.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index b7b65ad311..4ebf4926bc 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -7,6 +7,20 @@ Message… + + Encryption upgrade available + Verify yourself & others to keep your chats safe + + + Enter your %s to continue + Use File + + + + Enter %s + Recovery Passphrase + It‘s not a valid recovery key + From 8ae2f06044a128160ff55e0dc24660d5e32c5764 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 00:34:18 +0200 Subject: [PATCH 02/13] Add change log --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 42dfda9fa8..15c7493ade 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -21,6 +21,7 @@ Improvements 🙌: - Cross-Sign | QR code scan confirmation screens design update (#1187) - Emoji Verification | It's not the same butterfly! (#1220) - Cross-Signing | Composer decoration: shields (#1077) + - Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197) Bugfix 🐛: - Fix summary notification staying after "mark as read" From 0edc562120aa3f2ea72bed4a079ac2e6a1fadc9e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 00:39:21 +0200 Subject: [PATCH 03/13] Fix / test compilation --- .../matrix/android/internal/crypto/ssss/QuadSTests.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt index 1a0723c725..cbd175f53f 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/ssss/QuadSTests.kt @@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" mTestHelper.doSync { - quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) } // Assert Account data is updated @@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest { val TEST_KEY_ID = "my.test.Key" mTestHelper.doSync { - quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) + quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it) } // Test that we don't need to wait for an account data sync to access directly the keyid from DB @@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest { val quadS = session.sharedSecretStorageService val creationInfo = mTestHelper.doSync { - quadS.generateKey(keyId, keyId, emptyKeySigner, it) + quadS.generateKey(keyId, null, keyId, emptyKeySigner, it) } assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") From c1d39cefd5dc303d9ef8717ac066ec6a7cca9a7d Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 10:11:10 +0200 Subject: [PATCH 04/13] Fix / avoid upgrade secu popup on account creation --- .../main/java/im/vector/riotx/features/home/HomeActivity.kt | 3 ++- .../im/vector/riotx/features/home/HomeSharedActionViewModel.kt | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt index d090e5e639..2449c635e4 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeActivity.kt @@ -109,6 +109,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { } if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) { sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap) + sharedActionViewModel.isAccountCreation = true intent.removeExtra(EXTRA_ACCOUNT_CREATION) } @@ -163,7 +164,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable { .getMyCrossSigningKeys() val crossSigningEnabledOnAccount = myCrossSigningKeys != null - if (!crossSigningEnabledOnAccount) { + if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) { // We need to ask promptSecurityEvent( session, diff --git a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt index ecbe460b90..4af9262d05 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/HomeSharedActionViewModel.kt @@ -21,4 +21,5 @@ import javax.inject.Inject class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel() { var hasDisplayedCompleteSecurityPrompt : Boolean = false + var isAccountCreation : Boolean = false } From a2f32307f07d3f9a4a3f796a7538757e025729dc Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 10:14:18 +0200 Subject: [PATCH 05/13] Support back from migrate recovery key --- .../riotx/features/crypto/recover/BootstrapSharedViewModel.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index 58aee5ecb0..4755db728a 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -438,6 +438,10 @@ class BootstrapSharedViewModel @AssistedInject constructor( _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) } } + is BootstrapStep.GetBackupSecretKeyForMigration -> { + // do we let you cancel from here? + _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) + } is BootstrapStep.SetupPassphrase -> { // do we let you cancel from here? _viewEvents.post(BootstrapViewEvents.SkipBootstrap()) From b8e9cc70f249cedf9df22ff8fb95130a0630f4f0 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 10:21:21 +0200 Subject: [PATCH 06/13] fix / line too long --- .../im/vector/riotx/features/crypto/recover/BootstrapStep.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt index 42ed65f326..2c649ae99c 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapStep.kt @@ -73,6 +73,7 @@ package im.vector.riotx.features.crypto.recover * ┌────────────────────────────────────────┐ * │ BootstrapStep.DoneSuccess │ * └────────────────────────────────────────┘ + * */ sealed class BootstrapStep { From e79c8249138dd66efdf0eaef6558149bc1ad4a09 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 10:30:58 +0200 Subject: [PATCH 07/13] Fix / password visibility initial state bug --- .../riotx/features/crypto/recover/BootstrapSharedViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt index 4755db728a..55481b1f5b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BootstrapSharedViewModel.kt @@ -111,7 +111,7 @@ class BootstrapSharedViewModel @AssistedInject constructor( val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null if (isBackupCreatedFromPassphrase) { setState { - copy(step = BootstrapStep.GetBackupSecretPassForMigration(isBackupCreatedFromPassphrase, false)) + copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false)) } } else { setState { From a44cb876c907ed146a3d5f7a5c519081fc999b3a Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 16 Apr 2020 11:29:54 +0200 Subject: [PATCH 08/13] move strings to resources --- .../recover/BackupToQuadSMigrationTask.kt | 41 +++++++++++++------ vector/src/main/res/values/strings_riotX.xml | 7 ++++ 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt index 33d726d9b5..e29d0d636d 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/recover/BackupToQuadSMigrationTask.kt @@ -29,14 +29,17 @@ import im.vector.matrix.android.internal.crypto.keysbackup.deriveKey import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import im.vector.matrix.android.internal.util.awaitCallback +import im.vector.riotx.R import im.vector.riotx.core.platform.ViewModelTask import im.vector.riotx.core.platform.WaitingViewData +import im.vector.riotx.core.resources.StringProvider import timber.log.Timber import java.util.UUID import javax.inject.Inject class BackupToQuadSMigrationTask @Inject constructor( - val session: Session + val session: Session, + val stringProvider: StringProvider ) : ViewModelTask { sealed class Result { @@ -63,22 +66,25 @@ class BackupToQuadSMigrationTask @Inject constructor( val version = keysBackupService.keysBackupVersion ?: return Result.NoKeyBackupVersion - params.progressListener?.onProgress(WaitingViewData("Checking backup Key")) + reportProgress(params, R.string.bootstrap_progress_checking_backup) val curveKey = (if (params.recoveryKey != null) { extractCurveKeyFromRecoveryKey(params.recoveryKey) } else if (!params.passphrase.isNullOrEmpty() && version.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null) { version.getAuthDataAsMegolmBackupAuthData()?.let { authData -> - deriveKey(params.passphrase, authData.privateKeySalt!!, authData.privateKeyIterations!!, object: ProgressListener { + deriveKey(params.passphrase, authData.privateKeySalt!!, authData.privateKeyIterations!!, object : ProgressListener { override fun onProgress(progress: Int, total: Int) { - params.progressListener?.onProgress(WaitingViewData("Checking backup Key $progress/$total")) + params.progressListener?.onProgress(WaitingViewData( + stringProvider.getString(R.string.bootstrap_progress_checking_backup_with_info, + "$progress/$total") + )) } }) } } else null) ?: return Result.IllegalParams - params.progressListener?.onProgress(WaitingViewData("Getting curvekey")) + reportProgress(params, R.string.bootstrap_progress_compute_curve_key) val recoveryKey = computeRecoveryKey(curveKey) val isValid = awaitCallback { @@ -87,27 +93,32 @@ class BackupToQuadSMigrationTask @Inject constructor( if (!isValid) return Result.InvalidRecoverySecret - val info : SsssKeyCreationInfo = + val info: SsssKeyCreationInfo = when { params.passphrase?.isNotEmpty() == true -> { - params.progressListener?.onProgress(WaitingViewData("Generating SSSS key from passphrase")) + reportProgress(params, R.string.bootstrap_progress_generating_ssss) awaitCallback { quadS.generateKeyWithPassphrase( UUID.randomUUID().toString(), "ssss_key", params.passphrase, EmptyKeySigner(), - object: ProgressListener { + object : ProgressListener { override fun onProgress(progress: Int, total: Int) { - params.progressListener?.onProgress(WaitingViewData("Generating SSSS key from passphrase $progress/$total")) + params.progressListener?.onProgress( + WaitingViewData( + stringProvider.getString( + R.string.bootstrap_progress_generating_ssss_with_info, + "$progress/$total") + )) } }, it ) } } - params.recoveryKey != null -> { - params.progressListener?.onProgress(WaitingViewData("Generating SSSS key from recovery key")) + params.recoveryKey != null -> { + reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery) awaitCallback { quadS.generateKey( UUID.randomUUID().toString(), @@ -118,14 +129,14 @@ class BackupToQuadSMigrationTask @Inject constructor( ) } } - else -> { + else -> { return Result.IllegalParams } } // Ok, so now we have migrated the old keybackup secret as the quadS key // Now we need to store the keybackup key in SSSS in a compatible way - params.progressListener?.onProgress(WaitingViewData("Storing keybackup secret in SSSS")) + reportProgress(params, R.string.bootstrap_progress_storing_in_sss) awaitCallback { quadS.storeSecret( KEYBACKUP_SECRET_SSSS_NAME, @@ -154,4 +165,8 @@ class BackupToQuadSMigrationTask @Inject constructor( return Result.ErrorFailure(failure) } } + + private fun reportProgress(params: Params, stringRes: Int) { + params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(stringRes))) + } } diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 4ebf4926bc..c6b9717d7f 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -21,6 +21,13 @@ Recovery Passphrase It‘s not a valid recovery key + Checking backup Key + Checking backup Key (%s) + Getting curve key + Generating SSSS key from passphrase + Generating SSSS key from passphrase (%s) + Generating SSSS key from recovery key + Storing keybackup secret in SSSS From d934f92ebdd6ae6bb073257e6551d8a05366015a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Apr 2020 13:58:54 +0200 Subject: [PATCH 09/13] Fix bad apostrophe --- vector/src/main/res/values/strings.xml | 2 +- vector/src/main/res/values/strings_riotX.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 6b0591595a..17986addb5 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2243,7 +2243,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming This might take several seconds, please be patient. Setting up recovery. Your recovery key - You‘re done! + "You're done!" Keep it safe Finish diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index c6b9717d7f..807d94112d 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -19,7 +19,7 @@ Enter %s Recovery Passphrase - It‘s not a valid recovery key + "It's not a valid recovery key" Checking backup Key Checking backup Key (%s) From f93f50b5822dd5957a9cc2892fc10fdefab055eb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Apr 2020 14:00:24 +0200 Subject: [PATCH 10/13] Code readability --- .../internal/crypto/keysbackup/DefaultKeysBackupService.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt index 946bd4ace1..9245f77317 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/DefaultKeysBackupService.kt @@ -1101,10 +1101,10 @@ internal class DefaultKeysBackupService @Inject constructor( } override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) { - val keysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } + val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } cryptoCoroutineScope.launch(coroutineDispatchers.main) { - isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion).let { + isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let { callback.onSuccess(it) } } From 79e81dbdde400b4aece22046724249b243d19d56 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Apr 2020 14:01:15 +0200 Subject: [PATCH 11/13] ktlint --- .../vector/matrix/android/internal/session/account/AccountAPI.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt index 8acfb3abb8..23d8210e89 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/account/AccountAPI.kt @@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.account.model.ChangePasswordParams import im.vector.matrix.android.internal.network.NetworkConstants import retrofit2.Call import retrofit2.http.Body -import retrofit2.http.Headers import retrofit2.http.POST internal interface AccountAPI { From 15bd7d1c5b87c20314836d0001e791e60c37a17b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Apr 2020 14:35:23 +0200 Subject: [PATCH 12/13] Change the regex to detect long lines to allow schema with UTF-8 chars --- tools/check/forbidden_strings_in_code.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index a0bd725118..3c0337ac99 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -60,7 +60,7 @@ private short final short ### Line length is limited to 160 chars. Please split long lines -.{161} +[^─]{161} ### "DO NOT COMMIT" has been committed DO NOT COMMIT From 828e972c745e6af265e345300cf3b27505cac96e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Apr 2020 14:35:55 +0200 Subject: [PATCH 13/13] Split long lines --- .../session/room/model/message/MessageLocationContent.kt | 3 ++- .../keysbackup/model/rest/CreateKeysBackupVersionBody.kt | 3 ++- .../crypto/keysbackup/model/rest/KeysVersionResult.kt | 3 ++- .../keysbackup/model/rest/UpdateKeysBackupVersionBody.kt | 3 ++- .../java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt | 2 +- .../im/vector/riotx/core/ui/views/NotificationAreaView.kt | 2 +- .../crypto/keysbackup/setup/KeysBackupSetupActivity.kt | 5 ++++- .../room/detail/timeline/item/VerificationRequestItem.kt | 3 ++- .../vector/riotx/features/media/VideoMediaViewerActivity.kt | 6 +++++- .../settings/VectorSettingsSecurityPrivacyFragment.kt | 5 ++++- 10 files changed, 25 insertions(+), 10 deletions(-) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt index 02ba9eae6e..4bbd821819 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageLocationContent.kt @@ -29,7 +29,8 @@ data class MessageLocationContent( @Json(name = "msgtype") override val msgType: String, /** - * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'. + * Required. A description of the location e.g. 'Big Ben, London, UK', or some kind + * of content description for accessibility e.g. 'location attachment'. */ @Json(name = "body") override val body: String, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt index 3b267280e5..f0c0ada207 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/CreateKeysBackupVersionBody.kt @@ -29,7 +29,8 @@ data class CreateKeysBackupVersionBody( override val algorithm: String? = null, /** - * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ @Json(name = "auth_data") override val authData: JsonDict? = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt index 0addd1491e..ba5cb2a379 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/KeysVersionResult.kt @@ -29,7 +29,8 @@ data class KeysVersionResult( override val algorithm: String? = null, /** - * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ @Json(name = "auth_data") override val authData: JsonDict? = null, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt index 9d88af20ef..bb12911e42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/keysbackup/model/rest/UpdateKeysBackupVersionBody.kt @@ -29,7 +29,8 @@ data class UpdateKeysBackupVersionBody( override val algorithm: String? = null, /** - * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] + * algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" + * see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData] */ @Json(name = "auth_data") override val authData: JsonDict? = null, diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt index 83a4e88ad5..817575d91a 100755 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/KeysBackupBanner.kt @@ -159,7 +159,7 @@ class KeysBackupBanner @JvmOverloads constructor( render(state, true) } - // PRIVATE METHODS ***************************************************************************************************************************************** + // PRIVATE METHODS **************************************************************************************************************************************** private fun setupView() { inflate(context, R.layout.view_keys_backup_banner, this) diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt index 145a26aed2..1c1f3fae1a 100644 --- a/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt +++ b/vector/src/main/java/im/vector/riotx/core/ui/views/NotificationAreaView.kt @@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor( } } - // PRIVATE METHODS ***************************************************************************************************************************************** + // PRIVATE METHODS **************************************************************************************************************************************** private fun setupView() { inflate(context, R.layout.view_notification_area, this) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt index fbc69505fa..c7d3da30ea 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysbackup/setup/KeysBackupSetupActivity.kt @@ -128,7 +128,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() { } private fun exportKeysManually() { - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) { + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, + this, + PERMISSION_REQUEST_CODE_EXPORT_KEYS, + R.string.permissions_rationale_msg_keys_backup_export)) { ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { override fun onPassphrase(passphrase: String) { showWaitingView() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt index ef43605b04..12ef5c3577 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestItem.kt @@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem { holder.buttonBar.isVisible = false - holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName) + holder.statusTextView.text = holder.view.context + .getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName) holder.statusTextView.isVisible = true } VerificationState.CANCELED_BY_ME -> { diff --git a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt index e92b58c681..4c98472f6e 100644 --- a/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/media/VideoMediaViewerActivity.kt @@ -43,7 +43,11 @@ class VideoMediaViewerActivity : VectorBaseActivity() { configureToolbar(videoMediaViewerToolbar, mediaData) imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) - videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) + videoContentRenderer.render(mediaData, + videoMediaViewerThumbnailView, + videoMediaViewerLoading, + videoMediaViewerVideoView, + videoMediaViewerErrorView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt index d357c6ae82..9beeea0278 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -203,7 +203,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor( */ private fun exportKeys() { // We need WRITE_EXTERNAL permission - if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) { + if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, + this, + PERMISSION_REQUEST_CODE_EXPORT_KEYS, + R.string.permissions_rationale_msg_keys_backup_export)) { activity?.let { activity -> ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { override fun onPassphrase(passphrase: String) {