Crypto: export room keys

This commit is contained in:
Benoit Marty 2019-06-13 19:08:51 +02:00
parent 8c8a4dcbd1
commit 5f0d1d9536
9 changed files with 240 additions and 150 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

@ -64,6 +64,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
@ -560,10 +561,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)))
}
}
}
@ -700,7 +701,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 +788,32 @@ 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,6 +828,7 @@ internal class CryptoManager(
password: String,
progressListener: ProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) {
// TODO Use coroutines
Timber.v("## importRoomKeys starts")

val t0 = System.currentTimeMillis()
@ -898,7 +897,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)))
}
}
)

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

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

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

@ -0,0 +1,77 @@
/*
* 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 androidx.annotation.WorkerThread
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okio.Okio
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) {
copyToFile(context, data)
}
.foldToCallback(callback)
}
}

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

@WorkerThread
private fun copyToFile(context: Context, data: ByteArray): Try<String> {
return Try {
val parentDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "riotx-keys-" + System.currentTimeMillis() + ".txt")

val sink = Okio.sink(file)

val bufferedSink = Okio.buffer(sink)

bufferedSink.write(data)

bufferedSink.close()
sink.close()

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

file.absolutePath
}
}
}

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

@ -56,6 +56,7 @@ 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.platform.SimpleTextWatcher
import im.vector.riotredesign.core.platform.VectorBaseActivity
import im.vector.riotredesign.core.platform.VectorPreferenceFragment
import im.vector.riotredesign.core.preference.BingRule
import im.vector.riotredesign.core.preference.ProgressBarPreference
@ -64,6 +65,7 @@ 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.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.themes.ThemeUtils
import org.koin.android.ext.android.inject
@ -885,9 +887,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 +895,6 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
exportKeys()
}
}
*/
}

//==============================================================================================================
@ -2594,38 +2593,27 @@ 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) {
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()
}
})
*/
})
}
})
}

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>