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.model.EventEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.session.cache.ClearCacheTask import im.vector.matrix.android.internal.session.cache.ClearCacheTask
import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.membership.RoomMembers import im.vector.matrix.android.internal.session.room.membership.RoomMembers
@ -560,10 +561,10 @@ internal class CryptoManager(
} else { } else {
val algorithm = getEncryptionAlgorithm(roomId) val algorithm = getEncryptionAlgorithm(roomId)
val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON, val reason = String.format(MXCryptoError.UNABLE_TO_ENCRYPT_REASON,
algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON) algorithm ?: MXCryptoError.NO_MORE_ALGORITHM_REASON)
Timber.e("## encryptEventContent() : $reason") Timber.e("## encryptEventContent() : $reason")
callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE, callback.onFailure(Failure.CryptoError(MXCryptoError(MXCryptoError.UNABLE_TO_ENCRYPT_ERROR_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, reason))) MXCryptoError.UNABLE_TO_ENCRYPT, reason)))
} }
} }
} }
@ -700,7 +701,7 @@ internal class CryptoManager(
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
// Check whether the event content must be encrypted for the invited members. // Check whether the event content must be encrypted for the invited members.
val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser() val encryptForInvitedMembers = isEncryptionEnabledForInvitedUser()
&& shouldEncryptForInvitedMembers(roomId) && shouldEncryptForInvitedMembers(roomId)


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


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


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


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


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

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

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

val encryptedRoomKeys: ByteArray

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

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

callback.onSuccess(encryptedRoomKeys)
} }


/** /**
@ -830,6 +828,7 @@ internal class CryptoManager(
password: String, password: String,
progressListener: ProgressListener?, progressListener: ProgressListener?,
callback: MatrixCallback<ImportRoomKeysResult>) { callback: MatrixCallback<ImportRoomKeysResult>) {
// TODO Use coroutines
Timber.v("## importRoomKeys starts") Timber.v("## importRoomKeys starts")


val t0 = System.currentTimeMillis() val t0 = System.currentTimeMillis()
@ -898,7 +897,7 @@ internal class CryptoManager(
// trigger an an unknown devices exception // trigger an an unknown devices exception
callback.onFailure( callback.onFailure(
Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE, Failure.CryptoError(MXCryptoError(MXCryptoError.UNKNOWN_DEVICES_CODE,
MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices))) MXCryptoError.UNABLE_TO_ENCRYPT, MXCryptoError.UNKNOWN_DEVICES_REASON, unknownDevices)))
} }
} }
) )

View File

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

View File

@ -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.app.Activity
import android.text.Editable import android.text.Editable
import android.text.TextUtils import android.text.TextUtils
import android.text.TextWatcher
import android.widget.Button import android.widget.Button
import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.platform.SimpleTextWatcher


class ExportKeysDialog { class ExportKeysDialog {


var passwordVisible = false

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


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

}

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

}

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


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

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


exportButton.setOnClickListener { exportButton.setOnClickListener {

View File

@ -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.Observer
import androidx.lifecycle.ViewModelProviders import androidx.lifecycle.ViewModelProviders
import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel import im.vector.fragments.keysbackup.setup.KeysBackupSetupSharedViewModel
import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.dialogs.ExportKeysDialog import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.platform.SimpleFragmentActivity import im.vector.riotredesign.core.platform.SimpleFragmentActivity
import im.vector.riotredesign.core.utils.toast
import im.vector.riotredesign.features.crypto.keys.KeysExporter


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


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


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


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


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


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


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

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


} }





View File

@ -56,6 +56,7 @@ import im.vector.riotredesign.core.dialogs.ExportKeysDialog
import im.vector.riotredesign.core.extensions.showPassword import im.vector.riotredesign.core.extensions.showPassword
import im.vector.riotredesign.core.extensions.withArgs import im.vector.riotredesign.core.extensions.withArgs
import im.vector.riotredesign.core.platform.SimpleTextWatcher 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.platform.VectorPreferenceFragment
import im.vector.riotredesign.core.preference.BingRule import im.vector.riotredesign.core.preference.BingRule
import im.vector.riotredesign.core.preference.ProgressBarPreference 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.core.utils.*
import im.vector.riotredesign.features.MainActivity import im.vector.riotredesign.features.MainActivity
import im.vector.riotredesign.features.configuration.VectorConfiguration import im.vector.riotredesign.features.configuration.VectorConfiguration
import im.vector.riotredesign.features.crypto.keys.KeysExporter
import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotredesign.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotredesign.features.themes.ThemeUtils import im.vector.riotredesign.features.themes.ThemeUtils
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
@ -885,9 +887,7 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this) PreferenceManager.getDefaultSharedPreferences(context).unregisterOnSharedPreferenceChangeListener(this)
} }


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


//============================================================================================================== //==============================================================================================================
@ -2594,38 +2593,27 @@ class VectorSettingsPreferencesFragment : VectorPreferenceFragment(), SharedPref
activity?.let { activity -> activity?.let { activity ->
ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener { ExportKeysDialog().show(activity, object : ExportKeysDialog.ExportKeyDialogListener {
override fun onPassphrase(passphrase: String) { override fun onPassphrase(passphrase: String) {
notImplemented()
/*

displayLoadingView() displayLoadingView()


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


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


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


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

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

View File

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


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

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



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


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




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


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


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

</androidx.constraintlayout.widget.ConstraintLayout>