crypto: Support to send out backup HTTP requests

This commit is contained in:
Damir Jelić 2021-10-18 09:36:32 +02:00
parent 406fd0d8d5
commit 021041fc2e
6 changed files with 266 additions and 10 deletions

View file

@ -57,13 +57,17 @@ import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.registration.handleUIA 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.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService import org.matrix.android.sdk.internal.crypto.keysbackup.RustKeyBackupService
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.BackupKeysResult
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.CreateKeysBackupVersionBody
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysBackupData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion 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.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.RoomKeysBackupData
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.CreateKeysBackupVersionTask 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.DeleteBackupTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.GetKeysBackupLastVersionTask 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.keysbackup.tasks.GetKeysBackupVersionTask
import org.matrix.android.sdk.internal.crypto.keysbackup.tasks.StoreSessionsDataTask
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo 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.ImportRoomKeysResult
import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult import org.matrix.android.sdk.internal.crypto.model.MXEncryptEventContentResult
@ -132,7 +136,8 @@ internal class RequestSender @Inject constructor(
private val getKeysBackupVersionTask: GetKeysBackupVersionTask, private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
private val deleteBackupTask: DeleteBackupTask, private val deleteBackupTask: DeleteBackupTask,
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask, private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
) { private val backupRoomKeysTask: StoreSessionsDataTask,
) {
companion object { companion object {
const val REQUEST_RETRY_COUNT = 3 const val REQUEST_RETRY_COUNT = 3
} }
@ -287,6 +292,49 @@ internal class RequestSender @Inject constructor(
val params = DeleteBackupTask.Params(version) val params = DeleteBackupTask.Params(version)
deleteBackupTask.execute(params) deleteBackupTask.execute(params)
} }
suspend fun backupRoomKeys(request: Request.KeysBackup): String {
val adapter = MoshiProvider
.providesMoshi()
.newBuilder()
.build()
.adapter<MutableMap<String, RoomKeysBackupData>>(MutableMap::class.java)
val keys = adapter.fromJson(request.rooms)!!
Timber.d("BACKUP: CONVERTED KEYS TO HASHMAP $keys")
/*
val keyAdapter = MoshiProvider.providesMoshi().adapter(KeyBackupData::class.java)
val keysBackupData = KeysBackupData()
for (room in keys) {
val sessions = room.value.getOrDefault("sessions", mapOf())
for (session in sessions) {
Timber.d("BACKUP: HEEELOO CONVERTING KEY ${session.value}")
val key = keyAdapter.fromJson(session.value)!!
Timber.d("BACKUP: HEEELOO CONVERTED KEY $key")
keysBackupData
.roomIdToRoomKeysBackupData
.getOrPut(room.key, { RoomKeysBackupData() })
.sessionIdToKeyBackupData[session.key] = key
}
}
*/
/*
for ((roomId, backupData) in keys) {
val roomData = backup.roomIdToRoomKeysBackupData.getOrPut(roomId, { RoomKeysBackupData() })
for ((sessionId, key) in backupData.sessionIdToKeyBackupData) {
Timber.d("BACKUP INSERTING KEY $key")
roomData.sessionIdToKeyBackupData[sessionId] = key
}
}
*/
val params = StoreSessionsDataTask.Params(request.version, KeysBackupData())
val response = backupRoomKeysTask.execute(params)
val responseAdapter = MoshiProvider.providesMoshi().adapter(BackupKeysResult::class.java)
return responseAdapter.toJson(response)!!
}
} }
/** /**
@ -1025,7 +1073,7 @@ internal class DefaultCryptoService @Inject constructor(
signatureUpload(it) signatureUpload(it)
} }
} }
is Request.KeysBackup -> { is Request.KeysBackup -> {
async { async {
TODO() TODO()
} }

View file

@ -778,6 +778,10 @@ internal class OlmMachine(
} }
} }
fun backupEnabled(): Boolean {
return inner.backupEnabled()
}
fun roomKeyCounts(): RoomKeyCounts { fun roomKeyCounts(): RoomKeyCounts {
// TODO convert this to a suspendable method // TODO convert this to a suspendable method
return inner.roomKeyCounts() return inner.roomKeyCounts()
@ -792,4 +796,14 @@ internal class OlmMachine(
// TODO convert this to a suspendable method // TODO convert this to a suspendable method
inner.saveRecoveryKey(key, version) inner.saveRecoveryKey(key, version)
} }
@Throws(CryptoStoreErrorException::class)
suspend fun backupRoomKeys(): Request? {
return withContext(Dispatchers.Default) {
Timber.d("BACKUP CREATING REQUEST")
val request = inner.backupRoomKeys()
Timber.d("BACKUP CREATED REQUEST: $request")
request
}
}
} }

View file

@ -18,10 +18,15 @@ package org.matrix.android.sdk.internal.crypto.keysbackup
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import androidx.annotation.UiThread
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
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.listeners.ProgressListener
import org.matrix.android.sdk.api.listeners.StepProgressListener 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.KeysBackupService
@ -43,9 +48,13 @@ import org.matrix.android.sdk.internal.extensions.foldToCallback
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import org.matrix.olm.OlmException
import timber.log.Timber import timber.log.Timber
import uniffi.olm.BackupRecoveryKey import uniffi.olm.BackupRecoveryKey
import uniffi.olm.Request
import uniffi.olm.RequestType
import javax.inject.Inject import javax.inject.Inject
import kotlin.random.Random
/** /**
* A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys) * A DefaultKeysBackupService class instance manage incremental backup of e2e keys (megolm keys)
@ -58,6 +67,10 @@ internal class RustKeyBackupService @Inject constructor(
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
) : KeysBackupService { ) : KeysBackupService {
companion object {
// Maximum delay in ms in {@link maybeBackupKeys}
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10_000L
}
private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
@ -150,7 +163,16 @@ internal class RustKeyBackupService @Inject constructor(
// TODO reset our backup state here, i.e. the `backed_up` flag on inbound group sessions // TODO reset our backup state here, i.e. the `backed_up` flag on inbound group sessions
} }
olmMachine.enableBackup(keysBackupCreationInfo.authData.publicKey, data.version) val keyBackupVersion = KeysVersionResult(
algorithm = createKeysBackupVersionBody.algorithm,
authData = createKeysBackupVersionBody.authData,
version = data.version,
// We can assume that the server does not have keys yet
count = 0,
hash = ""
)
enableKeysBackup(keyBackupVersion)
callback.onSuccess(data) callback.onSuccess(data)
} catch (failure: Throwable) { } catch (failure: Throwable) {
@ -226,8 +248,12 @@ internal class RustKeyBackupService @Inject constructor(
} }
override fun canRestoreKeys(): Boolean { override fun canRestoreKeys(): Boolean {
// TODO val keyCountOnServer = keysBackupVersion?.count ?: return false
return false val keyCountLocally = getTotalNumbersOfKeys()
// TODO is this sensible? We may have the same number of keys, or even more keys locally
// but the set of keys doesn't necessarily overlap
return keyCountLocally < keyCountOnServer
} }
override fun getTotalNumbersOfKeys(): Int { override fun getTotalNumbersOfKeys(): Int {
@ -463,4 +489,157 @@ internal class RustKeyBackupService @Inject constructor(
?.getAuthDataAsMegolmBackupAuthData() ?.getAuthDataAsMegolmBackupAuthData()
?.takeIf { it.publicKey.isNotEmpty() } ?.takeIf { it.publicKey.isNotEmpty() }
} }
/**
* Enable backing up of keys.
* This method will update the state and will start sending keys in nominal case
*
* @param keysVersionResult backup information object as returned by [getCurrentVersion].
*/
private suspend fun enableKeysBackup(keysVersionResult: KeysVersionResult) {
val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
if (retrievedMegolmBackupAuthData != null) {
try {
olmMachine.enableBackup(retrievedMegolmBackupAuthData.publicKey, keysVersionResult.version)
keysBackupVersion = keysVersionResult
} catch (e: OlmException) {
Timber.e(e, "OlmException")
keysBackupStateManager.state = KeysBackupState.Disabled
return
}
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
maybeBackupKeys()
} else {
Timber.e("Invalid authentication data")
keysBackupStateManager.state = KeysBackupState.Disabled
}
}
/**
* Do a backup if there are new keys, with a delay
*/
private fun maybeBackupKeys() {
when {
isStucked -> {
// If not already done, or in error case, check for a valid backup version on the homeserver.
// If there is one, maybeBackupKeys will be called again.
checkAndStartKeysBackup()
}
state == KeysBackupState.ReadyToBackUp -> {
keysBackupStateManager.state = KeysBackupState.WillBackUp
// Wait between 0 and 10 seconds, to avoid backup requests from
// different clients hitting the server all at the same time when a
// new key is sent
val delayInMs = Random.nextLong(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS)
cryptoCoroutineScope.launch {
delay(delayInMs)
// TODO is this correct? we used to call uiHandler.post() instead of this
withContext(Dispatchers.Main) {
backupKeys()
}
}
}
else -> {
Timber.v("maybeBackupKeys: Skip it because state: $state")
}
}
}
/**
* Send a chunk of keys to backup
*/
@UiThread
private suspend fun backupKeys() {
Timber.v("backupKeys")
// 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"))
resetBackupAllGroupSessionsListeners()
return
}
if (state === KeysBackupState.BackingUp) {
// Do nothing if we are already backing up
Timber.v("backupKeys: Invalid state: $state")
return
}
Timber.d("BACKUP: CREATING REQUEST")
val request = olmMachine.backupRoomKeys()
Timber.d("BACKUP: GOT REQUEST $request")
if (request == null) {
// Backup is up to date
// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
backupAllGroupSessionsCallback?.onSuccess(Unit)
resetBackupAllGroupSessionsListeners()
} else {
try {
if (request is Request.KeysBackup) {
keysBackupStateManager.state = KeysBackupState.BackingUp
Timber.d("BACKUP SENDING REQUEST")
val response = 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()
}
} else {
// Can't happen, do we want to panic?
}
} catch (failure: Throwable) {
if (failure is Failure.ServerError) {
withContext(Dispatchers.Main) {
Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) {
MatrixError.M_NOT_FOUND,
MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using
// the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
resetKeysBackupData()
keysBackupVersion = null
// Do not stay in KeysBackupState.WrongBackUpVersion but check what
// is available on the homeserver
checkAndStartKeysBackup()
}
else ->
// Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
}
} else {
withContext(Dispatchers.Main) {
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
Timber.e("backupKeys: backupKeys failed: $failure")
// Retry a bit later
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
maybeBackupKeys()
}
}
}
}
}
} }

View file

@ -1256,6 +1256,11 @@ impl OlmMachine {
Ok(()) Ok(())
} }
/// TODO
pub fn backup_enabled(&self) -> bool {
self.runtime.block_on(self.inner.backup_machine().enabled())
}
/// TODO /// TODO
pub fn disable_backup(&self) -> Result<(), CryptoStoreError> { pub fn disable_backup(&self) -> Result<(), CryptoStoreError> {
Ok(self Ok(self
@ -1269,7 +1274,9 @@ impl OlmMachine {
.runtime .runtime
.block_on(self.inner.backup_machine().backup())?; .block_on(self.inner.backup_machine().backup())?;
Ok(request.map(|r| r.into())) let request = request.map(|r| r.into());
Ok(request)
} }
/// TODO /// TODO

View file

@ -208,7 +208,7 @@ interface Request {
KeysUpload(string request_id, string body); KeysUpload(string request_id, string body);
KeysQuery(string request_id, sequence<string> users); KeysQuery(string request_id, sequence<string> users);
KeysClaim(string request_id, record<DOMString, record<DOMString, string>> one_time_keys); KeysClaim(string request_id, record<DOMString, record<DOMString, string>> one_time_keys);
KeysBackup(string request_id, record<DOMString, record<DOMString, string>> rooms); KeysBackup(string request_id, string version, string rooms);
RoomMessage(string request_id, string room_id, string event_type, string content); RoomMessage(string request_id, string room_id, string event_type, string content);
SignatureUpload(string request_id, string body); SignatureUpload(string request_id, string body);
}; };
@ -359,6 +359,7 @@ interface OlmMachine {
RoomKeyCounts room_key_counts(); RoomKeyCounts room_key_counts();
[Throws=CryptoStoreError] [Throws=CryptoStoreError]
BackupKeys? get_backup_keys(); BackupKeys? get_backup_keys();
boolean backup_enabled();
}; };
dictionary PassphraseInfo { dictionary PassphraseInfo {

View file

@ -125,6 +125,7 @@ impl From<ToDeviceRequest> for OutgoingVerificationRequest {
} }
} }
#[derive(Debug)]
pub enum Request { pub enum Request {
ToDevice { ToDevice {
request_id: String, request_id: String,
@ -155,8 +156,9 @@ pub enum Request {
}, },
KeysBackup { KeysBackup {
request_id: String, request_id: String,
rooms: HashMap<String, HashMap<String, String>>, version: String,
} rooms: String,
},
} }
impl From<OutgoingRequest> for Request { impl From<OutgoingRequest> for Request {
@ -191,7 +193,12 @@ impl From<OutgoingRequest> for Request {
}, },
RoomMessage(r) => Request::from(r), RoomMessage(r) => Request::from(r),
KeysClaim(c) => (*r.request_id(), c.clone()).into(), KeysClaim(c) => (*r.request_id(), c.clone()).into(),
KeysBackup(_) => todo!(), KeysBackup(b) => Request::KeysBackup {
request_id: r.request_id().to_string(),
version: b.version.to_owned(),
rooms: serde_json::to_string(&b.rooms)
.expect("Can't serialize keys backup request"),
},
} }
} }
} }