Merge pull request #1235 from vector-im/feature/upgrate_cross_signing
Add migration state to bootstrap
This commit is contained in:
commit
621e78a864
@ -21,6 +21,7 @@ Improvements 🙌:
|
|||||||
- Cross-Sign | QR code scan confirmation screens design update (#1187)
|
- Cross-Sign | QR code scan confirmation screens design update (#1187)
|
||||||
- Emoji Verification | It's not the same butterfly! (#1220)
|
- Emoji Verification | It's not the same butterfly! (#1220)
|
||||||
- Cross-Signing | Composer decoration: shields (#1077)
|
- Cross-Signing | Composer decoration: shields (#1077)
|
||||||
|
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
- Fix summary notification staying after "mark as read"
|
- Fix summary notification staying after "mark as read"
|
||||||
|
@ -71,7 +71,7 @@ class QuadSTests : InstrumentedTest {
|
|||||||
val TEST_KEY_ID = "my.test.Key"
|
val TEST_KEY_ID = "my.test.Key"
|
||||||
|
|
||||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert Account data is updated
|
// Assert Account data is updated
|
||||||
@ -177,7 +177,7 @@ class QuadSTests : InstrumentedTest {
|
|||||||
val TEST_KEY_ID = "my.test.Key"
|
val TEST_KEY_ID = "my.test.Key"
|
||||||
|
|
||||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
quadS.generateKey(TEST_KEY_ID, null, "Test Key", emptyKeySigner, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
|
// Test that we don't need to wait for an account data sync to access directly the keyid from DB
|
||||||
@ -322,7 +322,7 @@ class QuadSTests : InstrumentedTest {
|
|||||||
val quadS = session.sharedSecretStorageService
|
val quadS = session.sharedSecretStorageService
|
||||||
|
|
||||||
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
|
val creationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||||
quadS.generateKey(keyId, keyId, emptyKeySigner, it)
|
quadS.generateKey(keyId, null, keyId, emptyKeySigner, it)
|
||||||
}
|
}
|
||||||
|
|
||||||
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
assertAccountData(session, "${DefaultSharedSecretStorageService.KEY_ID_BASE}.$keyId")
|
||||||
|
@ -217,4 +217,6 @@ interface KeysBackupService {
|
|||||||
// For gossiping
|
// For gossiping
|
||||||
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
|
||||||
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
fun getKeyBackupRecoveryKeyInfo() : SavedKeyBackupKeyInfo?
|
||||||
|
|
||||||
|
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,8 @@ data class MessageLocationContent(
|
|||||||
@Json(name = "msgtype") override val msgType: String,
|
@Json(name = "msgtype") override val msgType: String,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind of content description for accessibility e.g. 'location attachment'.
|
* Required. A description of the location e.g. 'Big Ben, London, UK', or some kind
|
||||||
|
* of content description for accessibility e.g. 'location attachment'.
|
||||||
*/
|
*/
|
||||||
@Json(name = "body") override val body: String,
|
@Json(name = "body") override val body: String,
|
||||||
|
|
||||||
|
@ -35,12 +35,14 @@ interface SharedSecretStorageService {
|
|||||||
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...)
|
* Use the SsssKeyCreationInfo object returned by the callback to get more information about the created key (recovery key ...)
|
||||||
*
|
*
|
||||||
* @param keyId the ID of the key
|
* @param keyId the ID of the key
|
||||||
|
* @param key keep null if you want to generate a random key
|
||||||
* @param keyName a human readable name
|
* @param keyName a human readable name
|
||||||
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
|
* @param keySigner Used to add a signature to the key (client should check key signature before storing secret)
|
||||||
*
|
*
|
||||||
* @param callback Get key creation info
|
* @param callback Get key creation info
|
||||||
*/
|
*/
|
||||||
fun generateKey(keyId: String,
|
fun generateKey(keyId: String,
|
||||||
|
key: SsssKeySpec?,
|
||||||
keyName: String,
|
keyName: String,
|
||||||
keySigner: KeySigner?,
|
keySigner: KeySigner?,
|
||||||
callback: MatrixCallback<SsssKeyCreationInfo>)
|
callback: MatrixCallback<SsssKeyCreationInfo>)
|
||||||
|
@ -1100,6 +1100,16 @@ internal class DefaultKeysBackupService @Inject constructor(
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) {
|
||||||
|
val safeKeysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) }
|
||||||
|
|
||||||
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
|
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, safeKeysBackupVersion).let {
|
||||||
|
callback.onSuccess(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable backing up of keys.
|
* Enable backing up of keys.
|
||||||
* This method will update the state and will start sending keys in nominal case
|
* This method will update the state and will start sending keys in nominal case
|
||||||
|
@ -29,7 +29,8 @@ data class CreateKeysBackupVersionBody(
|
|||||||
override val algorithm: String? = null,
|
override val algorithm: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
* see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth_data")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict? = null
|
override val authData: JsonDict? = null
|
||||||
|
@ -29,7 +29,8 @@ data class KeysVersionResult(
|
|||||||
override val algorithm: String? = null,
|
override val algorithm: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
* see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth_data")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict? = null,
|
override val authData: JsonDict? = null,
|
||||||
|
@ -29,7 +29,8 @@ data class UpdateKeysBackupVersionBody(
|
|||||||
override val algorithm: String? = null,
|
override val algorithm: String? = null,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2" see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
* algorithm-dependent data, for "m.megolm_backup.v1.curve25519-aes-sha2"
|
||||||
|
* see [im.vector.matrix.android.internal.crypto.keysbackup.MegolmBackupAuthData]
|
||||||
*/
|
*/
|
||||||
@Json(name = "auth_data")
|
@Json(name = "auth_data")
|
||||||
override val authData: JsonDict? = null,
|
override val authData: JsonDict? = null,
|
||||||
|
@ -65,12 +65,14 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||||||
) : SharedSecretStorageService {
|
) : SharedSecretStorageService {
|
||||||
|
|
||||||
override fun generateKey(keyId: String,
|
override fun generateKey(keyId: String,
|
||||||
|
key: SsssKeySpec?,
|
||||||
keyName: String,
|
keyName: String,
|
||||||
keySigner: KeySigner?,
|
keySigner: KeySigner?,
|
||||||
callback: MatrixCallback<SsssKeyCreationInfo>) {
|
callback: MatrixCallback<SsssKeyCreationInfo>) {
|
||||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||||
val key = try {
|
val bytes = try {
|
||||||
ByteArray(32).also {
|
(key as? RawBytesKeySpec)?.privateKey
|
||||||
|
?: ByteArray(32).also {
|
||||||
SecureRandom().nextBytes(it)
|
SecureRandom().nextBytes(it)
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
@ -102,8 +104,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||||||
callback.onSuccess(SsssKeyCreationInfo(
|
callback.onSuccess(SsssKeyCreationInfo(
|
||||||
keyId = keyId,
|
keyId = keyId,
|
||||||
content = storageKeyContent,
|
content = storageKeyContent,
|
||||||
recoveryKey = computeRecoveryKey(key),
|
recoveryKey = computeRecoveryKey(bytes),
|
||||||
keySpec = RawBytesKeySpec(key)
|
keySpec = RawBytesKeySpec(bytes)
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ import im.vector.matrix.android.api.session.account.model.ChangePasswordParams
|
|||||||
import im.vector.matrix.android.internal.network.NetworkConstants
|
import im.vector.matrix.android.internal.network.NetworkConstants
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
import retrofit2.http.Headers
|
|
||||||
import retrofit2.http.POST
|
import retrofit2.http.POST
|
||||||
|
|
||||||
internal interface AccountAPI {
|
internal interface AccountAPI {
|
||||||
|
@ -60,7 +60,7 @@ private short
|
|||||||
final short
|
final short
|
||||||
|
|
||||||
### Line length is limited to 160 chars. Please split long lines
|
### Line length is limited to 160 chars. Please split long lines
|
||||||
.{161}
|
[^─]{161}
|
||||||
|
|
||||||
### "DO NOT COMMIT" has been committed
|
### "DO NOT COMMIT" has been committed
|
||||||
DO NOT COMMIT
|
DO NOT COMMIT
|
||||||
|
@ -30,6 +30,7 @@ import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment
|
|||||||
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
||||||
|
import im.vector.riotx.features.crypto.recover.BootstrapMigrateBackupFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
||||||
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
||||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||||
@ -444,4 +445,8 @@ interface FragmentModule {
|
|||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
||||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(BootstrapMigrateBackupFragment::class)
|
||||||
|
fun bindBootstrapMigrateBackupFragment(fragment: BootstrapMigrateBackupFragment): Fragment
|
||||||
}
|
}
|
||||||
|
@ -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)
|
render(state, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************************************************************************
|
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
inflate(context, R.layout.view_keys_backup_banner, this)
|
inflate(context, R.layout.view_keys_backup_banner, this)
|
||||||
|
@ -87,7 +87,7 @@ class NotificationAreaView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PRIVATE METHODS *****************************************************************************************************************************************
|
// PRIVATE METHODS ****************************************************************************************************************************************
|
||||||
|
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
inflate(context, R.layout.view_notification_area, this)
|
inflate(context, R.layout.view_notification_area, this)
|
||||||
|
@ -128,7 +128,10 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun exportKeysManually() {
|
private fun exportKeysManually() {
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||||
|
R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||||
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
showWaitingView()
|
showWaitingView()
|
||||||
|
@ -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()
|
object SaveReqQueryStarted : BootstrapActions()
|
||||||
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
|
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
|
||||||
object SaveReqFailed : BootstrapActions()
|
object SaveReqFailed : BootstrapActions()
|
||||||
|
|
||||||
|
object HandleForgotBackupPassphrase : BootstrapActions()
|
||||||
|
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
|
||||||
|
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.riotx.features.crypto.recover
|
|||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -26,18 +27,26 @@ import android.view.WindowManager
|
|||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.FragmentManager
|
||||||
import com.airbnb.mvrx.fragmentViewModel
|
import com.airbnb.mvrx.fragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.di.ScreenComponent
|
import im.vector.riotx.core.di.ScreenComponent
|
||||||
import im.vector.riotx.core.extensions.commitTransaction
|
import im.vector.riotx.core.extensions.commitTransaction
|
||||||
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
|
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Args(
|
||||||
|
val isNewAccount: Boolean
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
override val showExpanded = true
|
override val showExpanded = true
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
@ -113,6 +122,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
|
|
||||||
when (state.step) {
|
when (state.step) {
|
||||||
|
is BootstrapStep.CheckingMigration -> {
|
||||||
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||||
|
bootstrapTitleText.text = getString(R.string.upgrade_security)
|
||||||
|
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||||
|
}
|
||||||
is BootstrapStep.SetupPassphrase -> {
|
is BootstrapStep.SetupPassphrase -> {
|
||||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||||
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||||
@ -143,10 +157,27 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
|||||||
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
||||||
showFragment(BootstrapConclusionFragment::class, Bundle())
|
showFragment(BootstrapConclusionFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
|
is BootstrapStep.GetBackupSecretForMigration -> {
|
||||||
|
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.key_small))
|
||||||
|
bootstrapTitleText.text = getString(R.string.upgrade_security)
|
||||||
|
showFragment(BootstrapMigrateBackupFragment::class, Bundle())
|
||||||
}
|
}
|
||||||
|
}.exhaustive
|
||||||
super.invalidate()
|
super.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val EXTRA_ARGS = "EXTRA_ARGS"
|
||||||
|
|
||||||
|
fun show(fragmentManager: FragmentManager, isAccountCreation: Boolean) {
|
||||||
|
BootstrapBottomSheet().apply {
|
||||||
|
isCancelable = false
|
||||||
|
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(isAccountCreation)) }
|
||||||
|
}.show(fragmentManager, "BootstrapBottomSheet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||||
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||||
childFragmentManager.commitTransaction {
|
childFragmentManager.commitTransaction {
|
||||||
|
@ -25,6 +25,7 @@ import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY
|
|||||||
import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
||||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
||||||
@ -33,11 +34,9 @@ import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
|||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||||
import im.vector.matrix.android.internal.util.awaitCallback
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.platform.ViewModelTask
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -67,24 +66,16 @@ interface BootstrapProgressListener {
|
|||||||
data class Params(
|
data class Params(
|
||||||
val userPasswordAuth: UserPasswordAuth? = null,
|
val userPasswordAuth: UserPasswordAuth? = null,
|
||||||
val progressListener: BootstrapProgressListener? = null,
|
val progressListener: BootstrapProgressListener? = null,
|
||||||
val passphrase: String?
|
val passphrase: String?,
|
||||||
|
val keySpec: SsssKeySpec? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
class BootstrapCrossSigningTask @Inject constructor(
|
class BootstrapCrossSigningTask @Inject constructor(
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val stringProvider: StringProvider
|
private val stringProvider: StringProvider
|
||||||
) {
|
) : ViewModelTask<Params, BootstrapResult> {
|
||||||
|
|
||||||
operator fun invoke(
|
override suspend fun execute(params: Params): BootstrapResult {
|
||||||
scope: CoroutineScope,
|
|
||||||
params: Params,
|
|
||||||
onResult: (BootstrapResult) -> Unit = {}
|
|
||||||
) {
|
|
||||||
val backgroundJob = scope.async { execute(params) }
|
|
||||||
scope.launch { onResult(backgroundJob.await()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun execute(params: Params): BootstrapResult {
|
|
||||||
params.progressListener?.onProgress(
|
params.progressListener?.onProgress(
|
||||||
WaitingViewData(
|
WaitingViewData(
|
||||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
||||||
@ -124,6 +115,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||||||
} ?: kotlin.run {
|
} ?: kotlin.run {
|
||||||
ssssService.generateKey(
|
ssssService.generateKey(
|
||||||
UUID.randomUUID().toString(),
|
UUID.randomUUID().toString(),
|
||||||
|
params.keySpec,
|
||||||
"ssss_key",
|
"ssss_key",
|
||||||
EmptyKeySigner(),
|
EmptyKeySigner(),
|
||||||
it
|
it
|
||||||
@ -205,6 +197,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
try {
|
try {
|
||||||
|
if (session.cryptoService().keysBackupService().keysBackupVersion == null) {
|
||||||
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
|
||||||
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
|
||||||
}
|
}
|
||||||
@ -213,6 +206,7 @@ class BootstrapCrossSigningTask @Inject constructor(
|
|||||||
}
|
}
|
||||||
// Save it for gossiping
|
// Save it for gossiping
|
||||||
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
|
||||||
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")
|
||||||
}
|
}
|
||||||
|
@ -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.Assisted
|
||||||
import com.squareup.inject.assisted.AssistedInject
|
import com.squareup.inject.assisted.AssistedInject
|
||||||
import im.vector.matrix.android.api.session.Session
|
import im.vector.matrix.android.api.session.Session
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||||
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.exhaustive
|
import im.vector.riotx.core.extensions.exhaustive
|
||||||
import im.vector.riotx.core.platform.VectorViewModel
|
import im.vector.riotx.core.platform.VectorViewModel
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.features.login.ReAuthHelper
|
import im.vector.riotx.features.login.ReAuthHelper
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
|
|
||||||
data class BootstrapViewState(
|
data class BootstrapViewState(
|
||||||
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
||||||
val passphrase: String? = null,
|
val passphrase: String? = null,
|
||||||
|
val migrationRecoveryKey: String? = null,
|
||||||
val passphraseRepeat: String? = null,
|
val passphraseRepeat: String? = null,
|
||||||
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
||||||
val passphraseStrength: Async<Strength> = Uninitialized,
|
val passphraseStrength: Async<Strength> = Uninitialized,
|
||||||
@ -55,20 +61,13 @@ data class BootstrapViewState(
|
|||||||
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
||||||
) : MvRxState
|
) : MvRxState
|
||||||
|
|
||||||
sealed class BootstrapStep {
|
|
||||||
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
|
||||||
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
|
||||||
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
|
||||||
object Initializing : BootstrapStep()
|
|
||||||
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
|
|
||||||
object DoneSuccess : BootstrapStep()
|
|
||||||
}
|
|
||||||
|
|
||||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||||
@Assisted initialState: BootstrapViewState,
|
@Assisted initialState: BootstrapViewState,
|
||||||
|
@Assisted val args: BootstrapBottomSheet.Args,
|
||||||
private val stringProvider: StringProvider,
|
private val stringProvider: StringProvider,
|
||||||
private val session: Session,
|
private val session: Session,
|
||||||
private val bootstrapTask: BootstrapCrossSigningTask,
|
private val bootstrapTask: BootstrapCrossSigningTask,
|
||||||
|
private val migrationTask: BackupToQuadSMigrationTask,
|
||||||
private val reAuthHelper: ReAuthHelper
|
private val reAuthHelper: ReAuthHelper
|
||||||
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
||||||
|
|
||||||
@ -76,7 +75,53 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
@AssistedInject.Factory
|
@AssistedInject.Factory
|
||||||
interface Factory {
|
interface Factory {
|
||||||
fun create(initialState: BootstrapViewState): BootstrapSharedViewModel
|
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// need to check if user have an existing keybackup
|
||||||
|
if (args.isNewAccount) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.SetupPassphrase(false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.CheckingMigration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to check if there is an existing backup
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val version = awaitCallback<KeysVersionResult?> {
|
||||||
|
session.cryptoService().keysBackupService().getCurrentVersion(it)
|
||||||
|
}
|
||||||
|
if (version == null) {
|
||||||
|
// we just resume plain bootstrap
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.SetupPassphrase(false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// we need to get existing backup passphrase/key and convert to SSSS
|
||||||
|
val keyVersion = awaitCallback<KeysVersionResult?> {
|
||||||
|
session.cryptoService().keysBackupService().getVersion(version.version ?: "", it)
|
||||||
|
}
|
||||||
|
if (keyVersion == null) {
|
||||||
|
// strange case... just finish?
|
||||||
|
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||||
|
} else {
|
||||||
|
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||||
|
if (isBackupCreatedFromPassphrase) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handle(action: BootstrapActions) = withState { state ->
|
override fun handle(action: BootstrapActions) = withState { state ->
|
||||||
@ -94,12 +139,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is BootstrapStep.AccountPassword -> {
|
is BootstrapStep.AccountPassword -> {
|
||||||
setState {
|
setState {
|
||||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
is BootstrapStep.GetBackupSecretPassForMigration -> {
|
||||||
|
setState {
|
||||||
|
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,12 +246,25 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
copy(step = BootstrapStep.AccountPassword(false))
|
copy(step = BootstrapStep.AccountPassword(false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
BootstrapActions.HandleForgotBackupPassphrase -> {
|
||||||
|
if (state.step is BootstrapStep.GetBackupSecretPassForMigration) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.GetBackupSecretPassForMigration(state.step.isPasswordVisible, true))
|
||||||
|
}
|
||||||
|
} else return@withState
|
||||||
|
}
|
||||||
is BootstrapActions.ReAuth -> {
|
is BootstrapActions.ReAuth -> {
|
||||||
startInitializeFlow(
|
startInitializeFlow(
|
||||||
state.currentReAuth?.copy(password = action.pass)
|
state.currentReAuth?.copy(password = action.pass)
|
||||||
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
is BootstrapActions.DoMigrateWithPassphrase -> {
|
||||||
|
startMigrationFlow(state.step, action.passphrase, null)
|
||||||
|
}
|
||||||
|
is BootstrapActions.DoMigrateWithRecoveryKey -> {
|
||||||
|
startMigrationFlow(state.step, null, action.recoveryKey)
|
||||||
|
}
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +272,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
// Business Logic
|
// Business Logic
|
||||||
// =======================================
|
// =======================================
|
||||||
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
|
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
os.use {
|
os.use {
|
||||||
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray())
|
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray())
|
||||||
@ -231,6 +293,57 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun startMigrationFlow(prevState: BootstrapStep, passphrase: String?, recoveryKey: String?) {
|
||||||
|
setState {
|
||||||
|
copy(step = BootstrapStep.Initializing)
|
||||||
|
}
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
val progressListener = object : BootstrapProgressListener {
|
||||||
|
override fun onProgress(data: WaitingViewData) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
initializationWaitingViewData = data
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
migrationTask.invoke(this, BackupToQuadSMigrationTask.Params(passphrase, recoveryKey, progressListener)) {
|
||||||
|
if (it is BackupToQuadSMigrationTask.Result.Success) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
passphrase = passphrase,
|
||||||
|
passphraseRepeat = passphrase,
|
||||||
|
migrationRecoveryKey = recoveryKey
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val auth = reAuthHelper.rememberedAuth()
|
||||||
|
if (auth == null) {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = BootstrapStep.AccountPassword(false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
startInitializeFlow(auth)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(
|
||||||
|
BootstrapViewEvents.ModalError(
|
||||||
|
(it as? BackupToQuadSMigrationTask.Result.Failure)?.error
|
||||||
|
?: stringProvider.getString(R.string.matrix_error
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = prevState
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
||||||
setState {
|
setState {
|
||||||
copy(step = BootstrapStep.Initializing)
|
copy(step = BootstrapStep.Initializing)
|
||||||
@ -247,11 +360,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
withState { state ->
|
withState { state ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
bootstrapTask.invoke(this, Params(
|
bootstrapTask.invoke(this, Params(
|
||||||
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
||||||
progressListener = progressListener,
|
progressListener = progressListener,
|
||||||
passphrase = state.passphrase
|
passphrase = state.passphrase,
|
||||||
|
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
|
||||||
)) {
|
)) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is BootstrapResult.Success -> {
|
is BootstrapResult.Success -> {
|
||||||
@ -309,6 +423,25 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun queryBack() = withState { state ->
|
private fun queryBack() = withState { state ->
|
||||||
when (state.step) {
|
when (state.step) {
|
||||||
|
is BootstrapStep.GetBackupSecretPassForMigration -> {
|
||||||
|
if (state.step.useKey) {
|
||||||
|
// go back to passphrase
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
step = BootstrapStep.GetBackupSecretPassForMigration(
|
||||||
|
isPasswordVisible = state.step.isPasswordVisible,
|
||||||
|
useKey = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is BootstrapStep.GetBackupSecretKeyForMigration -> {
|
||||||
|
// do we let you cancel from here?
|
||||||
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||||
|
}
|
||||||
is BootstrapStep.SetupPassphrase -> {
|
is BootstrapStep.SetupPassphrase -> {
|
||||||
// do we let you cancel from here?
|
// do we let you cancel from here?
|
||||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||||
@ -344,7 +477,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
||||||
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||||
return fragment.bootstrapViewModelFactory.create(state)
|
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
|
||||||
|
?: BootstrapBottomSheet.Args(true)
|
||||||
|
return fragment.bootstrapViewModelFactory.create(state, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
package im.vector.riotx.features.crypto.recover
|
||||||
|
|
||||||
import android.os.Bundle
|
import androidx.core.view.isVisible
|
||||||
import android.view.View
|
|
||||||
import com.airbnb.mvrx.parentFragmentViewModel
|
import com.airbnb.mvrx.parentFragmentViewModel
|
||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
@ -31,12 +30,22 @@ class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() {
|
|||||||
|
|
||||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||||
if (state.step !is BootstrapStep.Initializing) return@withState
|
when (state.step) {
|
||||||
|
is BootstrapStep.Initializing -> {
|
||||||
|
bootstrapLoadingStatusText.isVisible = true
|
||||||
|
bootstrapDescriptionText.isVisible = true
|
||||||
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
|
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
|
||||||
}
|
}
|
||||||
|
// is BootstrapStep.CheckingMigration -> {
|
||||||
|
// bootstrapLoadingStatusText.isVisible = false
|
||||||
|
// bootstrapDescriptionText.isVisible = false
|
||||||
|
// }
|
||||||
|
else -> {
|
||||||
|
// just show the spinner
|
||||||
|
bootstrapLoadingStatusText.isVisible = false
|
||||||
|
bootstrapDescriptionText.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,7 +97,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||||||
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||||
}
|
}
|
||||||
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
||||||
BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet")
|
BootstrapBottomSheet.show(supportFragmentManager, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -109,6 +109,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||||||
}
|
}
|
||||||
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
||||||
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
||||||
|
sharedActionViewModel.isAccountCreation = true
|
||||||
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,28 +164,47 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
|||||||
.getMyCrossSigningKeys()
|
.getMyCrossSigningKeys()
|
||||||
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
|
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
|
||||||
|
|
||||||
if (crossSigningEnabledOnAccount && myCrossSigningKeys?.isTrusted() == false) {
|
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
|
||||||
// We need to ask
|
// We need to ask
|
||||||
|
promptSecurityEvent(
|
||||||
|
session,
|
||||||
|
R.string.upgrade_security,
|
||||||
|
R.string.security_prompt_text
|
||||||
|
) {
|
||||||
|
it.navigator.upgradeSessionSecurity(it)
|
||||||
|
}
|
||||||
|
} else if (myCrossSigningKeys?.isTrusted() == false) {
|
||||||
|
// We need to ask
|
||||||
|
promptSecurityEvent(
|
||||||
|
session,
|
||||||
|
R.string.complete_security,
|
||||||
|
R.string.crosssigning_verify_this_session
|
||||||
|
) {
|
||||||
|
it.navigator.waitSessionVerification(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
|
||||||
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
|
||||||
popupAlertManager.postVectorAlert(
|
popupAlertManager.postVectorAlert(
|
||||||
VerificationVectorAlert(
|
VerificationVectorAlert(
|
||||||
uid = "completeSecurity",
|
uid = "upgradeSecurity",
|
||||||
title = getString(R.string.complete_security),
|
title = getString(titleRes),
|
||||||
description = getString(R.string.crosssigning_verify_this_session),
|
description = getString(descRes),
|
||||||
iconId = R.drawable.ic_shield_warning
|
iconId = R.drawable.ic_shield_warning
|
||||||
).apply {
|
).apply {
|
||||||
matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
|
matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
|
||||||
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
|
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
|
||||||
contentAction = Runnable {
|
contentAction = Runnable {
|
||||||
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
|
||||||
it.navigator.waitSessionVerification(it)
|
action(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismissedAction = Runnable {}
|
dismissedAction = Runnable {}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
super.onNewIntent(intent)
|
super.onNewIntent(intent)
|
||||||
|
@ -21,4 +21,5 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
|
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
|
||||||
var hasDisplayedCompleteSecurityPrompt : Boolean = false
|
var hasDisplayedCompleteSecurityPrompt : Boolean = false
|
||||||
|
var isAccountCreation : Boolean = false
|
||||||
}
|
}
|
||||||
|
@ -81,7 +81,8 @@ abstract class VerificationRequestItem : AbsBaseMessageItem<VerificationRequestI
|
|||||||
}
|
}
|
||||||
VerificationState.CANCELED_BY_OTHER -> {
|
VerificationState.CANCELED_BY_OTHER -> {
|
||||||
holder.buttonBar.isVisible = false
|
holder.buttonBar.isVisible = false
|
||||||
holder.statusTextView.text = holder.view.context.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
|
holder.statusTextView.text = holder.view.context
|
||||||
|
.getString(R.string.verification_request_other_cancelled, attributes.informationData.memberName)
|
||||||
holder.statusTextView.isVisible = true
|
holder.statusTextView.isVisible = true
|
||||||
}
|
}
|
||||||
VerificationState.CANCELED_BY_ME -> {
|
VerificationState.CANCELED_BY_ME -> {
|
||||||
|
@ -43,7 +43,11 @@ class VideoMediaViewerActivity : VectorBaseActivity() {
|
|||||||
|
|
||||||
configureToolbar(videoMediaViewerToolbar, mediaData)
|
configureToolbar(videoMediaViewerToolbar, mediaData)
|
||||||
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
|
imageContentRenderer.render(mediaData.thumbnailMediaData, ImageContentRenderer.Mode.FULL_SIZE, videoMediaViewerThumbnailView)
|
||||||
videoContentRenderer.render(mediaData, videoMediaViewerThumbnailView, videoMediaViewerLoading, videoMediaViewerVideoView, videoMediaViewerErrorView)
|
videoContentRenderer.render(mediaData,
|
||||||
|
videoMediaViewerThumbnailView,
|
||||||
|
videoMediaViewerLoading,
|
||||||
|
videoMediaViewerVideoView,
|
||||||
|
videoMediaViewerErrorView)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +34,7 @@ import im.vector.riotx.core.utils.toast
|
|||||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||||
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
import im.vector.riotx.features.crypto.keysbackup.setup.KeysBackupSetupActivity
|
||||||
|
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||||
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
import im.vector.riotx.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||||
import im.vector.riotx.features.debug.DebugMenuActivity
|
import im.vector.riotx.features.debug.DebugMenuActivity
|
||||||
@ -107,6 +108,12 @@ class DefaultNavigator @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun upgradeSessionSecurity(context: Context) {
|
||||||
|
if (context is VectorBaseActivity) {
|
||||||
|
BootstrapBottomSheet.show(context.supportFragmentManager, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) {
|
override fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String?, buildTask: Boolean) {
|
||||||
if (context is VectorBaseActivity) {
|
if (context is VectorBaseActivity) {
|
||||||
context.notImplemented("Open not joined room")
|
context.notImplemented("Open not joined room")
|
||||||
|
@ -34,6 +34,8 @@ interface Navigator {
|
|||||||
|
|
||||||
fun waitSessionVerification(context: Context)
|
fun waitSessionVerification(context: Context)
|
||||||
|
|
||||||
|
fun upgradeSessionSecurity(context: Context)
|
||||||
|
|
||||||
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
|
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)
|
||||||
|
|
||||||
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
|
fun openNotJoinedRoom(context: Context, roomIdOrAlias: String?, eventId: String? = null, buildTask: Boolean = false)
|
||||||
|
@ -203,7 +203,10 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
|
|||||||
*/
|
*/
|
||||||
private fun exportKeys() {
|
private fun exportKeys() {
|
||||||
// We need WRITE_EXTERNAL permission
|
// We need WRITE_EXTERNAL permission
|
||||||
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES, this, PERMISSION_REQUEST_CODE_EXPORT_KEYS, R.string.permissions_rationale_msg_keys_backup_export)) {
|
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
|
||||||
|
this,
|
||||||
|
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
|
||||||
|
R.string.permissions_rationale_msg_keys_backup_export)) {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
|
||||||
override fun onPassphrase(passphrase: String) {
|
override fun onPassphrase(passphrase: String) {
|
||||||
|
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_text">This might take several seconds, please be patient.</string>
|
||||||
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
||||||
<string name="your_recovery_key">Your recovery key</string>
|
<string name="your_recovery_key">Your recovery key</string>
|
||||||
<string name="bootstrap_finish_title">You‘re done!</string>
|
<string name="bootstrap_finish_title">"You're done!"</string>
|
||||||
<string name="keep_it_safe">Keep it safe</string>
|
<string name="keep_it_safe">Keep it safe</string>
|
||||||
<string name="finish">Finish</string>
|
<string name="finish">Finish</string>
|
||||||
|
|
||||||
|
@ -7,6 +7,27 @@
|
|||||||
|
|
||||||
<!-- BEGIN Strings added by Valere -->
|
<!-- BEGIN Strings added by Valere -->
|
||||||
<string name="room_message_placeholder">Message…</string>
|
<string name="room_message_placeholder">Message…</string>
|
||||||
|
|
||||||
|
<string name="upgrade_security">Encryption upgrade available</string>
|
||||||
|
<string name="security_prompt_text">Verify yourself & others to keep your chats safe</string>
|
||||||
|
|
||||||
|
<!-- %s will be replaced by recovery_key -->
|
||||||
|
<string name="bootstrap_enter_recovery">Enter your %s to continue</string>
|
||||||
|
<string name="use_file">Use File</string>
|
||||||
|
|
||||||
|
<!-- %s will be replaced by recovery_passphrase -->
|
||||||
|
<!-- <string name="upgrade_account_desc">Upgrade this session to allow it to verify other sessions, granting them access to encrypted messages and marking them as trusted for other users.</string>-->
|
||||||
|
<string name="enter_backup_passphrase">Enter %s</string>
|
||||||
|
<string name="backup_recovery_passphrase">Recovery Passphrase</string>
|
||||||
|
<string name="bootstrap_invalid_recovery_key">"It's not a valid recovery key"</string>
|
||||||
|
|
||||||
|
<string name="bootstrap_progress_checking_backup">Checking backup Key</string>
|
||||||
|
<string name="bootstrap_progress_checking_backup_with_info">Checking backup Key (%s)</string>
|
||||||
|
<string name="bootstrap_progress_compute_curve_key">Getting curve key</string>
|
||||||
|
<string name="bootstrap_progress_generating_ssss">Generating SSSS key from passphrase</string>
|
||||||
|
<string name="bootstrap_progress_generating_ssss_with_info">Generating SSSS key from passphrase (%s)</string>
|
||||||
|
<string name="bootstrap_progress_generating_ssss_recovery">Generating SSSS key from recovery key</string>
|
||||||
|
<string name="bootstrap_progress_storing_in_sss">Storing keybackup secret in SSSS</string>
|
||||||
<!-- END Strings added by Valere -->
|
<!-- END Strings added by Valere -->
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user