From 406fd0d8d52ee423746d975cf6872b0fdce66192 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Damir=20Jeli=C4=87?= Date: Sat, 9 Oct 2021 09:48:23 +0200 Subject: [PATCH] crypto: Initial support for server-side backups of room keys --- .../internal/crypto/DefaultCryptoService.kt | 71 ++- .../android/sdk/internal/crypto/OlmMachine.kt | 32 ++ .../crypto/keysbackup/RustKeyBackupService.kt | 466 ++++++++++++++++++ rust-sdk/Cargo.toml | 21 +- rust-sdk/src/backup_recovery_key.rs | 117 +++++ rust-sdk/src/lib.rs | 50 +- rust-sdk/src/machine.rs | 122 +++-- rust-sdk/src/olm.udl | 49 +- rust-sdk/src/responses.rs | 15 + .../settings/KeysBackupSettingsViewModel.kt | 5 + 10 files changed, 898 insertions(+), 50 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt create mode 100644 rust-sdk/src/backup_recovery_key.rs diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index 2272aec7f9..a551617858 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -37,10 +37,12 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor import org.matrix.android.sdk.api.crypto.MXCryptoConfig 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 import org.matrix.android.sdk.api.session.crypto.CryptoService import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService +import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keyshare.GossipingRequestListener import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.events.model.Content @@ -54,7 +56,14 @@ import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.internal.auth.registration.handleUIA import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel -import org.matrix.android.sdk.internal.crypto.keysbackup.DefaultKeysBackupService +import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +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.tasks.CreateKeysBackupVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.DeleteBackupTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask +import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupVersionTask import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.ImportRoomKeysResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult @@ -119,6 +128,10 @@ internal class RequestSender @Inject constructor( private val signaturesUploadTask: UploadSignaturesTask, private val sendVerificationMessageTask: Lazy, private val uploadSigningKeysTask: UploadSigningKeysTask, + private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask, + private val getKeysBackupVersionTask: GetKeysBackupVersionTask, + private val deleteBackupTask: DeleteBackupTask, + private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, ) { companion object { const val REQUEST_RETRY_COUNT = 3 @@ -192,7 +205,7 @@ internal class RequestSender @Inject constructor( request: UploadSigningKeysRequest, interactiveAuthInterceptor: UserInteractiveAuthInterceptor? ) { - val adapter = MoshiProvider.providesMoshi().adapter(RestKeyInfo::class.java) + val adapter = MoshiProvider.providesMoshi().adapter(RestKeyInfo::class.java) val masterKey = adapter.fromJson(request.masterKey)!!.toCryptoModel() val selfSigningKey = adapter.fromJson(request.selfSigningKey)!!.toCryptoModel() val userSigningKey = adapter.fromJson(request.userSigningKey)!!.toCryptoModel() @@ -248,6 +261,32 @@ internal class RequestSender @Inject constructor( val sendToDeviceParams = SendToDeviceTask.Params(eventType, userMap, transactionId) sendToDeviceTask.executeRetry(sendToDeviceParams, REQUEST_RETRY_COUNT) } + + suspend fun getKeyBackupVersion(version: String? = null): KeysVersionResult? { + return try { + if (version != null) { + getKeysBackupVersionTask.execute(version) + } else { + getKeysBackupLastVersionTask.execute(Unit) + } + } catch (failure: Throwable) { + if (failure is Failure.ServerError + && failure.error.code == MatrixError.M_NOT_FOUND) { + null + } else { + throw failure + } + } + } + + suspend fun createKeyBackup(body: CreateKeysBackupVersionBody): KeysVersion { + return createKeysBackupVersionTask.execute(body) + } + + suspend fun deleteKeyBackup(version: String) { + val params = DeleteBackupTask.Params(version) + deleteBackupTask.execute(params) + } } /** @@ -272,8 +311,6 @@ internal class DefaultCryptoService @Inject constructor( private val cryptoStore: IMXCryptoStore, // Set of parameters used to configure/customize the end-to-end crypto. private val mxCryptoConfig: MXCryptoConfig, - // The key backup service. - private val keysBackupService: DefaultKeysBackupService, // Actions private val warnOnUnknownDevicesRepository: WarnOnUnknownDeviceRepository, // Tasks @@ -283,6 +320,7 @@ internal class DefaultCryptoService @Inject constructor( private val setDeviceNameTask: SetDeviceNameTask, private val loadRoomMembersTask: LoadRoomMembersTask, private val cryptoSessionInfoProvider: CryptoSessionInfoProvider, + private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val taskExecutor: TaskExecutor, private val cryptoCoroutineScope: CoroutineScope, @@ -300,6 +338,9 @@ internal class DefaultCryptoService @Inject constructor( // The cross signing service. private var crossSigningService: RustCrossSigningService? = null + // The key backup service. + private var keysBackupService: RustKeyBackupService? = null + private val deviceObserver: DeviceUpdateObserver = DeviceUpdateObserver() // Locks for some of our operations @@ -448,9 +489,12 @@ internal class DefaultCryptoService @Inject constructor( cryptoStore.open() // this can throw if no backup + /* + TODO tryOrNull { keysBackupService.checkAndStartKeysBackup() } + */ } } @@ -466,6 +510,7 @@ internal class DefaultCryptoService @Inject constructor( olmMachine = machine verificationService = RustVerificationService(machine) crossSigningService = RustCrossSigningService(machine) + keysBackupService = RustKeyBackupService(machine, sender, coroutineDispatchers, cryptoCoroutineScope) Timber.v( "## CRYPTO | Successfully started up an Olm machine for " + "${userId}, ${deviceId}, identity keys: ${this.olmMachine?.identityKeys()}") @@ -473,6 +518,10 @@ internal class DefaultCryptoService @Inject constructor( Timber.v("Failed create an Olm machine: $throwable") } + tryOrNull { + keysBackupService!!.checkAndStartKeysBackup() + } + // Open the store cryptoStore.open() @@ -494,7 +543,12 @@ internal class DefaultCryptoService @Inject constructor( /** * @return the Keys backup Service */ - override fun keysBackupService() = keysBackupService + override fun keysBackupService(): KeysBackupService { + if (keysBackupService == null) { + internalStart() + } + return keysBackupService!! + } /** * @return the VerificationService @@ -693,7 +747,7 @@ internal class DefaultCryptoService @Inject constructor( eventType: String, roomId: String, callback: MatrixCallback) { - // moved to crypto scope to have uptodate values + // moved to crypto scope to have up to date values cryptoCoroutineScope.launch(coroutineDispatchers.crypto) { val algorithm = getEncryptionAlgorithm(roomId) @@ -971,6 +1025,11 @@ internal class DefaultCryptoService @Inject constructor( signatureUpload(it) } } + is Request.KeysBackup -> { + async { + TODO() + } + } } }.joinAll() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt index cec01c870c..7aa6478f4c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/OlmMachine.kt @@ -48,6 +48,8 @@ import org.matrix.android.sdk.internal.session.sync.model.DeviceListResponse import org.matrix.android.sdk.internal.session.sync.model.DeviceOneTimeKeysCountSyncResponse import org.matrix.android.sdk.internal.session.sync.model.ToDeviceSyncResponse import timber.log.Timber +import uniffi.olm.BackupKey +import uniffi.olm.BackupKeys import uniffi.olm.CrossSigningKeyExport import uniffi.olm.CrossSigningStatus import uniffi.olm.CryptoStoreErrorException @@ -59,6 +61,7 @@ import uniffi.olm.OlmMachine as InnerMachine import uniffi.olm.ProgressListener as RustProgressListener import uniffi.olm.Request import uniffi.olm.RequestType +import uniffi.olm.RoomKeyCounts import uniffi.olm.UserIdentity as RustUserIdentity import uniffi.olm.setLogger @@ -760,4 +763,33 @@ internal class OlmMachine( // TODO map the errors from importCrossSigningKeys to the UserTrustResult return UserTrustResult.Success } + + suspend fun sign(message: String): Map> { + return withContext(Dispatchers.Default) { + inner.sign(message) + } + } + + @Throws(CryptoStoreErrorException::class) + suspend fun enableBackup(key: String, version: String) { + return withContext(Dispatchers.Default) { + val backupKey = BackupKey(key, mapOf(), null) + inner.enableBackup(backupKey, version) + } + } + + fun roomKeyCounts(): RoomKeyCounts { + // TODO convert this to a suspendable method + return inner.roomKeyCounts() + } + + fun getBackupKeys(): BackupKeys? { + // TODO this needs to be suspendable + return inner.getBackupKeys() + } + + fun saveRecoveryKey(key: String?, version: String?) { + // TODO convert this to a suspendable method + inner.saveRecoveryKey(key, version) + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt new file mode 100644 index 0000000000..c75281304f --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/keysbackup/RustKeyBackupService.kt @@ -0,0 +1,466 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.keysbackup + +import android.os.Handler +import android.os.Looper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +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.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.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP +import org.matrix.android.sdk.internal.crypto.OlmMachine +import org.matrix.android.sdk.internal.crypto.RequestSender +import org.matrix.android.sdk.internal.crypto.keysbackup.model.KeysBackupVersionTrust +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo +import org.matrix.android.sdk.internal.crypto.keysbackup.model.SignalableMegolmBackupAuthData +import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody +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.model.ImportRoomKeysResult +import org.matrix.android.sdk.internal.crypto.store.SavedKeyBackupKeyInfo +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.MatrixCoroutineDispatchers +import timber.log.Timber +import uniffi.olm.BackupRecoveryKey +import javax.inject.Inject + +/** + * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) + * to the user's homeserver. + */ +@SessionScope +internal class RustKeyBackupService @Inject constructor( + private val olmMachine: OlmMachine, + private val sender: RequestSender, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val cryptoCoroutineScope: CoroutineScope, +) : KeysBackupService { + + private val uiHandler = Handler(Looper.getMainLooper()) + + private val keysBackupStateManager = KeysBackupStateManager(uiHandler) + + // The backup version + override var keysBackupVersion: KeysVersionResult? = null + private set + + private var backupAllGroupSessionsCallback: MatrixCallback? = null + + private var keysBackupStateListener: KeysBackupStateListener? = null + + override val isEnabled: Boolean + get() = keysBackupStateManager.isEnabled + + override val isStucked: Boolean + get() = keysBackupStateManager.isStucked + + override val state: KeysBackupState + get() = keysBackupStateManager.state + + override val currentBackupVersion: String? + get() = keysBackupVersion?.version + + override fun addListener(listener: KeysBackupStateListener) { + keysBackupStateManager.addListener(listener) + } + + override fun removeListener(listener: KeysBackupStateListener) { + keysBackupStateManager.removeListener(listener) + } + + override fun prepareKeysBackupVersion(password: String?, + progressListener: ProgressListener?, + callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + withContext(coroutineDispatchers.crypto) { + val key = if (password != null) { + BackupRecoveryKey.fromPassphrase(password) + } else { + BackupRecoveryKey() + } + + val publicKey = key.publicKey() + val backupAuthData = SignalableMegolmBackupAuthData( + publicKey = publicKey.publicKey, + privateKeySalt = publicKey.passphraseInfo?.privateKeySalt, + privateKeyIterations = publicKey.passphraseInfo?.privateKeyIterations + ) + val canonicalJson = JsonCanonicalizer.getCanonicalJson( + Map::class.java, + backupAuthData.signalableJSONDictionary() + ) + + val signedMegolmBackupAuthData = MegolmBackupAuthData( + publicKey = backupAuthData.publicKey, + privateKeySalt = backupAuthData.privateKeySalt, + privateKeyIterations = backupAuthData.privateKeyIterations, + signatures = olmMachine.sign(canonicalJson) + ) + + MegolmBackupCreationInfo( + algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP, + authData = signedMegolmBackupAuthData, + recoveryKey = key.toBase58() + ) + } + }.foldToCallback(callback) + } + } + + override fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo, + callback: MatrixCallback) { + @Suppress("UNCHECKED_CAST") + val createKeysBackupVersionBody = CreateKeysBackupVersionBody( + algorithm = keysBackupCreationInfo.algorithm, + authData = keysBackupCreationInfo.authData.toJsonDict() + ) + + keysBackupStateManager.state = KeysBackupState.Enabling + + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + try { + val data = 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) { + // TODO reset our backup state here, i.e. the `backed_up` flag on inbound group sessions + } + + olmMachine.enableBackup(keysBackupCreationInfo.authData.publicKey, data.version) + + callback.onSuccess(data) + } catch (failure: Throwable) { + keysBackupStateManager.state = KeysBackupState.Disabled + callback.onFailure(failure) + } + } + } + + override fun saveBackupRecoveryKey(recoveryKey: String?, version: String?) { + cryptoCoroutineScope.launch { + olmMachine.saveRecoveryKey(recoveryKey, version) + } + } + + private fun resetBackupAllGroupSessionsListeners() { + backupAllGroupSessionsCallback = null + + keysBackupStateListener?.let { + keysBackupStateManager.removeListener(it) + } + + keysBackupStateListener = null + } + + /** + * Reset all local key backup data. + * + * Note: This method does not update the state + */ + private fun resetKeysBackupData() { + resetBackupAllGroupSessionsListeners() + + /* + + TODO reset data on the rust side + cryptoStore.setKeyBackupVersion(null) + cryptoStore.setKeysBackupData(null) + backupOlmPkEncryption?.releaseEncryption() + backupOlmPkEncryption = null + + // Reset backup markers + cryptoStore.resetBackupMarkers() + */ + } + + override fun deleteBackup(version: String, callback: MatrixCallback?) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + withContext(coroutineDispatchers.crypto) { + if (keysBackupVersion != null && version == keysBackupVersion?.version) { + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Unknown + } + + fun eventuallyRestartBackup() { + // 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) } + } + } + } + } + + override fun canRestoreKeys(): Boolean { + // TODO + return false + } + + override fun getTotalNumbersOfKeys(): Int { + return olmMachine.roomKeyCounts().total.toInt() + } + + override fun getTotalNumbersOfBackedUpKeys(): Int { + return olmMachine.roomKeyCounts().backedUp.toInt() + } + + override fun backupAllGroupSessions(progressListener: ProgressListener?, + callback: MatrixCallback?) { + TODO() + } + + override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult, + callback: MatrixCallback) { + Timber.d("BACKUP: HELLOO TRYING TO CHECK THE TRUST") + // TODO + callback.onSuccess(KeysBackupVersionTrust(false)) + } + + override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult, + trust: Boolean, + callback: MatrixCallback) { + Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") + TODO() + } + + override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, + recoveryKey: String, + callback: MatrixCallback) { + Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") + TODO() + } + + override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, + password: String, + callback: MatrixCallback) { + TODO() + } + + override fun onSecretKeyGossip(secret: String) { + Timber.i("## CrossSigning - onSecretKeyGossip") + TODO() + } + + override fun getBackupProgress(progressListener: ProgressListener) { + val backedUpKeys = getTotalNumbersOfBackedUpKeys() + val total = getTotalNumbersOfKeys() + + progressListener.onProgress(backedUpKeys, total) + } + + override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, + recoveryKey: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) { + // TODO + Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") + } + + override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, + password: String, + roomId: String?, + sessionId: String?, + stepProgressListener: StepProgressListener?, + callback: MatrixCallback) { + // TODO + Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") + } + + override fun getVersion(version: String, callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + sender.getKeyBackupVersion(version) + }.foldToCallback(callback) + } + } + + override fun getCurrentVersion(callback: MatrixCallback) { + cryptoCoroutineScope.launch(coroutineDispatchers.main) { + runCatching { + sender.getKeyBackupVersion() + }.foldToCallback(callback) + } + } + + private suspend fun forceUsingLastVersionHelper(): Boolean { + val response = sender.getKeyBackupVersion() + val serverBackupVersion = response?.version + val localBackupVersion = keysBackupVersion?.version + + Timber.d("BACKUP: $serverBackupVersion") + + return if (serverBackupVersion == null) { + if (localBackupVersion == null) { + // No backup on the server, and backup is not active + true + } else { + // No backup on the server, and we are currently backing up, so stop backing up + resetKeysBackupData() + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.Disabled + false + } + } else { + if (localBackupVersion == null) { + // Do a check + checkAndStartWithKeysBackupVersion(response) + // backup on the server, and backup is not active + false + } else { + // Backup on the server, and we are currently backing up, compare version + if (localBackupVersion == serverBackupVersion) { + // We are already using the last version of the backup + true + } else { + // This will automatically check for the last version then + deleteBackup(localBackupVersion, null) + // We are not using the last version, so delete the current version we are using on the server + false + } + } + } + } + + override fun forceUsingLastVersion(callback: MatrixCallback) { + cryptoCoroutineScope.launch { + runCatching { + forceUsingLastVersionHelper() + }.foldToCallback(callback) + } + } + + override fun checkAndStartKeysBackup() { + 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 + } + + keysBackupVersion = null + keysBackupStateManager.state = KeysBackupState.CheckingBackUpOnHomeserver + + getCurrentVersion(object : MatrixCallback { + override fun onSuccess(data: KeysVersionResult?) { + checkAndStartWithKeysBackupVersion(data) + } + + override fun onFailure(failure: Throwable) { + Timber.e(failure, "checkAndStartKeysBackup: Failed to get current version") + keysBackupStateManager.state = KeysBackupState.Unknown + } + }) + } + + private fun checkAndStartWithKeysBackupVersion(keyBackupVersion: KeysVersionResult?) { + Timber.v("checkAndStartWithKeyBackupVersion: ${keyBackupVersion?.version}") + + keysBackupVersion = keyBackupVersion + + if (keyBackupVersion == null) { + Timber.v("checkAndStartWithKeysBackupVersion: Found no key backup version on the homeserver") + resetKeysBackupData() + keysBackupStateManager.state = KeysBackupState.Disabled + } else { + getKeysBackupTrust(keyBackupVersion, object : MatrixCallback { + override fun onSuccess(data: KeysBackupVersionTrust) { + val versionInStore = getKeyBackupRecoveryKeyInfo()?.version + + if (data.usable) { + Timber.v("checkAndStartWithKeysBackupVersion: Found usable key backup. version: ${keyBackupVersion.version}") + // Check the version we used at the previous app run + if (versionInStore != null && versionInStore != keyBackupVersion.version) { + Timber.v(" -> clean the previously used version $versionInStore") + resetKeysBackupData() + } + + Timber.v(" -> enabling key backups") + // TODO + // enableKeysBackup(keyBackupVersion) + } else { + Timber.v("checkAndStartWithKeysBackupVersion: No usable key backup. version: ${keyBackupVersion.version}") + if (versionInStore != null) { + Timber.v(" -> disabling key backup") + resetKeysBackupData() + } + + keysBackupStateManager.state = KeysBackupState.NotTrusted + } + } + + override fun onFailure(failure: Throwable) { + // Cannot happen + } + }) + } + } + + override fun isValidRecoveryKeyForCurrentVersion(recoveryKey: String, callback: MatrixCallback) { + val keysBackupVersion = keysBackupVersion ?: return Unit.also { callback.onSuccess(false) } + + try { + val key = BackupRecoveryKey.fromBase64(recoveryKey) + val publicKey = key.publicKey().publicKey + val authData = getMegolmBackupAuthData(keysBackupVersion) ?: return Unit.also { callback.onSuccess(false) } + + callback.onSuccess(authData.publicKey == publicKey) + } catch (error: Throwable) { + callback.onFailure(error) + } + } + + override fun getKeyBackupRecoveryKeyInfo(): SavedKeyBackupKeyInfo? { + val info = olmMachine.getBackupKeys() ?: return null + return SavedKeyBackupKeyInfo(info.recoveryKey, info.backupVersion) + } + + /** + * Extract MegolmBackupAuthData data from a backup version. + * + * @param keysBackupData the key backup data + * + * @return the authentication if found and valid, null in other case + */ + private fun getMegolmBackupAuthData(keysBackupData: KeysVersionResult): MegolmBackupAuthData? { + return keysBackupData + .takeIf { it.version.isNotEmpty() && it.algorithm == MXCRYPTO_ALGORITHM_MEGOLM_BACKUP } + ?.getAuthDataAsMegolmBackupAuthData() + ?.takeIf { it.publicKey.isNotEmpty() } + } +} diff --git a/rust-sdk/Cargo.toml b/rust-sdk/Cargo.toml index 6ce3f56a75..0b8d791281 100644 --- a/rust-sdk/Cargo.toml +++ b/rust-sdk/Cargo.toml @@ -18,19 +18,25 @@ thiserror = "1.0.25" tracing = "0.1.26" tracing-subscriber = "0.2.18" uniffi = "0.12.0" +pbkdf2 = "0.8.0" +sha2 = "0.9.5" +rand = "0.8.4" +hmac = "0.11.0" [dependencies.js_int] version = "0.2.1" features = ["lax_deserialize"] [dependencies.matrix-sdk-common] -git = "https://github.com/matrix-org/matrix-rust-sdk/" -rev = "9bae87b0ac213f9d37c033e76ea3a336e164cf02" +path = "/home/poljar/werk/priv/nio-rust/crates/matrix-sdk-common/" +# git = "https://github.com/matrix-org/matrix-rust-sdk/" +# rev = "9bae87b0ac213f9d37c033e76ea3a336e164cf02" [dependencies.matrix-sdk-crypto] -git = "https://github.com/matrix-org/matrix-rust-sdk/" -rev = "9bae87b0ac213f9d37c033e76ea3a336e164cf02" -features = ["sled_cryptostore"] +# git = "https://github.com/matrix-org/matrix-rust-sdk/" +# rev = "9bae87b0ac213f9d37c033e76ea3a336e164cf02" +path = "/home/poljar/werk/priv/nio-rust/crates/matrix-sdk-crypto/" +features = ["sled_cryptostore", "qrcode", "backups_v1"] [dependencies.tokio] version = "1.7.1" @@ -38,8 +44,9 @@ default_features = false features = ["rt-multi-thread"] [dependencies.ruma] -version = "0.3.0" -features = ["client-api"] +git = "https://github.com/ruma/ruma" +rev = "0101e110f" +features = ["client-api-c"] [build-dependencies] uniffi_build = "0.12.0" diff --git a/rust-sdk/src/backup_recovery_key.rs b/rust-sdk/src/backup_recovery_key.rs new file mode 100644 index 0000000000..13f7241176 --- /dev/null +++ b/rust-sdk/src/backup_recovery_key.rs @@ -0,0 +1,117 @@ +use hmac::Hmac; +use pbkdf2::pbkdf2; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use sha2::Sha512; +use std::{collections::HashMap, iter}; + +use matrix_sdk_crypto::backups::RecoveryKey; + +/// TODO +pub struct BackupRecoveryKey { + pub(crate) inner: RecoveryKey, + passphrase_info: Option, +} + +/// TODO +#[derive(Debug, Clone)] +pub struct PassphraseInfo { + /// TODO + pub private_key_salt: String, + /// TODO + pub private_key_iterations: i32, +} + +/// TODO +pub struct BackupKey { + /// TODO + pub public_key: String, + /// TODO + pub signatures: HashMap>, + /// TODO + pub passphrase_info: Option, +} + +impl BackupRecoveryKey { + const KEY_SIZE: usize = 32; + const SALT_SIZE: usize = 32; + const PBKDF_ROUNDS: u32 = 500_000; + + /// TODO + pub fn new() -> Self { + Self { + inner: RecoveryKey::new() + .expect("Can't gather enough randomness to create a recovery key"), + passphrase_info: None, + } + } + + /// TODO + pub fn from_base64(key: String) -> Self { + Self { + inner: RecoveryKey::from_base64(key).unwrap(), + passphrase_info: None, + } + } + + /// TODO + pub fn from_base58(key: String) -> Self { + Self { + inner: RecoveryKey::from_base58(&key).unwrap(), + passphrase_info: None, + } + } + + /// TODO + pub fn from_passphrase(passphrase: String) -> Self { + let mut key = [0u8; Self::KEY_SIZE]; + + let mut rng = thread_rng(); + let salt: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .map(char::from) + .take(Self::SALT_SIZE) + .collect(); + + pbkdf2::>( + passphrase.as_bytes(), + salt.as_bytes(), + Self::PBKDF_ROUNDS, + &mut key, + ); + + Self { + inner: RecoveryKey::from_bytes(key), + passphrase_info: Some(PassphraseInfo { + private_key_salt: salt, + private_key_iterations: Self::PBKDF_ROUNDS as i32, + }), + } + } + + /// TODO + pub fn public_key(&self) -> BackupKey { + let public_key = self.inner.public_key(); + + let signatures: HashMap> = public_key + .signatures() + .into_iter() + .map(|(k, v)| { + ( + k.to_string(), + v.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), + ) + }) + .collect(); + + BackupKey { + public_key: public_key.encoded_key(), + signatures, + passphrase_info: self.passphrase_info.clone(), + } + } + + /// TODO + pub fn to_base58(&self) -> String { + self.inner.to_base58() + } +} diff --git a/rust-sdk/src/lib.rs b/rust-sdk/src/lib.rs index 968632b914..c489b71602 100644 --- a/rust-sdk/src/lib.rs +++ b/rust-sdk/src/lib.rs @@ -10,6 +10,7 @@ //! TODO +mod backup_recovery_key; mod device; mod error; mod logger; @@ -18,18 +19,21 @@ mod responses; mod users; mod verification; +pub use backup_recovery_key::{BackupKey, BackupRecoveryKey, PassphraseInfo}; pub use device::Device; -pub use error::{CryptoStoreError, DecryptionError, KeyImportError, SignatureError, SecretImportError}; +pub use error::{ + CryptoStoreError, DecryptionError, KeyImportError, SecretImportError, SignatureError, +}; pub use logger::{set_logger, Logger}; pub use machine::{KeyRequestPair, OlmMachine}; pub use responses::{ - DeviceLists, KeysImportResult, OutgoingVerificationRequest, Request, RequestType, SignatureUploadRequest, - BootstrapCrossSigningResult, UploadSigningKeysRequest, + BootstrapCrossSigningResult, DeviceLists, KeysImportResult, OutgoingVerificationRequest, + Request, RequestType, SignatureUploadRequest, UploadSigningKeysRequest, }; pub use users::UserIdentity; pub use verification::{ - CancelInfo, QrCode, RequestVerificationResult, Sas, ScanResult, StartSasResult, Verification, - VerificationRequest, ConfirmVerificationResult, + CancelInfo, ConfirmVerificationResult, QrCode, RequestVerificationResult, Sas, ScanResult, + StartSasResult, Verification, VerificationRequest, }; /// Callback that will be passed over the FFI to report progress @@ -83,6 +87,42 @@ pub struct CrossSigningKeyExport { pub user_signing_key: Option, } +/// TODO +pub struct RoomKeyCounts { + /// TODO + pub total: i64, + /// TODO + pub backed_up: i64, +} + +/// TODO +pub struct BackupKeys { + /// TODO + pub recovery_key: String, + /// TODO + pub backup_version: String, +} + +impl std::convert::TryFrom for BackupKeys { + type Error = (); + + fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result { + Ok(Self { + recovery_key: keys.recovery_key.ok_or(())?.to_base64(), + backup_version: keys.backup_version.ok_or(())?, + }) + } +} + +impl From for RoomKeyCounts { + fn from(count: matrix_sdk_crypto::store::RoomKeyCounts) -> Self { + Self { + total: count.total as i64, + backed_up: count.backed_up as i64, + } + } +} + impl From for CrossSigningKeyExport { fn from(e: matrix_sdk_crypto::CrossSigningKeyExport) -> Self { Self { diff --git a/rust-sdk/src/machine.rs b/rust-sdk/src/machine.rs index 0b8e3cd7ba..a4d0260378 100644 --- a/rust-sdk/src/machine.rs +++ b/rust-sdk/src/machine.rs @@ -9,6 +9,7 @@ use js_int::UInt; use ruma::{ api::{ client::r0::{ + backup::add_backup_keys::Response as KeysBackupResponse, keys::{ claim_keys::Response as KeysClaimResponse, get_keys::Response as KeysQueryResponse, upload_keys::Response as KeysUploadResponse, @@ -31,17 +32,21 @@ use tokio::runtime::Runtime; use matrix_sdk_common::{deserialized_responses::AlgorithmInfo, uuid::Uuid}; use matrix_sdk_crypto::{ - decrypt_key_export, encrypt_key_export, matrix_qrcode::QrVerificationData, EncryptionSettings, - LocalTrust, OlmMachine as InnerMachine, UserIdentities, Verification as RustVerification, + backups::{MegolmV1BackupKey, RecoveryKey}, + decrypt_key_export, encrypt_key_export, + matrix_qrcode::QrVerificationData, + EncryptionSettings, LocalTrust, OlmMachine as InnerMachine, UserIdentities, + Verification as RustVerification, }; use crate::{ error::{CryptoStoreError, DecryptionError, SecretImportError, SignatureError}, responses::{response_from_string, OutgoingVerificationRequest, OwnedResponse}, - BootstrapCrossSigningResult, ConfirmVerificationResult, CrossSigningKeyExport, - CrossSigningStatus, DecryptedEvent, Device, DeviceLists, KeyImportError, KeysImportResult, - ProgressListener, QrCode, Request, RequestType, RequestVerificationResult, ScanResult, - SignatureUploadRequest, StartSasResult, UserIdentity, Verification, VerificationRequest, + BackupKey, BackupKeys, BootstrapCrossSigningResult, ConfirmVerificationResult, + CrossSigningKeyExport, CrossSigningStatus, DecryptedEvent, Device, DeviceLists, KeyImportError, + KeysImportResult, ProgressListener, QrCode, Request, RequestType, RequestVerificationResult, + RoomKeyCounts, ScanResult, SignatureUploadRequest, StartSasResult, UserIdentity, Verification, + VerificationRequest, }; /// A high level state machine that handles E2EE for Matrix. @@ -95,7 +100,7 @@ impl OlmMachine { /// Get the display name of our own device. pub fn display_name(&self) -> Result, CryptoStoreError> { - Ok(self.runtime.block_on(self.inner.dislpay_name())?) + Ok(self.runtime.block_on(self.inner.display_name())?) } /// Get a cross signing user identity for the given user ID. @@ -305,6 +310,9 @@ impl OlmMachine { RequestType::SignatureUpload => { SignatureUploadResponse::try_from_http_response(response).map(Into::into) } + RequestType::KeysBackup => { + KeysBackupResponse::try_from_http_response(response).map(Into::into) + } } .expect("Can't convert json string to response"); @@ -701,10 +709,7 @@ impl OlmMachine { methods: Vec, ) -> Option { let user_id = UserId::try_from(user_id).ok()?; - let methods = methods - .into_iter() - .map(VerificationMethod::from) - .collect(); + let methods = methods.into_iter().map(VerificationMethod::from).collect(); if let Some(verification) = self.inner.get_verification_request(&user_id, flow_id) { verification.accept_with_methods(methods).map(|r| r.into()) @@ -731,10 +736,7 @@ impl OlmMachine { let identity = self.runtime.block_on(self.inner.get_identity(&user_id))?; - let methods = methods - .into_iter() - .map(VerificationMethod::from) - .collect(); + let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok(if let Some(identity) = identity.and_then(|i| i.other()) { let content = self @@ -779,10 +781,7 @@ impl OlmMachine { let identity = self.runtime.block_on(self.inner.get_identity(&user_id))?; - let methods = methods - .into_iter() - .map(VerificationMethod::from) - .collect(); + let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok(if let Some(identity) = identity.and_then(|i| i.other()) { let request = self.runtime.block_on(identity.request_verification( @@ -816,10 +815,7 @@ impl OlmMachine { ) -> Result, CryptoStoreError> { let user_id = UserId::try_from(user_id)?; - let methods = methods - .into_iter() - .map(VerificationMethod::from) - .collect(); + let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok( if let Some(device) = self @@ -854,10 +850,7 @@ impl OlmMachine { .runtime .block_on(self.inner.get_identity(self.inner.user_id()))?; - let methods = methods - .into_iter() - .map(VerificationMethod::from) - .collect(); + let methods = methods.into_iter().map(VerificationMethod::from).collect(); Ok(if let Some(identity) = identity.and_then(|i| i.own()) { let (verification, request) = self @@ -1023,10 +1016,7 @@ impl OlmMachine { let user_id = UserId::try_from(user_id).ok()?; self.inner .get_verification(&user_id, flow_id) - .and_then(|v| { - v.qr_v1() - .and_then(|qr| qr.to_bytes().map(encode).ok()) - }) + .and_then(|v| v.qr_v1().and_then(|qr| qr.to_bytes().map(encode).ok())) } /// Pass data from a scanned QR code to an active verification request and @@ -1254,4 +1244,74 @@ impl OlmMachine { Ok(()) } + + /// TODO + pub fn enable_backup(&self, key: BackupKey, version: String) -> Result<(), CryptoStoreError> { + let backup_key = MegolmV1BackupKey::from_base64(&key.public_key).unwrap(); + backup_key.set_version(version); + + self.runtime + .block_on(self.inner.backup_machine().enable_backup(backup_key))?; + + Ok(()) + } + + /// TODO + pub fn disable_backup(&self) -> Result<(), CryptoStoreError> { + Ok(self + .runtime + .block_on(self.inner.backup_machine().disable_backup())?) + } + + /// TODO + pub fn backup_room_keys(&self) -> Result, CryptoStoreError> { + let request = self + .runtime + .block_on(self.inner.backup_machine().backup())?; + + Ok(request.map(|r| r.into())) + } + + /// TODO + pub fn room_key_counts(&self) -> Result { + Ok(self + .runtime + .block_on(self.inner.backup_machine().room_key_counts())? + .into()) + } + + /// TODO + pub fn save_recovery_key( + &self, + key: Option, + version: Option, + ) -> Result<(), CryptoStoreError> { + let key = key.map(RecoveryKey::from_base64).transpose().ok().flatten(); + Ok(self + .runtime + .block_on(self.inner.backup_machine().save_recovery_key(key, version))?) + } + + /// TODO + pub fn get_backup_keys(&self) -> Result, CryptoStoreError> { + Ok(self + .runtime + .block_on(self.inner.backup_machine().get_backup_keys())? + .try_into() + .ok()) + } + + /// TODO + pub fn sign(&self, message: &str) -> HashMap> { + self.runtime + .block_on(self.inner.sign(message)) + .into_iter() + .map(|(k, v)| { + ( + k.to_string(), + v.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), + ) + }) + .collect() + } } diff --git a/rust-sdk/src/olm.udl b/rust-sdk/src/olm.udl index 41c5747bf6..d3af9e6a7a 100644 --- a/rust-sdk/src/olm.udl +++ b/rust-sdk/src/olm.udl @@ -110,7 +110,7 @@ dictionary UploadSigningKeysRequest { }; dictionary BootstrapCrossSigningResult { - UploadSigningKeysRequest upload_signing_keys_request; + UploadSigningKeysRequest upload_signing_keys_request; SignatureUploadRequest signature_request; }; @@ -208,6 +208,7 @@ interface Request { KeysUpload(string request_id, string body); KeysQuery(string request_id, sequence users); KeysClaim(string request_id, record> one_time_keys); + KeysBackup(string request_id, record> rooms); RoomMessage(string request_id, string room_id, string event_type, string content); SignatureUpload(string request_id, string body); }; @@ -222,6 +223,7 @@ enum RequestType { "KeysUpload", "ToDevice", "SignatureUpload", + "KeysBackup", }; interface OlmMachine { @@ -343,4 +345,49 @@ interface OlmMachine { void import_cross_signing_keys(CrossSigningKeyExport export); [Throws=CryptoStoreError] boolean is_identity_verified([ByRef] string user_id); + + record> sign([ByRef] string message); + [Throws=CryptoStoreError] + void enable_backup(BackupKey key, string version); + [Throws=CryptoStoreError] + void disable_backup(); + [Throws=CryptoStoreError] + Request? backup_room_keys(); + [Throws=CryptoStoreError] + void save_recovery_key(string? key, string? version); + [Throws=CryptoStoreError] + RoomKeyCounts room_key_counts(); + [Throws=CryptoStoreError] + BackupKeys? get_backup_keys(); +}; + +dictionary PassphraseInfo { + string private_key_salt; + i32 private_key_iterations; +}; + +dictionary BackupKey { + string public_key; + record> signatures; + PassphraseInfo? passphrase_info; +}; + +dictionary BackupKeys { + string recovery_key; + string backup_version; +}; + +dictionary RoomKeyCounts { + i64 total; + i64 backed_up; +}; + +interface BackupRecoveryKey { + constructor(); + [Name=from_base64] + constructor(string key); + [Name=from_passphrase] + constructor(string key); + string to_base58(); + BackupKey public_key(); }; diff --git a/rust-sdk/src/responses.rs b/rust-sdk/src/responses.rs index 693f4f1845..ef2f836d8e 100644 --- a/rust-sdk/src/responses.rs +++ b/rust-sdk/src/responses.rs @@ -8,6 +8,7 @@ use serde_json::json; use ruma::{ api::client::r0::{ + backup::add_backup_keys::Response as KeysBackupResponse, keys::{ claim_keys::{Request as KeysClaimRequest, Response as KeysClaimResponse}, get_keys::Response as KeysQueryResponse, @@ -152,6 +153,10 @@ pub enum Request { request_id: String, body: String, }, + KeysBackup { + request_id: String, + rooms: HashMap>, + } } impl From for Request { @@ -186,6 +191,7 @@ impl From for Request { }, RoomMessage(r) => Request::from(r), KeysClaim(c) => (*r.request_id(), c.clone()).into(), + KeysBackup(_) => todo!(), } } } @@ -256,6 +262,7 @@ pub enum RequestType { KeysUpload, ToDevice, SignatureUpload, + KeysBackup, } pub struct DeviceLists { @@ -291,6 +298,7 @@ pub(crate) enum OwnedResponse { KeysQuery(KeysQueryResponse), ToDevice(ToDeviceResponse), SignatureUpload(SignatureUploadResponse), + KeysBackup(KeysBackupResponse), } impl From for OwnedResponse { @@ -323,6 +331,12 @@ impl From for OwnedResponse { } } +impl From for OwnedResponse { + fn from(r: KeysBackupResponse) -> Self { + Self::KeysBackup(r) + } +} + impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> { fn from(r: &'a OwnedResponse) -> Self { match r { @@ -331,6 +345,7 @@ impl<'a> From<&'a OwnedResponse> for IncomingResponse<'a> { OwnedResponse::KeysUpload(r) => IncomingResponse::KeysUpload(r), OwnedResponse::ToDevice(r) => IncomingResponse::ToDevice(r), OwnedResponse::SignatureUpload(r) => IncomingResponse::SignatureUpload(r), + OwnedResponse::KeysBackup(r) => IncomingResponse::KeysBackup(r), } } } diff --git a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt index cb8a6ce4e9..312bef06d1 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/keysbackup/settings/KeysBackupSettingsViewModel.kt @@ -34,6 +34,7 @@ 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, session: Session @@ -81,6 +82,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS private fun getKeysBackupTrust() = withState { state -> val versionResult = keysBackupService.keysBackupVersion + Timber.d("BACKUP: HEEEEEEE $versionResult ${state.keysBackupVersionTrust}") if (state.keysBackupVersionTrust is Uninitialized && versionResult != null) { setState { @@ -89,10 +91,12 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS deleteBackupRequest = Uninitialized ) } + Timber.d("BACKUP: HEEEEEEE TWO") keysBackupService .getKeysBackupTrust(versionResult, object : MatrixCallback { override fun onSuccess(data: KeysBackupVersionTrust) { + Timber.d("BACKUP: HEEEE suceeeded $data") setState { copy( keysBackupVersionTrust = Success(data) @@ -101,6 +105,7 @@ class KeysBackupSettingsViewModel @AssistedInject constructor(@Assisted initialS } override fun onFailure(failure: Throwable) { + Timber.d("BACKUP: HEEEE FAILED $failure") setState { copy( keysBackupVersionTrust = Fail(failure)