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

View file

@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key"
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
@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key"
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
@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest {
val quadS = session.sharedSecretStorageService
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")

View file

@ -217,4 +217,6 @@ interface KeysBackupService {
// For gossiping
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
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,
/**
* 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,

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 ...)
*
* @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<SsssKeyCreationInfo>)

View file

@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor(
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.
* 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,
/**
* 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

View file

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

View file

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

View file

@ -65,14 +65,16 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
) : SharedSecretStorageService {
override fun generateKey(keyId: String,
key: SsssKeySpec?,
keyName: String,
keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) {
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)
))
}
}

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 retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
internal interface AccountAPI {

View file

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

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

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)
}
// PRIVATE METHODS *****************************************************************************************************************************************
// PRIVATE METHODS ****************************************************************************************************************************************
private fun setupView() {
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() {
inflate(context, R.layout.view_notification_area, this)

View file

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

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

View file

@ -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<out Fragment>, bundle: Bundle) {
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
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.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<Params, BootstrapResult> {
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<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
// Save it for gossiping
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
}
val version = awaitCallback<KeysVersion> {
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")
}

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.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<Unit> = Uninitialized,
val passphraseStrength: Async<Strength> = Uninitialized,
@ -55,20 +61,13 @@ data class BootstrapViewState(
val recoverySaveFileProcess: Async<Unit> = 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<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(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<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 ->
@ -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,30 @@ 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.GetBackupSecretKeyForMigration -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}
is BootstrapStep.ConfirmPassphrase -> {
is BootstrapStep.SetupPassphrase -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}
is BootstrapStep.ConfirmPassphrase -> {
setState {
copy(
step = BootstrapStep.SetupPassphrase(
@ -322,15 +455,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 +477,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)
}
}
}

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

View file

@ -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)
}
}
}
@ -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,29 +164,48 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
.getMyCrossSigningKeys()
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
// 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) {

View file

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

View file

@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
}
VerificationState.CANCELED_BY_OTHER -> {
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 -> {

View file

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

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.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")

View file

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

View file

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

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_title">Setting up recovery.</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="finish">Finish</string>

View file

@ -7,6 +7,27 @@
<!-- BEGIN Strings added by Valere -->
<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 -->