Make keybackup service suspend + fixes

This commit is contained in:
Valere 2021-11-25 12:19:31 +01:00
parent f0f64d8380
commit 210e0241d3
23 changed files with 2105 additions and 2163 deletions

View file

@ -24,6 +24,7 @@ interface StepProgressListener {
sealed class Step {
data class ComputingKey(val progress: Int, val total: Int) : Step()
object DownloadingKey : Step()
data class DecryptingKey(val progress: Int, val total: Int) : Step()
data class ImportingKey(val progress: Int, val total: Int) : Step()
}

View file

@ -16,7 +16,6 @@
package org.matrix.android.sdk.api.session.crypto.keysbackup
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.listeners.StepProgressListener
import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust
@ -31,18 +30,17 @@ interface KeysBackupService {
* Retrieve the current version of the backup from the homeserver
*
* It can be different than keysBackupVersion.
* @param callback onSuccess(null) will be called if there is no backup on the server
*/
fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>)
suspend fun getCurrentVersion(): KeysVersionResult?
/**
* Create a new keys backup version and enable it, using the information return from [prepareKeysBackupVersion].
*
* @param keysBackupCreationInfo the info object from [prepareKeysBackupVersion].
* @param callback Asynchronous callback
* @return KeysVersion
*/
fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo,
callback: MatrixCallback<KeysVersion>)
@Throws
suspend fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo): KeysVersion
/**
* Facility method to get the total number of locally stored keys
@ -54,23 +52,21 @@ interface KeysBackupService {
*/
fun getTotalNumbersOfBackedUpKeys(): Int
/**
* Start to back up keys immediately.
*
* @param progressListener the callback to follow the progress
* @param callback the main callback
*/
fun backupAllGroupSessions(progressListener: ProgressListener?,
callback: MatrixCallback<Unit>?)
// /**
// * Start to back up keys immediately.
// *
// * @param progressListener the callback to follow the progress
// * @param callback the main callback
// */
// fun backupAllGroupSessions(progressListener: ProgressListener?,
// callback: MatrixCallback<Unit>?)
/**
* Check trust on a key backup version.
*
* @param keysBackupVersion the backup version to check.
* @param callback block called when the operations completes.
*/
fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult,
callback: MatrixCallback<KeysBackupVersionTrust>)
suspend fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust
/**
* Return the current progress of the backup
@ -82,18 +78,16 @@ interface KeysBackupService {
*
* It can be different than keysBackupVersion.
* @param version the backup version
* @param callback
*/
fun getVersion(version: String,
callback: MatrixCallback<KeysVersionResult?>)
suspend fun getVersion(version: String): KeysVersionResult?
/**
* This method fetches the last backup version on the server, then compare to the currently backup version use.
* If versions are not the same, the current backup is deleted (on server or locally), then the backup may be started again, using the last version.
*
* @param callback true if backup is already using the last version, and false if it is not the case
* @return true if backup is already using the last version, and false if it is not the case
*/
fun forceUsingLastVersion(callback: MatrixCallback<Boolean>)
suspend fun forceUsingLastVersion(): Boolean
/**
* Check the server for an active key backup.
@ -101,7 +95,7 @@ interface KeysBackupService {
* If one is present and has a valid signature from one of the user's verified
* devices, start backing up to it.
*/
fun checkAndStartKeysBackup()
suspend fun checkAndStartKeysBackup()
fun addListener(listener: KeysBackupStateListener)
@ -119,19 +113,16 @@ interface KeysBackupService {
* @param progressListener a progress listener, as generating private key from password may take a while
* @param callback Asynchronous callback
*/
fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>)
suspend fun prepareKeysBackupVersion(password: String?): MegolmBackupCreationInfo
/**
* Delete a keys backup version. It will delete all backed up keys on the server, and the backup itself.
* If we are backing up to this version. Backup will be stopped.
*
* @param version the backup version to delete.
* @param callback Asynchronous callback
*/
fun deleteBackup(version: String,
callback: MatrixCallback<Unit>?)
@Throws
suspend fun deleteBackup(version: String)
/**
* Ask if the backup on the server contains keys that we may do not have locally.
@ -145,35 +136,29 @@ interface KeysBackupService {
*
* @param keysBackupVersion the backup version to check.
* @param trust the trust to set to the keys backup.
* @param callback block called when the operations completes.
*/
fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
trust: Boolean,
callback: MatrixCallback<Unit>)
@Throws
suspend fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean)
/**
* Set trust on a keys backup version.
*
* @param keysBackupVersion the backup version to check.
* @param recoveryKey the recovery key to challenge with the key backup public key.
* @param callback block called when the operations completes.
*/
fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
recoveryKey: String,
callback: MatrixCallback<Unit>)
suspend fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
recoveryKey: String)
/**
* Set trust on a keys backup version.
*
* @param keysBackupVersion the backup version to check.
* @param password the pass phrase to challenge with the keyBackupVersion public key.
* @param callback block called when the operations completes.
*/
fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
password: String,
callback: MatrixCallback<Unit>)
suspend fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
password: String)
fun onSecretKeyGossip(secret: String)
suspend fun onSecretKeyGossip(secret: String)
/**
* Restore a backup with a recovery key from a given backup version stored on the homeserver.
@ -185,11 +170,10 @@ interface KeysBackupService {
* @param stepProgressListener the step progress listener
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
*/
fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
suspend fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
recoveryKey: String, roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>)
stepProgressListener: StepProgressListener?): ImportRoomKeysResult
/**
* Restore a backup with a password from a given backup version stored on the homeserver.
@ -201,12 +185,11 @@ interface KeysBackupService {
* @param stepProgressListener the step progress listener
* @param callback Callback. It provides the number of found keys and the number of successfully imported keys.
*/
fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
suspend fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
password: String,
roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>)
stepProgressListener: StepProgressListener?): ImportRoomKeysResult
val keysBackupVersion: KeysVersionResult?
val currentBackupVersion: String?
@ -218,5 +201,5 @@ interface KeysBackupService {
fun saveBackupRecoveryKey(recoveryKey: String?, version: String?)
fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo?
fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>)
suspend fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String): Boolean
}

View file

@ -256,7 +256,12 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun inboundGroupSessionsCount(onlyBackedUp: Boolean): Int {
return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
return if (onlyBackedUp) {
keysBackupService.getTotalNumbersOfBackedUpKeys()
} else {
keysBackupService.getTotalNumbersOfKeys()
}
// return cryptoStore.inboundGroupSessionsCount(onlyBackedUp)
}
/**
@ -331,9 +336,11 @@ internal class DefaultCryptoService @Inject constructor(
// We try to enable key backups, if the backup version on the server is trusted,
// we're gonna continue backing up.
cryptoCoroutineScope.launch {
tryOrNull {
keysBackupService.checkAndStartKeysBackup()
}
}
// Open the store
cryptoStore.open()

View file

@ -70,4 +70,15 @@ data class MegolmSessionData(
*/
@Json(name = "forwarding_curve25519_key_chain")
val forwardingCurve25519KeyChain: List<String>? = null
)
) {
fun isValid(): Boolean {
return roomId != null &&
forwardingCurve25519KeyChain != null &&
algorithm != null &&
senderKey != null &&
senderClaimedKeys != null &&
sessionId != null &&
sessionKey != null
}
}

View file

@ -213,13 +213,14 @@ internal class RequestSender @Inject constructor(
suspend fun getKeyBackupVersion(version: String? = null): KeysVersionResult? {
return try {
if (version != null) {
getKeysBackupVersionTask.execute(version)
getKeysBackupVersionTask.executeRetry(version, 3)
} else {
getKeysBackupLastVersionTask.execute(Unit)
getKeysBackupLastVersionTask.executeRetry(Unit, 3)
}
} catch (failure: Throwable) {
if (failure is Failure.ServerError &&
failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
null
} else {
throw failure

View file

@ -21,7 +21,6 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.internal.crypto.IncomingRoomKeyRequest
import org.matrix.android.sdk.internal.crypto.IncomingSecretShareRequest
import org.matrix.android.sdk.internal.crypto.MXEventDecryptionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
/**
* An interface for decrypting data
@ -44,7 +43,7 @@ internal interface IMXDecrypting {
*
* @param event the key event.
*/
fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {}
fun onRoomKeyEvent(event: Event/*, defaultKeysBackupService: DefaultKeysBackupService*/) {}
/**
* Check if the some messages can be decrypted with a new session

View file

@ -34,7 +34,6 @@ import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevice
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.algorithms.IMXDecrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXWithHeldExtension
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyContent
@ -232,7 +231,7 @@ internal class MXMegolmDecryption(private val userId: String,
*
* @param event the key event.
*/
override fun onRoomKeyEvent(event: Event, defaultKeysBackupService: DefaultKeysBackupService) {
override fun onRoomKeyEvent(event: Event/*, defaultKeysBackupService: DefaultKeysBackupService*/) {
Timber.tag(loggerTag.value).v("onRoomKeyEvent()")
var exportFormat = false
val roomKeyContent = event.getClearContent().toModel<RoomKeyContent>() ?: return
@ -295,7 +294,7 @@ internal class MXMegolmDecryption(private val userId: String,
exportFormat)
if (added) {
defaultKeysBackupService.maybeBackupKeys()
// defaultKeysBackupService.maybeBackupKeys()
val content = RoomKeyRequestBody(
algorithm = roomKeyContent.algorithm,

View file

@ -31,7 +31,7 @@ import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevice
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.algorithms.IMXEncrypting
import org.matrix.android.sdk.internal.crypto.algorithms.IMXGroupEncryption
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
// import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.RoomKeyWithHeldContent
@ -53,7 +53,7 @@ internal class MXMegolmEncryption(
// The id of the room we will be sending to.
private val roomId: String,
private val olmDevice: MXOlmDevice,
private val defaultKeysBackupService: DefaultKeysBackupService,
// private val defaultKeysBackupService: DefaultKeysBackupService,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
@ -149,7 +149,7 @@ internal class MXMegolmEncryption(
olmDevice.addInboundGroupSession(sessionId!!, olmDevice.getSessionKey(sessionId)!!, roomId, olmDevice.deviceCurve25519Key!!,
emptyList(), keysClaimedMap, false)
defaultKeysBackupService.maybeBackupKeys()
// defaultKeysBackupService.maybeBackupKeys()
return MXOutboundSessionInfo(sessionId, SharedWithHelper(roomId, sessionId, cryptoStore))
}

View file

@ -22,7 +22,6 @@ import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.actions.EnsureOlmSessionsForDevicesAction
import org.matrix.android.sdk.internal.crypto.actions.MessageEncrypter
import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService
import org.matrix.android.sdk.internal.crypto.repository.WarnOnUnknownDeviceRepository
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.tasks.SendToDeviceTask
@ -32,7 +31,7 @@ import javax.inject.Inject
internal class MXMegolmEncryptionFactory @Inject constructor(
private val olmDevice: MXOlmDevice,
private val defaultKeysBackupService: DefaultKeysBackupService,
// private val defaultKeysBackupService: DefaultKeysBackupService,
private val cryptoStore: IMXCryptoStore,
private val deviceListManager: DeviceListManager,
private val ensureOlmSessionsForDevicesAction: EnsureOlmSessionsForDevicesAction,
@ -48,7 +47,7 @@ internal class MXMegolmEncryptionFactory @Inject constructor(
return MXMegolmEncryption(
roomId = roomId,
olmDevice = olmDevice,
defaultKeysBackupService = defaultKeysBackupService,
// defaultKeysBackupService = defaultKeysBackupService,
cryptoStore = cryptoStore,
deviceListManager = deviceListManager,
ensureOlmSessionsForDevicesAction = ensureOlmSessionsForDevicesAction,

View file

@ -17,6 +17,7 @@
package org.matrix.android.sdk.internal.crypto.keysbackup
import android.os.Handler
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import timber.log.Timber
@ -33,15 +34,17 @@ internal class KeysBackupStateManager(private val uiHandler: Handler) {
field = newState
// Notify listeners about the state change, on the ui thread
uiHandler.post {
synchronized(listeners) {
listeners.forEach {
uiHandler.post {
// Use newState because state may have already changed again
tryOrNull {
it.onStateChange(newState)
}
}
}
}
}
val isEnabled: Boolean
get() = state == KeysBackupState.ReadyToBackUp ||

View file

@ -18,16 +18,16 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
import android.os.Handler
import android.os.Looper
import androidx.annotation.UiThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.listeners.ProgressListener
@ -52,10 +52,8 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.UpdateKeysBa
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.olm.OlmException
import timber.log.Timber
import uniffi.olm.BackupRecoveryKey
@ -91,7 +89,9 @@ internal class RustKeyBackupService @Inject constructor(
override var keysBackupVersion: KeysVersionResult? = null
private set
private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
// private var backupAllGroupSessionsCallback: MatrixCallback<Unit>? = null
private val importScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
private var keysBackupStateListener: KeysBackupStateListener? = null
@ -115,13 +115,10 @@ internal class RustKeyBackupService @Inject constructor(
keysBackupStateManager.removeListener(listener)
}
override fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
withContext(coroutineDispatchers.crypto) {
override suspend fun prepareKeysBackupVersion(password: String?): MegolmBackupCreationInfo {
return withContext(coroutineDispatchers.computation) {
val key = if (password != null) {
// this might be a bit slow as it's stretching the password
BackupRecoveryKey.newFromPassphrase(password)
} else {
BackupRecoveryKey()
@ -151,13 +148,10 @@ internal class RustKeyBackupService @Inject constructor(
recoveryKey = key.toBase58()
)
}
}.foldToCallback(callback)
}
}
override fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo,
callback: MatrixCallback<KeysVersion>) {
@Suppress("UNCHECKED_CAST")
override suspend fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo): KeysVersion {
return withContext(coroutineDispatchers.crypto) {
val createKeysBackupVersionBody = CreateKeysBackupVersionBody(
algorithm = keysBackupCreationInfo.algorithm,
authData = keysBackupCreationInfo.authData.toJsonDict()
@ -165,14 +159,13 @@ internal class RustKeyBackupService @Inject constructor(
keysBackupStateManager.state = KeysBackupState.Enabling
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
try {
val data = sender.createKeyBackup(createKeysBackupVersionBody)
val data = withContext(coroutineDispatchers.io) {
sender.createKeyBackup(createKeysBackupVersionBody)
}
// Reset backup markers.
// Don't we need to join the task here? Isn't this a race condition?
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
olmMachine.disableBackup()
}
val keyBackupVersion = KeysVersionResult(
algorithm = createKeysBackupVersionBody.algorithm,
@ -182,13 +175,11 @@ internal class RustKeyBackupService @Inject constructor(
count = 0,
hash = ""
)
enableKeysBackup(keyBackupVersion)
callback.onSuccess(data)
data
} catch (failure: Throwable) {
keysBackupStateManager.state = KeysBackupState.Disabled
callback.onFailure(failure)
throw failure
}
}
}
@ -200,7 +191,7 @@ internal class RustKeyBackupService @Inject constructor(
}
private fun resetBackupAllGroupSessionsListeners() {
backupAllGroupSessionsCallback = null
// backupAllGroupSessionsCallback = null
keysBackupStateListener?.let {
keysBackupStateManager.removeListener(it)
@ -219,8 +210,7 @@ internal class RustKeyBackupService @Inject constructor(
olmMachine.disableBackup()
}
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
override suspend fun deleteBackup(version: String) {
withContext(coroutineDispatchers.crypto) {
if (keysBackupVersion != null && version == keysBackupVersion?.version) {
resetKeysBackupData()
@ -228,20 +218,16 @@ internal class RustKeyBackupService @Inject constructor(
keysBackupStateManager.state = KeysBackupState.Unknown
}
fun eventuallyRestartBackup() {
try {
sender.deleteKeyBackup(version)
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
if (state == KeysBackupState.Unknown) {
checkAndStartKeysBackup()
}
}
try {
sender.deleteKeyBackup(version)
eventuallyRestartBackup()
uiHandler.post { callback?.onSuccess(Unit) }
} catch (failure: Throwable) {
eventuallyRestartBackup()
uiHandler.post { callback?.onFailure(failure) }
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
if (state == KeysBackupState.Unknown) {
checkAndStartKeysBackup()
}
}
}
@ -264,12 +250,12 @@ internal class RustKeyBackupService @Inject constructor(
return olmMachine.roomKeyCounts().backedUp.toInt()
}
override fun backupAllGroupSessions(progressListener: ProgressListener?,
callback: MatrixCallback<Unit>?) {
// This is only used in tests? While it's fine have methods that are
// only used for tests, this one has a lot of logic that is nowhere else used.
TODO()
}
// override fun backupAllGroupSessions(progressListener: ProgressListener?,
// callback: MatrixCallback<Unit>?) {
// // This is only used in tests? While it's fine have methods that are
// // only used for tests, this one has a lot of logic that is nowhere else used.
// TODO()
// }
private suspend fun checkBackupTrust(authData: MegolmBackupAuthData?): KeysBackupVersionTrust {
return if (authData == null || authData.publicKey.isEmpty() || authData.signatures.isNullOrEmpty()) {
@ -280,22 +266,15 @@ internal class RustKeyBackupService @Inject constructor(
}
}
override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult,
callback: MatrixCallback<KeysBackupVersionTrust>) {
override suspend fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
cryptoCoroutineScope.launch {
try {
callback.onSuccess(checkBackupTrust(authData))
} catch (exception: Throwable) {
callback.onFailure(exception)
}
return withContext(coroutineDispatchers.crypto) {
checkBackupTrust(authData)
}
}
override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
trust: Boolean,
callback: MatrixCallback<Unit>) {
override suspend fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, trust: Boolean) {
withContext(coroutineDispatchers.crypto) {
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
// Get auth data to update it
@ -303,11 +282,8 @@ internal class RustKeyBackupService @Inject constructor(
if (authData == null) {
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
callback.onFailure(IllegalArgumentException("Missing element"))
throw IllegalArgumentException("Missing element")
} else {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val body = withContext(coroutineDispatchers.crypto) {
// Get current signatures, or create an empty set
val userId = olmMachine.userId()
val signatures = authData.signatures?.get(userId).orEmpty().toMutableMap()
@ -328,14 +304,14 @@ internal class RustKeyBackupService @Inject constructor(
val newSignatures = newAuthData.signatures.orEmpty().toMutableMap()
newSignatures[userId] = signatures
@Suppress("UNCHECKED_CAST")
UpdateKeysBackupVersionBody(
val body = UpdateKeysBackupVersionBody(
algorithm = keysBackupVersion.algorithm,
authData = newAuthData.copy(signatures = newSignatures).toJsonDict(),
version = keysBackupVersion.version)
}
try {
withContext(coroutineDispatchers.io) {
sender.updateBackup(keysBackupVersion, body)
}
val newKeysBackupVersion = KeysVersionResult(
algorithm = keysBackupVersion.algorithm,
@ -346,16 +322,12 @@ internal class RustKeyBackupService @Inject constructor(
)
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
callback.onSuccess(Unit)
} catch (exception: Throwable) {
callback.onFailure(exception)
}
}
}
}
// Check that the recovery key matches to the public key that we downloaded from the server.
// If they match, we can trust the public key and enable backups since we have the private key.
// If they match, we can trust the public key and enable backups since we have the private key.
private fun checkRecoveryKey(recoveryKey: BackupRecoveryKey, keysBackupData: KeysVersionResult) {
val backupKey = recoveryKey.megolmV1PublicKey()
val authData = getMegolmBackupAuthData(keysBackupData)
@ -376,60 +348,50 @@ internal class RustKeyBackupService @Inject constructor(
}
}
override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
recoveryKey: String,
callback: MatrixCallback<Unit>) {
override suspend fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, recoveryKey: String) {
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
cryptoCoroutineScope.launch {
try {
withContext(coroutineDispatchers.crypto) {
// This is ~nowhere mentioned, the string here is actually a base58 encoded key.
// This not really supported by the spec for the backup key, the 4S key supports
// base58 encoding and the same method seems to be used here.
val key = BackupRecoveryKey.fromBase58(recoveryKey)
checkRecoveryKey(key, keysBackupVersion)
trustKeysBackupVersion(keysBackupVersion, true, callback)
} catch (exception: Throwable) {
callback.onFailure(exception)
}
trustKeysBackupVersion(keysBackupVersion, true)
}
}
override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
password: String,
callback: MatrixCallback<Unit>) {
cryptoCoroutineScope.launch {
try {
override suspend fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, password: String) {
withContext(coroutineDispatchers.crypto) {
val key = recoveryKeyFromPassword(password, keysBackupVersion)
checkRecoveryKey(key, keysBackupVersion)
trustKeysBackupVersion(keysBackupVersion, true, callback)
} catch (exception: Throwable) {
Timber.w(exception)
callback.onFailure(exception)
}
trustKeysBackupVersion(keysBackupVersion, true)
}
}
override fun onSecretKeyGossip(secret: String) {
override suspend fun onSecretKeyGossip(curveKeyBase64: String) {
Timber.i("## CrossSigning - onSecretKeyGossip")
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
try {
val version = sender.getKeyBackupVersion()
if (version != null) {
val key = BackupRecoveryKey.fromBase64(secret)
val key = BackupRecoveryKey.fromBase64(curveKeyBase64)
if (isValidRecoveryKey(key, version)) {
trustKeysBackupVersion(version, true)
// we don't want to wait for that
importScope.launch {
try {
val importResult = restoreBackup(version, key, null, null, null)
awaitCallback<Unit> {
trustKeysBackupVersion(version, true, it)
}
val importResult = awaitCallback<ImportRoomKeysResult> {
cryptoCoroutineScope.launch {
restoreBackup(version, key, null, null, null)
}
}
Timber.i("onSecretKeyGossip: Recovered keys ${importResult.successfullyNumberOfImportedKeys} out of ${importResult.totalNumberOfKeys}")
saveBackupRecoveryKey(secret, version.version)
} catch (failure: Throwable) {
// fail silently..
Timber.e(failure, "onSecretKeyGossip: Failed to import keys from backup")
}
}
// we can save, it's valid
saveBackupRecoveryKey(key.toBase64(), version.version)
}
} else {
Timber.e("onSecretKeyGossip: Failed to import backup recovery key, no backup version was found on the server")
}
@ -511,9 +473,14 @@ internal class RustKeyBackupService @Inject constructor(
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
throw InvalidParameterException("Invalid recovery key")
}
// Save for next time and for gossiping
saveBackupRecoveryKey(recoveryKey.toBase64(), keysVersionResult.version)
}
withContext(coroutineDispatchers.main) {
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
}
// Get backed up keys from the homeserver
val data = getKeys(sessionId, roomId, keysVersionResult.version)
@ -522,13 +489,33 @@ internal class RustKeyBackupService @Inject constructor(
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
cryptoCoroutineScope.launch(Dispatchers.Main) {
stepProgressListener?.onStepProgress(StepProgressListener.Step.DecryptingKey(0, data.roomIdToRoomKeysBackupData.size))
}
var progressDecryptIndex = 0
// TODO this is quite long, could we add some concurrency here?
for ((roomIdLoop, backupData) in data.roomIdToRoomKeysBackupData) {
val roomIndex = progressDecryptIndex
progressDecryptIndex++
cryptoCoroutineScope.launch(Dispatchers.Main) {
stepProgressListener?.onStepProgress(StepProgressListener.Step.DecryptingKey(roomIndex, data.roomIdToRoomKeysBackupData.size))
}
for ((sessionIdLoop, keyBackupData) in backupData.sessionIdToKeyBackupData) {
sessionsFromHsCount++
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, recoveryKey)
sessionData?.let {
// rust is not very lax and will throw if field are missing,
// add a check
// TODO maybe could be done on rust side?
sessionData?.takeIf {
it.isValid().also {
if (!it) {
Timber.w("restoreKeysWithRecoveryKey: malformed sessionData $sessionData")
}
}
}?.let {
sessionsData.add(it)
}
}
@ -548,9 +535,11 @@ internal class RustKeyBackupService @Inject constructor(
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
cryptoCoroutineScope.launch(Dispatchers.Main) {
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
}
}
}
} else {
null
}
@ -562,69 +551,60 @@ internal class RustKeyBackupService @Inject constructor(
maybeBackupKeys()
}
// Save for next time and for gossiping
saveBackupRecoveryKey(recoveryKey.toBase64(), keysVersionResult.version)
result
}
}
override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
override suspend fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
recoveryKey: String,
roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) {
stepProgressListener: StepProgressListener?): ImportRoomKeysResult {
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
val key = BackupRecoveryKey.fromBase58(recoveryKey)
restoreBackup(keysVersionResult, key, roomId, sessionId, stepProgressListener)
}.foldToCallback(callback)
}
return restoreBackup(keysVersionResult, key, roomId, sessionId, stepProgressListener)
}
override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
override suspend fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
password: String,
roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) {
stepProgressListener: StepProgressListener?): ImportRoomKeysResult {
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
val recoveryKey = withContext(coroutineDispatchers.crypto) {
recoveryKeyFromPassword(password, keysBackupVersion)
}
restoreBackup(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener)
}.foldToCallback(callback)
}
return restoreBackup(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener)
}
override fun getVersion(version: String, callback: MatrixCallback<KeysVersionResult?>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
override suspend fun getVersion(version: String): KeysVersionResult? {
return withContext(coroutineDispatchers.io) {
sender.getKeyBackupVersion(version)
}.foldToCallback(callback)
}
}
override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
@Throws
override suspend fun getCurrentVersion(): KeysVersionResult? {
return withContext(coroutineDispatchers.io) {
sender.getKeyBackupVersion()
}.foldToCallback(callback)
}
}
private suspend fun forceUsingLastVersionHelper(): Boolean {
val response = sender.getKeyBackupVersion()
override suspend fun forceUsingLastVersion(): Boolean {
val response = withContext(coroutineDispatchers.io) {
sender.getKeyBackupVersion()
}
return withContext(coroutineDispatchers.crypto) {
val serverBackupVersion = response?.version
val localBackupVersion = keysBackupVersion?.version
Timber.d("BACKUP: $serverBackupVersion")
return if (serverBackupVersion == null) {
if (serverBackupVersion == null) {
if (localBackupVersion == null) {
// No backup on the server, and backup is not active
true
@ -648,46 +628,43 @@ internal class RustKeyBackupService @Inject constructor(
true
} else {
// This will automatically check for the last version then
deleteBackup(localBackupVersion, null)
tryOrNull("Failed to automatically check for the last version") {
deleteBackup(localBackupVersion)
}
// We are not using the last version, so delete the current version we are using on the server
false
}
}
}
}
override fun forceUsingLastVersion(callback: MatrixCallback<Boolean>) {
cryptoCoroutineScope.launch {
runCatching {
forceUsingLastVersionHelper()
}.foldToCallback(callback)
}
}
override fun checkAndStartKeysBackup() {
override suspend fun checkAndStartKeysBackup() {
withContext(coroutineDispatchers.crypto) {
if (!isStucked) {
// Try to start or restart the backup only if it is in unknown or bad state
Timber.w("checkAndStartKeysBackup: invalid state: $state")
return
return@withContext
}
keysBackupVersion = null
keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver
getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
override fun onSuccess(data: KeysVersionResult?) {
withContext(coroutineDispatchers.io) {
try {
val data = getCurrentVersion()
withContext(coroutineDispatchers.crypto) {
checkAndStartWithKeysBackupVersion(data)
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version")
keysBackupStateManager.state = KeysBackupState.Unknown
}
})
}
}
}
private fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) {
private suspend fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) {
Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}")
keysBackupVersion = keyBackupVersion
@ -697,8 +674,8 @@ internal class RustKeyBackupService @Inject constructor(
resetKeysBackupData()
keysBackupStateManager.state = KeysBackupState.Disabled
} else {
getKeysBackupTrust(keyBackupVersion, object : MatrixCallback<KeysBackupVersionTrust> {
override fun onSuccess(data: KeysBackupVersionTrust) {
try {
val data = getKeysBackupTrust(keyBackupVersion)
val versionInStore = getKeyBackupRecoveryKeyInfo()?.version
if (data.usable) {
@ -722,12 +699,9 @@ internal class RustKeyBackupService @Inject constructor(
keysBackupStateManager.state = KeysBackupState.NotTrusted
}
} catch (failure: Throwable) {
Timber.e(failure, "Failed to checkAndStartWithKeysBackupVersion $keyBackupVersion")
}
override fun onFailure(failure: Throwable) {
// Cannot happen
}
})
}
}
@ -737,14 +711,17 @@ internal class RustKeyBackupService @Inject constructor(
return authData.publicKey == publicKey
}
override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback<Boolean>) {
val keysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) }
override suspend fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String): Boolean {
return withContext(coroutineDispatchers.crypto) {
val keysBackupVersion = keysBackupVersion ?: return@withContext false
try {
val key = BackupRecoveryKey.fromBase64(recoveryKey)
callback.onSuccess(isValidRecoveryKey(key, keysBackupVersion))
} catch (error: Throwable) {
callback.onFailure(error)
try {
isValidRecoveryKey(key, keysBackupVersion)
} catch (failure: Throwable) {
Timber.i("isValidRecoveryKeyForCurrentVersion: Invalid recovery key")
false
}
}
}
@ -822,7 +799,8 @@ internal class RustKeyBackupService @Inject constructor(
/**
* Do a backup if there are new keys, with a delay
*/
fun maybeBackupKeys() {
suspend fun maybeBackupKeys() {
withContext(coroutineDispatchers.crypto) {
when {
isStucked -> {
// If not already done, or in error case, check for a valid backup version on the homeserver.
@ -837,12 +815,9 @@ internal class RustKeyBackupService @Inject constructor(
// new key is sent
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS)
cryptoCoroutineScope.launch {
importScope.launch {
delay(delayInMs)
// TODO is this correct? we used to call uiHandler.post() instead of this
withContext(Dispatchers.Main) {
backupKeys()
}
tryOrNull("AUTO backup failed") { backupKeys() }
}
}
else -> {
@ -850,27 +825,27 @@ internal class RustKeyBackupService @Inject constructor(
}
}
}
}
/**
* Send a chunk of keys to backup
*/
@UiThread
private suspend fun backupKeys(forceRecheck: Boolean = false) {
Timber.v("backupKeys")
withContext(coroutineDispatchers.crypto) {
// Sanity check, as this method can be called after a delay, the state may have change during the delay
if (!isEnabled || !olmMachine.backupEnabled() || keysBackupVersion == null) {
Timber.v("backupKeys: Invalid configuration $isEnabled ${olmMachine.backupEnabled()} $keysBackupVersion")
backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
// backupAllGroupSessionsCallback?.onFailure(IllegalStateException("Invalid configuration"))
resetBackupAllGroupSessionsListeners()
return
return@withContext
}
if (state === KeysBackupState.BackingUp && !forceRecheck) {
// Do nothing if we are already backing up
Timber.v("backupKeys: Invalid state: $state")
return
return@withContext
}
Timber.d("BACKUP: CREATING REQUEST")
@ -884,7 +859,7 @@ internal class RustKeyBackupService @Inject constructor(
// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
backupAllGroupSessionsCallback?.onSuccess(Unit)
// backupAllGroupSessionsCallback?.onSuccess(Unit)
resetBackupAllGroupSessionsListeners()
} else {
try {
@ -892,15 +867,12 @@ internal class RustKeyBackupService @Inject constructor(
keysBackupStateManager.state = KeysBackupState.BackingUp
Timber.d("BACKUP SENDING REQUEST")
val response = sender.backupRoomKeys(request)
val response = withContext(coroutineDispatchers.io) { sender.backupRoomKeys(request) }
Timber.d("BACKUP GOT RESPONSE $response")
olmMachine.markRequestAsSent(request.requestId, RequestType.KEYS_BACKUP, response)
Timber.d("BACKUP MARKED REQUEST AS SENT")
// TODO, again is this correct?
withContext(Dispatchers.Main) {
backupKeys(true)
}
} else {
// Can't happen, do we want to panic?
}
@ -915,7 +887,7 @@ internal class RustKeyBackupService @Inject constructor(
// Backup has been deleted on the server, or we are not using
// the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
// backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
resetKeysBackupData()
keysBackupVersion = null
@ -930,8 +902,7 @@ internal class RustKeyBackupService @Inject constructor(
}
}
} else {
withContext(Dispatchers.Main) {
backupAllGroupSessionsCallback?.onFailure(failure)
// backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
Timber.e("backupKeys: backupKeys failed: $failure")

View file

@ -83,11 +83,6 @@ class KeysBackupRestoreActivity : SimpleFragmentActivity() {
.show()
}
if (viewModel.keyVersionResult.value == null) {
// We need to fetch from API
viewModel.getLatestVersion()
}
viewModel.navigateEvent.observeEvent(this) { uxStateEvent ->
when (uxStateEvent) {
KeysBackupRestoreSharedViewModel.NAVIGATE_TO_RECOVER_WITH_KEY -> {

View file

@ -23,9 +23,11 @@ import im.vector.app.R
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.LiveEvent
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.listeners.StepProgressListener
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@ -35,7 +37,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber
import javax.inject.Inject
@ -75,10 +76,15 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
var importRoomKeysFinishWithResult: MutableLiveData<LiveEvent<ImportRoomKeysResult>> = MutableLiveData()
fun initSession(session: Session) {
if (!this::session.isInitialized) {
this.session = session
viewModelScope.launch {
getLatestVersion()
}
}
}
val progressObserver = object : StepProgressListener {
private val progressObserver = object : StepProgressListener {
override fun onStepProgress(step: StepProgressListener.Step) {
when (step) {
is StepProgressListener.Step.ComputingKey -> {
@ -106,24 +112,33 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
step.total))
}
}
is StepProgressListener.Step.DecryptingKey -> {
if (step.progress == 0) {
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restoring_waiting_message) +
"\n" + stringProvider.getString(R.string.keys_backup_restoring_decrypting_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_decrypting_keys_waiting_message),
step.progress,
step.total))
}
}
}
}
}
fun getLatestVersion() {
private suspend fun getLatestVersion() {
val keysBackup = session.cryptoService().keysBackupService()
loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version))
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version)))
viewModelScope.launch(Dispatchers.IO) {
try {
val version = awaitCallback<KeysVersionResult?> {
keysBackup.getCurrentVersion(it)
}
val version = keysBackup.getCurrentVersion()
if (version?.version == null) {
loadingEvent.postValue(null)
_keyVersionResultError.postValue(LiveEvent(stringProvider.getString(R.string.keys_backup_get_version_error, "")))
return@launch
return
}
keyVersionResult.postValue(version)
@ -138,8 +153,9 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
)
// Go and use it!!
try {
recoverUsingBackupRecoveryKey(savedSecret.recoveryKey)
recoverUsingBackupRecoveryKey(computeRecoveryKey(savedSecret.recoveryKey.fromBase64()), version)
} catch (failure: Throwable) {
Timber.e(failure, "## recoverUsingBackupRecoveryKey FAILED")
keySourceModel.postValue(
KeySource(isInMemory = false, isInQuadS = true)
)
@ -163,7 +179,6 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
_keyVersionResultError.postValue(LiveEvent(stringProvider.getString(R.string.keys_backup_get_version_error, failure.localizedMessage)))
}
}
}
fun handleGotSecretFromSSSS(cipherData: String, alias: String) {
try {
@ -176,11 +191,11 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
)
return
}
loadingEvent.value = WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version))
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.keys_backup_restore_is_getting_backup_version)))
viewModelScope.launch(Dispatchers.IO) {
try {
recoverUsingBackupRecoveryKey(computeRecoveryKey(secret.fromBase64()))
recoverUsingBackupRecoveryKey(secret)
} catch (failure: Throwable) {
_navigateEvent.postValue(
LiveEvent(NAVIGATE_FAILED_TO_LOAD_4S)
@ -202,15 +217,12 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading)))
try {
val result = awaitCallback<ImportRoomKeysResult> {
keysBackup.restoreKeyBackupWithPassword(keyVersion,
val result = keysBackup.restoreKeyBackupWithPassword(keyVersion,
passphrase,
null,
session.myUserId,
progressObserver,
it
progressObserver
)
}
loadingEvent.postValue(null)
didRecoverSucceed(result)
trustOnDecrypt(keysBackup, keyVersion)
@ -220,26 +232,27 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
}
}
suspend fun recoverUsingBackupRecoveryKey(recoveryKey: String) {
suspend fun recoverUsingBackupRecoveryKey(recoveryKey: String, keyVersion: KeysVersionResult? = null) {
val keysBackup = session.cryptoService().keysBackupService()
val keyVersion = keyVersionResult.value ?: return
// This is badddddd
val version = keyVersion ?: keyVersionResult.value ?: return
loadingEvent.postValue(WaitingViewData(stringProvider.getString(R.string.loading)))
try {
val result = awaitCallback<ImportRoomKeysResult> {
keysBackup.restoreKeysWithRecoveryKey(keyVersion,
val result = keysBackup.restoreKeysWithRecoveryKey(version,
recoveryKey,
null,
session.myUserId,
progressObserver,
it
progressObserver
)
}
loadingEvent.postValue(null)
withContext(Dispatchers.Main) {
didRecoverSucceed(result)
trustOnDecrypt(keysBackup, keyVersion)
trustOnDecrypt(keysBackup, version)
}
} catch (failure: Throwable) {
Timber.e(failure, "## restoreKeysWithRecoveryKey failure")
loadingEvent.postValue(null)
throw failure
}
@ -258,19 +271,19 @@ class KeysBackupRestoreSharedViewModel @Inject constructor(
}
private fun trustOnDecrypt(keysBackup: KeysBackupService, keysVersionResult: KeysVersionResult) {
keysBackup.trustKeysBackupVersion(keysVersionResult, true,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("##### trustKeysBackupVersion onSuccess")
// do that on session scope because could happen outside of view model lifecycle
session.coroutineScope.launch {
tryOrNull("## Failed to trustKeysBackupVersion") {
keysBackup.trustKeysBackupVersion(keysVersionResult, true)
}
}
})
}
fun moveToRecoverWithKey() {
_navigateEvent.value = LiveEvent(NAVIGATE_TO_RECOVER_WITH_KEY)
_navigateEvent.postValue(LiveEvent(NAVIGATE_TO_RECOVER_WITH_KEY))
}
fun didRecoverSucceed(result: ImportRoomKeysResult) {
private fun didRecoverSucceed(result: ImportRoomKeysResult) {
importKeyResult = result
_navigateEvent.postValue(LiveEvent(NAVIGATE_TO_SUCCESS))
}

View file

@ -27,13 +27,11 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupStateListener
import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import timber.log.Timber
class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialState: KeysBackupSettingViewState,
@ -70,7 +68,9 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
}
private fun init() {
keysBackupService.forceUsingLastVersion(NoOpMatrixCallback())
viewModelScope.launch {
keysBackupService.forceUsingLastVersion()
}
}
private fun getKeysBackupTrust() = withState { state ->
@ -86,18 +86,16 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
}
Timber.d("BACKUP: HEEEEEEE TWO")
keysBackupService
.getKeysBackupTrust(versionResult, object : MatrixCallback<KeysBackupVersionTrust> {
override fun onSuccess(data: KeysBackupVersionTrust) {
viewModelScope.launch {
try {
val data = keysBackupService.getKeysBackupTrust(versionResult)
Timber.d("BACKUP: HEEEE suceeeded $data")
setState {
copy(
keysBackupVersionTrust = Success(data)
)
}
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
Timber.d("BACKUP: HEEEE FAILED $failure")
setState {
copy(
@ -105,7 +103,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
)
}
}
})
}
}
}
@ -128,15 +126,16 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
private fun deleteCurrentBackup() {
val keysBackupService = keysBackupService
if (keysBackupService.currentBackupVersion != null) {
val currentBackupVersion = keysBackupService.currentBackupVersion
if (currentBackupVersion != null) {
setState {
copy(
deleteBackupRequest = Loading()
)
}
keysBackupService.deleteBackup(keysBackupService.currentBackupVersion!!, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
viewModelScope.launch {
try {
keysBackupService.deleteBackup(currentBackupVersion)
setState {
copy(
keysBackupVersion = null,
@ -145,16 +144,14 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS
deleteBackupRequest = Uninitialized
)
}
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
setState {
copy(
deleteBackupRequest = Fail(failure)
)
}
}
})
}
}
}

View file

@ -19,17 +19,16 @@ package im.vector.app.features.crypto.keysbackup.setup
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nulabinc.zxcvbn.Strength
import im.vector.app.R
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.utils.LiveEvent
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.listeners.ProgressListener
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import timber.log.Timber
import javax.inject.Inject
@ -89,48 +88,30 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
recoveryKey.value = null
prepareRecoverFailError.value = null
session.let { mxSession ->
val requestedId = currentRequestId.value!!
mxSession.cryptoService().keysBackupService().prepareKeysBackupVersion(withPassphrase,
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
viewModelScope.launch {
try {
val data = session.cryptoService().keysBackupService().prepareKeysBackupVersion(withPassphrase)
if (requestedId != currentRequestId.value) {
// this is an old request, we can't cancel but we can ignore
return
return@launch
}
loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_step3_generating_key_status),
progress,
total)
}
},
object : MatrixCallback<MegolmBackupCreationInfo> {
override fun onSuccess(data: MegolmBackupCreationInfo) {
if (requestedId != currentRequestId.value) {
// this is an old request, we can't cancel but we can ignore
return
}
recoveryKey.value = data.recoveryKey
recoveryKey.postValue(data.recoveryKey)
megolmBackupCreationInfo = data
copyHasBeenMade = false
val keyBackup = session.cryptoService().keysBackupService()
createKeysBackup(context, keyBackup)
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
if (requestedId != currentRequestId.value) {
// this is an old request, we can't cancel but we can ignore
return
return@launch
}
loadingStatus.value = null
isCreatingBackupVersion.value = false
prepareRecoverFailError.value = failure
loadingStatus.postValue(null)
isCreatingBackupVersion.postValue(false)
prepareRecoverFailError.postValue(failure)
}
})
}
}
@ -140,55 +121,47 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
}
fun stopAndKeepAfterDetectingExistingOnServer() {
loadingStatus.value = null
navigateEvent.value = LiveEvent(NAVIGATE_FINISH)
loadingStatus.postValue(null)
navigateEvent.postValue(LiveEvent(NAVIGATE_FINISH))
viewModelScope.launch {
session.cryptoService().keysBackupService().checkAndStartKeysBackup()
}
}
private fun createKeysBackup(context: Context, keysBackup: KeysBackupService, forceOverride: Boolean = false) {
loadingStatus.value = WaitingViewData(context.getString(R.string.keys_backup_setup_creating_backup), isIndeterminate = true)
creatingBackupError.value = null
keysBackup.getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
override fun onSuccess(data: KeysVersionResult?) {
viewModelScope.launch {
try {
val data = keysBackup.getCurrentVersion()
if (data?.version.isNullOrBlank() || forceOverride) {
processOnCreate()
processOnCreate(keysBackup)
} else {
loadingStatus.value = null
loadingStatus.postValue(null)
// we should prompt
isCreatingBackupVersion.value = false
navigateEvent.value = LiveEvent(NAVIGATE_PROMPT_REPLACE)
isCreatingBackupVersion.postValue(false)
navigateEvent.postValue(LiveEvent(NAVIGATE_PROMPT_REPLACE))
}
} catch (failure: Throwable) {
}
}
}
override fun onFailure(failure: Throwable) {
Timber.e(failure, "## createKeyBackupVersion")
loadingStatus.value = null
isCreatingBackupVersion.value = false
creatingBackupError.value = failure
}
fun processOnCreate() {
keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!, object : MatrixCallback<KeysVersion> {
override fun onSuccess(data: KeysVersion) {
loadingStatus.value = null
isCreatingBackupVersion.value = false
keysVersion.value = data
suspend fun processOnCreate(keysBackup: KeysBackupService) {
try {
loadingStatus.postValue(null)
val created = keysBackup.createKeysBackupVersion(megolmBackupCreationInfo!!)
isCreatingBackupVersion.postValue(false)
keysVersion.postValue(created)
navigateEvent.value = LiveEvent(NAVIGATE_TO_STEP_3)
}
override fun onFailure(failure: Throwable) {
} catch (failure: Throwable) {
Timber.e(failure, "## createKeyBackupVersion")
loadingStatus.value = null
loadingStatus.postValue(null)
isCreatingBackupVersion.value = false
creatingBackupError.value = failure
isCreatingBackupVersion.postValue(false)
creatingBackupError.postValue(failure)
}
})
}
})
}
}

View file

@ -20,7 +20,9 @@ import im.vector.app.R
import im.vector.app.core.platform.ViewModelTask
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.NoOpMatrixCallback
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@ -32,7 +34,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.keysbackup.deriveKey
import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber
import java.util.UUID
import javax.inject.Inject
@ -87,9 +88,7 @@ class BackupToQuadSMigrationTask @Inject constructor(
reportProgress(params, R.string.bootstrap_progress_compute_curve_key)
val recoveryKey = computeRecoveryKey(curveKey)
val isValid = awaitCallback<Boolean> {
keysBackupService.isValidRecoveryKeyForCurrentVersion(recoveryKey, it)
}
val isValid = keysBackupService.isValidRecoveryKeyForCurrentVersion(recoveryKey)
if (!isValid) return Result.InvalidRecoverySecret
@ -141,14 +140,17 @@ class BackupToQuadSMigrationTask @Inject constructor(
keysBackupService.saveBackupRecoveryKey(recoveryKey, version.version)
// while we are there let's restore, but do not block
session.coroutineScope.launch {
tryOrNull {
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
version,
recoveryKey,
null,
null,
null,
NoOpMatrixCallback()
null
)
}
}
return Result.Success
} catch (failure: Throwable) {

View file

@ -33,9 +33,6 @@ import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageServi
import org.matrix.android.sdk.api.session.securestorage.SsssKeyCreationInfo
import org.matrix.android.sdk.api.session.securestorage.SsssKeySpec
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber
@ -221,9 +218,7 @@ class BootstrapCrossSigningTask @Inject constructor(
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Checking megolm backup")
// First ensure that in sync
var serverVersion = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
var serverVersion = session.cryptoService().keysBackupService().getCurrentVersion()
val knownMegolmSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
val isMegolmBackupSecretKnown = knownMegolmSecret != null && knownMegolmSecret.version == serverVersion?.version
@ -233,21 +228,14 @@ class BootstrapCrossSigningTask @Inject constructor(
if (shouldCreateKeyBackup) {
// clear all existing backups
while (serverVersion != null) {
awaitCallback<Unit> {
session.cryptoService().keysBackupService().deleteBackup(serverVersion!!.version, it)
}
serverVersion = awaitCallback {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
session.cryptoService().keysBackupService().deleteBackup(serverVersion.version)
serverVersion = session.cryptoService().keysBackupService().getCurrentVersion()
}
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Create megolm backup")
val creationInfo = awaitCallback<MegolmBackupCreationInfo> {
session.cryptoService().keysBackupService().prepareKeysBackupVersion(null, null, it)
}
val version = awaitCallback<KeysVersion> {
session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo, it)
}
val creationInfo = session.cryptoService().keysBackupService().prepareKeysBackupVersion(null)
val version = session.cryptoService().keysBackupService().createKeysBackupVersion(creationInfo)
// Save it for gossiping
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Save megolm backup key for gossiping")
session.cryptoService().keysBackupService().saveBackupRecoveryKey(creationInfo.recoveryKey, version = version.version)
@ -264,12 +252,10 @@ class BootstrapCrossSigningTask @Inject constructor(
// ensure we store existing backup secret if we have it!
if (isMegolmBackupSecretKnown) {
// check it matches
val isValid = awaitCallback<Boolean> {
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownMegolmSecret!!.recoveryKey, it)
}
val isValid = session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownMegolmSecret!!.recoveryKey)
if (isValid) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
extractCurveKeyFromRecoveryKey(knownMegolmSecret!!.recoveryKey)?.toBase64NoPadding()?.let { secret ->
extractCurveKeyFromRecoveryKey(knownMegolmSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,

View file

@ -42,14 +42,13 @@ import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.internal.util.awaitCallback
import java.io.OutputStream
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@ -104,9 +103,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// We need to check if there is an existing backup
viewModelScope.launch(Dispatchers.IO) {
val version = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
}
val version = tryOrNull { session.cryptoService().keysBackupService().getCurrentVersion() }
if (version == null) {
// we just resume plain bootstrap
doesKeyBackupExist = false
@ -115,8 +113,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
} else {
// we need to get existing backup passphrase/key and convert to SSSS
val keyVersion = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getVersion(version.version, it)
val keyVersion = tryOrNull {
session.cryptoService().keysBackupService().getVersion(version.version)
}
if (keyVersion == null) {
// strange case... just finish?

View file

@ -31,8 +31,10 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -51,10 +53,7 @@ import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.computeRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult
import org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber
data class VerificationBottomSheetViewState(
@ -419,30 +418,27 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
}
private fun tentativeRestoreBackup(res: Map<String, String>?) {
viewModelScope.launch(Dispatchers.IO) {
// on session scope because will happen after viewmodel is cleared
session.coroutineScope.launch {
try {
val secret = res?.get(KEYBACKUP_SECRET_SSSS_NAME) ?: return@launch Unit.also {
Timber.v("## Keybackup secret not restored from SSSS")
}
val version = awaitCallback<KeysVersionResult?> {
session.cryptoService().keysBackupService().getCurrentVersion(it)
} ?: return@launch
val version = tryOrNull { session.cryptoService().keysBackupService().getCurrentVersion() }
?: return@launch
// TODO maybe mark as trusted earlier by checking recovery key early, then download?
awaitCallback<ImportRoomKeysResult> {
session.cryptoService().keysBackupService().restoreKeysWithRecoveryKey(
version,
computeRecoveryKey(secret.fromBase64()),
null,
null,
null,
it
null
)
}
awaitCallback<Unit> {
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true, it)
}
session.cryptoService().keysBackupService().trustKeysBackupVersion(version, true)
} catch (failure: Throwable) {
// Just ignore for now
Timber.e(failure, "## Failed to restore backup after SSSS recovery")

View file

@ -155,9 +155,11 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
fun refreshRemoteStateIfNeeded() {
if (keysBackupState.value == KeysBackupState.Disabled) {
viewModelScope.launch {
session.cryptoService().keysBackupService().checkAndStartKeysBackup()
}
}
}
override fun handle(action: EmptyAction) {}
}

View file

@ -73,7 +73,9 @@ class SignoutCheckViewModel @AssistedInject constructor(
init {
session.cryptoService().keysBackupService().addListener(this)
viewModelScope.launch {
session.cryptoService().keysBackupService().checkAndStartKeysBackup()
}
val quad4SIsSetup = session.sharedSecretStorageService.isRecoverySetup()
val allKeysKnown = session.cryptoService().crossSigningService().allPrivateKeysKnown()
@ -112,9 +114,11 @@ class SignoutCheckViewModel @AssistedInject constructor(
fun refreshRemoteStateIfNeeded() = withState { state ->
if (state.keysBackupState == KeysBackupState.Disabled) {
viewModelScope.launch {
session.cryptoService().keysBackupService().checkAndStartKeysBackup()
}
}
}
override fun handle(action: Actions) {
when (action) {

View file

@ -2041,6 +2041,7 @@
<string name="keys_backup_restoring_computing_key_waiting_message">Computing recovery key…</string>
<string name="keys_backup_restoring_downloading_backup_waiting_message">Downloading keys…</string>
<string name="keys_backup_restoring_importing_keys_waiting_message">Importing keys…</string>
<string name="keys_backup_restoring_decrypting_keys_waiting_message">Decrypting keys…</string>
<string name="keys_backup_unlock_button">Unlock History</string>
<string name="keys_backup_recovery_code_empty_error_message">Please enter a recovery key</string>
<string name="keys_backup_recovery_code_error_decrypt">Backup could not be decrypted with this recovery key: please verify that you entered the correct recovery key.</string>
@ -3442,6 +3443,7 @@
<string name="command_description_join_space">Join the Space with the given id</string>
<string name="command_description_leave_room">Leave room with given id (or current room if null)</string>
<string name="command_description_upgrade_room">Upgrades a room to a new version</string>
<string name="command_description_gen_keys">Gen keys</string>
<string name="event_status_a11y_sending">Sending</string>
<string name="event_status_a11y_sent">Sent</string>