crypto: Initial support for server-side backups of room keys

This commit is contained in:
Damir Jelić 2021-10-09 09:48:23 +02:00
parent d3a761a73a
commit 406fd0d8d5
10 changed files with 898 additions and 50 deletions

View File

@ -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<DefaultSendVerificationMessageTask>,
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>(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<MXEncryptEventContentResult>) {
// 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()
}

View File

@ -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<String, Map<String, String>> {
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)
}
}

View File

@ -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<Unit>? = 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<MegolmBackupCreationInfo>) {
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<KeysVersion>) {
@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<Unit>?) {
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<Unit>?) {
TODO()
}
override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult,
callback: MatrixCallback<KeysBackupVersionTrust>) {
Timber.d("BACKUP: HELLOO TRYING TO CHECK THE TRUST")
// TODO
callback.onSuccess(KeysBackupVersionTrust(false))
}
override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
trust: Boolean,
callback: MatrixCallback<Unit>) {
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
TODO()
}
override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
recoveryKey: String,
callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
TODO()
}
override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
password: String,
callback: MatrixCallback<Unit>) {
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<ImportRoomKeysResult>) {
// TODO
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
}
override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
password: String,
roomId: String?,
sessionId: String?,
stepProgressListener: StepProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) {
// TODO
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
}
override fun getVersion(version: String, callback: MatrixCallback<KeysVersionResult?>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
runCatching {
sender.getKeyBackupVersion(version)
}.foldToCallback(callback)
}
}
override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
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<Boolean>) {
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<KeysVersionResult?> {
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<KeysBackupVersionTrust> {
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<Boolean>) {
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() }
}
}

View File

@ -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"

View File

@ -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<PassphraseInfo>,
}
/// 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<String, HashMap<String, String>>,
/// TODO
pub passphrase_info: Option<PassphraseInfo>,
}
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::<Hmac<Sha512>>(
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<String, HashMap<String, String>> = 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()
}
}

View File

@ -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<String>,
}
/// 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<matrix_sdk_crypto::store::BackupKeys> for BackupKeys {
type Error = ();
fn try_from(keys: matrix_sdk_crypto::store::BackupKeys) -> Result<Self, Self::Error> {
Ok(Self {
recovery_key: keys.recovery_key.ok_or(())?.to_base64(),
backup_version: keys.backup_version.ok_or(())?,
})
}
}
impl From<matrix_sdk_crypto::store::RoomKeyCounts> 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<matrix_sdk_crypto::CrossSigningKeyExport> for CrossSigningKeyExport {
fn from(e: matrix_sdk_crypto::CrossSigningKeyExport) -> Self {
Self {

View File

@ -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<Option<String>, 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<String>,
) -> Option<OutgoingVerificationRequest> {
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<Option<RequestVerificationResult>, 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<Option<Request>, 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<RoomKeyCounts, CryptoStoreError> {
Ok(self
.runtime
.block_on(self.inner.backup_machine().room_key_counts())?
.into())
}
/// TODO
pub fn save_recovery_key(
&self,
key: Option<String>,
version: Option<String>,
) -> 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<Option<BackupKeys>, CryptoStoreError> {
Ok(self
.runtime
.block_on(self.inner.backup_machine().get_backup_keys())?
.try_into()
.ok())
}
/// TODO
pub fn sign(&self, message: &str) -> HashMap<String, HashMap<String, String>> {
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()
}
}

View File

@ -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<string> users);
KeysClaim(string request_id, record<DOMString, record<DOMString, string>> one_time_keys);
KeysBackup(string request_id, record<DOMString, record<DOMString, string>> 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<DOMString, record<DOMString, string>> 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<DOMString, record<DOMString, string>> 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();
};

View File

@ -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<String, HashMap<String, String>>,
}
}
impl From<OutgoingRequest> for Request {
@ -186,6 +191,7 @@ impl From<OutgoingRequest> 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<KeysClaimResponse> for OwnedResponse {
@ -323,6 +331,12 @@ impl From<SignatureUploadResponse> for OwnedResponse {
}
}
impl From<KeysBackupResponse> 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),
}
}
}

View File

@ -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<KeysBackupVersionTrust> {
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)