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.api.auth.data.SessionParams
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.session.DefaultSession
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 sessionOrFailure = authenticate(homeServerConnectionConfig, login, password)
sessionOrFailure.fold({ callback.onFailure(it) }, { callback.onSuccess(it) })
sessionOrFailure.foldToCallback(callback)
}
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

import android.content.Context
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import arrow.core.Try
import com.squareup.moshi.Types
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
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.query.where
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.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
@ -137,6 +141,8 @@ internal class CryptoManager(
private val taskExecutor: TaskExecutor
) : CryptoService {

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

// MXEncrypting instance for each room.
private val roomEncryptors: MutableMap<String, IMXEncrypting> = HashMap()
private val isStarting = AtomicBoolean(false)
@ -560,10 +566,10 @@ internal class CryptoManager(
} else {
val algorithm = getEncryptionAlgorithm(roomId)
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")
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) {
internalDecryptEvent(event, timeline)
}
result.fold(
{ callback.onFailure(it) },
{ callback.onSuccess(it) }
)
result.foldToCallback(callback)
}
}

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

userIds = if (encryptForInvitedMembers) {
RoomMembers(realm, roomId).getActiveRoomMemberIds()
@ -787,35 +790,31 @@ internal class CryptoManager(
* @param anIterationCount the encryption iteration count (0 means no encryption)
* @param callback the exported keys
*/
fun exportRoomKeys(password: String, anIterationCount: Int, callback: MatrixCallback<ByteArray>) {
val iterationCount = Math.max(0, anIterationCount)
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 exportedSessions = ArrayList<MegolmSessionData>()
val exportedSessions = ArrayList<MegolmSessionData>()

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

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

if (null != megolmSessionData) {
exportedSessions.add(megolmSessionData)
}
if (null != 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,
progressListener: ProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("## importRoomKeys starts")
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
Timber.v("## importRoomKeys starts")

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

try {
roomKeys = MXMegolmExportEncryption.decryptMegolmKeyFile(roomKeysAsArray, password)
} catch (e: Exception) {
callback.onFailure(e)
return
Timber.v("## importRoomKeys : decryptMegolmKeyFile done in " + (t1 - t0) + " ms")

val importedSessions = MoshiProvider.providesMoshi()
.adapter<List<MegolmSessionData>>(Types.newParameterizedType(List::class.java, MegolmSessionData::class.java))
.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
callback.onFailure(
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 {
deviceListManager
.downloadKeys(userIds, forceDownload)
.fold(
{ callback.onFailure(it) },
{ callback.onSuccess(it) }
)
.foldToCallback(callback)
}
}


View File

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


View File

@ -21,7 +21,7 @@ import android.text.TextUtils
import arrow.core.Try
import im.vector.matrix.android.api.MatrixPatterns
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.MXUsersDevicesMap
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.util.Base64
import im.vector.matrix.android.internal.extensions.toUnsignedInt
import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.nio.charset.Charset
@ -43,16 +44,6 @@ object MXMegolmExportEncryption {
// default iteration count to export the e2e keys
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.
*
@ -108,7 +99,8 @@ object MXMegolmExportEncryption {

val salt = Arrays.copyOfRange(body, 1, 1 + 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 hmac = Arrays.copyOfRange(body, body.size - 32, body.size)


View File

@ -16,9 +16,9 @@

package im.vector.matrix.android.internal.crypto.actions

import im.vector.matrix.android.api.MatrixCallback
import android.os.Handler
import androidx.annotation.WorkerThread
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.OutgoingRoomKeyRequestManager
@ -35,34 +35,32 @@ internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,

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

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

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

for (megolmSessionData in megolmSessionsData) {
cpt++


megolmSessionsData.forEachIndexed { cpt, megolmSessionData ->
val decrypting = roomDecryptorProvider.getOrCreateRoomDecryptor(megolmSessionData.roomId, megolmSessionData.algorithm)

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

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

if (lastProgress != progress) {
@ -111,10 +109,6 @@ internal class MegolmSessionDataImporter(private val olmDevice: MXOlmDevice,

Timber.v("## importMegolmSessionsData : sessions import " + (t1 - t0) + " ms (" + megolmSessionsData.size + " sessions)")

val finalTotalNumbersOfImportedKeys = totalNumbersOfImportedKeys

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

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.KeysBackupState
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupStateListener
import im.vector.matrix.android.internal.crypto.*
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import im.vector.matrix.android.internal.crypto.MXOlmDevice
import im.vector.matrix.android.internal.crypto.MegolmSessionData
import im.vector.matrix.android.internal.crypto.ObjectSigner
import im.vector.matrix.android.internal.crypto.actions.MegolmSessionDataImporter
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrust
import im.vector.matrix.android.internal.crypto.keysbackup.model.KeysBackupVersionTrustSignature
@ -47,10 +50,15 @@ import im.vector.matrix.android.internal.crypto.model.OlmInboundGroupSessionWrap
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.store.db.model.KeysBackupDataEntity
import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.matrix.olm.OlmException
import org.matrix.olm.OlmPkDecryption
import org.matrix.olm.OlmPkEncryption
@ -87,7 +95,8 @@ internal class KeysBackup(
private val storeSessionDataTask: StoreSessionsDataTask,
private val updateKeysBackupVersionTask: UpdateKeysBackupVersionTask,
// Task executor
private val taskExecutor: TaskExecutor
private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers
) : KeysBackupService {

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

if (password != null) {
// Generate a private key from the password
val backgroundProgressListener = if (progressListener == null) {
null
} else {
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
uiHandler.post {
try {
progressListener.onProgress(progress, total)
} catch (e: Exception) {
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
if (password != null) {
// Generate a private key from the password
val backgroundProgressListener = if (progressListener == null) {
null
} else {
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
uiHandler.post {
try {
progressListener.onProgress(progress, total)
} catch (e: Exception) {
Timber.e(e, "prepareKeysBackupVersion: onProgress failure")
}
}
}
}
}

val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey)
megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt
megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations
} else {
val publicKey = olmPkDecryption.generateKey()

megolmBackupAuthData.publicKey = publicKey
}

val generatePrivateKeyResult = generatePrivateKeyWithPassword(password, backgroundProgressListener)
megolmBackupAuthData.publicKey = olmPkDecryption.setPrivateKey(generatePrivateKeyResult.privateKey)
megolmBackupAuthData.privateKeySalt = generatePrivateKeyResult.salt
megolmBackupAuthData.privateKeyIterations = generatePrivateKeyResult.iterations
} else {
val publicKey = olmPkDecryption.generateKey()
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())

megolmBackupAuthData.publicKey = publicKey
megolmBackupAuthData.signatures = objectSigner.signObject(canonicalJson)


val megolmBackupCreationInfo = MegolmBackupCreationInfo()
megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
megolmBackupCreationInfo.authData = megolmBackupAuthData
megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())

megolmBackupCreationInfo
}

val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, megolmBackupAuthData.signalableJSONDictionary())

megolmBackupAuthData.signatures = objectSigner.signObject(canonicalJson)


val megolmBackupCreationInfo = MegolmBackupCreationInfo()
megolmBackupCreationInfo.algorithm = MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
megolmBackupCreationInfo.authData = megolmBackupAuthData
megolmBackupCreationInfo.recoveryKey = computeRecoveryKey(olmPkDecryption.privateKey())

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

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

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

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

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

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

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

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

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

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

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

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

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

uiHandler.post {
callback.onFailure(IllegalArgumentException("Missing element"))
// Get current signatures, or create an empty set
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap())

if (trust) {
// Add current device signature
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())

val deviceSignatures = objectSigner.signObject(canonicalJson)

deviceSignatures[myUserId]?.forEach { entry ->
myUserSignatures[entry.key] = entry.value
}
} else {
// Remove current device signature
myUserSignatures.remove("ed25519:${credentials.deviceId}")
}

// Create an updated version of KeysVersionResult
val updateKeysBackupVersionBody = UpdateKeysBackupVersionBody(keysBackupVersion.version!!)

updateKeysBackupVersionBody.algorithm = keysBackupVersion.algorithm

val newMegolmBackupAuthData = authData.copy()

val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
newSignatures[myUserId] = myUserSignatures

newMegolmBackupAuthData.signatures = newSignatures

val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(Map::class.java)

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

updateKeysBackupVersionBody
}

return@post
}
// And send it to the homeserver
updateKeysBackupVersionTask
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
.dispatchTo(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// Relaunch the state machine on this updated backup version
val newKeysBackupVersion = KeysVersionResult()

// Get current signatures, or create an empty set
val myUserSignatures = (authData.signatures!![myUserId]?.toMutableMap() ?: HashMap())
newKeysBackupVersion.version = keysBackupVersion.version
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
newKeysBackupVersion.count = keysBackupVersion.count
newKeysBackupVersion.hash = keysBackupVersion.hash
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData

if (trust) {
// Add current device signature
val canonicalJson = MoshiProvider.getCanonicalJson(Map::class.java, authData.signalableJSONDictionary())
checkAndStartWithKeysBackupVersion(newKeysBackupVersion)

val deviceSignatures = objectSigner.signObject(canonicalJson)

deviceSignatures[myUserId]?.forEach { entry ->
myUserSignatures[entry.key] = entry.value
}
} else {
// Remove current device signature
myUserSignatures.remove("ed25519:${credentials.deviceId}")
}

// Create an updated version of KeysVersionResult
val updateKeysBackupVersionBody = UpdateKeysBackupVersionBody(keysBackupVersion.version!!)

updateKeysBackupVersionBody.algorithm = keysBackupVersion.algorithm

val newMegolmBackupAuthData = authData.copy()

val newSignatures = newMegolmBackupAuthData.signatures!!.toMutableMap()
newSignatures[myUserId] = myUserSignatures

newMegolmBackupAuthData.signatures = newSignatures

val moshi = MoshiProvider.providesMoshi()
val adapter = moshi.adapter(Map::class.java)


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

// And send it to the homeserver
updateKeysBackupVersionTask
.configureWith(UpdateKeysBackupVersionTask.Params(keysBackupVersion.version!!, updateKeysBackupVersionBody))
.dispatchTo(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
// Relaunch the state machine on this updated backup version
val newKeysBackupVersion = KeysVersionResult()

newKeysBackupVersion.version = keysBackupVersion.version
newKeysBackupVersion.algorithm = keysBackupVersion.algorithm
newKeysBackupVersion.count = keysBackupVersion.count
newKeysBackupVersion.hash = keysBackupVersion.hash
newKeysBackupVersion.authData = updateKeysBackupVersionBody.authData

checkAndStartWithKeysBackupVersion(newKeysBackupVersion)

uiHandler.post {
callback.onSuccess(data)
}
}

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

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

CryptoAsyncHelper.getDecryptBackgroundHandler().post {
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)) {
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")

uiHandler.post {
callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
}
return@post
GlobalScope.launch(coroutineDispatchers.main) {
val isValid = withContext(coroutineDispatchers.crypto) {
isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysBackupVersion)
}

trustKeysBackupVersion(keysBackupVersion, true, callback)
if (!isValid) {
Timber.w("trustKeyBackupVersionWithRecoveryKey: Invalid recovery key.")

callback.onFailure(IllegalArgumentException("Invalid recovery key or password"))
} else {
trustKeysBackupVersion(keysBackupVersion, true, callback)
}
}
}

@ -532,21 +537,19 @@ internal class KeysBackup(
callback: MatrixCallback<Unit>) {
Timber.v("trustKeysBackupVersionWithPassphrase: version ${keysBackupVersion.version}")

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

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

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

return@post
callback.onFailure(IllegalArgumentException("Missing element"))
} else {
// Check trust using the recovery key
trustKeysBackupVersionWithRecoveryKey(keysBackupVersion, recoveryKey, callback)
}

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

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

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

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

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

CryptoAsyncHelper.getDecryptBackgroundHandler().post(Runnable {
// Check if the recovery is valid before going any further
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) }
return@Runnable
}

// Get a PK decryption instance
val decryption = pkDecryptionFromRecoveryKey(recoveryKey)
if (decryption == null) {
// This should not happen anymore
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key. Error")
uiHandler.post { callback.onFailure(InvalidParameterException("Invalid recovery key")) }
return@Runnable
}

if (stepProgressListener != null) {
uiHandler.post { stepProgressListener.onStepProgress(StepProgressListener.Step.DownloadingKey) }
}

// Get backed up keys from the homeserver
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
override fun onSuccess(data: KeysBackupData) {
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) {
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) {
sessionsFromHsCount++

val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!

val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)

sessionData?.let {
sessionsData.add(it)
}
}
}
Timber.v("restoreKeysWithRecoveryKey: Decrypted " + sessionsData.size + " keys out of "
+ sessionsFromHsCount + " from the backup store on the homeserver")

// Do not trigger a backup for them if they come from the backup version we are using
val backUp = keysVersionResult.version != keysBackupVersion?.version
if (backUp) {
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up to backup version: " + keysBackupVersion?.version)
GlobalScope.launch(coroutineDispatchers.main) {
withContext(coroutineDispatchers.crypto) {
Try {
// Check if the recovery is valid before going any further
if (!isValidRecoveryKeyForKeysBackupVersion(recoveryKey, keysVersionResult)) {
Timber.e("restoreKeysWithRecoveryKey: Invalid recovery key for this keys version")
throw InvalidParameterException("Invalid recovery key")
}

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

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

callback.onSuccess(data)
}

override fun onFailure(failure: Throwable) {
callback.onFailure(failure)
}
})
decryption!!
}
}.fold(
{
callback.onFailure(it)
},
{ decryption ->
stepProgressListener?.onStepProgress(StepProgressListener.Step.DownloadingKey)

override fun onFailure(failure: Throwable) {
uiHandler.post { callback.onFailure(failure) }
}
})
})
// Get backed up keys from the homeserver
getKeys(sessionId, roomId, keysVersionResult.version!!, object : MatrixCallback<KeysBackupData> {
override fun onSuccess(data: KeysBackupData) {
GlobalScope.launch(coroutineDispatchers.main) {
val importRoomKeysResult = withContext(coroutineDispatchers.crypto) {
val sessionsData = ArrayList<MegolmSessionData>()
// Restore that data
var sessionsFromHsCount = 0
for (roomIdLoop in data.roomIdToRoomKeysBackupData.keys) {
for (sessionIdLoop in data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData.keys) {
sessionsFromHsCount++

val keyBackupData = data.roomIdToRoomKeysBackupData[roomIdLoop]!!.sessionIdToKeyBackupData[sessionIdLoop]!!

val sessionData = decryptKeyBackupData(keyBackupData, sessionIdLoop, roomIdLoop, decryption)

sessionData?.let {
sessionsData.add(it)
}
}
}
Timber.v("restoreKeysWithRecoveryKey: Decrypted " + sessionsData.size + " keys out of "
+ sessionsFromHsCount + " from the backup store on the homeserver")

// Do not trigger a backup for them if they come from the backup version we are using
val backUp = keysVersionResult.version != keysBackupVersion?.version
if (backUp) {
Timber.v("restoreKeysWithRecoveryKey: Those keys will be backed up to backup version: " + keysBackupVersion?.version)
}

// Import them into the crypto store
val progressListener = if (stepProgressListener != null) {
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
// Note: no need to post to UI thread, importMegolmSessionsData() will do it
stepProgressListener.onStepProgress(StepProgressListener.Step.ImportingKey(progress, total))
}
}
} else {
null
}

val result = megolmSessionDataImporter.handle(sessionsData, !backUp, uiHandler, progressListener)

// Do not back up the key if it comes from a backup recovery
if (backUp) {
maybeBackupKeys()
}

result
}

callback.onSuccess(importRoomKeysResult)
}
}

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

override fun restoreKeyBackupWithPassword(keysBackupVersion: KeysVersionResult,
@ -699,31 +707,36 @@ internal class KeysBackup(
callback: MatrixCallback<ImportRoomKeysResult>) {
Timber.v("[MXKeyBackup] restoreKeyBackup with password: From backup version: ${keysBackupVersion.version}")

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

val recoveryKey = recoveryKeyFromPassword(password, keysBackupVersion, progressListener)

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

return@post
}

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

@ -989,9 +1002,9 @@ internal class KeysBackup(
}
}

/* ==========================================================================================
* Private
* ========================================================================================== */
/* ==========================================================================================
* Private
* ========================================================================================== */

/**
* Extract MegolmBackupAuthData data from a backup version.
@ -1190,94 +1203,96 @@ internal class KeysBackup(

keysBackupStateManager.state = KeysBackupState.BackingUp

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

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

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

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

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

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

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

// Mark keys as backed up
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)

if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
Timber.v("backupKeys: All keys have been backed up")
onServerDataRetrieved(data.count, data.hash)

// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
} else {
Timber.v("backupKeys: Continue to back up keys")
keysBackupStateManager.state = KeysBackupState.WillBackUp

backupKeys()
}
}
}

override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError) {
// Make the request
storeSessionDataTask
.configureWith(StoreSessionsDataTask.Params(keysBackupVersion!!.version!!, keysBackupData))
.dispatchTo(object : MatrixCallback<BackupKeysResult> {
override fun onSuccess(data: BackupKeysResult) {
uiHandler.post {
Timber.e(failure, "backupKeys: backupKeys failed.")
Timber.v("backupKeys: 5a - Request complete")

when (failure.error.code) {
MatrixError.NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
resetKeysBackupData()
keysBackupVersion = null
// Mark keys as backed up
cryptoStore.markBackupDoneForInboundGroupSessions(olmInboundGroupSessionWrappers)

// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
else -> // Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
if (olmInboundGroupSessionWrappers.size < KEY_BACKUP_SEND_KEYS_MAX_COUNT) {
Timber.v("backupKeys: All keys have been backed up")
onServerDataRetrieved(data.count, data.hash)

// Note: Changing state will trigger the call to backupAllGroupSessionsCallback.onSuccess()
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
} else {
Timber.v("backupKeys: Continue to back up keys")
keysBackupStateManager.state = KeysBackupState.WillBackUp

backupKeys()
}
}
} else {
uiHandler.post {
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
}

Timber.e("backupKeys: backupKeys failed.")
override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError) {
uiHandler.post {
Timber.e(failure, "backupKeys: backupKeys failed.")

// Retry a bit later
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
maybeBackupKeys()
when (failure.error.code) {
MatrixError.NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()
resetKeysBackupData()
keysBackupVersion = null

// Do not stay in KeysBackupState.WrongBackUpVersion but check what is available on the homeserver
checkAndStartKeysBackup()
}
else -> // Come back to the ready state so that we will retry on the next received key
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
}
}
} else {
uiHandler.post {
backupAllGroupSessionsCallback?.onFailure(failure)
resetBackupAllGroupSessionsListeners()

Timber.e("backupKeys: backupKeys failed.")

// Retry a bit later
keysBackupStateManager.state = KeysBackupState.ReadyToBackUp
maybeBackupKeys()
}
}
}
}
})
.executeBy(taskExecutor)
})
.executeBy(taskExecutor)
}
}
}

@ -1376,9 +1391,9 @@ internal class KeysBackup(
private const val KEY_BACKUP_SEND_KEYS_MAX_COUNT = 100
}

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

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

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.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
import im.vector.matrix.android.internal.crypto.DeviceListManager
import im.vector.matrix.android.internal.crypto.MyDeviceInfoHolder
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.SendToDeviceTask
import im.vector.matrix.android.internal.task.TaskExecutor
@ -138,8 +133,8 @@ internal class DefaultSasVerificationService(private val credentials: Credential

override fun markedLocallyAsManuallyVerified(userId: String, deviceID: String) {
setDeviceVerificationAction.handle(MXDeviceInfo.DEVICE_VERIFICATION_VERIFIED,
deviceID,
userId)
deviceID,
userId)

listeners.forEach {
try {
@ -206,7 +201,7 @@ internal class DefaultSasVerificationService(private val credentials: Credential
} else {
Timber.e("## SAS onStartRequestReceived - unknown method ${startReq.method}")
cancelTransaction(tid, otherUserId, startReq.fromDevice
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
?: event.getSenderKey()!!, CancelCode.UnknownMethod)
}
}
},
@ -372,9 +367,8 @@ internal class DefaultSasVerificationService(private val credentials: Credential
userId,
deviceID)
addTransaction(tx)
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
tx.start()
}

tx.start()
return txID
} else {
throw IllegalArgumentException("Unknown verification method")
@ -399,9 +393,9 @@ internal class DefaultSasVerificationService(private val credentials: Credential
override fun transactionUpdated(tx: VerificationTransaction) {
dispatchTxUpdated(tx)
if (tx is SASVerificationTransaction
&& (tx.state == SasVerificationTxState.Cancelled
|| tx.state == SasVerificationTxState.OnCancelled
|| tx.state == SasVerificationTxState.Verified)
&& (tx.state == SasVerificationTxState.Cancelled
|| tx.state == SasVerificationTxState.OnCancelled
|| tx.state == SasVerificationTxState.Verified)
) {
//remove
this.removeTransaction(tx.otherUserId, tx.transactionId)

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

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


View File

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

fun start() {

if (state != SasVerificationTxState.None) {
Timber.e("## start verification from invalid state")
//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.SasVerificationTxState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXKey
@ -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.store.IMXCryptoStore
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.configureWith
import org.matrix.olm.OlmSAS
@ -278,21 +278,17 @@ internal abstract class SASVerificationTransaction(
.dispatchTo(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.v("## SAS verification [$transactionId] toDevice type '$type' success.")
CryptoAsyncHelper.getDecryptBackgroundHandler().post {
if (onDone != null) {
onDone()
} else {
state = nextState
}
if (onDone != null) {
onDone()
} else {
state = nextState
}
}

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

CryptoAsyncHelper.getDecryptBackgroundHandler().post {
cancel(onErrorReason)
}
cancel(onErrorReason)
}
})
.executeBy(taskExecutor)
@ -359,11 +355,11 @@ internal abstract class SASVerificationTransaction(
* or with the three numbers on separate lines.
*/
fun getDecimalCodeRepresentation(byteArray: ByteArray): String {
val b0 = byteArray[0].toInt().and(0xff) //need unsigned byte
val b1 = byteArray[1].toInt().and(0xff) //need unsigned byte
val b2 = byteArray[2].toInt().and(0xff) //need unsigned byte
val b3 = byteArray[3].toInt().and(0xff) //need unsigned byte
val b4 = byteArray[4].toInt().and(0xff) //need unsigned byte
val b0 = byteArray[0].toUnsignedInt() //need unsigned byte
val b1 = byteArray[1].toUnsignedInt() //need unsigned byte
val b2 = byteArray[2].toUnsignedInt() //need unsigned byte
val b3 = byteArray[3].toUnsignedInt() //need unsigned byte
val b4 = byteArray[4].toUnsignedInt() //need unsigned byte
//(B0 << 5 | B1 >> 3) + 1000
val first = (b0.shl(5) or b1.shr(3)) + 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)
*/
fun getEmojiCodeRepresentation(byteArray: ByteArray): List<EmojiRepresentation> {
val b0 = byteArray[0].toInt().and(0xff)
val b1 = byteArray[1].toInt().and(0xff)
val b2 = byteArray[2].toInt().and(0xff)
val b3 = byteArray[3].toInt().and(0xff)
val b4 = byteArray[4].toInt().and(0xff)
val b5 = byteArray[5].toInt().and(0xff)
val b0 = byteArray[0].toUnsignedInt()
val b1 = byteArray[1].toUnsignedInt()
val b2 = byteArray[2].toUnsignedInt()
val b3 = byteArray[3].toUnsignedInt()
val b4 = byteArray[4].toUnsignedInt()
val b5 = byteArray[5].toUnsignedInt()
return listOf(
getEmojiForCode((b0 and 0xFC).shr(2)),
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

import android.content.Context
import im.vector.matrix.android.internal.crypto.CryptoAsyncHelper
import android.os.Handler
import android.os.HandlerThread
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
@ -36,11 +37,14 @@ class MatrixModule(private val context: Context) {
}

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

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


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 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.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
@ -38,12 +40,10 @@ internal class TaskExecutor(private val coroutineDispatchers: MatrixCoroutineDis
task.execute(task.params)
}
}
resultOrFailure.fold({
resultOrFailure.onError {
Timber.d(it, "Task failed")
task.callback.onFailure(it)
}, {
task.callback.onSuccess(it)
})
}
.foldToCallback(task.callback)
}
return CancelableCoroutine(job)
}

View File

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

class ExportKeysDialog {

var passwordVisible = false

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

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

}

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

}

val passPhrase1EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEt)
val passPhrase2EditText = dialogLayout.findViewById<TextInputEditText>(R.id.exportDialogEtConfirm)
val passPhrase2Til = dialogLayout.findViewById<TextInputLayout>(R.id.exportDialogTilConfirm)
val exportButton = dialogLayout.findViewById<Button>(R.id.exportDialogSubmit)
val textWatcher = object : SimpleTextWatcher() {
override fun afterTextChanged(s: Editable) {
when {
TextUtils.isEmpty(passPhrase1EditText.text) -> {
@ -68,6 +64,14 @@ class ExportKeysDialog {
passPhrase1EditText.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()

exportButton.setOnClickListener {

View File

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

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

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

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

bufferedSink.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,
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.ViewModelProviders
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.observeEvent
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() {

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

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

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

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

override fun onNetworkError(e: Exception) {
super.onNetworkError(e)
hideWaitingView()
}
AlertDialog.Builder(this@KeysBackupSetupActivity)
.setMessage(getString(R.string.encryption_export_saved_as, data))
.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) {
super.onMatrixError(e)
hideWaitingView()
}

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


}



View File

@ -25,15 +25,20 @@ import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import arrow.core.Try
import butterknife.BindView
import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.riotredesign.R
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.utils.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File

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

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

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

context?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, file.absolutePath))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
addEntryToDownloadManager(requireContext(), file, "text/plain")

file.absolutePath
}
}
.fold(
{ throwable ->
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(throwable.localizedMessage)
}
},
{ path ->
viewModel.copyHasBeenMade = true

viewModel.copyHasBeenMade = true
} else {
context?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.unknown_error))
.setCancelable(false)
.setPositiveButton(R.string.ok, null)
.show()
}
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.session.Session
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.DevicesListResponse
import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.showPassword
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.VectorPreferenceFragment
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.features.MainActivity
import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.crypto.keys.KeysExporter
import im.vector.riotredesign.features.crypto.keys.KeysImporter
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.themes.ThemeUtils
import org.koin.android.ext.android.inject
@ -885,9 +891,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this)
}

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

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

displayLoadingView()

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

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

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

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
}

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

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

if (sharedDataItems.isNotEmpty() && thisActivity != null) {
val sharedDataItem = sharedDataItems[0]
val dialogLayout = thisActivity.layoutInflater.inflate(R.layout.dialog_import_e2e_keys, null)
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 : 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()")
val uri = when (sharedDataItem) {
is ExternalIntentData.IntentDataUri -> sharedDataItem.uri
is ExternalIntentData.IntentDataClipData -> sharedDataItem.clipDataItem.uri
else -> null
}

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"?>
<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"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
@ -13,21 +13,41 @@
android:paddingBottom="12dp">

<TextView
android:id="@+id/exportDialogText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/encryption_export_notice"
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
android:id="@+id/exportDialogTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
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
android:id="@+id/dialog_e2e_keys_passphrase_edit_text"
android:id="@+id/exportDialogEt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_create_passphrase"
@ -38,16 +58,19 @@


<com.google.android.material.textfield.TextInputLayout
android:id="@+id/dialog_e2e_keys_confirm_passphrase_til"
android:id="@+id/exportDialogTilConfirm"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
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
android:id="@+id/dialog_e2e_keys_confirm_passphrase_edit_text"
android:id="@+id/exportDialogEtConfirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/passphrase_confirm_passphrase"
@ -57,10 +80,14 @@
</com.google.android.material.textfield.TextInputLayout>

<com.google.android.material.button.MaterialButton
android:id="@+id/dialog_e2e_keys_export_button"
android:id="@+id/exportDialogSubmit"
style="@style/VectorButtonStyle"
android:layout_width="wrap_content"
android:layout_gravity="end"
android:enabled="false"
android:text="@string/encryption_export_export" />
</LinearLayout>
android:text="@string/encryption_export_export"
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"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -11,6 +12,18 @@
android:paddingRight="?dialogPreferredPadding"
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
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"

View File

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


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

</resources>