mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-29 14:38:45 +03:00
Merge pull request #1259 from vector-im/feature/restore_backup_from_ssss
KeyBackup / Use 4S if key in quadS
This commit is contained in:
commit
491f0e6032
8 changed files with 303 additions and 174 deletions
|
@ -455,6 +455,7 @@ Bugfix:
|
||||||
- Fix messages with empty `in_reply_to` not rendering (#447)
|
- Fix messages with empty `in_reply_to` not rendering (#447)
|
||||||
- Fix clear cache (#408) and Logout (#205)
|
- Fix clear cache (#408) and Logout (#205)
|
||||||
- Fix `(edited)` link can be copied to clipboard (#402)
|
- Fix `(edited)` link can be copied to clipboard (#402)
|
||||||
|
- KeyBackup / SSSS | Should get the key from SSSS instead of asking recovery Key (#1163)
|
||||||
|
|
||||||
Build:
|
Build:
|
||||||
- Split APK: generate one APK per arch, to reduce APK size of about 30%
|
- Split APK: generate one APK per arch, to reduce APK size of about 30%
|
||||||
|
|
|
@ -728,7 +728,8 @@ internal class DefaultKeysBackupService @Inject constructor(
|
||||||
if (backUp) {
|
if (backUp) {
|
||||||
maybeBackupKeys()
|
maybeBackupKeys()
|
||||||
}
|
}
|
||||||
|
// Save for next time and for gossiping
|
||||||
|
saveBackupRecoveryKey(recoveryKey, keysVersionResult.version)
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}.foldToCallback(callback)
|
}.foldToCallback(callback)
|
||||||
|
|
|
@ -20,16 +20,22 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
import im.vector.riotx.core.extensions.addFragmentToBackstack
|
||||||
import im.vector.riotx.core.extensions.observeEvent
|
import im.vector.riotx.core.extensions.observeEvent
|
||||||
import im.vector.riotx.core.extensions.replaceFragment
|
import im.vector.riotx.core.extensions.replaceFragment
|
||||||
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
import im.vector.riotx.core.platform.SimpleFragmentActivity
|
||||||
|
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
||||||
|
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||||
|
|
||||||
class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
private const val REQUEST_4S_SECRET = 100
|
||||||
|
const val SECRET_ALIAS = SharedSecureStorageActivity.DEFAULT_RESULT_KEYSTORE_ALIAS
|
||||||
|
|
||||||
fun intent(context: Context): Intent {
|
fun intent(context: Context): Intent {
|
||||||
return Intent(context, KeysBackupRestoreActivity::class.java)
|
return Intent(context, KeysBackupRestoreActivity::class.java)
|
||||||
}
|
}
|
||||||
|
@ -39,14 +45,20 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
|
private lateinit var viewModel: KeysBackupRestoreSharedViewModel
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
hideWaitingView()
|
||||||
|
super.onBackPressed()
|
||||||
|
}
|
||||||
|
|
||||||
override fun initUiAndData() {
|
override fun initUiAndData() {
|
||||||
super.initUiAndData()
|
super.initUiAndData()
|
||||||
viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java)
|
viewModel = viewModelProvider.get(KeysBackupRestoreSharedViewModel::class.java)
|
||||||
viewModel.initSession(session)
|
viewModel.initSession(session)
|
||||||
viewModel.keyVersionResult.observe(this, Observer { keyVersion ->
|
|
||||||
|
|
||||||
if (keyVersion != null && supportFragmentManager.fragments.isEmpty()) {
|
viewModel.keySourceModel.observe(this, Observer { keySource ->
|
||||||
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
if (keySource != null && !keySource.isInQuadS && supportFragmentManager.fragments.isEmpty()) {
|
||||||
|
val isBackupCreatedFromPassphrase =
|
||||||
|
viewModel.keyVersionResult.value?.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
|
||||||
if (isBackupCreatedFromPassphrase) {
|
if (isBackupCreatedFromPassphrase) {
|
||||||
replaceFragment(R.id.container, KeysBackupRestoreFromPassphraseFragment::class.java)
|
replaceFragment(R.id.container, KeysBackupRestoreFromPassphraseFragment::class.java)
|
||||||
} else {
|
} else {
|
||||||
|
@ -69,7 +81,7 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
|
|
||||||
if (viewModel.keyVersionResult.value == null) {
|
if (viewModel.keyVersionResult.value == null) {
|
||||||
// We need to fetch from API
|
// We need to fetch from API
|
||||||
viewModel.getLatestVersion(this)
|
viewModel.getLatestVersion()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
|
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
|
||||||
|
@ -78,8 +90,25 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java)
|
addFragmentToBackstack(R.id.container, KeysBackupRestoreFromKeyFragment::class.java)
|
||||||
}
|
}
|
||||||
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
|
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_SUCCESS -> {
|
||||||
|
viewModel.keyVersionResult.value?.version?.let {
|
||||||
|
KeysBackupBanner.onRecoverDoneForVersion(this, it)
|
||||||
|
}
|
||||||
replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java)
|
replaceFragment(R.id.container, KeysBackupRestoreSuccessFragment::class.java)
|
||||||
}
|
}
|
||||||
|
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_4S -> {
|
||||||
|
launch4SActivity()
|
||||||
|
}
|
||||||
|
KeysBackupRestoreSharedViewModel.NAVIGATE_FAILED_TO_LOAD_4S -> {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle(R.string.unknown_error)
|
||||||
|
.setMessage(R.string.error_failed_to_import_keys)
|
||||||
|
.setCancelable(false)
|
||||||
|
.setPositiveButton(R.string.ok) { _, _ ->
|
||||||
|
// nop
|
||||||
|
launch4SActivity()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,4 +122,30 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun launch4SActivity() {
|
||||||
|
SharedSecureStorageActivity.newIntent(
|
||||||
|
context = this,
|
||||||
|
keyId = null, // default key
|
||||||
|
requestedSecrets = listOf(KEYBACKUP_SECRET_SSSS_NAME),
|
||||||
|
resultKeyStoreAlias = SECRET_ALIAS
|
||||||
|
).let {
|
||||||
|
startActivityForResult(it, REQUEST_4S_SECRET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
if (requestCode == REQUEST_4S_SECRET) {
|
||||||
|
val extraResult = data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)
|
||||||
|
if (resultCode == Activity.RESULT_OK && extraResult != null) {
|
||||||
|
viewModel.handleGotSecretFromSSSS(
|
||||||
|
extraResult,
|
||||||
|
SECRET_ALIAS
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,7 +82,7 @@ class KeysBackupRestoreFromKeyFragment @Inject constructor()
|
||||||
if (value.isNullOrBlank()) {
|
if (value.isNullOrBlank()) {
|
||||||
viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message)
|
viewModel.recoveryCodeErrorText.value = context?.getString(R.string.keys_backup_recovery_code_empty_error_message)
|
||||||
} else {
|
} else {
|
||||||
viewModel.recoverKeys(requireContext(), sharedViewModel)
|
viewModel.recoverKeys(sharedViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,21 +15,19 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.crypto.keysbackup.restore
|
package im.vector.riotx.features.crypto.keysbackup.restore
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import androidx.lifecycle.viewModelScope
|
||||||
import im.vector.matrix.android.api.listeners.StepProgressListener
|
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.platform.WaitingViewData
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
|
class KeysBackupRestoreFromKeyViewModel @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
var recoveryCode: MutableLiveData<String> = MutableLiveData()
|
var recoveryCode: MutableLiveData<String> = MutableLiveData()
|
||||||
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
|
var recoveryCodeErrorText: MutableLiveData<String> = MutableLiveData()
|
||||||
|
@ -45,66 +43,16 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor() : ViewModel() {
|
||||||
recoveryCodeErrorText.value = null
|
recoveryCodeErrorText.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
fun recoverKeys(sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
||||||
val session = sharedViewModel.session
|
sharedViewModel.loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading)))
|
||||||
val keysBackup = session.cryptoService().keysBackupService()
|
|
||||||
|
|
||||||
recoveryCodeErrorText.value = null
|
recoveryCodeErrorText.value = null
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val recoveryKey = recoveryCode.value!!
|
val recoveryKey = recoveryCode.value!!
|
||||||
|
try {
|
||||||
val keysVersionResult = sharedViewModel.keyVersionResult.value!!
|
sharedViewModel.recoverUsingBackupPass(recoveryKey)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
keysBackup.restoreKeysWithRecoveryKey(keysVersionResult,
|
recoveryCodeErrorText.value = stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt)
|
||||||
recoveryKey,
|
|
||||||
null,
|
|
||||||
session.myUserId,
|
|
||||||
object : StepProgressListener {
|
|
||||||
override fun onStepProgress(step: StepProgressListener.Step) {
|
|
||||||
when (step) {
|
|
||||||
is StepProgressListener.Step.DownloadingKey -> {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
|
|
||||||
isIndeterminate = true))
|
|
||||||
}
|
|
||||||
is StepProgressListener.Step.ImportingKey -> {
|
|
||||||
// Progress 0 can take a while, display an indeterminate progress in this case
|
|
||||||
if (step.progress == 0) {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
|
|
||||||
isIndeterminate = true))
|
|
||||||
} else {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
|
|
||||||
step.progress,
|
|
||||||
step.total))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
object : MatrixCallback<ImportRoomKeysResult> {
|
|
||||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
|
||||||
sharedViewModel.loadingEvent.value = null
|
|
||||||
sharedViewModel.didRecoverSucceed(data)
|
|
||||||
|
|
||||||
KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!)
|
|
||||||
trustOnDecrypt(keysBackup, keysVersionResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
sharedViewModel.loadingEvent.value = null
|
|
||||||
recoveryCodeErrorText.value = context.getString(R.string.keys_backup_recovery_code_error_decrypt)
|
|
||||||
Timber.e(failure, "## onUnexpectedError")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) {
|
|
||||||
keysBackup.trustKeysBackupVersion(keysVersionResult, true,
|
|
||||||
object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.v("##### trustKeysBackupVersion onSuccess")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ import im.vector.riotx.core.extensions.showPassword
|
||||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseFragment() {
|
class KeysBackupRestoreFromPassphraseFragment @Inject constructor() : VectorBaseFragment() {
|
||||||
|
|
||||||
override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase
|
override fun getLayoutResId() = R.layout.fragment_keys_backup_restore_from_passphrase
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class KeysBackupRestoreFromPassphraseFragment @Inject constructor(): VectorBaseF
|
||||||
if (value.isNullOrBlank()) {
|
if (value.isNullOrBlank()) {
|
||||||
viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message)
|
viewModel.passphraseErrorText.value = context?.getString(R.string.passphrase_empty_error_message)
|
||||||
} else {
|
} else {
|
||||||
viewModel.recoverKeys(context!!, sharedViewModel)
|
viewModel.recoverKeys(sharedViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,21 +15,18 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.crypto.keysbackup.restore
|
package im.vector.riotx.features.crypto.keysbackup.restore
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import androidx.lifecycle.viewModelScope
|
||||||
import im.vector.matrix.android.api.listeners.StepProgressListener
|
|
||||||
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.platform.WaitingViewData
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.core.ui.views.KeysBackupBanner
|
import kotlinx.coroutines.Dispatchers
|
||||||
import timber.log.Timber
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel() {
|
class KeysBackupRestoreFromPassphraseViewModel @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
var passphrase: MutableLiveData<String> = MutableLiveData()
|
var passphrase: MutableLiveData<String> = MutableLiveData()
|
||||||
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
|
var passphraseErrorText: MutableLiveData<String> = MutableLiveData()
|
||||||
|
@ -48,71 +45,14 @@ class KeysBackupRestoreFromPassphraseViewModel @Inject constructor() : ViewModel
|
||||||
passphraseErrorText.value = null
|
passphraseErrorText.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun recoverKeys(context: Context, sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
fun recoverKeys(sharedViewModel: KeysBackupRestoreSharedViewModel) {
|
||||||
val keysBackup = sharedViewModel.session.cryptoService().keysBackupService()
|
|
||||||
|
|
||||||
passphraseErrorText.value = null
|
passphraseErrorText.value = null
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val keysVersionResult = sharedViewModel.keyVersionResult.value!!
|
try {
|
||||||
|
sharedViewModel.recoverUsingBackupPass(passphrase.value!!)
|
||||||
keysBackup.restoreKeyBackupWithPassword(keysVersionResult,
|
} catch (failure: Throwable) {
|
||||||
passphrase.value!!,
|
passphraseErrorText.value = stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)
|
||||||
null,
|
|
||||||
sharedViewModel.session.myUserId,
|
|
||||||
object : StepProgressListener {
|
|
||||||
override fun onStepProgress(step: StepProgressListener.Step) {
|
|
||||||
when (step) {
|
|
||||||
is StepProgressListener.Step.ComputingKey -> {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
|
|
||||||
step.progress,
|
|
||||||
step.total))
|
|
||||||
}
|
|
||||||
is StepProgressListener.Step.DownloadingKey -> {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
|
|
||||||
isIndeterminate = true))
|
|
||||||
}
|
|
||||||
is StepProgressListener.Step.ImportingKey -> {
|
|
||||||
Timber.d("backupKeys.ImportingKey.progress: ${step.progress}")
|
|
||||||
// Progress 0 can take a while, display an indeterminate progress in this case
|
|
||||||
if (step.progress == 0) {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
|
|
||||||
isIndeterminate = true))
|
|
||||||
} else {
|
|
||||||
sharedViewModel.loadingEvent.postValue(WaitingViewData(context.getString(R.string.keys_backup_restoring_waiting_message)
|
|
||||||
+ "\n" + context.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
|
|
||||||
step.progress,
|
|
||||||
step.total))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
object : MatrixCallback<ImportRoomKeysResult> {
|
|
||||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
|
||||||
sharedViewModel.loadingEvent.value = null
|
|
||||||
sharedViewModel.didRecoverSucceed(data)
|
|
||||||
|
|
||||||
KeysBackupBanner.onRecoverDoneForVersion(context, keysVersionResult.version!!)
|
|
||||||
trustOnDecrypt(keysBackup, keysVersionResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
sharedViewModel.loadingEvent.value = null
|
|
||||||
passphraseErrorText.value = context.getString(R.string.keys_backup_passphrase_error_decrypt)
|
|
||||||
Timber.e(failure, "## onUnexpectedError")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) {
|
|
||||||
keysBackup.trustKeysBackupVersion(keysVersionResult, true,
|
|
||||||
object : MatrixCallback<Unit> {
|
|
||||||
override fun onSuccess(data: Unit) {
|
|
||||||
Timber.v("##### trustKeysBackupVersion onSuccess")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,30 +15,52 @@
|
||||||
*/
|
*/
|
||||||
package im.vector.riotx.features.crypto.keysbackup.restore
|
package im.vector.riotx.features.crypto.keysbackup.restore
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.listeners.StepProgressListener
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.crypto.keysbackup.KeysBackupService
|
||||||
|
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
|
||||||
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
|
||||||
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||||
|
import im.vector.matrix.android.internal.util.awaitCallback
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
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.utils.LiveEvent
|
import im.vector.riotx.core.utils.LiveEvent
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() {
|
class KeysBackupRestoreSharedViewModel @Inject constructor(
|
||||||
|
private val stringProvider: StringProvider
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
data class KeySource(
|
||||||
|
val isInMemory: Boolean,
|
||||||
|
val isInQuadS: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY"
|
const val NAVIGATE_TO_RECOVER_WITH_KEY = "NAVIGATE_TO_RECOVER_WITH_KEY"
|
||||||
const val NAVIGATE_TO_SUCCESS = "NAVIGATE_TO_SUCCESS"
|
const val NAVIGATE_TO_SUCCESS = "NAVIGATE_TO_SUCCESS"
|
||||||
|
const val NAVIGATE_TO_4S = "NAVIGATE_TO_4S"
|
||||||
|
const val NAVIGATE_FAILED_TO_LOAD_4S = "NAVIGATE_FAILED_TO_LOAD_4S"
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var session: Session
|
lateinit var session: Session
|
||||||
|
|
||||||
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
|
var keyVersionResult: MutableLiveData<KeysVersionResult> = MutableLiveData()
|
||||||
|
|
||||||
|
var keySourceModel: MutableLiveData<KeySource> = MutableLiveData()
|
||||||
|
|
||||||
private var _keyVersionResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
|
private var _keyVersionResultError: MutableLiveData<LiveEvent<String>> = MutableLiveData()
|
||||||
val keyVersionResultError: LiveData<LiveEvent<String>>
|
val keyVersionResultError: LiveData<LiveEvent<String>>
|
||||||
get() = _keyVersionResultError
|
get() = _keyVersionResultError
|
||||||
|
@ -62,28 +84,190 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() {
|
||||||
this.session = session
|
this.session = session
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLatestVersion(context: Context) {
|
val progressObserver = object : StepProgressListener {
|
||||||
|
override fun onStepProgress(step: StepProgressListener.Step) {
|
||||||
|
when (step) {
|
||||||
|
is StepProgressListener.Step.ComputingKey -> {
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message)
|
||||||
|
+ "\n" + stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
|
||||||
|
step.progress,
|
||||||
|
step.total))
|
||||||
|
}
|
||||||
|
is StepProgressListener.Step.DownloadingKey -> {
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message)
|
||||||
|
+ "\n" + stringProvider.getString(R.string.keys_backup_restoring_downloading_backup_waiting_message),
|
||||||
|
isIndeterminate = true))
|
||||||
|
}
|
||||||
|
is StepProgressListener.Step.ImportingKey -> {
|
||||||
|
Timber.d("backupKeys.ImportingKey.progress: ${step.progress}")
|
||||||
|
// Progress 0 can take a while, display an indeterminate progress in this case
|
||||||
|
if (step.progress == 0) {
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message)
|
||||||
|
+ "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
|
||||||
|
isIndeterminate = true))
|
||||||
|
} else {
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message)
|
||||||
|
+ "\n" + stringProvider.getString(R.string.keys_backup_restoring_importing_keys_waiting_message),
|
||||||
|
step.progress,
|
||||||
|
step.total))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLatestVersion() {
|
||||||
val keysBackup = session.cryptoService().keysBackupService()
|
val keysBackup = session.cryptoService().keysBackupService()
|
||||||
|
|
||||||
loadingEvent.value = WaitingViewData(context.getString(R.string.keys_backup_restore_is_getting_backup_version))
|
loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version))
|
||||||
|
|
||||||
keysBackup.getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
override fun onSuccess(data: KeysVersionResult?) {
|
try {
|
||||||
loadingEvent.value = null
|
val version = awaitCallback<KeysVersionResult?> {
|
||||||
if (data?.version.isNullOrBlank()) {
|
keysBackup.getCurrentVersion(it)
|
||||||
// should not happen
|
}
|
||||||
_keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, ""))
|
if (version?.version == null) {
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
_keyVersionResultError.postValue(LiveEvent(stringProvider.getString(R.string.keys_backup_get_version_error, "")))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
keyVersionResult.postValue(version)
|
||||||
|
// Let's check if there is quads
|
||||||
|
val isBackupKeyInQuadS = isBackupKeyInQuadS()
|
||||||
|
|
||||||
|
val savedSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
|
||||||
|
if (savedSecret != null && savedSecret.version == version.version) {
|
||||||
|
// key is in memory!
|
||||||
|
keySourceModel.postValue(
|
||||||
|
KeySource(isInMemory = true, isInQuadS = true)
|
||||||
|
)
|
||||||
|
// Go and use it!!
|
||||||
|
try {
|
||||||
|
recoverUsingBackupRecoveryKey(savedSecret.recoveryKey)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
keySourceModel.postValue(
|
||||||
|
KeySource(isInMemory = false, isInQuadS = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if (isBackupKeyInQuadS) {
|
||||||
|
// key is in QuadS!
|
||||||
|
keySourceModel.postValue(
|
||||||
|
KeySource(isInMemory = false, isInQuadS = true)
|
||||||
|
)
|
||||||
|
_navigateEvent.postValue(LiveEvent(NAVIGATE_TO_4S))
|
||||||
} else {
|
} else {
|
||||||
keyVersionResult.value = data
|
// we need to restore directly
|
||||||
|
keySourceModel.postValue(
|
||||||
|
KeySource(isInMemory = false, isInQuadS = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
_keyVersionResultError.postValue(LiveEvent(stringProvider.getString(R.string.keys_backup_get_version_error, failure.localizedMessage)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
fun handleGotSecretFromSSSS(cipherData: String, alias: String) {
|
||||||
loadingEvent.value = null
|
try {
|
||||||
_keyVersionResultError.value = LiveEvent(context.getString(R.string.keys_backup_get_version_error, failure.localizedMessage))
|
cipherData.fromBase64().inputStream().use { ins ->
|
||||||
|
val res = session.loadSecureSecret<Map<String, String>>(ins, alias)
|
||||||
|
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME)
|
||||||
|
if (secret == null) {
|
||||||
|
_navigateEvent.postValue(
|
||||||
|
LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version))
|
||||||
|
|
||||||
// TODO For network error
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
// _keyVersionResultError.value = LiveEvent(context.getString(R.string.network_error_please_check_and_retry))
|
try {
|
||||||
|
recoverUsingBackupRecoveryKey(computeRecoveryKey(secret.fromBase64()))
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_navigateEvent.postValue(
|
||||||
|
LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
_navigateEvent.postValue(
|
||||||
|
LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun recoverUsingBackupPass(passphrase: String) {
|
||||||
|
val keysBackup = session.cryptoService().keysBackupService()
|
||||||
|
val keyVersion = keyVersionResult.value ?: return
|
||||||
|
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading)))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = awaitCallback<ImportRoomKeysResult> {
|
||||||
|
keysBackup.restoreKeyBackupWithPassword(keyVersion,
|
||||||
|
passphrase,
|
||||||
|
null,
|
||||||
|
session.myUserId,
|
||||||
|
progressObserver,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
didRecoverSucceed(result)
|
||||||
|
trustOnDecrypt(keysBackup, keyVersion)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun recoverUsingBackupRecoveryKey(recoveryKey: String) {
|
||||||
|
val keysBackup = session.cryptoService().keysBackupService()
|
||||||
|
val keyVersion = keyVersionResult.value ?: return
|
||||||
|
|
||||||
|
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading)))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = awaitCallback<ImportRoomKeysResult> {
|
||||||
|
keysBackup.restoreKeysWithRecoveryKey(keyVersion,
|
||||||
|
recoveryKey,
|
||||||
|
null,
|
||||||
|
session.myUserId,
|
||||||
|
progressObserver,
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
didRecoverSucceed(result)
|
||||||
|
trustOnDecrypt(keysBackup, keyVersion)
|
||||||
|
} catch (failure: Throwable) {
|
||||||
|
loadingEvent.postValue(null)
|
||||||
|
throw failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isBackupKeyInQuadS(): Boolean {
|
||||||
|
val sssBackupSecret = session.getAccountDataEvent(KEYBACKUP_SECRET_SSSS_NAME)
|
||||||
|
?: return false
|
||||||
|
|
||||||
|
// Some sanity ?
|
||||||
|
val defaultKeyResult = session.sharedSecretStorageService.getDefaultKey()
|
||||||
|
val keyInfo = (defaultKeyResult as? KeyInfoResult.Success)?.keyInfo
|
||||||
|
?: return false
|
||||||
|
|
||||||
|
return (sssBackupSecret.content["encrypted"] as? Map<*, *>)?.containsKey(keyInfo.id) == true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) {
|
||||||
|
keysBackup.trustKeysBackupVersion(keysVersionResult, true,
|
||||||
|
object : MatrixCallback<Unit> {
|
||||||
|
override fun onSuccess(data: Unit) {
|
||||||
|
Timber.v("##### trustKeysBackupVersion onSuccess")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -94,6 +278,6 @@ class KeysBackupRestoreSharedViewModel @Inject constructor() : ViewModel() {
|
||||||
|
|
||||||
fun didRecoverSucceed(result: ImportRoomKeysResult) {
|
fun didRecoverSucceed(result: ImportRoomKeysResult) {
|
||||||
importKeyResult = result
|
importKeyResult = result
|
||||||
_navigateEvent.value = LiveEvent(NAVIGATE_TO_SUCCESS)
|
_navigateEvent.postValue(LiveEvent(NAVIGATE_TO_SUCCESS))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue