Merge pull request #182 from vector-im/feature/cryptoKeys

Crypto: Import/export room keys (the old way)
This commit is contained in:
Benoit Marty 2019-06-17 19:05:43 +02:00 committed by GitHub
commit 0b6b95110f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1185 additions and 790 deletions

View File

@ -1,30 +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.api.util

import arrow.core.*

inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
.fold(
{
f(it)
Failure(it)
},
{ Success(it) }
)

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.data.ThreePidMedium import im.vector.matrix.android.internal.auth.data.ThreePidMedium
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.CancelableCoroutine
@ -57,7 +58,7 @@ internal class DefaultAuthenticator(private val retrofitBuilder: Retrofit.Builde


val job = GlobalScope.launch(coroutineDispatchers.main) { val job = GlobalScope.launch(coroutineDispatchers.main) {
val sessionOrFailure = authenticate(homeServerConnectionConfig, login, password) val sessionOrFailure = authenticate(homeServerConnectionConfig, login, password)
sessionOrFailure.fold({ callback.onFailure(it) }, { callback.onSuccess(it) }) sessionOrFailure.foldToCallback(callback)
} }
return CancelableCoroutine(job) return CancelableCoroutine(job)



View File

@ -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)
}


}

View File

@ -19,8 +19,11 @@
package im.vector.matrix.android.internal.crypto package im.vector.matrix.android.internal.crypto


import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils import android.text.TextUtils
import arrow.core.Try import arrow.core.Try
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
@ -64,6 +67,7 @@ import im.vector.matrix.android.internal.crypto.verification.DefaultSasVerificat
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.room.membership.RoomMembers
@ -137,6 +141,8 @@ internal class CryptoManager(
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : CryptoService { ) : CryptoService {


private val uiHandler = Handler(Looper.getMainLooper())

// MXEncrypting instance for each room. // MXEncrypting instance for each room.
private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap() private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap()
private val isStarting = AtomicBoolean(false) private val isStarting = AtomicBoolean(false)
@ -560,10 +566,10 @@ internal class CryptoManager(
} else { } else {
val algorithm = getEncryptionAlgorithm(roomId) val algorithm = getEncryptionAlgorithm(roomId)
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.e("## encryptEventContent() : $reason") Timber.e("## encryptEventContent() : $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE, callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, reason))) MXCryptoError.UNABLE_TO_ENCRYPT, reason)))
} }
} }
} }
@ -597,10 +603,7 @@ internal class CryptoManager(
val result = withContext(coroutineDispatchers.crypto) { val result = withContext(coroutineDispatchers.crypto) {
internalDecryptEvent(event, timeline) internalDecryptEvent(event, timeline)
} }
result.fold( result.foldToCallback(callback)
{ callback.onFailure(it) },
{ callback.onSuccess(it) }
)
} }
} }


@ -700,7 +703,7 @@ internal class CryptoManager(
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
// Check whether the event content must be encrypted for the invited members. // Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId) && shouldEncryptForInvitedMembers(roomId)


userIds = if (encryptForInvitedMembers) { userIds = if (encryptForInvitedMembers) {
RoomMembers(realm, roomId).getActiveRoomMemberIds() RoomMembers(realm, roomId).getActiveRoomMemberIds()
@ -787,35 +790,31 @@ internal class CryptoManager(
* @param anIterationCount the encryption iteration count (0 means no encryption) * @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys * @param callback the exported keys
*/ */
fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) { private fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
val iterationCount = Math.max(0, anIterationCount) GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
val iterationCount = Math.max(0, anIterationCount)


val exportedSessions = ArrayList<MegolmSessionData>() val exportedSessions = ArrayList<MegolmSessionData>()


val inboundGroupSessions = cryptoStore.getInboundGroupSessions() val inboundGroupSessions = cryptoStore.getInboundGroupSessions()


for (session in inboundGroupSessions) { for (session in inboundGroupSessions) {
val megolmSessionData = session.exportKeys() val megolmSessionData = session.exportKeys()


if (null != megolmSessionData) { if (null != megolmSessionData) {
exportedSessions.add(megolmSessionData) exportedSessions.add(megolmSessionData)
} }
}

val adapter = MoshiProvider.providesMoshi()
.adapter(List::class.java)

MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
}
}.foldToCallback(callback)
} }

val encryptedRoomKeys: ByteArray

try {
val adapter = MoshiProvider.providesMoshi()
.adapter(List::class.java)

encryptedRoomKeys = MXMegolmExportEncryption
.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
} catch (e: Exception) {
callback.onFailure(e)
return
}

callback.onSuccess(encryptedRoomKeys)
} }


/** /**
@ -830,40 +829,33 @@ internal class CryptoManager(
password: String, password: String,
progressListener: ProgressListener?, progressListener: ProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("## importRoomKeys starts") GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
Timber.v("## importRoomKeys starts")


val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
val roomKeys: String val roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
val t1 = System.currentTimeMillis()


try { Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")
roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
} catch (e: Exception) { val importedSessions = MoshiProvider.providesMoshi()
callback.onFailure(e) .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
return .fromJson(roomKeys)

val t2 = System.currentTimeMillis()

Timber.v("## importRoomKeys : JSON parsing " + (t2 - t1) + " ms")

if (importedSessions == null) {
throw Exception("Error")
}

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)
} }


/** /**
@ -898,7 +890,7 @@ internal class CryptoManager(
// trigger an an unknown devices exception // trigger an an unknown devices exception
callback.onFailure( callback.onFailure(
Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE, Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices))) MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)))
} }
} }
) )
@ -1061,10 +1053,7 @@ internal class CryptoManager(
CoroutineScope(coroutineDispatchers.crypto).launch { CoroutineScope(coroutineDispatchers.crypto).launch {
deviceListManager deviceListManager
.downloadKeys(userIds, forceDownload) .downloadKeys(userIds, forceDownload)
.fold( .foldToCallback(callback)
{ callback.onFailure(it) },
{ callback.onSuccess(it) }
)
} }
} }



View File

@ -277,6 +277,7 @@ internal class CryptoModule {
// Task // Task
get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(),
// Task executor // Task executor
get(),
get()) get())
} }



View File

@ -21,7 +21,7 @@ import android.text.TextUtils
import arrow.core.Try import arrow.core.Try
import im.vector.matrix.android.api.MatrixPatterns import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.onError import im.vector.matrix.android.internal.extensions.onError
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo 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.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore

View File

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto


import android.text.TextUtils import android.text.TextUtils
import android.util.Base64 import android.util.Base64
import im.vector.matrix.android.internal.extensions.toUnsignedInt
import timber.log.Timber import timber.log.Timber
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.nio.charset.Charset import java.nio.charset.Charset
@ -43,16 +44,6 @@ object MXMegolmExportEncryption {
// default iteration count to export the e2e keys // default iteration count to export the e2e keys
const val DEFAULT_ITERATION_COUNT = 500000 const val DEFAULT_ITERATION_COUNT = 500000


/**
* Convert a signed byte to a int value
*
* @param bVal the byte value to convert
* @return the matched int value
*/
private fun byteToInt(bVal: Byte): Int {
return (bVal and 0xFF.toByte()).toInt()
}

/** /**
* Extract the AES key from the deriveKeys result. * Extract the AES key from the deriveKeys result.
* *
@ -108,7 +99,8 @@ object MXMegolmExportEncryption {


val salt = Arrays.copyOfRange(body, 1, 1 + 16) val salt = Arrays.copyOfRange(body, 1, 1 + 16)
val iv = Arrays.copyOfRange(body, 17, 17 + 16) val iv = Arrays.copyOfRange(body, 17, 17 + 16)
val iterations = byteToInt(body[33]) shl 24 or (byteToInt(body[34]) shl 16) or (byteToInt(body[35]) shl 8) or byteToInt(body[36]) val iterations =
(body[33].toUnsignedInt() shl 24) or (body[34].toUnsignedInt() shl 16) or (body[35].toUnsignedInt() shl 8) or body[36].toUnsignedInt()
val ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength) val ciphertext = Arrays.copyOfRange(body, 37, 37 + ciphertextLength)
val hmac = Arrays.copyOfRange(body, body.size - 32, body.size) val hmac = Arrays.copyOfRange(body, body.size - 32, body.size)



View File

@ -16,9 +16,9 @@


package im.vector.matrix.android.internal.crypto.actions 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.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.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MegolmSessionData import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager 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. * Import a list of megolm session keys.
* Must be call on the crypto coroutine thread
* *
* @param megolmSessionsData megolm sessions. * @param megolmSessionsData megolm sessions.
* @param backUpKeys true to back up them to the homeserver. * @param backUpKeys true to back up them to the homeserver.
* @param progressListener the progress listener * @param progressListener the progress listener
* @param callback * @return import room keys result
*/ */
@WorkerThread
fun handle(megolmSessionsData: List<MegolmSessionData>, fun handle(megolmSessionsData: List<MegolmSessionData>,
fromBackup: Boolean, fromBackup: Boolean,
progressListener: ProgressListener?, uiHandler: Handler,
callback: MatrixCallback<ImportRoomKeysResult>) { progressListener: ProgressListener?): ImportRoomKeysResult {
val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()


val totalNumbersOfKeys = megolmSessionsData.size val totalNumbersOfKeys = megolmSessionsData.size
var cpt = 0
var lastProgress = 0 var lastProgress = 0
var totalNumbersOfImportedKeys = 0 var totalNumbersOfImportedKeys = 0


if (progressListener != null) { if (progressListener != null) {
CryptoAsyncHelper.getUiHandler().post { uiHandler.post {
progressListener.onProgress(0, 100) progressListener.onProgress(0, 100)
} }
} }
val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData) val olmInboundGroupSessionWrappers = olmDevice.importInboundGroupSessions(megolmSessionsData)


for (megolmSessionData in megolmSessionsData) { megolmSessionsData.forEachIndexed { cpt, megolmSessionData ->
cpt++


val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm) val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)


if (null != decrypting) { if (null != decrypting) {
@ -90,7 +88,7 @@ internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,
} }


if (progressListener != null) { if (progressListener != null) {
CryptoAsyncHelper.getUiHandler().post { uiHandler.post {
val progress = 100 * cpt / totalNumbersOfKeys val progress = 100 * cpt / totalNumbersOfKeys


if (lastProgress != progress) { 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)") Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")


val finalTotalNumbersOfImportedKeys = totalNumbersOfImportedKeys return ImportRoomKeysResult(totalNumbersOfKeys, totalNumbersOfImportedKeys)

CryptoAsyncHelper.getUiHandler().post {
callback.onSuccess(ImportRoomKeysResult(totalNumbersOfKeys, finalTotalNumbersOfImportedKeys))
}
} }
} }

View File

@ -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.KeysBackupService
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState 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.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.actions.MegolmSessionDataImporter
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust 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.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.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity 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.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.Task
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith 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.OlmException
import org.matrix.olm.OlmPkDecryption import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption import org.matrix.olm.OlmPkEncryption
@ -87,7 +95,8 @@ internal class KeysBackup(
private val storeSessionDataTask: StoreSessionsDataTask, private val storeSessionDataTask: StoreSessionsDataTask,
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask, private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor // Task executor
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : KeysBackupService { ) : KeysBackupService {


private val uiHandler = Handler(Looper.getMainLooper()) private val uiHandler = Handler(Looper.getMainLooper())
@ -130,55 +139,53 @@ internal class KeysBackup(
override fun prepareKeysBackupVersion(password: String?, override fun prepareKeysBackupVersion(password: String?,
progressListener: ProgressListener?, progressListener: ProgressListener?,
callback: MatrixCallback<MegolmBackupCreationInfo>) { callback: MatrixCallback<MegolmBackupCreationInfo>) {
CryptoAsyncHelper.getDecryptBackgroundHandler().post { GlobalScope.launch(coroutineDispatchers.main) {
try { withContext(coroutineDispatchers.crypto) {
val olmPkDecryption = OlmPkDecryption() Try {
val megolmBackupAuthData = MegolmBackupAuthData() val olmPkDecryption = OlmPkDecryption()
val megolmBackupAuthData = MegolmBackupAuthData()


if (password != null) { if (password != null) {
// Generate a private key from the password // Generate a private key from the password
val backgroundProgressListener = if (progressListener == null) { val backgroundProgressListener = if (progressListener == null) {
null null
} else { } else {
object : ProgressListener { object : ProgressListener {
override fun onProgress(progress: Int, total: Int) { override fun onProgress(progress: Int, total: Int) {
uiHandler.post { uiHandler.post {
try { try {
progressListener.onProgress(progress, total) progressListener.onProgress(progress, total)
} catch (e: Exception) { } catch (e: Exception) {
Timber.e(e, "prepareKeysBackupVersion: onProgress failure") 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) val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())
megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey)
megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt
megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations
} else {
val publicKey = olmPkDecryption.generateKey()


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
} }

}.foldToCallback(callback)
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) }
}
} }
} }


@ -221,37 +228,39 @@ internal class KeysBackup(
} }


override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) { override fun deleteBackup(version: String, callback: MatrixCallback<Unit>?) {
CryptoAsyncHelper.getDecryptBackgroundHandler().post { GlobalScope.launch(coroutineDispatchers.main) {
// If we're currently backing up to this backup... stop. withContext(coroutineDispatchers.crypto) {
// (We start using it automatically in createKeysBackupVersion so this is symmetrical). // If we're currently backing up to this backup... stop.
if (keysBackupVersion != null && version == keysBackupVersion!!.version) { // (We start using it automatically in createKeysBackupVersion so this is symmetrical).
resetKeysBackupData() if (keysBackupVersion != null && version == keysBackupVersion!!.version) {
keysBackupVersion = null resetKeysBackupData()
keysBackupStateManager.state = KeysBackupState.Unknown keysBackupVersion = null
} keysBackupStateManager.state = KeysBackupState.Unknown
}


deleteBackupTask.configureWith(DeleteBackupTask.Params(version)) deleteBackupTask.configureWith(DeleteBackupTask.Params(version))
.dispatchTo(object : MatrixCallback<Unit> { .dispatchTo(object : MatrixCallback<Unit> {
private fun eventuallyRestartBackup() { private fun eventuallyRestartBackup() {
// Do not stay in KeysBackupState.Unknown but check what is available on the homeserver // Do not stay in KeysBackupState.Unknown but check what is available on the homeserver
if (state == KeysBackupState.Unknown) { if (state == KeysBackupState.Unknown) {
checkAndStartKeysBackup() checkAndStartKeysBackup()
}
} }
}


override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
eventuallyRestartBackup() eventuallyRestartBackup()


uiHandler.post { callback?.onSuccess(Unit) } uiHandler.post { callback?.onSuccess(Unit) }
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
eventuallyRestartBackup() eventuallyRestartBackup()


uiHandler.post { callback?.onFailure(failure) } uiHandler.post { callback?.onFailure(failure) }
} }
}) })
.executeBy(taskExecutor) .executeBy(taskExecutor)
}
} }
} }


@ -426,85 +435,80 @@ internal class KeysBackup(
callback: MatrixCallback<Unit>) { callback: MatrixCallback<Unit>) {
Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}") Timber.v("trustKeyBackupVersion: $trust, version ${keysBackupVersion.version}")


CryptoAsyncHelper.getDecryptBackgroundHandler().post { // Get auth data to update it
val myUserId = credentials.userId val authData = getMegolmBackupAuthData(keysBackupVersion)


// Get auth data to update it if (authData == null) {
val authData = getMegolmBackupAuthData(keysBackupVersion) Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")


if (authData == null) { callback.onFailure(IllegalArgumentException("Missing element"))
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") } else {
GlobalScope.launch(coroutineDispatchers.main) {
val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
val myUserId = credentials.userId


uiHandler.post { // Get current signatures, or create an empty set
callback.onFailure(IllegalArgumentException("Missing element")) 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 newKeysBackupVersion.version = keysBackupVersion.version
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap()) newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
newKeysBackupVersion.count = keysBackupVersion.count
newKeysBackupVersion.hash = keysBackupVersion.hash
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData


if (trust) { checkAndStartWithKeysBackupVersion(newKeysBackupVersion)
// 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>?

// 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) callback.onSuccess(data)
} }
}


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
uiHandler.post {
callback.onFailure(failure) callback.onFailure(failure)
} }
} })
}) .executeBy(taskExecutor)
.executeBy(taskExecutor) }
} }
} }


@ -513,17 +517,18 @@ internal class KeysBackup(
callback: MatrixCallback<Unit>) { callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}") Timber.v("trustKeysBackupVersionWithRecoveryKey: version ${keysBackupVersion.version}")


CryptoAsyncHelper.getDecryptBackgroundHandler().post { GlobalScope.launch(coroutineDispatchers.main) {
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) { val isValid = withContext(coroutineDispatchers.crypto) {
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)

uiHandler.post {
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
}
return@post
} }


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>) { callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}") Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")


CryptoAsyncHelper.getDecryptBackgroundHandler().post { GlobalScope.launch(coroutineDispatchers.main) {
val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, null) val recoveryKey = withContext(coroutineDispatchers.crypto) {
recoveryKeyFromPassword(password, keysBackupVersion, null)
}


if (recoveryKey == null) { if (recoveryKey == null) {
Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data") Timber.w("trustKeysBackupVersionWithPassphrase: Key backup is missing required data")


uiHandler.post { callback.onFailure(IllegalArgumentException("Missing element"))
callback.onFailure(IllegalArgumentException("Missing element")) } else {
} // Check trust using the recovery key

trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
return@post
} }

// Check trust using the recovery key
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
} }
} }


@ -591,12 +594,10 @@ internal class KeysBackup(
} }


override fun getBackupProgress(progressListener: ProgressListener) { override fun getBackupProgress(progressListener: ProgressListener) {
CryptoAsyncHelper.getDecryptBackgroundHandler().post { val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true)
val backedUpKeys = cryptoStore.inboundGroupSessionsCount(true) val total = cryptoStore.inboundGroupSessionsCount(false)
val total = cryptoStore.inboundGroupSessionsCount(false)


uiHandler.post { progressListener.onProgress(backedUpKeys, total) } progressListener.onProgress(backedUpKeys, total)
}
} }


override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult, override fun restoreKeysWithRecoveryKey(keysVersionResult: KeysVersionResult,
@ -607,88 +608,95 @@ internal class KeysBackup(
callback: MatrixCallback<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}") Timber.v("restoreKeysWithRecoveryKey: From backup version: ${keysVersionResult.version}")


CryptoAsyncHelper.getDecryptBackgroundHandler().post(Runnable { GlobalScope.launch(coroutineDispatchers.main) {
// Check if the recovery is valid before going any further withContext(coroutineDispatchers.crypto) {
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { Try {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") // Check if the recovery is valid before going any further
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) } if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
return@Runnable 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")
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)
} }


// Import them into the crypto store // Get a PK decryption instance
val progressListener = if (stepProgressListener != null) { val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
object : ProgressListener { if (decryption == null) {
override fun onProgress(progress: Int, total: Int) { // This should not happen anymore
// Note: no need to post to UI thread, importMegolmSessionsData() will do it Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total)) throw InvalidParameterException("Invalid recovery key")
}
}
} else {
null
} }


megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener, object : MatrixCallback<ImportRoomKeysResult> { decryption!!
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)
}
})
} }
}.fold(
{
callback.onFailure(it)
},
{ decryption ->
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)


override fun onFailure(failure: Throwable) { // Get backed up keys from the homeserver
uiHandler.post { callback.onFailure(failure) } 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, override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
@ -699,31 +707,36 @@ internal class KeysBackup(
callback: MatrixCallback<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}") Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")


CryptoAsyncHelper.getDecryptBackgroundHandler().post { GlobalScope.launch(coroutineDispatchers.main) {
val progressListener = if (stepProgressListener != null) { withContext(coroutineDispatchers.crypto) {
object : ProgressListener { val progressListener = if (stepProgressListener != null) {
override fun onProgress(progress: Int, total: Int) { object : ProgressListener {
uiHandler.post { override fun onProgress(progress: Int, total: Int) {
stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total)) uiHandler.post {
stepProgressListener.onStepProgress(StepProgressListener.Step.ComputingKey(progress, total))
}
} }
} }
} } else {
} else { null
null
}

val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, progressListener)

if (recoveryKey == null) {
uiHandler.post {
Timber.v("backupKeys: Invalid configuration")
callback.onFailure(IllegalStateException("Invalid configuration"))
} }


return@post Try {
} recoveryKeyFromPassword(password, keysBackupVersion, progressListener)

}
restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback) }.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. * Extract MegolmBackupAuthData data from a backup version.
@ -1190,94 +1203,96 @@ internal class KeysBackup(


keysBackupStateManager.state = KeysBackupState.BackingUp keysBackupStateManager.state = KeysBackupState.BackingUp


CryptoAsyncHelper.getEncryptBackgroundHandler().post { GlobalScope.launch(coroutineDispatchers.main) {
Timber.v("backupKeys: 2 - Encrypting keys") withContext(coroutineDispatchers.crypto) {
Timber.v("backupKeys: 2 - Encrypting keys")


// Gather data to send to the homeserver // Gather data to send to the homeserver
// roomId -> sessionId -> MXKeyBackupData // roomId -> sessionId -> MXKeyBackupData
val keysBackupData = KeysBackupData() val keysBackupData = KeysBackupData()
keysBackupData.roomIdToRoomKeysBackupData = HashMap() keysBackupData.roomIdToRoomKeysBackupData = HashMap()


for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) { for (olmInboundGroupSessionWrapper in olmInboundGroupSessionWrappers) {
val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper) val keyBackupData = encryptGroupSession(olmInboundGroupSessionWrapper)
if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) { if (keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId] == null) {
val roomKeysBackupData = RoomKeysBackupData() val roomKeysBackupData = RoomKeysBackupData()
roomKeysBackupData.sessionIdToKeyBackupData = HashMap() roomKeysBackupData.sessionIdToKeyBackupData = HashMap()
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId!!] = roomKeysBackupData
}

try {
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
} catch (e: OlmException) {
Timber.e(e, "OlmException")
}
} }


try { Timber.v("backupKeys: 4 - Sending request")
keysBackupData.roomIdToRoomKeysBackupData[olmInboundGroupSessionWrapper.roomId]!!.sessionIdToKeyBackupData[olmInboundGroupSessionWrapper.olmInboundGroupSession!!.sessionIdentifier()] = keyBackupData
} catch (e: OlmException) {
Timber.e(e, "OlmException")
}
}


Timber.v("backupKeys: 4 - Sending request") // Make the request

storeSessionDataTask
// Make the request .configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData))
storeSessionDataTask .dispatchTo(object : MatrixCallback<BackupKeysResult> {
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData)) override fun onSuccess(data: BackupKeysResult) {
.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) {
uiHandler.post { uiHandler.post {
Timber.e(failure, "backupKeys: backupKeys failed.") Timber.v("backupKeys: 5a - Request complete")


when (failure.error.code) { // Mark keys as backed up
MatrixError.NOT_FOUND, cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)
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 if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
checkAndStartKeysBackup() Timber.v("backupKeys: All keys have been backed up")
} onServerDataRetrieved(data.count, data.hash)
else -> // Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp // 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 when (failure.error.code) {
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp MatrixError.NOT_FOUND,
maybeBackupKeys() 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 private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100
} }


/* ========================================================================================== /* ==========================================================================================
* DEBUG INFO * DEBUG INFO
* ========================================================================================== */ * ========================================================================================== */


override fun toString() = "KeysBackup for ${credentials.userId}" override fun toString() = "KeysBackup for ${credentials.userId}"
} }

View File

@ -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.Event
import im.vector.matrix.android.api.session.events.model.EventType 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.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.DeviceListManager
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction 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.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap 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.*
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.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor 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) { override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED, setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
deviceID, deviceID,
userId) userId)


listeners.forEach { listeners.forEach {
try { try {
@ -206,7 +201,7 @@ internal class DefaultSasVerificationService(private val credentials: Credential
} else { } else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}") Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice 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, userId,
deviceID) deviceID)
addTransaction(tx) addTransaction(tx)
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
tx.start() tx.start()
}
return txID return txID
} else { } else {
throw IllegalArgumentException("Unknown verification method") throw IllegalArgumentException("Unknown verification method")
@ -399,9 +393,9 @@ internal class DefaultSasVerificationService(private val credentials: Credential
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
dispatchTxUpdated(tx) dispatchTxUpdated(tx)
if (tx is SASVerificationTransaction if (tx is SASVerificationTransaction
&& (tx.state == SasVerificationTxState.Cancelled && (tx.state == SasVerificationTxState.Cancelled
|| tx.state == SasVerificationTxState.OnCancelled || tx.state == SasVerificationTxState.OnCancelled
|| tx.state == SasVerificationTxState.Verified) || tx.state == SasVerificationTxState.Verified)
) { ) {
//remove //remove
this.removeTransaction(tx.otherUserId, tx.transactionId) this.removeTransaction(tx.otherUserId, tx.transactionId)

View File

@ -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.SasMode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState 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.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.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.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey 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.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart 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.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@ -61,21 +60,21 @@ internal class IncomingSASVerificationTransaction(
override val uxState: IncomingSasVerificationTransaction.UxState override val uxState: IncomingSasVerificationTransaction.UxState
get() { get() {
return when (state) { return when (state) {
SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT SasVerificationTxState.OnStarted -> IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT
SasVerificationTxState.SendingAccept, SasVerificationTxState.SendingAccept,
SasVerificationTxState.Accepted, SasVerificationTxState.Accepted,
SasVerificationTxState.OnKeyReceived, SasVerificationTxState.OnKeyReceived,
SasVerificationTxState.SendingKey, 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.ShortCodeReady -> IncomingSasVerificationTransaction.UxState.SHOW_SAS
SasVerificationTxState.ShortCodeAccepted, SasVerificationTxState.ShortCodeAccepted,
SasVerificationTxState.SendingMac, SasVerificationTxState.SendingMac,
SasVerificationTxState.MacSent, SasVerificationTxState.MacSent,
SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION SasVerificationTxState.Verifying -> IncomingSasVerificationTransaction.UxState.WAIT_FOR_VERIFICATION
SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED SasVerificationTxState.Verified -> IncomingSasVerificationTransaction.UxState.VERIFIED
SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME SasVerificationTxState.Cancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_ME
SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER SasVerificationTxState.OnCancelled -> IncomingSasVerificationTransaction.UxState.CANCELLED_BY_OTHER
else -> IncomingSasVerificationTransaction.UxState.UNKNOWN else -> IncomingSasVerificationTransaction.UxState.UNKNOWN
} }
} }


@ -125,9 +124,7 @@ internal class IncomingSASVerificationTransaction(
//TODO force download keys!! //TODO force download keys!!
//would be probably better to download the keys //would be probably better to download the keys
//for now I cancel //for now I cancel
CryptoAsyncHelper.getDecryptBackgroundHandler().post { cancel(CancelCode.User)
cancel(CancelCode.User)
}
} else { } else {
// val otherKey = info.identityKey() // val otherKey = info.identityKey()
//need to jump back to correct thread //need to jump back to correct thread
@ -139,9 +136,7 @@ internal class IncomingSASVerificationTransaction(
shortAuthenticationStrings = agreedShortCode, shortAuthenticationStrings = agreedShortCode,
commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT) commitment = Base64.encodeToString("temporary commitment".toByteArray(), Base64.DEFAULT)
) )
CryptoAsyncHelper.getDecryptBackgroundHandler().post { doAccept(accept)
doAccept(accept)
}
} }
} }



View File

@ -86,7 +86,6 @@ internal class OutgoingSASVerificationRequest(
} }


fun start() { fun start() {

if (state != SasVerificationTxState.None) { if (state != SasVerificationTxState.None) {
Timber.e("## start verification from invalid state") Timber.e("## start verification from invalid state")
//should I cancel?? //should I cancel??

View File

@ -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.SasMode
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTxState 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.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.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXKey import im.vector.matrix.android.internal.crypto.model.MXKey
@ -31,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.* 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.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.extensions.toUnsignedInt
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import org.matrix.olm.OlmSAS import org.matrix.olm.OlmSAS
@ -278,21 +278,17 @@ internal abstract class SASVerificationTransaction(
.dispatchTo(object : MatrixCallback<Unit> { .dispatchTo(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.") Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.")
CryptoAsyncHelper.getDecryptBackgroundHandler().post { if (onDone != null) {
if (onDone != null) { onDone()
onDone() } else {
} else { state = nextState
state = nextState
}
} }
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state") Timber.e("## SAS verification [$transactionId] failed to send toDevice in state : $state")


CryptoAsyncHelper.getDecryptBackgroundHandler().post { cancel(onErrorReason)
cancel(onErrorReason)
}
} }
}) })
.executeBy(taskExecutor) .executeBy(taskExecutor)
@ -359,11 +355,11 @@ internal abstract class SASVerificationTransaction(
* or with the three numbers on separate lines. * or with the three numbers on separate lines.
*/ */
fun getDecimalCodeRepresentation(byteArray: ByteArray): String { fun getDecimalCodeRepresentation(byteArray: ByteArray): String {
val b0 = byteArray[0].toInt().and(0xff) //need unsigned byte val b0 = byteArray[0].toUnsignedInt() //need unsigned byte
val b1 = byteArray[1].toInt().and(0xff) //need unsigned byte val b1 = byteArray[1].toUnsignedInt() //need unsigned byte
val b2 = byteArray[2].toInt().and(0xff) //need unsigned byte val b2 = byteArray[2].toUnsignedInt() //need unsigned byte
val b3 = byteArray[3].toInt().and(0xff) //need unsigned byte val b3 = byteArray[3].toUnsignedInt() //need unsigned byte
val b4 = byteArray[4].toInt().and(0xff) //need unsigned byte val b4 = byteArray[4].toUnsignedInt() //need unsigned byte
//(B0 << 5 | B1 >> 3) + 1000 //(B0 << 5 | B1 >> 3) + 1000
val first = (b0.shl(5) or b1.shr(3)) + 1000 val first = (b0.shl(5) or b1.shr(3)) + 1000
//((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000 //((B1 & 0x7) << 10 | B2 << 2 | B3 >> 6) + 1000
@ -384,12 +380,12 @@ internal abstract class SASVerificationTransaction(
* to that number 7 emoji are selected from a list of 64 emoji (see Appendix A) * to that number 7 emoji are selected from a list of 64 emoji (see Appendix A)
*/ */
fun getEmojiCodeRepresentation(byteArray: ByteArray): List<EmojiRepresentation> { fun getEmojiCodeRepresentation(byteArray: ByteArray): List<EmojiRepresentation> {
val b0 = byteArray[0].toInt().and(0xff) val b0 = byteArray[0].toUnsignedInt()
val b1 = byteArray[1].toInt().and(0xff) val b1 = byteArray[1].toUnsignedInt()
val b2 = byteArray[2].toInt().and(0xff) val b2 = byteArray[2].toUnsignedInt()
val b3 = byteArray[3].toInt().and(0xff) val b3 = byteArray[3].toUnsignedInt()
val b4 = byteArray[4].toInt().and(0xff) val b4 = byteArray[4].toUnsignedInt()
val b5 = byteArray[5].toInt().and(0xff) val b5 = byteArray[5].toUnsignedInt()
return listOf( return listOf(
getEmojiForCode((b0 and 0xFC).shr(2)), getEmojiForCode((b0 and 0xFC).shr(2)),
getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)), getEmojiForCode((b0 and 0x3).shl(4) or (b1 and 0xF0).shr(4)),

View File

@ -17,7 +17,8 @@
package im.vector.matrix.android.internal.di package im.vector.matrix.android.internal.di


import android.content.Context 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.task.TaskExecutor
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -36,11 +37,14 @@ class MatrixModule(private val context: Context) {
} }


single { single {
val cryptoHandler = CryptoAsyncHelper.getDecryptBackgroundHandler() val THREAD_CRYPTO_NAME = "Crypto_Thread"
val handlerThread = HandlerThread(THREAD_CRYPTO_NAME)
handlerThread.start()

MatrixCoroutineDispatchers(io = Dispatchers.IO, MatrixCoroutineDispatchers(io = Dispatchers.IO,
computation = Dispatchers.IO, computation = Dispatchers.IO,
main = Dispatchers.Main, main = Dispatchers.Main,
crypto = cryptoHandler.asCoroutineDispatcher("crypto") crypto = Handler(handlerThread.looper).asCoroutineDispatcher("crypto")
) )
} }



View File

@ -0,0 +1,22 @@
/*
* 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.extensions

/**
* Convert a signed byte to a int value
*/
fun Byte.toUnsignedInt() = toInt() and 0xff

View File

@ -0,0 +1,33 @@
/*
* 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.extensions

import arrow.core.*
import im.vector.matrix.android.api.MatrixCallback

inline fun <A> TryOf<A>.onError(f: (Throwable) -> Unit): Try<A> = fix()
.fold(
{
f(it)
Failure(it)
},
{ Success(it) }
)

fun <A> Try<A>.foldToCallback(callback: MatrixCallback<A>): Unit = fold(
{ callback.onFailure(it) },
{ callback.onSuccess(it) })

View File

@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.task


import arrow.core.Try import arrow.core.Try
import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.extensions.onError
import im.vector.matrix.android.internal.util.CancelableCoroutine import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -38,12 +40,10 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis
task.execute(task.params) task.execute(task.params)
} }
} }
resultOrFailure.fold({ resultOrFailure.onError {
Timber.d(it, "Task failed") Timber.d(it, "Task failed")
task.callback.onFailure(it) }
}, { .foldToCallback(task.callback)
task.callback.onSuccess(it)
})
} }
return CancelableCoroutine(job) return CancelableCoroutine(job)
} }

View File

@ -19,34 +19,30 @@ package im.vector.riotredesign.core.dialogs
import android.app.Activity import android.app.Activity
import android.text.Editable import android.text.Editable
import android.text.TextUtils import android.text.TextUtils
import android.text.TextWatcher
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.SimpleTextWatcher


class ExportKeysDialog { class ExportKeysDialog {


var passwordVisible = false

fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) { fun show(activity: Activity, exportKeyDialogListener: ExportKeyDialogListener) {
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null) val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_export_e2e_keys, null)
val builder = AlertDialog.Builder(activity) val builder = AlertDialog.Builder(activity)
.setTitle(R.string.encryption_export_room_keys) .setTitle(R.string.encryption_export_room_keys)
.setView(dialogLayout) .setView(dialogLayout)


val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text) val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEt)
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_confirm_passphrase_edit_text) val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEtConfirm)
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.dialog_e2e_keys_confirm_passphrase_til) val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.exportDialogTilConfirm)
val exportButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_export_button) val exportButton = dialogLayout.findViewById<Button>(R.id.exportDialogSubmit)
val textWatcher = object : TextWatcher { val textWatcher = object : SimpleTextWatcher() {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {

}

override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {

}

override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
when { when {
TextUtils.isEmpty(passPhrase1EditText.text) -> { TextUtils.isEmpty(passPhrase1EditText.text) -> {
@ -68,6 +64,14 @@ class ExportKeysDialog {
passPhrase1EditText.addTextChangedListener(textWatcher) passPhrase1EditText.addTextChangedListener(textWatcher)
passPhrase2EditText.addTextChangedListener(textWatcher) passPhrase2EditText.addTextChangedListener(textWatcher)


val showPassword = dialogLayout.findViewById<ImageView>(R.id.exportDialogShowPassword)
showPassword.setOnClickListener {
passwordVisible = !passwordVisible
passPhrase1EditText.showPassword(passwordVisible)
passPhrase2EditText.showPassword(passwordVisible)
showPassword.setImageResource(if (passwordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
}

val exportDialog = builder.show() val exportDialog = builder.show()


exportButton.setOnClickListener { exportButton.setOnClickListener {

View File

@ -18,16 +18,18 @@ package im.vector.riotredesign.core.files


import android.app.DownloadManager import android.app.DownloadManager
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread
import arrow.core.Try
import okio.Okio import okio.Okio
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File


/** /**
* Save a string to a file with Okio * Save a string to a file with Okio
* @return true in case of success
*/ */
fun saveStringToFile(str: String, file: File): Boolean { @WorkerThread
return try { fun writeToFile(str: String, file: File): Try<Unit> {
return Try {
val sink = Okio.sink(file) val sink = Okio.sink(file)


val bufferedSink = Okio.buffer(sink) val bufferedSink = Okio.buffer(sink)
@ -36,14 +38,25 @@ fun saveStringToFile(str: String, file: File): Boolean {


bufferedSink.close() bufferedSink.close()
sink.close() sink.close()

true
} catch (e: Exception) {
Timber.e(e, "Error saving file")
false
} }
} }


/**
* Save a byte array to a file with Okio
*/
@WorkerThread
fun writeToFile(data: ByteArray, file: File): Try<Unit> {
return Try {
val sink = Okio.sink(file)

val bufferedSink = Okio.buffer(sink)

bufferedSink.write(data)

bufferedSink.close()
sink.close()
}
}


fun addEntryToDownloadManager(context: Context, fun addEntryToDownloadManager(context: Context,
file: File, file: File,

View File

@ -0,0 +1,152 @@
/*
* 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.core.intent

import android.content.ClipData
import android.content.ClipDescription
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.text.TextUtils
import androidx.core.util.PatternsCompat.WEB_URL
import java.util.*

/**
* Inspired from Riot code: RoomMediaMessage.java
*/
sealed class ExternalIntentData {
/**
* Constructor for a text message.
*
* @param text the text
* @param htmlText the HTML text
* @param format the formatted text format
*/
data class IntentDataText(
val text: CharSequence? = null,
val htmlText: String? = null,
val format: String? = null,
val clipDataItem: ClipData.Item = ClipData.Item(text, htmlText),
val mimeType: String? = if (null == htmlText) ClipDescription.MIMETYPE_TEXT_PLAIN else format
) : ExternalIntentData()

/**
* Clip data
*/
data class IntentDataClipData(
val clipDataItem: ClipData.Item,
val mimeType: String?
) : ExternalIntentData()

/**
* Constructor from a media Uri/
*
* @param uri the media uri
* @param filename the media file name
*/
data class IntentDataUri(
val uri: Uri,
val filename: String? = null
) : ExternalIntentData()
}


fun analyseIntent(intent: Intent): List<ExternalIntentData> {
val externalIntentDataList = ArrayList<ExternalIntentData>()


// chrome adds many items when sharing an web page link
// so, test first the type
if (TextUtils.equals(intent.type, ClipDescription.MIMETYPE_TEXT_PLAIN)) {
var message: String? = intent.getStringExtra(Intent.EXTRA_TEXT)

if (null == message) {
val sequence = intent.getCharSequenceExtra(Intent.EXTRA_TEXT)
if (null != sequence) {
message = sequence.toString()
}
}

val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)

if (!TextUtils.isEmpty(subject)) {
if (TextUtils.isEmpty(message)) {
message = subject
} else if (WEB_URL.matcher(message!!).matches()) {
message = subject + "\n" + message
}
}

if (!TextUtils.isEmpty(message)) {
externalIntentDataList.add(ExternalIntentData.IntentDataText(message!!, null, intent.type))
return externalIntentDataList
}
}

var clipData: ClipData? = null
var mimetypes: MutableList<String>? = null

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
clipData = intent.clipData
}

// multiple data
if (null != clipData) {
if (null != clipData.description) {
if (0 != clipData.description.mimeTypeCount) {
mimetypes = ArrayList()

for (i in 0 until clipData.description.mimeTypeCount) {
mimetypes.add(clipData.description.getMimeType(i))
}

// if the filter is "accept anything" the mimetype does not make sense
if (1 == mimetypes.size) {
if (mimetypes[0].endsWith("/*")) {
mimetypes = null
}
}
}
}

val count = clipData.itemCount

for (i in 0 until count) {
val item = clipData.getItemAt(i)
var mimetype: String? = null

if (null != mimetypes) {
if (i < mimetypes.size) {
mimetype = mimetypes[i]
} else {
mimetype = mimetypes[0]
}

// uris list is not a valid mimetype
if (TextUtils.equals(mimetype, ClipDescription.MIMETYPE_TEXT_URILIST)) {
mimetype = null
}
}

externalIntentDataList.add(ExternalIntentData.IntentDataClipData(item, mimetype))
}
} else if (null != intent.data) {
externalIntentDataList.add(ExternalIntentData.IntentDataUri(intent.data!!))
}

return externalIntentDataList
}

View File

@ -0,0 +1,44 @@
/*
* 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.core.intent

import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns

fun getFilenameFromUri(context: Context, uri: Uri): String? {
var result: String? = null
if (uri.scheme == "content") {
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
}
} finally {
cursor?.close()
}
}
if (result == null) {
result = uri.path
val cut = result.lastIndexOf('/')
if (cut != -1) {
result = result.substring(cut + 1)
}
}
return result
}

View File

@ -0,0 +1,54 @@
/*
* 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.core.intent

import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import im.vector.riotredesign.core.utils.getFileExtension
import timber.log.Timber

/**
* Returns the mimetype from a uri.
*
* @param context the context
* @return the mimetype
*/
fun getMimeTypeFromUri(context: Context, uri: Uri): String? {
var mimeType: String? = null

try {
mimeType = context.contentResolver.getType(uri)

// try to find the mimetype from the filename
if (null == mimeType) {
val extension = getFileExtension(uri.toString())
if (extension != null) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
}
}

if (null != mimeType) {
// the mimetype is sometimes in uppercase.
mimeType = mimeType.toLowerCase()
}
} catch (e: Exception) {
Timber.e(e, "Failed to open resource input stream")
}

return mimeType
}

View File

@ -0,0 +1,64 @@
/*
* 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.os.Environment
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.extensions.foldToCallback
import im.vector.riotredesign.core.files.addEntryToDownloadManager
import im.vector.riotredesign.core.files.writeToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File

class KeysExporter(private val session: Session) {

/**
* Export keys and return the file path with the callback
*/
fun export(context: Context, password: String, callback: MatrixCallback<String>) {
session.exportRoomKeys(password, object : MatrixCallback<ByteArray> {

override fun onSuccess(data: ByteArray) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")

writeToFile(data, file)

addEntryToDownloadManager(context, file, "text/plain")

file.absolutePath
}
}
.foldToCallback(callback)
}
}

override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
}
}

View File

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

/**
* Import keys from provided Uri
*/
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)
}
)
}
}
}

View File

@ -23,10 +23,13 @@ import androidx.fragment.app.FragmentManager
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.platform.SimpleFragmentActivity import im.vector.riotredesign.core.platform.SimpleFragmentActivity
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.crypto.keys.KeysExporter


class KeysBackupSetupActivity : SimpleFragmentActivity() { class KeysBackupSetupActivity : SimpleFragmentActivity() {


@ -118,49 +121,38 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
}) })
} }


fun exportKeysManually() { private fun exportKeysManually() {
ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener { ExportKeysDialog().show(this, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {
notImplemented()
/*
showWaitingView() showWaitingView()


CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(this@KeysBackupSetupActivity) { KeysExporter(session)
override fun onSuccess(filename: String) { .export(this@KeysBackupSetupActivity,
hideWaitingView() passphrase,
object : MatrixCallback<String> {


AlertDialog.Builder(this@KeysBackupSetupActivity) override fun onSuccess(data: String) {
.setMessage(getString(R.string.encryption_export_saved_as, filename)) hideWaitingView()
.setCancelable(false)
.setPositiveButton(R.string.ok) { dialog, which ->
val resultIntent = Intent()
resultIntent.putExtra(MANUAL_EXPORT, true)
setResult(RESULT_OK, resultIntent)
finish()
}
.show()
}


override fun onNetworkError(e: Exception) { AlertDialog.Builder(this@KeysBackupSetupActivity)
super.onNetworkError(e) .setMessage(getString(R.string.encryption_export_saved_as, data))
hideWaitingView() .setCancelable(false)
} .setPositiveButton(R.string.ok) { dialog, which ->
val resultIntent = Intent()
resultIntent.putExtra(MANUAL_EXPORT, true)
setResult(RESULT_OK, resultIntent)
finish()
}
.show()
}


override fun onMatrixError(e: MatrixError) { override fun onFailure(failure: Throwable) {
super.onMatrixError(e) toast(failure.localizedMessage)
hideWaitingView() hideWaitingView()
} }

})
override fun onUnexpectedError(e: Exception) {
super.onUnexpectedError(e)
hideWaitingView()
}
})
*/
} }
}) })


} }





View File

@ -25,15 +25,20 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import arrow.core.Try
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.files.addEntryToDownloadManager import im.vector.riotredesign.core.files.addEntryToDownloadManager
import im.vector.riotredesign.core.files.saveStringToFile import im.vector.riotredesign.core.files.writeToFile
import im.vector.riotredesign.core.platform.VectorBaseFragment import im.vector.riotredesign.core.platform.VectorBaseFragment
import im.vector.riotredesign.core.utils.* import im.vector.riotredesign.core.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File


class KeysBackupSetupStep3Fragment : VectorBaseFragment() { class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
@ -157,30 +162,39 @@ class KeysBackupSetupStep3Fragment : VectorBaseFragment() {
} }


private fun exportRecoveryKeyToFile(data: String) { private fun exportRecoveryKeyToFile(data: String) {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) GlobalScope.launch(Dispatchers.Main) {
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt") withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")


if (saveStringToFile(data, file)) { writeToFile(data, file)
addEntryToDownloadManager(requireContext(), file, "text/plain")


context?.let { addEntryToDownloadManager(requireContext(), file, "text/plain")
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, file.absolutePath)) file.absolutePath
.setCancelable(false) }
.setPositiveButton(R.string.ok, null)
.show()
} }
.fold(
{ throwable ->
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(throwable.localizedMessage)
}
},
{ path ->
viewModel.copyHasBeenMade = true


viewModel.copyHasBeenMade = true context?.let {
} else { AlertDialog.Builder(it)
context?.let { .setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
AlertDialog.Builder(it) }
.setTitle(R.string.dialog_title_error) }
.setMessage(getString(R.string.unknown_error)) )
.setCancelable(false) ?.setCancelable(false)
.setPositiveButton(R.string.ok, null) ?.setPositiveButton(R.string.ok, null)
.show() ?.show()
}
} }
} }



View File

@ -49,12 +49,16 @@ import im.vector.matrix.android.api.extensions.sortByLastSeen
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.showPassword import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.extensions.withArgs 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.platform.SimpleTextWatcher import im.vector.riotredesign.core.platform.SimpleTextWatcher
import im.vector.riotredesign.core.platform.VectorPreferenceFragment import im.vector.riotredesign.core.platform.VectorPreferenceFragment
import im.vector.riotredesign.core.preference.BingRule import im.vector.riotredesign.core.preference.BingRule
@ -64,6 +68,8 @@ import im.vector.riotredesign.core.preference.VectorPreference
import im.vector.riotredesign.core.utils.* import im.vector.riotredesign.core.utils.*
import im.vector.riotredesign.features.MainActivity import im.vector.riotredesign.features.MainActivity
import im.vector.riotredesign.features.configuration.VectorConfiguration 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.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.themes.ThemeUtils import im.vector.riotredesign.features.themes.ThemeUtils
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -885,9 +891,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this) PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this)
} }


// TODO Test
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
/* TODO
if (allGranted(grantResults)) { if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) { if (requestCode == PERMISSION_REQUEST_CODE_LAUNCH_CAMERA) {
changeAvatar() changeAvatar()
@ -895,7 +899,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
exportKeys() exportKeys()
} }
} }
*/
} }


//============================================================================================================== //==============================================================================================================
@ -1782,6 +1785,9 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
* @param errorMessage the error message * @param errorMessage the error message
*/ */
private fun onCommonDone(errorMessage: String?) { private fun onCommonDone(errorMessage: String?) {
if (!isAdded) {
return
}
activity?.runOnUiThread { activity?.runOnUiThread {
if (!TextUtils.isEmpty(errorMessage) && errorMessage != null) { if (!TextUtils.isEmpty(errorMessage) && errorMessage != null) {
activity?.toast(errorMessage!!) activity?.toast(errorMessage!!)
@ -2594,38 +2600,29 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
activity?.let { activity -> activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {
notImplemented()
/*

displayLoadingView() displayLoadingView()


CommonActivityUtils.exportKeys(session, passphrase, object : SimpleApiCallback<String>(activity) { KeysExporter(mSession)
override fun onSuccess(filename: String) { .export(requireContext(),
hideLoadingView() passphrase,
object : MatrixCallback<String> {
override fun onSuccess(data: String) {
if (isAdded) {
hideLoadingView()


AlertDialog.Builder(activity) AlertDialog.Builder(activity)
.setMessage(getString(R.string.encryption_export_saved_as, filename)) .setMessage(getString(R.string.encryption_export_saved_as, data))
.setCancelable(false) .setCancelable(false)
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
}


override fun onNetworkError(e: Exception) { override fun onFailure(failure: Throwable) {
super.onNetworkError(e) onCommonDone(failure.localizedMessage)
hideLoadingView() }
}


override fun onMatrixError(e: MatrixError) { })
super.onMatrixError(e)
hideLoadingView()
}

override fun onUnexpectedError(e: Exception) {
super.onUnexpectedError(e)
hideLoadingView()
}
})
*/
} }
}) })
} }
@ -2651,100 +2648,92 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
return return
} }


notImplemented() val sharedDataItems = analyseIntent(intent)
val thisActivity = activity


/* if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent)) val sharedDataItem = sharedDataItems[0]
val thisActivity = activity


if (sharedDataItems.isNotEmpty() && thisActivity != null) { val uri = when (sharedDataItem) {
val sharedDataItem = sharedDataItems[0] is ExternalIntentData.IntentDataUri -> sharedDataItem.uri
val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null) is ExternalIntentData.IntentDataClipData -> sharedDataItem.clipDataItem.uri
val builder = AlertDialog.Builder(thisActivity) else -> null
.setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout)

val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)

passPhraseEditText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {

}

override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
}

override fun afterTextChanged(s: Editable) {

}
})

val importDialog = builder.show()
val appContext = thisActivity.applicationContext

importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString()
val resource = openResource(appContext, sharedDataItem.uri, sharedDataItem.getMimeType(appContext))

if (resource?.mContentStream == null) {
appContext.toast("Error")

return@OnClickListener
}

val data: ByteArray

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()")
} }


appContext.toast(e.localizedMessage) val mimetype = when (sharedDataItem) {
is ExternalIntentData.IntentDataClipData -> sharedDataItem.mimeType
else -> null
}


return@OnClickListener if (uri == null) {
return
}

val appContext = thisActivity.applicationContext

val filename = getFilenameFromUri(appContext, uri)

val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)

val textView = dialogLayout.findViewById<TextView>(R.id.dialog_e2e_keys_passphrase_filename)

if (filename.isNullOrBlank()) {
textView.isVisible = false
} else {
textView.isVisible = true
textView.text = getString(R.string.import_e2e_keys_from_file, filename)
}

val builder = AlertDialog.Builder(thisActivity)
.setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout)

val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text)
val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)

passPhraseEditText.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
}
})

val importDialog = builder.show()

importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString()

displayLoadingView()

KeysImporter(mSession)
.import(requireContext(),
uri,
mimetype,
password,
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()
}
})

importDialog.dismiss()
})
} }

displayLoadingView()

session.importRoomKeys(data,
password,
null,
object : MatrixCallback<ImportRoomKeysResult> {
override fun onSuccess(info: ImportRoomKeysResult) {
if (!isAdded) {
return
}

hideLoadingView()

info?.let {
AlertDialog.Builder(thisActivity)
.setMessage(getString(R.string.encryption_import_room_keys_success,
it.successfullyNumberOfImportedKeys,
it.totalNumberOfKeys))
.setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() }
.show()
}
}

override fun onFailure(failure: Throwable) {
appContext.toast(failure.localizedMessage)
hideLoadingView()
}
})

importDialog.dismiss()
})
}
*/
} }


//============================================================================================================== //==============================================================================================================

View File

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/layout_root" android:id="@+id/layout_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding" android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding" android:paddingLeft="?dialogPreferredPadding"
@ -13,21 +13,41 @@
android:paddingBottom="12dp"> android:paddingBottom="12dp">


<TextView <TextView
android:id="@+id/exportDialogText"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/encryption_export_notice" android:text="@string/encryption_export_notice"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="16sp" /> android:textSize="16sp"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/exportDialogShowPassword"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye_black"
android:tint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/exportDialogTil"
app:layout_constraintTop_toTopOf="@id/exportDialogTil" />



<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/exportDialogTil"
style="@style/VectorTextInputLayout" style="@style/VectorTextInputLayout"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:textColorHint="?attr/vctr_default_text_hint_color"> android:textColorHint="?attr/vctr_default_text_hint_color"
app:layout_constraintEnd_toStartOf="@+id/exportDialogShowPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/exportDialogText">


<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_e2e_keys_passphrase_edit_text" android:id="@+id/exportDialogEt"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/passphrase_create_passphrase" android:hint="@string/passphrase_create_passphrase"
@ -38,16 +58,19 @@




<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/dialog_e2e_keys_confirm_passphrase_til" android:id="@+id/exportDialogTilConfirm"
style="@style/VectorTextInputLayout" style="@style/VectorTextInputLayout"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="10dp" android:layout_marginTop="10dp"
android:textColorHint="?attr/vctr_default_text_hint_color" android:textColorHint="?attr/vctr_default_text_hint_color"
app:errorEnabled="true"> app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="@+id/exportDialogTil"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/exportDialogTil">


<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/dialog_e2e_keys_confirm_passphrase_edit_text" android:id="@+id/exportDialogEtConfirm"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/passphrase_confirm_passphrase" android:hint="@string/passphrase_confirm_passphrase"
@ -57,10 +80,14 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>


<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/dialog_e2e_keys_export_button" android:id="@+id/exportDialogSubmit"
style="@style/VectorButtonStyle" style="@style/VectorButtonStyle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:enabled="false" android:enabled="false"
android:text="@string/encryption_export_export" /> android:text="@string/encryption_export_export"
</LinearLayout> app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/exportDialogTilConfirm" />

</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root" android:id="@+id/layout_root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -11,6 +12,18 @@
android:paddingRight="?dialogPreferredPadding" android:paddingRight="?dialogPreferredPadding"
android:paddingBottom="12dp"> android:paddingBottom="12dp">


<TextView
android:id="@+id/dialog_e2e_keys_passphrase_filename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
android:visibility="gone"
tools:text="filename.txt"
tools:visibility="visible" />

<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
style="@style/VectorTextInputLayout" style="@style/VectorTextInputLayout"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -4,4 +4,6 @@
<!-- Strings not defined in Riot --> <!-- Strings not defined in Riot -->




<string name="import_e2e_keys_from_file">"Import e2e keys from file \"%1$s\"."</string>

</resources> </resources>