mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-26 19:36:08 +03:00
Merge pull request #1235 from vector-im/feature/upgrate_cross_signing
Add migration state to bootstrap
This commit is contained in:
commit
621e78a864
36 changed files with 986 additions and 105 deletions
|
@ -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"
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -217,4 +217,6 @@ interface KeysBackupService {
|
|||
// For gossiping
|
||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||
|
||||
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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>)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// PRIVATE METHODS *****************************************************************************************************************************************
|
||||
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||
|
||||
private fun setupView() {
|
||||
inflate(context, R.layout.view_notification_area, this)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -21,4 +21,5 @@ import javax.inject.Inject
|
|||
|
||||
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
|
||||
var hasDisplayedCompleteSecurityPrompt : Boolean = false
|
||||
var isAccountCreation : Boolean = false
|
||||
}
|
||||
|
|
|
@ -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 -> {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
21
vector/src/main/res/drawable/ic_file.xml
Normal file
21
vector/src/main/res/drawable/ic_file.xml
Normal 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>
|
|
@ -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>
|
|
@ -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">You‘re 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>
|
||||
|
||||
|
|
|
@ -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 & 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 -->
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue