Merge pull request #1235 from vector-im/feature/upgrate_cross_signing

Add migration state to bootstrap
This commit is contained in:
Valere 2020-04-16 15:04:06 +02:00 committed by GitHub
commit 621e78a864
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 986 additions and 105 deletions

View File

@ -21,6 +21,7 @@ Improvements 🙌:
- Cross-Sign | QR code scan confirmation screens design update (#1187) - Cross-Sign | QR code scan confirmation screens design update (#1187)
- Emoji Verification | It's not the same butterfly! (#1220) - Emoji Verification | It's not the same butterfly! (#1220)
- Cross-Signing | Composer decoration: shields (#1077) - Cross-Signing | Composer decoration: shields (#1077)
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
Bugfix 🐛: Bugfix 🐛:
- Fix summary notification staying after "mark as read" - Fix summary notification staying after "mark as read"

View File

@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" val TEST_KEY_ID = "my.test.Key"
mTestHelper.doSync<SsssKeyCreationInfo> { mTestHelper.doSync<SsssKeyCreationInfo> {
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
} }
// Assert Account data is updated // Assert Account data is updated
@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" val TEST_KEY_ID = "my.test.Key"
mTestHelper.doSync<SsssKeyCreationInfo> { mTestHelper.doSync<SsssKeyCreationInfo> {
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 // 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 quadS = session.sharedSecretStorageService
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
quadS.generateKey(keyId, keyId, emptyKeySigner, it) quadS.generateKey(keyId, null, keyId, emptyKeySigner, it)
} }
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId") assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")

View File

@ -217,4 +217,6 @@ interface KeysBackupService {
// For gossiping // For gossiping
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo? fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
} }

View File

@ -29,7 +29,8 @@ data class MessageLocationContent(
@Json(name = "msgtype") override val msgType: String, @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, @Json(name = "body") override val body: String,

View File

@ -35,12 +35,14 @@ interface SharedSecretStorageService {
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...) * 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 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 keyName a human readable name
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret) * @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
* *
* @param callback Get key creation info * @param callback Get key creation info
*/ */
fun generateKey(keyId: String, fun generateKey(keyId: String,
key: SsssKeySpec?,
keyName: String, keyName: String,
keySigner: KeySigner?, keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) callback: MatrixCallback<SsssKeyCreationInfo>)

View File

@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor(
return true return true
} }
override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) {
val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) }
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let {
callback.onSuccess(it)
}
}
}
/** /**
* Enable backing up of keys. * Enable backing up of keys.
* This method will update the state and will start sending keys in nominal case * This method will update the state and will start sending keys in nominal case

View File

@ -29,7 +29,8 @@ data class CreateKeysBackupVersionBody(
override val algorithm: String? = null, 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") @Json(name = "auth_data")
override val authData: JsonDict? = null override val authData: JsonDict? = null

View File

@ -29,7 +29,8 @@ data class KeysVersionResult(
override val algorithm: String? = null, 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") @Json(name = "auth_data")
override val authData: JsonDict? = null, override val authData: JsonDict? = null,

View File

@ -29,7 +29,8 @@ data class UpdateKeysBackupVersionBody(
override val algorithm: String? = null, 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") @Json(name = "auth_data")
override val authData: JsonDict? = null, override val authData: JsonDict? = null,

View File

@ -65,12 +65,14 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
) : SharedSecretStorageService { ) : SharedSecretStorageService {
override fun generateKey(keyId: String, override fun generateKey(keyId: String,
key: SsssKeySpec?,
keyName: String, keyName: String,
keySigner: KeySigner?, keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) { callback: MatrixCallback<SsssKeyCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val key = try { val bytes = try {
ByteArray(32).also { (key as? RawBytesKeySpec)?.privateKey
?: ByteArray(32).also {
SecureRandom().nextBytes(it) SecureRandom().nextBytes(it)
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
@ -102,8 +104,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
callback.onSuccess(SsssKeyCreationInfo( callback.onSuccess(SsssKeyCreationInfo(
keyId = keyId, keyId = keyId,
content = storageKeyContent, content = storageKeyContent,
recoveryKey = computeRecoveryKey(key), recoveryKey = computeRecoveryKey(bytes),
keySpec = RawBytesKeySpec(key) keySpec = RawBytesKeySpec(bytes)
)) ))
} }
} }

View File

@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
internal interface AccountAPI { internal interface AccountAPI {

View File

@ -60,7 +60,7 @@ private short
final short final short
### Line length is limited to 160 chars. Please split long lines ### Line length is limited to 160 chars. Please split long lines
.{161} [^─]{161}
### "DO NOT COMMIT" has been committed ### "DO NOT COMMIT" has been committed
DO NOT COMMIT DO NOT COMMIT

View File

@ -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.BootstrapConclusionFragment
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment 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.BootstrapSaveRecoveryKeyFragment
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
@ -444,4 +445,8 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class) @FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BootstrapMigrateBackupFragment::class)
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
} }

View File

@ -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<Params, Result> {
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
}

View File

@ -159,7 +159,7 @@ class KeysBackupBanner @JvmOverloads constructor(
render(state, true) render(state, true)
} }
// PRIVATE METHODS ***************************************************************************************************************************************** // PRIVATE METHODS ****************************************************************************************************************************************
private fun setupView() { private fun setupView() {
inflate(context, R.layout.view_keys_backup_banner, this) inflate(context, R.layout.view_keys_backup_banner, this)

View File

@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor(
} }
} }
// PRIVATE METHODS ***************************************************************************************************************************************** // PRIVATE METHODS ****************************************************************************************************************************************
private fun setupView() { private fun setupView() {
inflate(context, R.layout.view_notification_area, this) inflate(context, R.layout.view_notification_area, this)

View File

@ -128,7 +128,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
} }
private fun exportKeysManually() { 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 { ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {
showWaitingView() showWaitingView()

View File

@ -0,0 +1,172 @@
/*
* 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.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 stringProvider: StringProvider
) : ViewModelTask<BackupToQuadSMigrationTask.Params, BackupToQuadSMigrationTask.Result> {
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
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 {
override fun onProgress(progress: Int, total: Int) {
params.progressListener?.onProgress(WaitingViewData(
stringProvider.getString(R.string.bootstrap_progress_checking_backup_with_info,
"$progress/$total")
))
}
})
}
} else null)
?: return Result.IllegalParams
reportProgress(params, R.string.bootstrap_progress_compute_curve_key)
val recoveryKey = computeRecoveryKey(curveKey)
val isValid = awaitCallback<Boolean> {
keysBackupService.isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
}
if (!isValid) return Result.InvalidRecoverySecret
val info: SsssKeyCreationInfo =
when {
params.passphrase?.isNotEmpty() == true -> {
reportProgress(params, R.string.bootstrap_progress_generating_ssss)
awaitCallback {
quadS.generateKeyWithPassphrase(
UUID.randomUUID().toString(),
"ssss_key",
params.passphrase,
EmptyKeySigner(),
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(
R.string.bootstrap_progress_generating_ssss_with_info,
"$progress/$total")
))
}
},
it
)
}
}
params.recoveryKey != null -> {
reportProgress(params, R.string.bootstrap_progress_generating_ssss_recovery)
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
reportProgress(params, R.string.bootstrap_progress_storing_in_sss)
awaitCallback<Unit> {
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)
}
}
private fun reportProgress(params: Params, stringRes: Int) {
params.progressListener?.onProgress(WaitingViewData(stringProvider.getString(stringRes)))
}
}

View File

@ -40,4 +40,8 @@ sealed class BootstrapActions : VectorViewModelAction {
object SaveReqQueryStarted : BootstrapActions() object SaveReqQueryStarted : BootstrapActions()
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions() data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
object SaveReqFailed : BootstrapActions() object SaveReqFailed : BootstrapActions()
object HandleForgotBackupPassphrase : BootstrapActions()
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
} }

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -26,18 +27,26 @@ import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.extensions.commitTransaction import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.* import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() { class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class Args(
val isNewAccount: Boolean
) : Parcelable
override val showExpanded = true override val showExpanded = true
@Inject @Inject
@ -113,6 +122,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
when (state.step) { when (state.step) {
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 -> { is BootstrapStep.SetupPassphrase -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password)) bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase)) bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
@ -143,10 +157,27 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title) bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
showFragment(BootstrapConclusionFragment::class, Bundle()) 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() 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<out Fragment>, bundle: Bundle) { private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) { if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
childFragmentManager.commitTransaction { childFragmentManager.commitTransaction {

View File

@ -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.EmptyKeySigner
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService 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.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.data.LoginFlowTypes
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo 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.di.MoshiProvider
import im.vector.matrix.android.internal.util.awaitCallback import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.ViewModelTask
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
@ -67,24 +66,16 @@ interface BootstrapProgressListener {
data class Params( data class Params(
val userPasswordAuth: UserPasswordAuth? = null, val userPasswordAuth: UserPasswordAuth? = null,
val progressListener: BootstrapProgressListener? = null, val progressListener: BootstrapProgressListener? = null,
val passphrase: String? val passphrase: String?,
val keySpec: SsssKeySpec? = null
) )
class BootstrapCrossSigningTask @Inject constructor( class BootstrapCrossSigningTask @Inject constructor(
private val session: Session, private val session: Session,
private val stringProvider: StringProvider private val stringProvider: StringProvider
) { ) : ViewModelTask<Params, BootstrapResult> {
operator fun invoke( override suspend fun execute(params: Params): BootstrapResult {
scope: CoroutineScope,
params: Params,
onResult: (BootstrapResult) -> Unit = {}
) {
val backgroundJob = scope.async { execute(params) }
scope.launch { onResult(backgroundJob.await()) }
}
suspend fun execute(params: Params): BootstrapResult {
params.progressListener?.onProgress( params.progressListener?.onProgress(
WaitingViewData( WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing), stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
@ -124,6 +115,7 @@ class BootstrapCrossSigningTask @Inject constructor(
} ?: kotlin.run { } ?: kotlin.run {
ssssService.generateKey( ssssService.generateKey(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
params.keySpec,
"ssss_key", "ssss_key",
EmptyKeySigner(), EmptyKeySigner(),
it it
@ -205,6 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor(
) )
) )
try { try {
if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
val creationInfo = awaitCallback<MegolmBackupCreationInfo> { val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it) session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
} }
@ -213,6 +206,7 @@ class BootstrapCrossSigningTask @Inject constructor(
} }
// Save it for gossiping // Save it for gossiping
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version) session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
}
} catch (failure: Throwable) { } catch (failure: Throwable) {
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup") Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
} }

View File

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

View File

@ -31,20 +31,26 @@ import com.nulabinc.zxcvbn.Zxcvbn
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session 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.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.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.login.ReAuthHelper import im.vector.riotx.features.login.ReAuthHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.OutputStream import java.io.OutputStream
data class BootstrapViewState( data class BootstrapViewState(
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false), val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
val passphrase: String? = null, val passphrase: String? = null,
val migrationRecoveryKey: String? = null,
val passphraseRepeat: String? = null, val passphraseRepeat: String? = null,
val crossSigningInitialization: Async<Unit> = Uninitialized, val crossSigningInitialization: Async<Unit> = Uninitialized,
val passphraseStrength: Async<Strength> = Uninitialized, val passphraseStrength: Async<Strength> = Uninitialized,
@ -55,20 +61,13 @@ data class BootstrapViewState(
val recoverySaveFileProcess: Async<Unit> = Uninitialized val recoverySaveFileProcess: Async<Unit> = Uninitialized
) : MvRxState ) : 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( class BootstrapSharedViewModel @AssistedInject constructor(
@Assisted initialState: BootstrapViewState, @Assisted initialState: BootstrapViewState,
@Assisted val args: BootstrapBottomSheet.Args,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val session: Session, private val session: Session,
private val bootstrapTask: BootstrapCrossSigningTask, private val bootstrapTask: BootstrapCrossSigningTask,
private val migrationTask: BackupToQuadSMigrationTask,
private val reAuthHelper: ReAuthHelper private val reAuthHelper: ReAuthHelper
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) { ) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
@ -76,7 +75,53 @@ class BootstrapSharedViewModel @AssistedInject constructor(
@AssistedInject.Factory @AssistedInject.Factory
interface 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<KeysVersionResult?> {
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<KeysVersionResult?> {
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(isPasswordVisible = false, useKey = false))
}
} else {
setState {
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
}
}
}
}
}
}
} }
override fun handle(action: BootstrapActions) = withState { state -> override fun handle(action: BootstrapActions) = withState { state ->
@ -94,12 +139,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
} }
} }
is BootstrapStep.AccountPassword -> { is BootstrapStep.AccountPassword -> {
setState { setState {
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
} }
} }
is BootstrapStep.GetBackupSecretPassForMigration -> {
setState {
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
}
else -> { else -> {
} }
} }
@ -197,12 +246,25 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = BootstrapStep.AccountPassword(false)) 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 -> { is BootstrapActions.ReAuth -> {
startInitializeFlow( startInitializeFlow(
state.currentReAuth?.copy(password = action.pass) state.currentReAuth?.copy(password = action.pass)
?: UserPasswordAuth(user = session.myUserId, 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 }.exhaustive
} }
@ -210,7 +272,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// Business Logic // Business Logic
// ======================================= // =======================================
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state -> private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
kotlin.runCatching { kotlin.runCatching {
os.use { os.use {
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray()) 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?) { private fun startInitializeFlow(auth: UserPasswordAuth?) {
setState { setState {
copy(step = BootstrapStep.Initializing) copy(step = BootstrapStep.Initializing)
@ -247,11 +360,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
withState { state -> withState { state ->
viewModelScope.launch { viewModelScope.launch(Dispatchers.IO) {
bootstrapTask.invoke(this, Params( bootstrapTask.invoke(this, Params(
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(), userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
progressListener = progressListener, progressListener = progressListener,
passphrase = state.passphrase passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
)) { )) {
when (it) { when (it) {
is BootstrapResult.Success -> { is BootstrapResult.Success -> {
@ -309,6 +423,25 @@ class BootstrapSharedViewModel @AssistedInject constructor(
private fun queryBack() = withState { state -> private fun queryBack() = withState { state ->
when (state.step) { when (state.step) {
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.GetBackupSecretKeyForMigration -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}
is BootstrapStep.SetupPassphrase -> { is BootstrapStep.SetupPassphrase -> {
// do we let you cancel from here? // do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap()) _viewEvents.post(BootstrapViewEvents.SkipBootstrap())
@ -344,7 +477,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? { override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment() 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)
} }
} }
} }

View File

@ -0,0 +1,92 @@
/*
* 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()
}

View File

@ -16,8 +16,7 @@
package im.vector.riotx.features.crypto.recover package im.vector.riotx.features.crypto.recover
import android.os.Bundle import androidx.core.view.isVisible
import android.view.View
import com.airbnb.mvrx.parentFragmentViewModel import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.riotx.R import im.vector.riotx.R
@ -31,12 +30,22 @@ class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() {
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel() val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
override fun invalidate() = withState(sharedViewModel) { state -> override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.Initializing) return@withState when (state.step) {
is BootstrapStep.Initializing -> {
bootstrapLoadingStatusText.isVisible = true
bootstrapDescriptionText.isVisible = true
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message 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
}
}
}
} }

View File

@ -97,7 +97,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java) replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
} }
is HomeActivitySharedAction.PromptForSecurityBootstrap -> { is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet") BootstrapBottomSheet.show(supportFragmentManager, true)
} }
} }
} }
@ -109,6 +109,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
} }
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) { if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap) sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
sharedActionViewModel.isAccountCreation = true
intent.removeExtra(EXTRA_ACCOUNT_CREATION) intent.removeExtra(EXTRA_ACCOUNT_CREATION)
} }
@ -163,28 +164,47 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.getMyCrossSigningKeys() .getMyCrossSigningKeys()
val crossSigningEnabledOnAccount = myCrossSigningKeys != null val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) { if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
// We need to ask // We need to ask
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 sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
popupAlertManager.postVectorAlert( popupAlertManager.postVectorAlert(
VerificationVectorAlert( VerificationVectorAlert(
uid = "completeSecurity", uid = "upgradeSecurity",
title = getString(R.string.complete_security), title = getString(titleRes),
description = getString(R.string.crosssigning_verify_this_session), description = getString(descRes),
iconId = R.drawable.ic_shield_warning iconId = R.drawable.ic_shield_warning
).apply { ).apply {
matrixItem = session.getUser(session.myUserId)?.toMatrixItem() matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent) colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
contentAction = Runnable { contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let { (weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
it.navigator.waitSessionVerification(it) action(it)
} }
} }
dismissedAction = Runnable {} dismissedAction = Runnable {}
} }
) )
} }
}
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)

View File

@ -21,4 +21,5 @@ import javax.inject.Inject
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() { class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
var hasDisplayedCompleteSecurityPrompt : Boolean = false var hasDisplayedCompleteSecurityPrompt : Boolean = false
var isAccountCreation : Boolean = false
} }

View File

@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
} }
VerificationState.CANCELED_BY_OTHER -> { VerificationState.CANCELED_BY_OTHER -> {
holder.buttonBar.isVisible = false 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 holder.statusTextView.isVisible = true
} }
VerificationState.CANCELED_BY_ME -> { VerificationState.CANCELED_BY_ME -> {

View File

@ -43,7 +43,11 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
configureToolbar(videoMediaViewerToolbar, mediaData) configureToolbar(videoMediaViewerToolbar, mediaData)
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView) imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView) videoContentRenderer.render(mediaData,
videoMediaViewerThumbnailView,
videoMediaViewerLoading,
videoMediaViewerVideoView,
videoMediaViewerErrorView)
} }
} }

View File

@ -34,6 +34,7 @@ import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity 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.SupportedVerificationMethodsProvider
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
import im.vector.riotx.features.debug.DebugMenuActivity 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) { override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) {
if (context is VectorBaseActivity) { if (context is VectorBaseActivity) {
context.notImplemented("Open not joined room") context.notImplemented("Open not joined room")

View File

@ -34,6 +34,8 @@ interface Navigator {
fun waitSessionVerification(context: Context) fun waitSessionVerification(context: Context)
fun upgradeSessionSecurity(context: Context)
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData) fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false) fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)

View File

@ -203,7 +203,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
*/ */
private fun exportKeys() { private fun exportKeys() {
// We need WRITE_EXTERNAL permission // 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 -> activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M13,2H6C4.8954,2 4,2.8954 4,4V20C4,21.1046 4.8954,22 6,22H18C19.1046,22 20,21.1046 20,20V9L13,2Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M13,2V9H20"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/bootstrapDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/bootstrapRecoveryKeyEnterTil"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/bootstrap_enter_recovery" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/bootstrapRecoveryKeyEnterTil"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@id/bootstrapMigrateShowPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/bootstrapMigrateEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:maxLines="3"
android:singleLine="false"
android:textColor="?android:textColorPrimary"
tools:hint="@string/keys_backup_restore_key_enter_hint"
tools:inputType="textPassword" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapMigrateUseFile"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_file"
android:textAllCaps="false"
app:icon="@drawable/ic_file"
app:iconTint="@color/button_positive_text_color_selector"
tools:visibility="visible" />
<TextView
android:id="@+id/bootstrapMigrateForgotPassphrase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:text="@string/keys_backup_restore_with_passphrase_helper_with_link"
tools:visibility="visible" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/bootstrapMigrateShowPassword"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye_black"
android:tint="?colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bootstrapRecoveryKeyEnterTil"
app:layout_constraintTop_toTopOf="@+id/bootstrapRecoveryKeyEnterTil" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapMigrateContinueButton"
style="@style/VectorButtonStyleText"
android:layout_gravity="end"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bootstrapRecoveryKeyEnterTil" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2243,7 +2243,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string> <string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
<string name="bootstrap_loading_title">Setting up recovery.</string> <string name="bootstrap_loading_title">Setting up recovery.</string>
<string name="your_recovery_key">Your recovery key</string> <string name="your_recovery_key">Your recovery key</string>
<string name="bootstrap_finish_title">Youre done!</string> <string name="bootstrap_finish_title">"You're done!"</string>
<string name="keep_it_safe">Keep it safe</string> <string name="keep_it_safe">Keep it safe</string>
<string name="finish">Finish</string> <string name="finish">Finish</string>

View File

@ -7,6 +7,27 @@
<!-- BEGIN Strings added by Valere --> <!-- BEGIN Strings added by Valere -->
<string name="room_message_placeholder">Message…</string> <string name="room_message_placeholder">Message…</string>
<string name="upgrade_security">Encryption upgrade available</string>
<string name="security_prompt_text">Verify yourself &amp; others to keep your chats safe</string>
<!-- %s will be replaced by recovery_key -->
<string name="bootstrap_enter_recovery">Enter your %s to continue</string>
<string name="use_file">Use File</string>
<!-- %s will be replaced by recovery_passphrase -->
<!-- <string name="upgrade_account_desc">Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.</string>-->
<string name="enter_backup_passphrase">Enter %s</string>
<string name="backup_recovery_passphrase">Recovery Passphrase</string>
<string name="bootstrap_invalid_recovery_key">"It's not a valid recovery key"</string>
<string name="bootstrap_progress_checking_backup">Checking backup Key</string>
<string name="bootstrap_progress_checking_backup_with_info">Checking backup Key (%s)</string>
<string name="bootstrap_progress_compute_curve_key">Getting curve key</string>
<string name="bootstrap_progress_generating_ssss">Generating SSSS key from passphrase</string>
<string name="bootstrap_progress_generating_ssss_with_info">Generating SSSS key from passphrase (%s)</string>
<string name="bootstrap_progress_generating_ssss_recovery">Generating SSSS key from recovery key</string>
<string name="bootstrap_progress_storing_in_sss">Storing keybackup secret in SSSS</string>
<!-- END Strings added by Valere --> <!-- END Strings added by Valere -->