forked from GitHub-Mirror/riotX-android
Remove CryptoAsyncHelper and use only coroutine
This commit is contained in:
parent
907a1d1a4b
commit
659ba34fb3
@ -1,61 +0,0 @@
|
||||
/*
|
||||
*
|
||||
* * Copyright 2019 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
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.os.Looper
|
||||
|
||||
private const val THREAD_CRYPTO_NAME = "Crypto_Thread"
|
||||
|
||||
// TODO Remove and replace by Task
|
||||
internal object CryptoAsyncHelper {
|
||||
|
||||
private var uiHandler: Handler? = null
|
||||
private var cryptoBackgroundHandler: Handler? = null
|
||||
|
||||
fun getUiHandler(): Handler {
|
||||
return uiHandler
|
||||
?: Handler(Looper.getMainLooper())
|
||||
.also { uiHandler = it }
|
||||
}
|
||||
|
||||
|
||||
fun getDecryptBackgroundHandler(): Handler {
|
||||
return getCryptoBackgroundHandler()
|
||||
}
|
||||
|
||||
fun getEncryptBackgroundHandler(): Handler {
|
||||
return getCryptoBackgroundHandler()
|
||||
}
|
||||
|
||||
private fun getCryptoBackgroundHandler(): Handler {
|
||||
return cryptoBackgroundHandler
|
||||
?: createCryptoBackgroundHandler()
|
||||
.also { cryptoBackgroundHandler = it }
|
||||
}
|
||||
|
||||
private fun createCryptoBackgroundHandler(): Handler {
|
||||
val handlerThread = HandlerThread(THREAD_CRYPTO_NAME)
|
||||
handlerThread.start()
|
||||
return Handler(handlerThread.looper)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -19,6 +19,8 @@
|
||||
package im.vector.matrix.android.internal.crypto
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.text.TextUtils
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
@ -138,6 +140,8 @@ internal class CryptoManager(
|
||||
private val taskExecutor: TaskExecutor
|
||||
) : CryptoService {
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
// MXEncrypting instance for each room.
|
||||
private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap()
|
||||
private val isStarting = AtomicBoolean(false)
|
||||
@ -825,41 +829,33 @@ internal class CryptoManager(
|
||||
password: String,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
// TODO Use coroutines
|
||||
Timber.v("## importRoomKeys starts")
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
withContext(coroutineDispatchers.crypto) {
|
||||
Try {
|
||||
Timber.v("## importRoomKeys starts")
|
||||
|
||||
val t0 = System.currentTimeMillis()
|
||||
val roomKeys: String
|
||||
val t0 = System.currentTimeMillis()
|
||||
val roomKeys: String = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
||||
|
||||
try {
|
||||
roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
|
||||
} catch (e: Exception) {
|
||||
callback.onFailure(e)
|
||||
return
|
||||
val importedSessions: List<MegolmSessionData>
|
||||
|
||||
val t1 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")
|
||||
|
||||
val list = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
.fromJson(roomKeys)
|
||||
importedSessions = list as List<MegolmSessionData>
|
||||
|
||||
val t2 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importRoomKeys : JSON parsing " + (t2 - t1) + " ms")
|
||||
|
||||
megolmSessionDataImporter.handle(importedSessions, true, uiHandler, progressListener)
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
|
||||
val importedSessions: List<MegolmSessionData>
|
||||
|
||||
val t1 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")
|
||||
|
||||
try {
|
||||
val list = MoshiProvider.providesMoshi()
|
||||
.adapter(List::class.java)
|
||||
.fromJson(roomKeys)
|
||||
importedSessions = list as List<MegolmSessionData>
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## importRoomKeys failed")
|
||||
callback.onFailure(e)
|
||||
return
|
||||
}
|
||||
|
||||
val t2 = System.currentTimeMillis()
|
||||
|
||||
Timber.v("## importRoomKeys : JSON parsing " + (t2 - t1) + " ms")
|
||||
|
||||
megolmSessionDataImporter.handle(importedSessions, true, progressListener, callback)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1067,9 +1063,9 @@ internal class CryptoManager(
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString(): String {
|
||||
return "CryptoManager of " + credentials.userId + " (" + credentials.deviceId + ")"
|
||||
|
@ -277,6 +277,7 @@ internal class CryptoModule {
|
||||
// Task
|
||||
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
|
||||
// Task executor
|
||||
get(),
|
||||
get())
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,9 @@
|
||||
|
||||
package im.vector.matrix.android.internal.crypto.actions
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import android.os.Handler
|
||||
import androidx.annotation.WorkerThread
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
|
||||
import im.vector.matrix.android.internal.crypto.MXOlmDevice
|
||||
import im.vector.matrix.android.internal.crypto.MegolmSessionData
|
||||
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
|
||||
@ -35,34 +35,32 @@ internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,
|
||||
|
||||
/**
|
||||
* Import a list of megolm session keys.
|
||||
* Must be call on the crypto coroutine thread
|
||||
*
|
||||
* @param megolmSessionsData megolm sessions.
|
||||
* @param backUpKeys true to back up them to the homeserver.
|
||||
* @param progressListener the progress listener
|
||||
* @param callback
|
||||
* @return import room keys result
|
||||
*/
|
||||
@WorkerThread
|
||||
fun handle(megolmSessionsData: List<MegolmSessionData>,
|
||||
fromBackup: Boolean,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
uiHandler: Handler,
|
||||
progressListener: ProgressListener?): ImportRoomKeysResult {
|
||||
val t0 = System.currentTimeMillis()
|
||||
|
||||
val totalNumbersOfKeys = megolmSessionsData.size
|
||||
var cpt = 0
|
||||
var lastProgress = 0
|
||||
var totalNumbersOfImportedKeys = 0
|
||||
|
||||
if (progressListener != null) {
|
||||
CryptoAsyncHelper.getUiHandler().post {
|
||||
uiHandler.post {
|
||||
progressListener.onProgress(0, 100)
|
||||
}
|
||||
}
|
||||
val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData)
|
||||
|
||||
for (megolmSessionData in megolmSessionsData) {
|
||||
cpt++
|
||||
|
||||
|
||||
megolmSessionsData.forEachIndexed { cpt, megolmSessionData ->
|
||||
val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)
|
||||
|
||||
if (null != decrypting) {
|
||||
@ -90,7 +88,7 @@ internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,
|
||||
}
|
||||
|
||||
if (progressListener != null) {
|
||||
CryptoAsyncHelper.getUiHandler().post {
|
||||
uiHandler.post {
|
||||
val progress = 100 * cpt / totalNumbersOfKeys
|
||||
|
||||
if (lastProgress != progress) {
|
||||
@ -111,10 +109,6 @@ internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,
|
||||
|
||||
Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")
|
||||
|
||||
val finalTotalNumbersOfImportedKeys = totalNumbersOfImportedKeys
|
||||
|
||||
CryptoAsyncHelper.getUiHandler().post {
|
||||
callback.onSuccess(ImportRoomKeysResult(totalNumbersOfKeys, finalTotalNumbersOfImportedKeys))
|
||||
}
|
||||
return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)
|
||||
}
|
||||
}
|
@ -31,7 +31,10 @@ 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.*
|
||||
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
|
||||
@ -47,10 +50,15 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap
|
||||
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.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.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
|
||||
@ -87,7 +95,8 @@ internal class KeysBackup(
|
||||
private val storeSessionDataTask: StoreSessionsDataTask,
|
||||
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
|
||||
// Task executor
|
||||
private val taskExecutor: TaskExecutor
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val coroutineDispatchers: MatrixCoroutineDispatchers
|
||||
) : KeysBackupService {
|
||||
|
||||
private val uiHandler = Handler(Looper.getMainLooper())
|
||||
@ -130,55 +139,53 @@ internal class KeysBackup(
|
||||
override fun prepareKeysBackupVersion(password: String?,
|
||||
progressListener: ProgressListener?,
|
||||
callback: MatrixCallback<MegolmBackupCreationInfo>) {
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
try {
|
||||
val olmPkDecryption = OlmPkDecryption()
|
||||
val megolmBackupAuthData = MegolmBackupAuthData()
|
||||
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")
|
||||
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 generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
|
||||
megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey)
|
||||
megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt
|
||||
megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations
|
||||
} else {
|
||||
val publicKey = olmPkDecryption.generateKey()
|
||||
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())
|
||||
|
||||
megolmBackupAuthData.publicKey = publicKey
|
||||
megolmBackupAuthData.signatures = objectSigner.signObject(canonicalJson)
|
||||
|
||||
|
||||
val megolmBackupCreationInfo = MegolmBackupCreationInfo()
|
||||
megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
|
||||
megolmBackupCreationInfo.authData = megolmBackupAuthData
|
||||
megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())
|
||||
|
||||
megolmBackupCreationInfo
|
||||
}
|
||||
|
||||
val canonicalJson = MoshiProvider.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())
|
||||
|
||||
uiHandler.post { callback.onSuccess(megolmBackupCreationInfo) }
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
|
||||
uiHandler.post { callback.onFailure(e) }
|
||||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
}
|
||||
|
||||
@ -221,37 +228,39 @@ internal class KeysBackup(
|
||||
}
|
||||
|
||||
override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
// 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
|
||||
}
|
||||
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))
|
||||
.dispatchTo(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()
|
||||
deleteBackupTask.configureWith(DeleteBackupTask.Params(version))
|
||||
.dispatchTo(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()
|
||||
override fun onSuccess(data: Unit) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onSuccess(Unit) }
|
||||
}
|
||||
uiHandler.post { callback?.onSuccess(Unit) }
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
eventuallyRestartBackup()
|
||||
override fun onFailure(failure: Throwable) {
|
||||
eventuallyRestartBackup()
|
||||
|
||||
uiHandler.post { callback?.onFailure(failure) }
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
uiHandler.post { callback?.onFailure(failure) }
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -426,85 +435,80 @@ internal class KeysBackup(
|
||||
callback: MatrixCallback<Unit>) {
|
||||
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")
|
||||
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
val myUserId = credentials.userId
|
||||
// Get auth data to update it
|
||||
val authData = getMegolmBackupAuthData(keysBackupVersion)
|
||||
|
||||
// Get auth data to update it
|
||||
val authData = getMegolmBackupAuthData(keysBackupVersion)
|
||||
if (authData == null) {
|
||||
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")
|
||||
|
||||
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
|
||||
|
||||
uiHandler.post {
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
// Get current signatures, or create an empty set
|
||||
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap())
|
||||
|
||||
if (trust) {
|
||||
// Add current device signature
|
||||
val canonicalJson = MoshiProvider.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
|
||||
}
|
||||
|
||||
return@post
|
||||
}
|
||||
// And send it to the homeserver
|
||||
updateKeysBackupVersionTask
|
||||
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
|
||||
.dispatchTo(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
// Relaunch the state machine on this updated backup version
|
||||
val newKeysBackupVersion = KeysVersionResult()
|
||||
|
||||
// Get current signatures, or create an empty set
|
||||
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap())
|
||||
newKeysBackupVersion.version = keysBackupVersion.version
|
||||
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
|
||||
newKeysBackupVersion.count = keysBackupVersion.count
|
||||
newKeysBackupVersion.hash = keysBackupVersion.hash
|
||||
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData
|
||||
|
||||
if (trust) {
|
||||
// Add current device signature
|
||||
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())
|
||||
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
|
||||
|
||||
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>?
|
||||
|
||||
// And send it to the homeserver
|
||||
updateKeysBackupVersionTask
|
||||
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
|
||||
.dispatchTo(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)
|
||||
|
||||
uiHandler.post {
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
uiHandler.post {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -513,17 +517,18 @@ internal class KeysBackup(
|
||||
callback: MatrixCallback<Unit>) {
|
||||
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")
|
||||
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
|
||||
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
||||
|
||||
uiHandler.post {
|
||||
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
||||
}
|
||||
return@post
|
||||
GlobalScope.launch(coroutineDispatchers.main) {
|
||||
val isValid = withContext(coroutineDispatchers.crypto) {
|
||||
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
|
||||
}
|
||||
|
||||
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
||||
if (!isValid) {
|
||||
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")
|
||||
|
||||
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
|
||||
} else {
|
||||
trustKeysBackupVersion(keysBackupVersion, true, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -532,21 +537,19 @@ internal class KeysBackup(
|
||||
callback: MatrixCallback<Unit>) {
|
||||
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")
|
||||
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null)
|
||||
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")
|
||||
|
||||
uiHandler.post {
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
}
|
||||
|
||||
return@post
|
||||
callback.onFailure(IllegalArgumentException("Missing element"))
|
||||
} else {
|
||||
// Check trust using the recovery key
|
||||
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
||||
}
|
||||
|
||||
// Check trust using the recovery key
|
||||
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
|
||||
}
|
||||
}
|
||||
|
||||
@ -591,12 +594,10 @@ internal class KeysBackup(
|
||||
}
|
||||
|
||||
override fun getBackupProgress(progressListener: ProgressListener) {
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true)
|
||||
val total = cryptoStore.inboundGroupSessionsCount(false)
|
||||
val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true)
|
||||
val total = cryptoStore.inboundGroupSessionsCount(false)
|
||||
|
||||
uiHandler.post { progressListener.onProgress(backedUpKeys, total) }
|
||||
}
|
||||
progressListener.onProgress(backedUpKeys, total)
|
||||
}
|
||||
|
||||
override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
|
||||
@ -607,88 +608,95 @@ internal class KeysBackup(
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")
|
||||
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post(Runnable {
|
||||
// Check if the recovery is valid before going any further
|
||||
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
|
||||
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) }
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
// Get a PK decryption instance
|
||||
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
|
||||
if (decryption == null) {
|
||||
// This should not happen anymore
|
||||
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
|
||||
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) }
|
||||
return@Runnable
|
||||
}
|
||||
|
||||
if (stepProgressListener != null) {
|
||||
uiHandler.post { 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) {
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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")
|
||||
}
|
||||
|
||||
megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener, object : MatrixCallback<ImportRoomKeysResult> {
|
||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||
// Do not back up the key if it comes from a backup recovery
|
||||
if (backUp) {
|
||||
maybeBackupKeys()
|
||||
}
|
||||
|
||||
callback.onSuccess(data)
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
}
|
||||
})
|
||||
decryption!!
|
||||
}
|
||||
}.fold(
|
||||
{
|
||||
callback.onFailure(it)
|
||||
},
|
||||
{ decryption ->
|
||||
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
uiHandler.post { callback.onFailure(failure) }
|
||||
}
|
||||
})
|
||||
})
|
||||
// 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,
|
||||
@ -699,31 +707,36 @@ internal class KeysBackup(
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")
|
||||
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
val progressListener = if (stepProgressListener != null) {
|
||||
object : ProgressListener {
|
||||
override fun onProgress(progress: Int, total: Int) {
|
||||
uiHandler.post {
|
||||
stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total))
|
||||
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
|
||||
}
|
||||
|
||||
val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
|
||||
|
||||
if (recoveryKey == null) {
|
||||
uiHandler.post {
|
||||
Timber.v("backupKeys: Invalid configuration")
|
||||
callback.onFailure(IllegalStateException("Invalid configuration"))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return@post
|
||||
}
|
||||
|
||||
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback)
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -989,9 +1002,9 @@ internal class KeysBackup(
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Private
|
||||
* ========================================================================================== */
|
||||
/* ==========================================================================================
|
||||
* Private
|
||||
* ========================================================================================== */
|
||||
|
||||
/**
|
||||
* Extract MegolmBackupAuthData data from a backup version.
|
||||
@ -1190,94 +1203,96 @@ internal class KeysBackup(
|
||||
|
||||
keysBackupStateManager.state = KeysBackupState.BackingUp
|
||||
|
||||
CryptoAsyncHelper.getEncryptBackgroundHandler().post {
|
||||
Timber.v("backupKeys: 2 - Encrypting keys")
|
||||
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()
|
||||
// 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
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
|
||||
} catch (e: OlmException) {
|
||||
Timber.e(e, "OlmException")
|
||||
}
|
||||
}
|
||||
Timber.v("backupKeys: 4 - Sending request")
|
||||
|
||||
Timber.v("backupKeys: 4 - Sending request")
|
||||
|
||||
// Make the request
|
||||
storeSessionDataTask
|
||||
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData))
|
||||
.dispatchTo(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) {
|
||||
// Make the request
|
||||
storeSessionDataTask
|
||||
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData))
|
||||
.dispatchTo(object : MatrixCallback<BackupKeysResult> {
|
||||
override fun onSuccess(data: BackupKeysResult) {
|
||||
uiHandler.post {
|
||||
Timber.e(failure, "backupKeys: backupKeys failed.")
|
||||
Timber.v("backupKeys: 5a - Request complete")
|
||||
|
||||
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
|
||||
// Mark keys as backed up
|
||||
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
|
||||
|
||||
// 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
|
||||
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()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uiHandler.post {
|
||||
backupAllGroupSessionsCallback?.onFailure(failure)
|
||||
resetBackupAllGroupSessionsListeners()
|
||||
}
|
||||
|
||||
Timber.e("backupKeys: backupKeys failed.")
|
||||
override fun onFailure(failure: Throwable) {
|
||||
if (failure is Failure.ServerError) {
|
||||
uiHandler.post {
|
||||
Timber.e(failure, "backupKeys: backupKeys failed.")
|
||||
|
||||
// Retry a bit later
|
||||
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
|
||||
maybeBackupKeys()
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1376,9 +1391,9 @@ internal class KeysBackup(
|
||||
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
/* ==========================================================================================
|
||||
* DEBUG INFO
|
||||
* ========================================================================================== */
|
||||
|
||||
override fun toString() = "KeysBackup for ${credentials.userId}"
|
||||
}
|
||||
|
@ -27,17 +27,12 @@ import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
|
||||
import im.vector.matrix.android.internal.crypto.DeviceListManager
|
||||
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.*
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
@ -138,8 +133,8 @@ internal class DefaultSasVerificationService(private val credentials: Credential
|
||||
|
||||
override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
|
||||
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
|
||||
deviceID,
|
||||
userId)
|
||||
deviceID,
|
||||
userId)
|
||||
|
||||
listeners.forEach {
|
||||
try {
|
||||
@ -206,7 +201,7 @@ internal class DefaultSasVerificationService(private val credentials: Credential
|
||||
} else {
|
||||
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
|
||||
cancelTransaction(tid, otherUserId, startReq.fromDevice
|
||||
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
|
||||
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -372,9 +367,8 @@ internal class DefaultSasVerificationService(private val credentials: Credential
|
||||
userId,
|
||||
deviceID)
|
||||
addTransaction(tx)
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
tx.start()
|
||||
}
|
||||
|
||||
tx.start()
|
||||
return txID
|
||||
} else {
|
||||
throw IllegalArgumentException("Unknown verification method")
|
||||
@ -399,9 +393,9 @@ internal class DefaultSasVerificationService(private val credentials: Credential
|
||||
override fun transactionUpdated(tx: VerificationTransaction) {
|
||||
dispatchTxUpdated(tx)
|
||||
if (tx is SASVerificationTransaction
|
||||
&& (tx.state == SasVerificationTxState.Cancelled
|
||||
|| tx.state == SasVerificationTxState.OnCancelled
|
||||
|| tx.state == SasVerificationTxState.Verified)
|
||||
&& (tx.state == SasVerificationTxState.Cancelled
|
||||
|| tx.state == SasVerificationTxState.OnCancelled
|
||||
|| tx.state == SasVerificationTxState.Verified)
|
||||
) {
|
||||
//remove
|
||||
this.removeTransaction(tx.otherUserId, tx.transactionId)
|
||||
|
@ -22,13 +22,12 @@ import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTr
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasMode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
@ -61,21 +60,21 @@ internal class IncomingSASVerificationTransaction(
|
||||
override val uxState: IncomingSasVerificationTransaction.UxState
|
||||
get() {
|
||||
return when (state) {
|
||||
SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT
|
||||
SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT
|
||||
SasVerificationTxState.SendingAccept,
|
||||
SasVerificationTxState.Accepted,
|
||||
SasVerificationTxState.OnKeyReceived,
|
||||
SasVerificationTxState.SendingKey,
|
||||
SasVerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT
|
||||
SasVerificationTxState.KeySent -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_KEY_AGREEMENT
|
||||
SasVerificationTxState.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS
|
||||
SasVerificationTxState.ShortCodeAccepted,
|
||||
SasVerificationTxState.SendingMac,
|
||||
SasVerificationTxState.MacSent,
|
||||
SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
|
||||
SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED
|
||||
SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME
|
||||
SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
|
||||
else -> IncomingSasVerificationTransaction.UxState.UNKNOWN
|
||||
SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
|
||||
SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED
|
||||
SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME
|
||||
SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
|
||||
else -> IncomingSasVerificationTransaction.UxState.UNKNOWN
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,9 +124,7 @@ internal class IncomingSASVerificationTransaction(
|
||||
//TODO force download keys!!
|
||||
//would be probably better to download the keys
|
||||
//for now I cancel
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
cancel(CancelCode.User)
|
||||
}
|
||||
cancel(CancelCode.User)
|
||||
} else {
|
||||
// val otherKey = info.identityKey()
|
||||
//need to jump back to correct thread
|
||||
@ -139,9 +136,7 @@ internal class IncomingSASVerificationTransaction(
|
||||
shortAuthenticationStrings = agreedShortCode,
|
||||
commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT)
|
||||
)
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
doAccept(accept)
|
||||
}
|
||||
doAccept(accept)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -86,7 +86,6 @@ internal class OutgoingSASVerificationRequest(
|
||||
}
|
||||
|
||||
fun start() {
|
||||
|
||||
if (state != SasVerificationTxState.None) {
|
||||
Timber.e("## start verification from invalid state")
|
||||
//should I cancel??
|
||||
|
@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.crypto.sas.EmojiRepresentation
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasMode
|
||||
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
|
||||
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
|
||||
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.MXKey
|
||||
@ -278,21 +277,17 @@ internal abstract class SASVerificationTransaction(
|
||||
.dispatchTo(object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.")
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
state = nextState
|
||||
}
|
||||
if (onDone != null) {
|
||||
onDone()
|
||||
} else {
|
||||
state = nextState
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state")
|
||||
|
||||
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
|
||||
cancel(onErrorReason)
|
||||
}
|
||||
cancel(onErrorReason)
|
||||
}
|
||||
})
|
||||
.executeBy(taskExecutor)
|
||||
|
@ -17,7 +17,8 @@
|
||||
package im.vector.matrix.android.internal.di
|
||||
|
||||
import android.content.Context
|
||||
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
@ -36,11 +37,14 @@ class MatrixModule(private val context: Context) {
|
||||
}
|
||||
|
||||
single {
|
||||
val cryptoHandler = CryptoAsyncHelper.getDecryptBackgroundHandler()
|
||||
val THREAD_CRYPTO_NAME = "Crypto_Thread"
|
||||
val handlerThread = HandlerThread(THREAD_CRYPTO_NAME)
|
||||
handlerThread.start()
|
||||
|
||||
MatrixCoroutineDispatchers(io = Dispatchers.IO,
|
||||
computation = Dispatchers.IO,
|
||||
main = Dispatchers.Main,
|
||||
crypto = cryptoHandler.asCoroutineDispatcher("crypto")
|
||||
crypto = Handler(handlerThread.looper).asCoroutineDispatcher("crypto")
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2019 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.riotredesign.features.crypto.keys
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import arrow.core.Try
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
|
||||
import im.vector.riotredesign.core.intent.getMimeTypeFromUri
|
||||
import im.vector.riotredesign.core.resources.openResource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class KeysImporter(private val session: Session) {
|
||||
|
||||
/**
|
||||
* Export keys and return the file path with the callback
|
||||
*/
|
||||
fun import(context: Context,
|
||||
uri: Uri,
|
||||
mimetype: String?,
|
||||
password: String,
|
||||
callback: MatrixCallback<ImportRoomKeysResult>) {
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Try {
|
||||
val resource = openResource(context, uri, mimetype ?: getMimeTypeFromUri(context, uri))
|
||||
|
||||
if (resource?.mContentStream == null) {
|
||||
throw Exception("Error")
|
||||
}
|
||||
|
||||
val data: ByteArray
|
||||
try {
|
||||
data = ByteArray(resource.mContentStream!!.available())
|
||||
resource.mContentStream!!.read(data)
|
||||
resource.mContentStream!!.close()
|
||||
|
||||
data
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
resource.mContentStream!!.close()
|
||||
} catch (e2: Exception) {
|
||||
Timber.e(e2, "## importKeys()")
|
||||
}
|
||||
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
.fold(
|
||||
{
|
||||
callback.onFailure(it)
|
||||
},
|
||||
{ byteArray ->
|
||||
session.importRoomKeys(byteArray,
|
||||
password,
|
||||
null,
|
||||
callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -59,18 +59,17 @@ import im.vector.riotredesign.core.extensions.withArgs
|
||||
import im.vector.riotredesign.core.intent.ExternalIntentData
|
||||
import im.vector.riotredesign.core.intent.analyseIntent
|
||||
import im.vector.riotredesign.core.intent.getFilenameFromUri
|
||||
import im.vector.riotredesign.core.intent.getMimeTypeFromUri
|
||||
import im.vector.riotredesign.core.platform.SimpleTextWatcher
|
||||
import im.vector.riotredesign.core.platform.VectorPreferenceFragment
|
||||
import im.vector.riotredesign.core.preference.BingRule
|
||||
import im.vector.riotredesign.core.preference.ProgressBarPreference
|
||||
import im.vector.riotredesign.core.preference.UserAvatarPreference
|
||||
import im.vector.riotredesign.core.preference.VectorPreference
|
||||
import im.vector.riotredesign.core.resources.openResource
|
||||
import im.vector.riotredesign.core.utils.*
|
||||
import im.vector.riotredesign.features.MainActivity
|
||||
import im.vector.riotredesign.features.configuration.VectorConfiguration
|
||||
import im.vector.riotredesign.features.crypto.keys.KeysExporter
|
||||
import im.vector.riotredesign.features.crypto.keys.KeysImporter
|
||||
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotredesign.features.themes.ThemeUtils
|
||||
import org.koin.android.ext.android.inject
|
||||
@ -2702,58 +2701,33 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
|
||||
|
||||
importButton.setOnClickListener(View.OnClickListener {
|
||||
val password = passPhraseEditText.text.toString()
|
||||
val resource = openResource(appContext, uri, mimetype ?: getMimeTypeFromUri(appContext, uri))
|
||||
|
||||
if (resource?.mContentStream == null) {
|
||||
appContext.toast("Error")
|
||||
KeysImporter(mSession)
|
||||
.import(requireContext(),
|
||||
uri,
|
||||
mimetype,
|
||||
password,
|
||||
object : MatrixCallback<ImportRoomKeysResult> {
|
||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
return@OnClickListener
|
||||
}
|
||||
hideLoadingView()
|
||||
|
||||
val data: ByteArray
|
||||
// TODO BG
|
||||
try {
|
||||
data = ByteArray(resource.mContentStream!!.available())
|
||||
resource.mContentStream!!.read(data)
|
||||
resource.mContentStream!!.close()
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
resource.mContentStream!!.close()
|
||||
} catch (e2: Exception) {
|
||||
Timber.e(e2, "## importKeys()")
|
||||
}
|
||||
AlertDialog.Builder(thisActivity)
|
||||
.setMessage(getString(R.string.encryption_import_room_keys_success,
|
||||
data.successfullyNumberOfImportedKeys,
|
||||
data.totalNumberOfKeys))
|
||||
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
appContext.toast(e.localizedMessage)
|
||||
|
||||
return@OnClickListener
|
||||
}
|
||||
|
||||
displayLoadingView()
|
||||
|
||||
mSession.importRoomKeys(data,
|
||||
password,
|
||||
null,
|
||||
object : MatrixCallback<ImportRoomKeysResult> {
|
||||
override fun onSuccess(data: ImportRoomKeysResult) {
|
||||
if (!isAdded) {
|
||||
return
|
||||
}
|
||||
|
||||
hideLoadingView()
|
||||
|
||||
AlertDialog.Builder(thisActivity)
|
||||
.setMessage(getString(R.string.encryption_import_room_keys_success,
|
||||
data.successfullyNumberOfImportedKeys,
|
||||
data.totalNumberOfKeys))
|
||||
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
appContext.toast(failure.localizedMessage)
|
||||
hideLoadingView()
|
||||
}
|
||||
})
|
||||
override fun onFailure(failure: Throwable) {
|
||||
appContext.toast(failure.localizedMessage)
|
||||
hideLoadingView()
|
||||
}
|
||||
})
|
||||
|
||||
importDialog.dismiss()
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user