forked from GitHub-Mirror/riotX-android
1418 lines
62 KiB
Kotlin
1418 lines
62 KiB
Kotlin
/*
|
|
* Copyright 2018 New Vector Ltd
|
|
*
|
|
* 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 im.vector.matrix.android.internal.crypto.keysbackup
|
|
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import androidx.annotation.UiThread
|
|
import androidx.annotation.VisibleForTesting
|
|
import androidx.annotation.WorkerThread
|
|
import arrow.core.Try
|
|
import im.vector.matrix.android.api.MatrixCallback
|
|
import im.vector.matrix.android.api.auth.data.Credentials
|
|
import im.vector.matrix.android.api.failure.Failure
|
|
import im.vector.matrix.android.api.failure.MatrixError
|
|
import im.vector.matrix.android.api.listeners.ProgressListener
|
|
import im.vector.matrix.android.api.listeners.StepProgressListener
|
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupService
|
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
|
|
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
|
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
|
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
|
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
|
import im.vector.matrix.android.internal.crypto.ObjectSigner
|
|
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupAuthData
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.*
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.tasks.*
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
|
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
|
|
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
|
import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrapper
|
|
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
|
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
|
|
import im.vector.matrix.android.internal.di.MoshiProvider
|
|
import im.vector.matrix.android.internal.extensions.foldToCallback
|
|
import im.vector.matrix.android.internal.session.SessionScope
|
|
import im.vector.matrix.android.internal.task.Task
|
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
|
import im.vector.matrix.android.internal.task.TaskThread
|
|
import im.vector.matrix.android.internal.task.configureWith
|
|
import im.vector.matrix.android.internal.util.JsonCanonicalizer
|
|
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
|
import kotlinx.coroutines.GlobalScope
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.withContext
|
|
import org.matrix.olm.OlmException
|
|
import org.matrix.olm.OlmPkDecryption
|
|
import org.matrix.olm.OlmPkEncryption
|
|
import org.matrix.olm.OlmPkMessage
|
|
import timber.log.Timber
|
|
import java.security.InvalidParameterException
|
|
import javax.inject.Inject
|
|
import kotlin.collections.HashMap
|
|
import kotlin.random.Random
|
|
|
|
/**
|
|
* A KeysBackup class instance manage incremental backup of e2e keys (megolm keys)
|
|
* to the user's homeserver.
|
|
*/
|
|
|
|
@SessionScope
|
|
internal class KeysBackup @Inject constructor(
|
|
private val credentials: Credentials,
|
|
private val cryptoStore: IMXCryptoStore,
|
|
private val olmDevice: MXOlmDevice,
|
|
private val objectSigner: ObjectSigner,
|
|
// Actions
|
|
private val megolmSessionDataImporter: MegolmSessionDataImporter,
|
|
// Tasks
|
|
private val createKeysBackupVersionTask: CreateKeysBackupVersionTask,
|
|
private val deleteBackupTask: DeleteBackupTask,
|
|
private val deleteRoomSessionDataTask: DeleteRoomSessionDataTask,
|
|
private val deleteRoomSessionsDataTask: DeleteRoomSessionsDataTask,
|
|
private val deleteSessionDataTask: DeleteSessionsDataTask,
|
|
private val getKeysBackupLastVersionTask: GetKeysBackupLastVersionTask,
|
|
private val getKeysBackupVersionTask: GetKeysBackupVersionTask,
|
|
private val getRoomSessionDataTask: GetRoomSessionDataTask,
|
|
private val getRoomSessionsDataTask: GetRoomSessionsDataTask,
|
|
private val getSessionsDataTask: GetSessionsDataTask,
|
|
private val storeRoomSessionDataTask: StoreRoomSessionDataTask,
|
|
private val storeSessionsDataTask: StoreRoomSessionsDataTask,
|
|
private val storeSessionDataTask: StoreSessionsDataTask,
|
|
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
|
// Task executor
|
|
private val taskExecutor: TaskExecutor,
|
|
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
|
) : KeysBackupService {
|
|
|
|
private val uiHandler = Handler(Looper.getMainLooper())
|
|
|
|
private val keysBackupStateManager = KeysBackupStateManager(uiHandler)
|
|
|
|
// The backup version
|
|
override var keysBackupVersion: KeysVersionResult? = null
|
|
private set
|
|
|
|
// The backup key being used.
|
|
private var backupOlmPkEncryption: OlmPkEncryption? = null
|
|
|
|
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>) {
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
Try {
|
|
val olmPkDecryption = OlmPkDecryption()
|
|
val megolmBackupAuthData = MegolmBackupAuthData()
|
|
|
|
if (password != null) {
|
|
// Generate a private key from the password
|
|
val backgroundProgressListener = if (progressListener == null) {
|
|
null
|
|
} else {
|
|
object : ProgressListener {
|
|
override fun onProgress(progress: Int, total: Int) {
|
|
uiHandler.post {
|
|
try {
|
|
progressListener.onProgress(progress, total)
|
|
} catch (e: Exception) {
|
|
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
|
megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey)
|
|
megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt
|
|
megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations
|
|
} else {
|
|
val publicKey = olmPkDecryption.generateKey()
|
|
|
|
megolmBackupAuthData.publicKey = publicKey
|
|
}
|
|
|
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())
|
|
|
|
megolmBackupAuthData.signatures = objectSigner.signObject(canonicalJson)
|
|
|
|
|
|
val megolmBackupCreationInfo = MegolmBackupCreationInfo()
|
|
megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
|
megolmBackupCreationInfo.authData = megolmBackupAuthData
|
|
megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
|
|
|
megolmBackupCreationInfo
|
|
}
|
|
}.foldToCallback(callback)
|
|
}
|
|
}
|
|
|
|
override fun createKeysBackupVersion(keysBackupCreationInfo: MegolmBackupCreationInfo,
|
|
callback: MatrixCallback<KeysVersion>) {
|
|
val createKeysBackupVersionBody = CreateKeysBackupVersionBody()
|
|
createKeysBackupVersionBody.algorithm = keysBackupCreationInfo.algorithm
|
|
createKeysBackupVersionBody.authData = MoshiProvider.providesMoshi().adapter(Map::class.java)
|
|
.fromJson(keysBackupCreationInfo.authData?.toJsonString()) as Map<String, Any>?
|
|
|
|
keysBackupStateManager.state = KeysBackupState.Enabling
|
|
|
|
createKeysBackupVersionTask
|
|
.configureWith(createKeysBackupVersionBody) {
|
|
this.callback = object : MatrixCallback<KeysVersion> {
|
|
override fun onSuccess(info: KeysVersion) {
|
|
// Reset backup markers.
|
|
cryptoStore.resetBackupMarkers()
|
|
|
|
val keyBackupVersion = KeysVersionResult()
|
|
keyBackupVersion.algorithm = createKeysBackupVersionBody.algorithm
|
|
keyBackupVersion.authData = createKeysBackupVersionBody.authData
|
|
keyBackupVersion.version = info.version
|
|
|
|
// We can consider that the server does not have keys yet
|
|
keyBackupVersion.count = 0
|
|
keyBackupVersion.hash = null
|
|
|
|
enableKeysBackup(keyBackupVersion)
|
|
|
|
callback.onSuccess(info)
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
keysBackupStateManager.state = KeysBackupState.Disabled
|
|
callback.onFailure(failure)
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
// If we're currently backing up to this backup... stop.
|
|
// (We start using it automatically in createKeysBackupVersion so this is symmetrical).
|
|
if (keysBackupVersion != null && version == keysBackupVersion!!.version) {
|
|
resetKeysBackupData()
|
|
keysBackupVersion = null
|
|
keysBackupStateManager.state = KeysBackupState.Unknown
|
|
}
|
|
|
|
deleteBackupTask
|
|
.configureWith(DeleteBackupTask.Params(version)) {
|
|
this.callback = object : MatrixCallback<Unit> {
|
|
private fun eventuallyRestartBackup() {
|
|
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
|
|
if (state == KeysBackupState.Unknown) {
|
|
checkAndStartKeysBackup()
|
|
}
|
|
}
|
|
|
|
override fun onSuccess(data: Unit) {
|
|
eventuallyRestartBackup()
|
|
|
|
uiHandler.post { callback?.onSuccess(Unit) }
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
eventuallyRestartBackup()
|
|
|
|
uiHandler.post { callback?.onFailure(failure) }
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun canRestoreKeys(): Boolean {
|
|
// Server contains more keys than locally
|
|
val totalNumberOfKeysLocally = getTotalNumbersOfKeys()
|
|
|
|
val keysBackupData = cryptoStore.getKeysBackupData()
|
|
|
|
val totalNumberOfKeysServer = keysBackupData?.backupLastServerNumberOfKeys ?: -1
|
|
val hashServer = keysBackupData?.backupLastServerHash
|
|
|
|
return when {
|
|
totalNumberOfKeysLocally < totalNumberOfKeysServer -> {
|
|
// Server contains more keys than this device
|
|
true
|
|
}
|
|
totalNumberOfKeysLocally == totalNumberOfKeysServer -> {
|
|
// Same number, compare hash?
|
|
// TODO We have not found any algorithm to determine if a restore is recommended here. Return false for the moment
|
|
false
|
|
}
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
override fun getTotalNumbersOfKeys(): Int {
|
|
return cryptoStore.inboundGroupSessionsCount(false)
|
|
|
|
}
|
|
|
|
override fun getTotalNumbersOfBackedUpKeys(): Int {
|
|
return cryptoStore.inboundGroupSessionsCount(true)
|
|
}
|
|
|
|
override fun backupAllGroupSessions(progressListener: ProgressListener?,
|
|
callback: MatrixCallback<Unit>?) {
|
|
// Get a status right now
|
|
getBackupProgress(object : ProgressListener {
|
|
override fun onProgress(progress: Int, total: Int) {
|
|
// Reset previous listeners if any
|
|
resetBackupAllGroupSessionsListeners()
|
|
Timber.v("backupAllGroupSessions: backupProgress: $progress/$total")
|
|
try {
|
|
progressListener?.onProgress(progress, total)
|
|
} catch (e: Exception) {
|
|
Timber.e(e, "backupAllGroupSessions: onProgress failure")
|
|
}
|
|
|
|
if (progress == total) {
|
|
Timber.v("backupAllGroupSessions: complete")
|
|
callback?.onSuccess(Unit)
|
|
return
|
|
}
|
|
|
|
backupAllGroupSessionsCallback = callback
|
|
|
|
// Listen to `state` change to determine when to call onBackupProgress and onComplete
|
|
keysBackupStateListener = object : KeysBackupStateListener {
|
|
override fun onStateChange(newState: KeysBackupState) {
|
|
getBackupProgress(object : ProgressListener {
|
|
override fun onProgress(progress: Int, total: Int) {
|
|
try {
|
|
progressListener?.onProgress(progress, total)
|
|
} catch (e: Exception) {
|
|
Timber.e(e, "backupAllGroupSessions: onProgress failure 2")
|
|
}
|
|
|
|
// If backup is finished, notify the main listener
|
|
if (state === KeysBackupState.ReadyToBackUp) {
|
|
backupAllGroupSessionsCallback?.onSuccess(Unit)
|
|
resetBackupAllGroupSessionsListeners()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
keysBackupStateManager.addListener(keysBackupStateListener!!)
|
|
|
|
backupKeys()
|
|
}
|
|
})
|
|
}
|
|
|
|
override fun getKeysBackupTrust(keysBackupVersion: KeysVersionResult,
|
|
callback: MatrixCallback<KeysBackupVersionTrust>) {
|
|
// TODO Validate with François that this is correct
|
|
object : Task<KeysVersionResult, KeysBackupVersionTrust> {
|
|
override suspend fun execute(params: KeysVersionResult): KeysBackupVersionTrust {
|
|
return getKeysBackupTrustBg(params)
|
|
}
|
|
}
|
|
.configureWith(keysBackupVersion) {
|
|
this.callback = callback
|
|
this.executionThread = TaskThread.COMPUTATION
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
/**
|
|
* Check trust on a key backup version.
|
|
* This has to be called on background thread.
|
|
*
|
|
* @param keysBackupVersion the backup version to check.
|
|
* @return a KeysBackupVersionTrust object
|
|
*/
|
|
@WorkerThread
|
|
private fun getKeysBackupTrustBg(keysBackupVersion: KeysVersionResult): KeysBackupVersionTrust {
|
|
val myUserId = credentials.userId
|
|
|
|
val keysBackupVersionTrust = KeysBackupVersionTrust()
|
|
val authData = keysBackupVersion.getAuthDataAsMegolmBackupAuthData()
|
|
|
|
if (keysBackupVersion.algorithm == null
|
|
|| authData == null
|
|
|| authData.publicKey.isEmpty()
|
|
|| authData.signatures.isNullOrEmpty()) {
|
|
Timber.v("getKeysBackupTrust: Key backup is absent or missing required data")
|
|
return keysBackupVersionTrust
|
|
}
|
|
|
|
val mySigs = authData.signatures?.get(myUserId)
|
|
if (mySigs.isNullOrEmpty()) {
|
|
Timber.v("getKeysBackupTrust: Ignoring key backup because it lacks any signatures from this user")
|
|
return keysBackupVersionTrust
|
|
}
|
|
|
|
for (keyId in mySigs.keys) {
|
|
// XXX: is this how we're supposed to get the device id?
|
|
var deviceId: String? = null
|
|
val components = keyId.split(":")
|
|
if (components.size == 2) {
|
|
deviceId = components[1]
|
|
}
|
|
|
|
if (deviceId != null) {
|
|
val device = cryptoStore.getUserDevice(deviceId, myUserId)
|
|
var isSignatureValid = false
|
|
|
|
if (device == null) {
|
|
Timber.v("getKeysBackupTrust: Signature from unknown device $deviceId")
|
|
} else {
|
|
val fingerprint = device.fingerprint()
|
|
if (fingerprint != null) {
|
|
try {
|
|
olmDevice.verifySignature(fingerprint, authData.signalableJSONDictionary(), mySigs[keyId] as String)
|
|
isSignatureValid = true
|
|
} catch (e: OlmException) {
|
|
Timber.v("getKeysBackupTrust: Bad signature from device " + device.deviceId + " " + e.localizedMessage)
|
|
}
|
|
}
|
|
|
|
if (isSignatureValid && device.isVerified) {
|
|
keysBackupVersionTrust.usable = true
|
|
}
|
|
}
|
|
|
|
val signature = KeysBackupVersionTrustSignature()
|
|
signature.device = device
|
|
signature.valid = isSignatureValid
|
|
signature.deviceId = deviceId
|
|
keysBackupVersionTrust.signatures.add(signature)
|
|
}
|
|
}
|
|
|
|
return keysBackupVersionTrust
|
|
}
|
|
|
|
override fun trustKeysBackupVersion(keysBackupVersion: KeysVersionResult,
|
|
trust: Boolean,
|
|
callback: MatrixCallback<Unit>) {
|
|
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
|
|
|
|
// Get auth data to update it
|
|
val authData = getMegolmBackupAuthData(keysBackupVersion)
|
|
|
|
if (authData == null) {
|
|
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
|
|
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
|
} else {
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
|
|
val myUserId = credentials.userId
|
|
|
|
// Get current signatures, or create an empty set
|
|
val myUserSignatures = authData.signatures?.get(myUserId)?.toMutableMap()
|
|
?: HashMap()
|
|
|
|
if (trust) {
|
|
// Add current device signature
|
|
val canonicalJson = JsonCanonicalizer.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())
|
|
|
|
val deviceSignatures = objectSigner.signObject(canonicalJson)
|
|
|
|
deviceSignatures[myUserId]?.forEach { entry ->
|
|
myUserSignatures[entry.key] = entry.value
|
|
}
|
|
} else {
|
|
// Remove current device signature
|
|
myUserSignatures.remove("ed25519:${credentials.deviceId}")
|
|
}
|
|
|
|
// Create an updated version of KeysVersionResult
|
|
val updateKeysBackupVersionBody = UpdateKeysBackupVersionBody(keysBackupVersion.version!!)
|
|
|
|
updateKeysBackupVersionBody.algorithm = keysBackupVersion.algorithm
|
|
|
|
val newMegolmBackupAuthData = authData.copy()
|
|
|
|
val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
|
|
newSignatures[myUserId] = myUserSignatures
|
|
|
|
newMegolmBackupAuthData.signatures = newSignatures
|
|
|
|
val moshi = MoshiProvider.providesMoshi()
|
|
val adapter = moshi.adapter(Map::class.java)
|
|
|
|
updateKeysBackupVersionBody.authData = adapter.fromJson(newMegolmBackupAuthData.toJsonString()) as Map<String, Any>?
|
|
|
|
updateKeysBackupVersionBody
|
|
}
|
|
|
|
// And send it to the homeserver
|
|
updateKeysBackupVersionTask
|
|
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) {
|
|
this.callback = object : MatrixCallback<Unit> {
|
|
override fun onSuccess(data: Unit) {
|
|
// Relaunch the state machine on this updated backup version
|
|
val newKeysBackupVersion = KeysVersionResult()
|
|
|
|
newKeysBackupVersion.version = keysBackupVersion.version
|
|
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
|
|
newKeysBackupVersion.count = keysBackupVersion.count
|
|
newKeysBackupVersion.hash = keysBackupVersion.hash
|
|
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData
|
|
|
|
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
|
|
|
callback.onSuccess(data)
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
callback.onFailure(failure)
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
|
|
recoveryKey: String,
|
|
callback: MatrixCallback<Unit>) {
|
|
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
|
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
val isValid = withContext(coroutineDispatchers.crypto) {
|
|
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
|
}
|
|
|
|
if (!isValid) {
|
|
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
|
|
|
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
|
} else {
|
|
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
|
|
password: String,
|
|
callback: MatrixCallback<Unit>) {
|
|
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
|
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
val recoveryKey = withContext(coroutineDispatchers.crypto) {
|
|
recoveryKeyFromPassword(password, keysBackupVersion, null)
|
|
}
|
|
|
|
if (recoveryKey == null) {
|
|
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")
|
|
|
|
callback.onFailure(IllegalArgumentException("Missing element"))
|
|
} else {
|
|
// Check trust using the recovery key
|
|
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get public key from a Recovery key
|
|
*
|
|
* @param recoveryKey the recovery key
|
|
* @return the corresponding public key, from Olm
|
|
*/
|
|
@WorkerThread
|
|
private fun pkPublicKeyFromRecoveryKey(recoveryKey: String): String? {
|
|
// Extract the primary key
|
|
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
|
|
|
if (privateKey == null) {
|
|
Timber.w("pkPublicKeyFromRecoveryKey: private key is null")
|
|
|
|
return null
|
|
}
|
|
|
|
// Built the PK decryption with it
|
|
val pkPublicKey: String
|
|
|
|
try {
|
|
val decryption = OlmPkDecryption()
|
|
pkPublicKey = decryption.setPrivateKey(privateKey)
|
|
} catch (e: OlmException) {
|
|
return null
|
|
}
|
|
|
|
return pkPublicKey
|
|
}
|
|
|
|
private fun resetBackupAllGroupSessionsListeners() {
|
|
backupAllGroupSessionsCallback = null
|
|
|
|
keysBackupStateListener?.let {
|
|
keysBackupStateManager.removeListener(it)
|
|
}
|
|
|
|
keysBackupStateListener = null
|
|
}
|
|
|
|
override fun getBackupProgress(progressListener: ProgressListener) {
|
|
val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true)
|
|
val total = cryptoStore.inboundGroupSessionsCount(false)
|
|
|
|
progressListener.onProgress(backedUpKeys, total)
|
|
}
|
|
|
|
override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
|
|
recoveryKey: String,
|
|
roomId: String?,
|
|
sessionId: String?,
|
|
stepProgressListener: StepProgressListener?,
|
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
|
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
|
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
Try {
|
|
// Check if the recovery is valid before going any further
|
|
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
|
throw InvalidParameterException("Invalid recovery key")
|
|
}
|
|
|
|
// Get a PK decryption instance
|
|
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
|
|
if (decryption == null) {
|
|
// This should not happen anymore
|
|
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
|
throw InvalidParameterException("Invalid recovery key")
|
|
}
|
|
|
|
decryption!!
|
|
}
|
|
}.fold(
|
|
{
|
|
callback.onFailure(it)
|
|
},
|
|
{ decryption ->
|
|
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
|
|
|
// Get backed up keys from the homeserver
|
|
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
|
|
override fun onSuccess(data: KeysBackupData) {
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
|
|
val sessionsData = ArrayList<MegolmSessionData>()
|
|
// Restore that data
|
|
var sessionsFromHsCount = 0
|
|
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) {
|
|
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) {
|
|
sessionsFromHsCount++
|
|
|
|
val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!
|
|
|
|
val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)
|
|
|
|
sessionData?.let {
|
|
sessionsData.add(it)
|
|
}
|
|
}
|
|
}
|
|
Timber.v("restoreKeysWithRecoveryKey: Decrypted " + sessionsData.size + " keys out of "
|
|
+ sessionsFromHsCount + " from the backup store on the homeserver")
|
|
|
|
// Do not trigger a backup for them if they come from the backup version we are using
|
|
val backUp = keysVersionResult.version != keysBackupVersion?.version
|
|
if (backUp) {
|
|
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up to backup version: "
|
|
+ keysBackupVersion?.version)
|
|
}
|
|
|
|
// Import them into the crypto store
|
|
val progressListener = if (stepProgressListener != null) {
|
|
object : ProgressListener {
|
|
override fun onProgress(progress: Int, total: Int) {
|
|
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
|
|
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
|
|
}
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
|
|
val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)
|
|
|
|
// Do not back up the key if it comes from a backup recovery
|
|
if (backUp) {
|
|
maybeBackupKeys()
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
callback.onSuccess(importRoomKeysResult)
|
|
}
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
callback.onFailure(failure)
|
|
}
|
|
})
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
|
|
password: String,
|
|
roomId: String?,
|
|
sessionId: String?,
|
|
stepProgressListener: StepProgressListener?,
|
|
callback: MatrixCallback<ImportRoomKeysResult>) {
|
|
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
|
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
val progressListener = if (stepProgressListener != null) {
|
|
object : ProgressListener {
|
|
override fun onProgress(progress: Int, total: Int) {
|
|
uiHandler.post {
|
|
stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total))
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
null
|
|
}
|
|
|
|
Try {
|
|
recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
|
|
}
|
|
}.fold(
|
|
{
|
|
callback.onFailure(it)
|
|
},
|
|
{ recoveryKey ->
|
|
if (recoveryKey == null) {
|
|
Timber.v("backupKeys: Invalid configuration")
|
|
callback.onFailure(IllegalStateException("Invalid configuration"))
|
|
} else {
|
|
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback)
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable
|
|
* parameters and always returns a KeysBackupData object through the Callback
|
|
*/
|
|
private fun getKeys(sessionId: String?,
|
|
roomId: String?,
|
|
version: String,
|
|
callback: MatrixCallback<KeysBackupData>) {
|
|
if (roomId != null && sessionId != null) {
|
|
// Get key for the room and for the session
|
|
getRoomSessionDataTask
|
|
.configureWith(GetRoomSessionDataTask.Params(roomId, sessionId, version)) {
|
|
this.callback = object : MatrixCallback<KeyBackupData> {
|
|
override fun onSuccess(data: KeyBackupData) {
|
|
// Convert to KeysBackupData
|
|
val keysBackupData = KeysBackupData()
|
|
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
|
val roomKeysBackupData = RoomKeysBackupData()
|
|
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
|
|
roomKeysBackupData.sessionIdToKeyBackupData[sessionId] = data
|
|
keysBackupData.roomIdToRoomKeysBackupData[roomId] = roomKeysBackupData
|
|
|
|
callback.onSuccess(keysBackupData)
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
callback.onFailure(failure)
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
} else if (roomId != null) {
|
|
// Get all keys for the room
|
|
getRoomSessionsDataTask
|
|
.configureWith(GetRoomSessionsDataTask.Params(roomId, version)) {
|
|
this.callback = object : MatrixCallback<RoomKeysBackupData> {
|
|
override fun onSuccess(data: RoomKeysBackupData) {
|
|
// Convert to KeysBackupData
|
|
val keysBackupData = KeysBackupData()
|
|
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
|
keysBackupData.roomIdToRoomKeysBackupData[roomId] = data
|
|
|
|
callback.onSuccess(keysBackupData)
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
callback.onFailure(failure)
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
} else {
|
|
// Get all keys
|
|
getSessionsDataTask
|
|
.configureWith(GetSessionsDataTask.Params(version)) {
|
|
this.callback = callback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
@WorkerThread
|
|
fun pkDecryptionFromRecoveryKey(recoveryKey: String): OlmPkDecryption? {
|
|
// Extract the primary key
|
|
val privateKey = extractCurveKeyFromRecoveryKey(recoveryKey)
|
|
|
|
// Built the PK decryption with it
|
|
var decryption: OlmPkDecryption? = null
|
|
if (privateKey != null) {
|
|
try {
|
|
decryption = OlmPkDecryption()
|
|
decryption.setPrivateKey(privateKey)
|
|
} catch (e: OlmException) {
|
|
Timber.e(e, "OlmException")
|
|
}
|
|
|
|
}
|
|
|
|
return decryption
|
|
}
|
|
|
|
/**
|
|
* Do a backup if there are new keys, with a delay
|
|
*/
|
|
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.nextInt(KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS).toLong()
|
|
|
|
uiHandler.postDelayed({ backupKeys() }, delayInMs)
|
|
}
|
|
else -> {
|
|
Timber.v("maybeBackupKeys: Skip it because state: $state")
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun getVersion(version: String,
|
|
callback: MatrixCallback<KeysVersionResult?>) {
|
|
getKeysBackupVersionTask
|
|
.configureWith(version) {
|
|
this.callback = object : MatrixCallback<KeysVersionResult> {
|
|
override fun onSuccess(data: KeysVersionResult) {
|
|
callback.onSuccess(data)
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
if (failure is Failure.ServerError
|
|
&& failure.error.code == MatrixError.NOT_FOUND) {
|
|
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
|
callback.onSuccess(null)
|
|
} else {
|
|
// Transmit the error
|
|
callback.onFailure(failure)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun getCurrentVersion(callback: MatrixCallback<KeysVersionResult?>) {
|
|
getKeysBackupLastVersionTask
|
|
.configureWith {
|
|
this.callback = object : MatrixCallback<KeysVersionResult> {
|
|
override fun onSuccess(data: KeysVersionResult) {
|
|
callback.onSuccess(data)
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
if (failure is Failure.ServerError
|
|
&& failure.error.code == MatrixError.NOT_FOUND) {
|
|
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
|
|
callback.onSuccess(null)
|
|
} else {
|
|
// Transmit the error
|
|
callback.onFailure(failure)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
|
|
override fun forceUsingLastVersion(callback: MatrixCallback<Boolean>) {
|
|
getCurrentVersion(object : MatrixCallback<KeysVersionResult?> {
|
|
override fun onSuccess(data: KeysVersionResult?) {
|
|
val localBackupVersion = keysBackupVersion?.version
|
|
val serverBackupVersion = data?.version
|
|
|
|
if (serverBackupVersion == null) {
|
|
if (localBackupVersion == null) {
|
|
// No backup on the server, and backup is not active
|
|
callback.onSuccess(true)
|
|
} else {
|
|
// No backup on the server, and we are currently backing up, so stop backing up
|
|
callback.onSuccess(false)
|
|
resetKeysBackupData()
|
|
keysBackupVersion = null
|
|
keysBackupStateManager.state = KeysBackupState.Disabled
|
|
}
|
|
} else {
|
|
if (localBackupVersion == null) {
|
|
// backup on the server, and backup is not active
|
|
callback.onSuccess(false)
|
|
// Do a check
|
|
checkAndStartWithKeysBackupVersion(data)
|
|
} 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
|
|
callback.onSuccess(true)
|
|
} else {
|
|
// We are not using the last version, so delete the current version we are using on the server
|
|
callback.onSuccess(false)
|
|
|
|
// This will automatically check for the last version then
|
|
deleteBackup(localBackupVersion, null)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
callback.onFailure(failure)
|
|
}
|
|
})
|
|
}
|
|
|
|
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 = cryptoStore.getKeyBackupVersion()
|
|
|
|
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")
|
|
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
|
|
}
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
/* ==========================================================================================
|
|
* Private
|
|
* ========================================================================================== */
|
|
|
|
/**
|
|
* 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? {
|
|
if (keysBackupData.version.isNullOrBlank()
|
|
|| keysBackupData.algorithm != MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
|
|| keysBackupData.authData == null) {
|
|
return null
|
|
}
|
|
|
|
val authData = keysBackupData.getAuthDataAsMegolmBackupAuthData()
|
|
|
|
if (authData?.signatures == null || authData.publicKey.isBlank()) {
|
|
return null
|
|
}
|
|
|
|
return authData
|
|
}
|
|
|
|
/**
|
|
* Compute the recovery key from a password and key backup version.
|
|
*
|
|
* @param password the password.
|
|
* @param keysBackupData the backup and its auth data.
|
|
*
|
|
* @return the recovery key if successful, null in other cases
|
|
*/
|
|
@WorkerThread
|
|
private fun recoveryKeyFromPassword(password: String, keysBackupData: KeysVersionResult, progressListener: ProgressListener?): String? {
|
|
val authData = getMegolmBackupAuthData(keysBackupData)
|
|
|
|
if (authData == null) {
|
|
Timber.w("recoveryKeyFromPassword: invalid parameter")
|
|
return null
|
|
}
|
|
|
|
if (authData.privateKeySalt.isNullOrBlank()
|
|
|| authData.privateKeyIterations == null) {
|
|
Timber.w("recoveryKeyFromPassword: Salt and/or iterations not found in key backup auth data")
|
|
|
|
return null
|
|
}
|
|
|
|
// Extract the recovery key from the passphrase
|
|
val data = retrievePrivateKeyWithPassword(password, authData.privateKeySalt!!, authData.privateKeyIterations!!, progressListener)
|
|
|
|
return computeRecoveryKey(data)
|
|
}
|
|
|
|
/**
|
|
* Check if a recovery key matches key backup authentication data.
|
|
*
|
|
* @param recoveryKey the recovery key to challenge.
|
|
* @param keysBackupData the backup and its auth data.
|
|
*
|
|
* @return true if successful.
|
|
*/
|
|
@WorkerThread
|
|
private fun isValidRecoveryKeyForKeysBackupVersion(recoveryKey: String, keysBackupData: KeysVersionResult): Boolean {
|
|
// Build PK decryption instance with the recovery key
|
|
val publicKey = pkPublicKeyFromRecoveryKey(recoveryKey)
|
|
|
|
if (publicKey == null) {
|
|
Timber.w("isValidRecoveryKeyForKeysBackupVersion: public key is null")
|
|
|
|
return false
|
|
}
|
|
|
|
val authData = getMegolmBackupAuthData(keysBackupData)
|
|
|
|
if (authData == null) {
|
|
Timber.w("isValidRecoveryKeyForKeysBackupVersion: Key backup is missing required data")
|
|
|
|
return false
|
|
}
|
|
|
|
// Compare both
|
|
if (publicKey != authData.publicKey) {
|
|
Timber.w("isValidRecoveryKeyForKeysBackupVersion: Public keys mismatch")
|
|
|
|
return false
|
|
}
|
|
|
|
// Public keys match!
|
|
return true
|
|
}
|
|
|
|
/**
|
|
* 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 fun enableKeysBackup(keysVersionResult: KeysVersionResult) {
|
|
if (keysVersionResult.authData != null) {
|
|
val retrievedMegolmBackupAuthData = keysVersionResult.getAuthDataAsMegolmBackupAuthData()
|
|
|
|
if (retrievedMegolmBackupAuthData != null) {
|
|
keysBackupVersion = keysVersionResult
|
|
cryptoStore.setKeyBackupVersion(keysVersionResult.version)
|
|
|
|
onServerDataRetrieved(keysVersionResult.count, keysVersionResult.hash)
|
|
|
|
try {
|
|
backupOlmPkEncryption = OlmPkEncryption().apply {
|
|
setRecipientKey(retrievedMegolmBackupAuthData.publicKey)
|
|
}
|
|
} 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
|
|
}
|
|
} else {
|
|
Timber.e("Invalid authentication data")
|
|
keysBackupStateManager.state = KeysBackupState.Disabled
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the DB with data fetch from the server
|
|
*/
|
|
private fun onServerDataRetrieved(count: Int?, hash: String?) {
|
|
cryptoStore.setKeysBackupData(KeysBackupDataEntity()
|
|
.apply {
|
|
backupLastServerNumberOfKeys = count
|
|
backupLastServerHash = hash
|
|
}
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Reset all local key backup data.
|
|
*
|
|
* Note: This method does not update the state
|
|
*/
|
|
private fun resetKeysBackupData() {
|
|
resetBackupAllGroupSessionsListeners()
|
|
|
|
cryptoStore.setKeyBackupVersion(null)
|
|
cryptoStore.setKeysBackupData(null)
|
|
backupOlmPkEncryption = null
|
|
|
|
// Reset backup markers
|
|
cryptoStore.resetBackupMarkers()
|
|
}
|
|
|
|
/**
|
|
* Send a chunk of keys to backup
|
|
*/
|
|
@UiThread
|
|
private 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 || backupOlmPkEncryption == null || keysBackupVersion == null) {
|
|
Timber.v("backupKeys: Invalid configuration")
|
|
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
|
|
}
|
|
|
|
// Get a chunk of keys to backup
|
|
val olmInboundGroupSessionWrappers = cryptoStore.inboundGroupSessionsToBackup(KEY_BACKUP_SEND_KEYS_MAX_COUNT)
|
|
|
|
Timber.v("backupKeys: 1 - " + olmInboundGroupSessionWrappers.size + " sessions to back up")
|
|
|
|
if (olmInboundGroupSessionWrappers.isEmpty()) {
|
|
// Backup is up to date
|
|
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
|
|
|
backupAllGroupSessionsCallback?.onSuccess(Unit)
|
|
resetBackupAllGroupSessionsListeners()
|
|
return
|
|
}
|
|
|
|
keysBackupStateManager.state = KeysBackupState.BackingUp
|
|
|
|
GlobalScope.launch(coroutineDispatchers.main) {
|
|
withContext(coroutineDispatchers.crypto) {
|
|
Timber.v("backupKeys: 2 - Encrypting keys")
|
|
|
|
// Gather data to send to the homeserver
|
|
// roomId -> sessionId -> MXKeyBackupData
|
|
val keysBackupData = KeysBackupData()
|
|
keysBackupData.roomIdToRoomKeysBackupData = HashMap()
|
|
|
|
for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) {
|
|
val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper)
|
|
if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) {
|
|
val roomKeysBackupData = RoomKeysBackupData()
|
|
roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
|
|
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData
|
|
}
|
|
|
|
try {
|
|
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!
|
|
.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
|
|
} catch (e: OlmException) {
|
|
Timber.e(e, "OlmException")
|
|
}
|
|
}
|
|
|
|
Timber.v("backupKeys: 4 - Sending request")
|
|
|
|
val sendingRequestCallback = object : MatrixCallback<BackupKeysResult> {
|
|
override fun onSuccess(data: BackupKeysResult) {
|
|
uiHandler.post {
|
|
Timber.v("backupKeys: 5a - Request complete")
|
|
|
|
// Mark keys as backed up
|
|
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
|
|
|
if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
|
|
Timber.v("backupKeys: All keys have been backed up")
|
|
onServerDataRetrieved(data.count, data.hash)
|
|
|
|
// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
|
|
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
|
} else {
|
|
Timber.v("backupKeys: Continue to back up keys")
|
|
keysBackupStateManager.state = KeysBackupState.WillBackUp
|
|
|
|
backupKeys()
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onFailure(failure: Throwable) {
|
|
if (failure is Failure.ServerError) {
|
|
uiHandler.post {
|
|
Timber.e(failure, "backupKeys: backupKeys failed.")
|
|
|
|
when (failure.error.code) {
|
|
MatrixError.NOT_FOUND,
|
|
MatrixError.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 {
|
|
uiHandler.post {
|
|
backupAllGroupSessionsCallback?.onFailure(failure)
|
|
resetBackupAllGroupSessionsListeners()
|
|
|
|
Timber.e("backupKeys: backupKeys failed.")
|
|
|
|
// Retry a bit later
|
|
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
|
maybeBackupKeys()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Make the request
|
|
storeSessionDataTask
|
|
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)){
|
|
this.callback = sendingRequestCallback
|
|
}
|
|
.executeBy(taskExecutor)
|
|
}
|
|
}
|
|
}
|
|
|
|
@VisibleForTesting
|
|
@WorkerThread
|
|
fun encryptGroupSession(olmInboundGroupSessionWrapper: OlmInboundGroupSessionWrapper): KeyBackupData {
|
|
// Gather information for each key
|
|
val device = cryptoStore.deviceWithIdentityKey(olmInboundGroupSessionWrapper.senderKey!!)
|
|
|
|
// Build the m.megolm_backup.v1.curve25519-aes-sha2 data as defined at
|
|
// https://github.com/uhoreg/matrix-doc/blob/e2e_backup/proposals/1219-storing-megolm-keys-serverside.md#mmegolm_backupv1curve25519-aes-sha2-key-format
|
|
val sessionData = olmInboundGroupSessionWrapper.exportKeys()
|
|
val sessionBackupData = mapOf(
|
|
"algorithm" to sessionData!!.algorithm,
|
|
"sender_key" to sessionData.senderKey,
|
|
"sender_claimed_keys" to sessionData.senderClaimedKeys,
|
|
"forwarding_curve25519_key_chain" to (sessionData.forwardingCurve25519KeyChain
|
|
?: ArrayList<Any>()),
|
|
"session_key" to sessionData.sessionKey)
|
|
|
|
var encryptedSessionBackupData: OlmPkMessage? = null
|
|
|
|
val moshi = MoshiProvider.providesMoshi()
|
|
val adapter = moshi.adapter(Map::class.java)
|
|
|
|
try {
|
|
val json = adapter.toJson(sessionBackupData)
|
|
|
|
encryptedSessionBackupData = backupOlmPkEncryption?.encrypt(json)
|
|
} catch (e: OlmException) {
|
|
Timber.e(e, "OlmException")
|
|
}
|
|
|
|
// Build backup data for that key
|
|
val keyBackupData = KeyBackupData()
|
|
try {
|
|
keyBackupData.firstMessageIndex = olmInboundGroupSessionWrapper.olmInboundGroupSession!!.firstKnownIndex
|
|
} catch (e: OlmException) {
|
|
Timber.e(e, "OlmException")
|
|
}
|
|
|
|
keyBackupData.forwardedCount = olmInboundGroupSessionWrapper.forwardingCurve25519KeyChain!!.size
|
|
keyBackupData.isVerified = device?.isVerified == true
|
|
|
|
val data = mapOf(
|
|
"ciphertext" to encryptedSessionBackupData!!.mCipherText,
|
|
"mac" to encryptedSessionBackupData.mMac,
|
|
"ephemeral" to encryptedSessionBackupData.mEphemeralKey)
|
|
|
|
keyBackupData.sessionData = data
|
|
|
|
return keyBackupData
|
|
}
|
|
|
|
@VisibleForTesting
|
|
@WorkerThread
|
|
fun decryptKeyBackupData(keyBackupData: KeyBackupData, sessionId: String, roomId: String, decryption: OlmPkDecryption): MegolmSessionData? {
|
|
var sessionBackupData: MegolmSessionData? = null
|
|
|
|
val jsonObject = keyBackupData.sessionData
|
|
|
|
val ciphertext = jsonObject?.get("ciphertext")?.toString()
|
|
val mac = jsonObject?.get("mac")?.toString()
|
|
val ephemeralKey = jsonObject?.get("ephemeral")?.toString()
|
|
|
|
if (ciphertext != null && mac != null && ephemeralKey != null) {
|
|
val encrypted = OlmPkMessage()
|
|
encrypted.mCipherText = ciphertext
|
|
encrypted.mMac = mac
|
|
encrypted.mEphemeralKey = ephemeralKey
|
|
|
|
try {
|
|
val decrypted = decryption.decrypt(encrypted)
|
|
|
|
val moshi = MoshiProvider.providesMoshi()
|
|
val adapter = moshi.adapter(MegolmSessionData::class.java)
|
|
|
|
sessionBackupData = adapter.fromJson(decrypted)
|
|
} catch (e: OlmException) {
|
|
Timber.e(e, "OlmException")
|
|
}
|
|
|
|
if (sessionBackupData != null) {
|
|
sessionBackupData.sessionId = sessionId
|
|
sessionBackupData.roomId = roomId
|
|
}
|
|
}
|
|
|
|
return sessionBackupData
|
|
}
|
|
|
|
companion object {
|
|
// Maximum delay in ms in {@link maybeBackupKeys}
|
|
private const val KEY_BACKUP_WAITING_TIME_TO_SEND_KEY_BACKUP_MILLIS = 10000
|
|
|
|
// Maximum number of keys to send at a time to the homeserver.
|
|
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100
|
|
}
|
|
|
|
/* ==========================================================================================
|
|
* DEBUG INFO
|
|
* ========================================================================================== */
|
|
|
|
override fun toString() = "KeysBackup for ${credentials.userId}"
|
|
}
|