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


@ -787,7 +790,10 @@ 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>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
val iterationCount = Math.max(0, anIterationCount) val iterationCount = Math.max(0, anIterationCount)


val exportedSessions = ArrayList<MegolmSessionData>() val exportedSessions = ArrayList<MegolmSessionData>()
@ -802,20 +808,13 @@ internal class CryptoManager(
} }
} }


val encryptedRoomKeys: ByteArray

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


encryptedRoomKeys = MXMegolmExportEncryption MXMegolmExportEncryption.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount)
.encryptMegolmKeyFile(adapter.toJson(exportedSessions), password, iterationCount) }
} catch (e: Exception) { }.foldToCallback(callback)
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>) {
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
Timber.v("## importRoomKeys starts") Timber.v("## importRoomKeys starts")


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

try {
roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
} catch (e: Exception) {
callback.onFailure(e)
return
}

val importedSessions: List<MegolmSessionData>

val t1 = System.currentTimeMillis() val t1 = System.currentTimeMillis()


Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms") Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")


try { val importedSessions = MoshiProvider.providesMoshi()
val list = MoshiProvider.providesMoshi() .adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
.adapter(List::class.java)
.fromJson(roomKeys) .fromJson(roomKeys)
importedSessions = list as List<MegolmSessionData>
} catch (e: Exception) {
Timber.e(e, "## importRoomKeys failed")
callback.onFailure(e)
return
}


val t2 = System.currentTimeMillis() val t2 = System.currentTimeMillis()


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


megolmSessionDataImporter.handle(importedSessions, true, progressListener, callback) if (importedSessions == null) {
throw Exception("Error")
}

megolmSessionDataImporter.handle(importedSessions, true, uiHandler, progressListener)
}
}.foldToCallback(callback)
}
} }


/** /**
@ -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,8 +139,9 @@ 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) {
Try {
val olmPkDecryption = OlmPkDecryption() val olmPkDecryption = OlmPkDecryption()
val megolmBackupAuthData = MegolmBackupAuthData() val megolmBackupAuthData = MegolmBackupAuthData()


@ -173,12 +183,9 @@ internal class KeysBackup(
megolmBackupCreationInfo.authData = megolmBackupAuthData megolmBackupCreationInfo.authData = megolmBackupAuthData
megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey()) megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())


uiHandler.post { callback.onSuccess(megolmBackupCreationInfo) } megolmBackupCreationInfo
} catch (e: OlmException) {
Timber.e(e, "OlmException")

uiHandler.post { callback.onFailure(e) }
} }
}.foldToCallback(callback)
} }
} }


@ -221,7 +228,8 @@ 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) {
withContext(coroutineDispatchers.crypto) {
// If we're currently backing up to this backup... stop. // If we're currently backing up to this backup... stop.
// (We start using it automatically in createKeysBackupVersion so this is symmetrical). // (We start using it automatically in createKeysBackupVersion so this is symmetrical).
if (keysBackupVersion != null && version == keysBackupVersion!!.version) { if (keysBackupVersion != null && version == keysBackupVersion!!.version) {
@ -254,6 +262,7 @@ internal class KeysBackup(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }
}


override fun canRestoreKeys(): Boolean { override fun canRestoreKeys(): Boolean {
// Server contains more keys than locally // Server contains more keys than locally
@ -426,21 +435,17 @@ 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 {
val myUserId = credentials.userId

// Get auth data to update it // Get auth data to update it
val authData = getMegolmBackupAuthData(keysBackupVersion) val authData = getMegolmBackupAuthData(keysBackupVersion)


if (authData == null) { if (authData == null) {
Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data") Timber.w("trustKeyBackupVersion:trust: Key backup is missing required data")


uiHandler.post {
callback.onFailure(IllegalArgumentException("Missing element")) callback.onFailure(IllegalArgumentException("Missing element"))
} } else {

GlobalScope.launch(coroutineDispatchers.main) {
return@post val updateKeysBackupVersionBody = withContext(coroutineDispatchers.crypto) {
} val myUserId = credentials.userId


// Get current signatures, or create an empty set // Get current signatures, or create an empty set
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap()) val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap())
@ -474,9 +479,11 @@ internal class KeysBackup(
val moshi = MoshiProvider.providesMoshi() val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(Map::class.java) val adapter = moshi.adapter(Map::class.java)



updateKeysBackupVersionBody.authData = adapter.fromJson(newMegolmBackupAuthData.toJsonString()) as Map<String, Any>? updateKeysBackupVersionBody.authData = adapter.fromJson(newMegolmBackupAuthData.toJsonString()) as Map<String, Any>?


updateKeysBackupVersionBody
}

// And send it to the homeserver // And send it to the homeserver
updateKeysBackupVersionTask updateKeysBackupVersionTask
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody)) .configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
@ -493,62 +500,58 @@ internal class KeysBackup(


checkAndStartWithKeysBackupVersion(newKeysBackupVersion) 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)
} }
} }
}


override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult, override fun trustKeysBackupVersionWithRecoveryKey(keysBackupVersion: KeysVersionResult,
recoveryKey: String, recoveryKey: String,
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) {
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
}

if (!isValid) {
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.") Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")


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

trustKeysBackupVersion(keysBackupVersion, true, callback) trustKeysBackupVersion(keysBackupVersion, true, callback)
} }
} }
}


override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult, override fun trustKeysBackupVersionWithPassphrase(keysBackupVersion: KeysVersionResult,
password: String, password: String,
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 {

return@post
}

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


/** /**
* Get public key from a Recovery key * Get public key from a Recovery key
@ -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,12 +608,13 @@ 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) {
withContext(coroutineDispatchers.crypto) {
Try {
// Check if the recovery is valid before going any further // Check if the recovery is valid before going any further
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) { if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version") Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) } throw InvalidParameterException("Invalid recovery key")
return@Runnable
} }


// Get a PK decryption instance // Get a PK decryption instance
@ -620,17 +622,23 @@ internal class KeysBackup(
if (decryption == null) { if (decryption == null) {
// This should not happen anymore // This should not happen anymore
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error") Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) } throw InvalidParameterException("Invalid recovery key")
return@Runnable
} }


if (stepProgressListener != null) { decryption!!
uiHandler.post { stepProgressListener.onStepProgress(StepProgressListener.Step.DownloadingKey) }
} }
}.fold(
{
callback.onFailure(it)
},
{ decryption ->
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)


// Get backed up keys from the homeserver // Get backed up keys from the homeserver
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> { getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
override fun onSuccess(data: KeysBackupData) { override fun onSuccess(data: KeysBackupData) {
GlobalScope.launch(coroutineDispatchers.main) {
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
val sessionsData = ArrayList<MegolmSessionData>() val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data // Restore that data
var sessionsFromHsCount = 0 var sessionsFromHsCount = 0
@ -668,14 +676,18 @@ internal class KeysBackup(
null null
} }


megolmSessionDataImporter.handle(sessionsData, !backUp, progressListener, object : MatrixCallback<ImportRoomKeysResult> { val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)
override fun onSuccess(data: ImportRoomKeysResult) {
// Do not back up the key if it comes from a backup recovery // Do not back up the key if it comes from a backup recovery
if (backUp) { if (backUp) {
maybeBackupKeys() maybeBackupKeys()
} }


callback.onSuccess(data) result
}

callback.onSuccess(importRoomKeysResult)
}
} }


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -683,12 +695,8 @@ internal class KeysBackup(
} }
}) })
} }

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


override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult, override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
@ -699,7 +707,8 @@ 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) {
withContext(coroutineDispatchers.crypto) {
val progressListener = if (stepProgressListener != null) { val progressListener = if (stepProgressListener != null) {
object : ProgressListener { object : ProgressListener {
override fun onProgress(progress: Int, total: Int) { override fun onProgress(progress: Int, total: Int) {
@ -712,20 +721,24 @@ internal class KeysBackup(
null null
} }


val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, progressListener) Try {

recoveryKeyFromPassword(password, keysBackupVersion, progressListener)
}
}.fold(
{
callback.onFailure(it)
},
{ recoveryKey ->
if (recoveryKey == null) { if (recoveryKey == null) {
uiHandler.post {
Timber.v("backupKeys: Invalid configuration") Timber.v("backupKeys: Invalid configuration")
callback.onFailure(IllegalStateException("Invalid configuration")) callback.onFailure(IllegalStateException("Invalid configuration"))
} } else {

return@post
}

restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback) restoreKeysWithRecoveryKey(keysBackupVersion, recoveryKey, roomId, sessionId, stepProgressListener, callback)
} }
} }
)
}
}


/** /**
* Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable * Same method as [RoomKeysRestClient.getRoomKey] except that it accepts nullable
@ -1190,7 +1203,8 @@ internal class KeysBackup(


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


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


// Gather data to send to the homeserver // Gather data to send to the homeserver
@ -1280,6 +1294,7 @@ internal class KeysBackup(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
} }
}


@VisibleForTesting @VisibleForTesting
@WorkerThread @WorkerThread

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

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
@ -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,11 +136,9 @@ 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)
} }
} }
}




private fun doAccept(accept: KeyVerificationAccept) { private fun doAccept(accept: KeyVerificationAccept) {

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,22 +278,18 @@ 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,19 +121,21 @@ 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,
passphrase,
object : MatrixCallback<String> {

override fun onSuccess(data: String) {
hideWaitingView() hideWaitingView()


AlertDialog.Builder(this@KeysBackupSetupActivity) AlertDialog.Builder(this@KeysBackupSetupActivity)
.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) { dialog, which -> .setPositiveButton(R.string.ok) { dialog, which ->
val resultIntent = Intent() val resultIntent = Intent()
@ -141,26 +146,13 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
.show() .show()
} }


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

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

override fun onUnexpectedError(e: Exception) {
super.onUnexpectedError(e)
hideWaitingView() 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) {
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt") val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")


if (saveStringToFile(data, file)) { writeToFile(data, file)

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


context?.let { file.absolutePath
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, file.absolutePath))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
} }

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

context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
}
}
)
?.setCancelable(false)
?.setPositiveButton(R.string.ok, null)
?.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(),
passphrase,
object : MatrixCallback<String> {
override fun onSuccess(data: String) {
if (isAdded) {
hideLoadingView() 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) {
super.onNetworkError(e)
hideLoadingView()
} }


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


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


notImplemented() val sharedDataItems = analyseIntent(intent)

/*
val sharedDataItems = ArrayList(RoomMediaMessage.listRoomMediaMessages(intent))
val thisActivity = activity val thisActivity = activity


if (sharedDataItems.isNotEmpty() && thisActivity != null) { if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItem = sharedDataItems[0] val sharedDataItem = sharedDataItems[0]

val uri = when (sharedDataItem) {
is ExternalIntentData.IntentDataUri -> sharedDataItem.uri
is ExternalIntentData.IntentDataClipData -> sharedDataItem.clipDataItem.uri
else -> null
}

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

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 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) val builder = AlertDialog.Builder(thisActivity)
.setTitle(R.string.encryption_import_room_keys) .setTitle(R.string.encryption_import_room_keys)
.setView(dialogLayout) .setView(dialogLayout)
@ -2667,73 +2691,39 @@ if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val passPhraseEditText = dialogLayout.findViewById<TextInputEditText>(R.id.dialog_e2e_keys_passphrase_edit_text) 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) val importButton = dialogLayout.findViewById<Button>(R.id.dialog_e2e_keys_import_button)


passPhraseEditText.addTextChangedListener(object : TextWatcher { passPhraseEditText.addTextChangedListener(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 onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text) importButton.isEnabled = !TextUtils.isEmpty(passPhraseEditText.text)
} }

override fun afterTextChanged(s: Editable) {

}
}) })


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


importButton.setOnClickListener(View.OnClickListener { importButton.setOnClickListener(View.OnClickListener {
val password = passPhraseEditText.text.toString() 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)

return@OnClickListener
}


displayLoadingView() displayLoadingView()


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


hideLoadingView() hideLoadingView()


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


override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
appContext.toast(failure.localizedMessage) appContext.toast(failure.localizedMessage)
@ -2744,7 +2734,6 @@ if (sharedDataItems.isNotEmpty() && thisActivity != null) {
importDialog.dismiss() 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>