Merge branch 'develop' into feature/misleading_url_target

This commit is contained in:
Onuray Sahin 2020-04-24 11:24:39 +03:00 committed by GitHub
commit f0648ee52a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 666 additions and 78 deletions

View File

@ -24,8 +24,10 @@ Improvements 🙌:
- Cross-Signing | Composer decoration: shields (#1077)
- Cross-Signing | Migrate existing keybackup to cross signing with 4S from mobile (#1197)
- Show a warning dialog if the text of the clicked link does not match the link target (#922)
- Cross-Signing | Consider not using a spinner on the 'complete security' prompt (#1271)
- Restart broken Olm sessions ([MSC1719](https://github.com/matrix-org/matrix-doc/pull/1719))
- Cross-Signing | Hide Use recovery key when 4S is not setup (#1007)
- Cross-Signing | Trust account xSigning keys by entering Recovery Key (select file or copy) #1199
Bugfix 🐛:
- Fix summary notification staying after "mark as read"

View File

@ -26,6 +26,8 @@ import im.vector.riotx.features.attachments.preview.AttachmentsPreviewFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomDirectoryUsersFragment
import im.vector.riotx.features.createdirect.CreateDirectRoomKnownUsersFragment
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupSettingsFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.riotx.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.riotx.features.crypto.recover.BootstrapAccountPasswordFragment
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
@ -456,4 +458,14 @@ interface FragmentModule {
@IntoMap
@FragmentKey(DeactivateAccountFragment::class)
fun bindDeactivateAccountFragment(fragment: DeactivateAccountFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SharedSecuredStoragePassphraseFragment::class)
fun bindSharedSecuredStoragePassphraseFragment(fragment: SharedSecuredStoragePassphraseFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SharedSecuredStorageKeyFragment::class)
fun bindSharedSecuredStorageKeyFragment(fragment: SharedSecuredStorageKeyFragment): Fragment
}

View File

@ -23,8 +23,11 @@ import im.vector.riotx.core.platform.WaitingViewData
sealed class SharedSecureStorageAction : VectorViewModelAction {
object TogglePasswordVisibility : SharedSecureStorageAction()
object UseKey : SharedSecureStorageAction()
object Back : SharedSecureStorageAction()
object Cancel : SharedSecureStorageAction()
data class SubmitPassphrase(val passphrase: String) : SharedSecureStorageAction()
data class SubmitKey(val recoveryKey: String) : SharedSecureStorageAction()
}
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
@ -33,6 +36,7 @@ sealed class SharedSecureStorageViewEvent : VectorViewEvents {
data class FinishSuccess(val cypherResult: String) : SharedSecureStorageViewEvent()
data class Error(val message: String, val dismiss: Boolean = false) : SharedSecureStorageViewEvent()
data class InlineError(val message: String) : SharedSecureStorageViewEvent()
data class KeyInlineError(val message: String) : SharedSecureStorageViewEvent()
object ShowModalLoading : SharedSecureStorageViewEvent()
object HideModalLoading : SharedSecureStorageViewEvent()
data class UpdateLoadingState(val waitingData: WaitingViewData) : SharedSecureStorageViewEvent()

View File

@ -23,17 +23,19 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.addFragment
import im.vector.riotx.core.extensions.commitTransaction
import im.vector.riotx.core.platform.SimpleFragmentActivity
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import javax.inject.Inject
import kotlin.reflect.KClass
class SharedSecureStorageActivity : SimpleFragmentActivity() {
@ -56,9 +58,6 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.visibility = View.GONE
if (isFirstCreation()) {
addFragment(R.id.container, SharedSecuredStoragePassphraseFragment::class.java)
}
viewModel.viewEvents
.observe()
@ -69,10 +68,22 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
.disposeOnDestroy()
viewModel.subscribe(this) {
// renderState(it)
renderState(it)
}
}
override fun onBackPressed() {
viewModel.handle(SharedSecureStorageAction.Back)
}
private fun renderState(state: SharedSecureStorageViewState) {
if (!state.ready) return
val fragment = if (state.hasPassphrase) {
if (state.useKey) SharedSecuredStorageKeyFragment::class else SharedSecuredStoragePassphraseFragment::class
} else SharedSecuredStorageKeyFragment::class
showFragment(fragment, Bundle())
}
private fun observeViewEvents(it: SharedSecureStorageViewEvent?) {
when (it) {
is SharedSecureStorageViewEvent.Dismiss -> {
@ -108,6 +119,18 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
}
}
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
if (supportFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
supportFragmentManager.commitTransaction {
replace(R.id.container,
fragmentClass.java,
bundle,
fragmentClass.simpleName
)
}
}
}
companion object {
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"

View File

@ -17,9 +17,14 @@
package im.vector.riotx.features.crypto.quads
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
@ -31,6 +36,7 @@ import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider
@ -41,7 +47,11 @@ import timber.log.Timber
import java.io.ByteArrayOutputStream
data class SharedSecureStorageViewState(
val passphraseVisible: Boolean = false
val ready: Boolean = false,
val hasPassphrase: Boolean = true,
val useKey: Boolean = false,
val passphraseVisible: Boolean = false,
val checkingSSSSAction: Async<Unit> = Uninitialized
) : MvRxState
class SharedSecureStorageViewModel @AssistedInject constructor(
@ -66,6 +76,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
)
)
}
val keyResult = args.keyId?.let { session.sharedSecretStorageService.getKey(it) }
?: session.sharedSecretStorageService.getDefaultKey()
if (!keyResult.isSuccess()) {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
} else {
val info = (keyResult as KeyInfoResult.Success).keyInfo
if (info.content.passphrase != null) {
setState {
copy(
ready = true,
hasPassphrase = true,
useKey = false
)
}
} else {
setState {
copy(
ready = true,
hasPassphrase = false
)
}
}
}
}
override fun handle(action: SharedSecureStorageAction) = withState {
@ -73,14 +107,98 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
is SharedSecureStorageAction.TogglePasswordVisibility -> handleTogglePasswordVisibility()
is SharedSecureStorageAction.Cancel -> handleCancel()
is SharedSecureStorageAction.SubmitPassphrase -> handleSubmitPassphrase(action)
SharedSecureStorageAction.UseKey -> handleUseKey()
is SharedSecureStorageAction.SubmitKey -> handleSubmitKey(action)
SharedSecureStorageAction.Back -> handleBack()
}.exhaustive
}
private fun handleUseKey() {
setState {
copy(
useKey = true
)
}
}
private fun handleBack() = withState { state ->
if (state.checkingSSSSAction is Loading) return@withState // ignore
if (state.hasPassphrase && state.useKey) {
setState {
copy(
useKey = false
)
}
} else {
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
}
}
private fun handleSubmitKey(action: SharedSecureStorageAction.SubmitKey) {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val decryptedSecretMap = HashMap<String, String>()
setState { copy(checkingSSSSAction = Loading()) }
viewModelScope.launch(Dispatchers.IO) {
runCatching {
val recoveryKey = action.recoveryKey
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
if (!keyInfoResult.isSuccess()) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.Error(stringProvider.getString(R.string.failed_to_access_secure_storage)))
return@launch
}
val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = true
)
))
val keySpec = RawBytesKeySpec.fromRecoveryKey(recoveryKey) ?: return@launch Unit.also {
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.bootstrap_invalid_recovery_key)))
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
}
withContext(Dispatchers.IO) {
args.requestedSecrets.forEach {
if (session.getAccountDataEvent(it) != null) {
val res = awaitCallback<String> { callback ->
session.sharedSecretStorageService.getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec,
callback = callback)
}
decryptedSecretMap[it] = res
} else {
Timber.w("## Cannot find secret $it in SSSS, skip")
}
}
}
}.fold({
setState { copy(checkingSSSSAction = Success(Unit)) }
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
val safeForIntentCypher = ByteArrayOutputStream().also {
it.use {
session.securelyStoreObject(decryptedSecretMap as Map<String, String>, args.resultKeyStoreAlias, it)
}
}.toByteArray().toBase64NoPadding()
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
}, {
setState { copy(checkingSSSSAction = Fail(it)) }
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.KeyInlineError(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)))
})
}
}
private fun handleSubmitPassphrase(action: SharedSecureStorageAction.SubmitPassphrase) {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val decryptedSecretMap = HashMap<String, String>()
setState { copy(checkingSSSSAction = Loading()) }
viewModelScope.launch(Dispatchers.IO) {
runCatching {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val passphrase = action.passphrase
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
if (!keyInfoResult.isSuccess()) {
@ -100,7 +218,6 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
passphrase,
keyInfo.content.passphrase?.salt ?: "",
keyInfo.content.passphrase?.iterations ?: 0,
// TODO
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
@ -132,6 +249,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
}
}
}.fold({
setState { copy(checkingSSSSAction = Success(Unit)) }
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
val safeForIntentCypher = ByteArrayOutputStream().also {
it.use {
@ -140,6 +258,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
}.toByteArray().toBase64NoPadding()
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
}, {
setState { copy(checkingSSSSAction = Fail(it)) }
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.InlineError(stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)))
})

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2020 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.riotx.features.crypto.quads
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.view.clicks
import com.jakewharton.rxbinding3.widget.editorActionEvents
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.startImportTextFromFileIntent
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_ssss_access_from_key.*
import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class SharedSecuredStorageKeyFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_ssss_access_from_key
val sharedViewModel: SharedSecureStorageViewModel by activityViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ssss_restore_with_key_text.text = getString(R.string.enter_secret_storage_input_key)
ssss_key_enter_edittext.editorActionEvents()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
submit()
}
}
.disposeOnDestroyView()
ssss_key_enter_edittext.textChanges()
.skipInitialValue()
.subscribe {
ssss_key_enter_til.error = null
ssss_key_submit.isEnabled = it.isNotBlank()
}
.disposeOnDestroyView()
ssss_key_use_file.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
startImportTextFromFileIntent(this, IMPORT_FILE_REQ)
}
.disposeOnDestroyView()
sharedViewModel.observeViewEvents {
when (it) {
is SharedSecureStorageViewEvent.KeyInlineError -> {
ssss_key_enter_til.error = it.message
}
}
}
ssss_key_submit.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
submit()
}
.disposeOnDestroyView()
}
fun submit() {
val text = ssss_key_enter_edittext.text.toString()
if (text.isBlank()) return // Should not reach this point as button disabled
ssss_key_submit.isEnabled = false
sharedViewModel.handle(SharedSecureStorageAction.SubmitKey(text))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == IMPORT_FILE_REQ && resultCode == Activity.RESULT_OK) {
data?.data?.let { dataURI ->
tryThis {
activity?.contentResolver?.openInputStream(dataURI)
?.bufferedReader()
?.use { it.readText() }
?.let {
ssss_key_enter_edittext.setText(it)
}
}
}
return
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun invalidate() = withState(sharedViewModel) { _ ->
}
companion object {
private const val IMPORT_FILE_REQ = 0
}
}

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.crypto.quads
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.core.text.toSpannable
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.view.clicks
@ -27,12 +28,16 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.colorizeMatchingText
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_ssss_access_from_passphrase.*
import me.gujun.android.span.span
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
class SharedSecuredStoragePassphraseFragment @Inject constructor(
private val colorProvider: ColorProvider
): VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_ssss_access_from_passphrase
@ -41,15 +46,17 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
ssss_restore_with_passphrase_warning_text.text = span {
span(getString(R.string.enter_secret_storage_passphrase_warning)) {
textStyle = "bold"
}
+" "
+getString(R.string.enter_secret_storage_passphrase_warning_text)
}
ssss_restore_with_passphrase_warning_reason.text = getString(R.string.enter_secret_storage_passphrase_reason_verify)
// If has passphrase
val pass = getString(R.string.recovery_passphrase)
val key = getString(R.string.recovery_key)
ssss_restore_with_passphrase_warning_text.text = getString(
R.string.enter_secret_storage_passphrase_or_key,
pass,
key
)
.toSpannable()
.colorizeMatchingText(pass, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
.colorizeMatchingText(key, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
ssss_passphrase_enter_edittext.editorActionEvents()
.debounce(300, TimeUnit.MILLISECONDS)
@ -84,11 +91,11 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
}
.disposeOnDestroyView()
ssss_passphrase_cancel.clicks()
ssss_passphrase_use_key.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
sharedViewModel.handle(SharedSecureStorageAction.Cancel)
sharedViewModel.handle(SharedSecureStorageAction.UseKey)
}
.disposeOnDestroyView()

View File

@ -211,6 +211,12 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
} else {
setState {
copy(
passphrase = null,
passphraseRepeat = null
)
}
startInitializeFlow(action.auth)
}
}
@ -438,7 +444,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}
}
is BootstrapStep.GetBackupSecretKeyForMigration -> {
is BootstrapStep.GetBackupSecretKeyForMigration -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}

View File

@ -324,10 +324,6 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
override fun dismiss() {
super.dismiss()
}
companion object {
const val SECRET_REQUEST_CODE = 101

View File

@ -76,7 +76,7 @@ class VerificationCancelController @Inject constructor(
bottomSheetVerificationActionItem {
id("cancel")
title(stringProvider.getString(R.string.cancel))
title(stringProvider.getString(R.string.skip))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))

View File

@ -0,0 +1,30 @@
/*
* Copyright 2020 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.riotx.features.crypto.verification.epoxy
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel
/**
* A action for bottom sheet.
*/
@EpoxyModelClass(layout = R.layout.item_verification_wait)
abstract class BottomSheetSelfWaitItem : VectorEpoxyModel<BottomSheetSelfWaitItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View File

@ -27,6 +27,7 @@ import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.colorizeMatchingText
import im.vector.riotx.features.crypto.verification.VerificationBottomSheetViewState
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetSelfWaitItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationNoticeItem
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationWaitingItem
@ -56,13 +57,12 @@ class VerificationRequestController @Inject constructor(
notice(stringProvider.getString(R.string.verification_open_other_to_verify))
}
dividerItem {
id("sep")
bottomSheetSelfWaitItem {
id("waiting")
}
bottomSheetVerificationWaitingItem {
id("waiting")
title(stringProvider.getString(R.string.verification_request_waiting, matrixItem.getBestName()))
dividerItem {
id("sep")
}
if (state.quadSContainsSecrets) {
@ -76,6 +76,19 @@ class VerificationRequestController @Inject constructor(
listener { listener?.onClickRecoverFromPassphrase() }
}
}
dividerItem {
id("sep1")
}
bottomSheetVerificationActionItem {
id("skip")
title(stringProvider.getString(R.string.skip))
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener { listener?.onClickSkip() }
}
} else {
val styledText =
if (state.isMe) {
@ -155,5 +168,6 @@ class VerificationRequestController @Inject constructor(
fun onClickOnWasNotMe()
fun onClickRecoverFromPassphrase()
fun onClickDismiss()
fun onClickSkip()
}
}

View File

@ -70,6 +70,10 @@ class VerificationRequestFragment @Inject constructor(
viewModel.handle(VerificationAction.SkipVerification)
}
override fun onClickSkip() {
viewModel.queryCancel()
}
override fun onClickOnWasNotMe() {
viewModel.itWasNotMe()
}

View File

@ -0,0 +1,28 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M2,5C2,3.8954 2.8954,3 4,3H20C21.1046,3 22,3.8954 22,5V15C22,16.1046 21.1046,17 20,17H4C2.8954,17 2,16.1046 2,15V5Z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M8,21H16"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:strokeWidth="1"
android:pathData="M12,17V21"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,17 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:strokeWidth="1"
android:pathData="M5,4C5,2.8954 5.8954,2 7,2H17C18.1046,2 19,2.8954 19,4V20C19,21.1046 18.1046,22 17,22H7C5.8954,22 5,21.1046 5,20V4Z"
android:strokeLineJoin="round"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M12,18m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:fillColor="#2E2F32"/>
</vector>

View File

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/ssss__root"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/ssss_shield"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
android:src="@drawable/ic_message_key" />
<TextView
android:id="@+id/ssss_restore_with_key"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="16dp"
android:text="@string/recovery_key"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/ssss_shield"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ssss_restore_with_key_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_key"
tools:text="@string/enter_secret_storage_input_key" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/ssss_key_enter_til"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
app:errorEnabled="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_key_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssss_key_enter_edittext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/recovery_key"
android:imeOptions="actionDone"
android:maxLines="3"
android:singleLine="false"
android:textColor="?android:textColorPrimary"
tools:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<!-- -->
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_key_use_file"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_file"
app:icon="@drawable/ic_file"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_key_submit"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/_continue"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
app:constraint_referenced_ids="ssss_key_use_file,ssss_key_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_key_enter_til" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -12,14 +12,14 @@
<ImageView
android:id="@+id/ssss_shield"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:src="@drawable/key_big"
android:tint="?riotx_text_primary"
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_passphrase"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase" />
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase"
android:src="@drawable/ic_message_password" />
<TextView
android:id="@+id/ssss_restore_with_passphrase"
@ -28,7 +28,7 @@
android:layout_marginStart="8dp"
android:layout_marginTop="36dp"
android:layout_marginEnd="16dp"
android:text="@string/enter_secret_storage_passphrase"
android:text="@string/recovery_passphrase"
android:textColor="?riotx_text_primary"
android:textSize="20sp"
android:textStyle="bold"
@ -47,20 +47,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase"
tools:text="@string/enter_secret_storage_passphrase_warning_text" />
<TextView
android:id="@+id/ssss_restore_with_passphrase_warning_reason"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_text"
tools:text="@string/enter_secret_storage_passphrase_reason_verify" />
tools:text="@string/enter_secret_storage_passphrase_or_key" />
<com.google.android.material.textfield.TextInputLayout
@ -74,7 +61,7 @@
app:errorEnabled="true"
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_reason">
app:layout_constraintTop_toBottomOf="@id/ssss_restore_with_passphrase_warning_text">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssss_passphrase_enter_edittext"
@ -102,30 +89,37 @@
app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"
app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" />
<!-- -->
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_use_key"
style="@style/Widget.MaterialComponents.Button.TextButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_recovery_key"
app:icon="@drawable/ic_message_key"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_submit"
style="@style/VectorButtonStylePositive"
android:layout_marginTop="@dimen/layout_vertical_margin_big"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:minWidth="200dp"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/_continue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ssss_passphrase_cancel"
style="@style/VectorButtonStyleDestructive"
android:layout_marginTop="8dp"
<androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="@dimen/layout_vertical_margin_big"
android:minWidth="200dp"
android:text="@string/cancel"
app:constraint_referenced_ids="ssss_passphrase_use_key,ssss_passphrase_submit"
app:flow_horizontalStyle="spread_inside"
app:flow_wrapMode="chain"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_submit" />
app:layout_constraintTop_toBottomOf="@+id/ssss_passphrase_enter_til" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<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/itemVerificationNoticeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:contentDescription="@string/use_other_session_content_description"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/use_latest_riot"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/use_latest_riot"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/monitorIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_monitor"
android:tint="?vctr_notice_secondary"
app:layout_constraintEnd_toStartOf="@id/smartphoneIcon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/use_latest_riot" />
<ImageView
android:id="@+id/smartphoneIcon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_smartphone"
android:tint="?vctr_notice_secondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/monitorIcon"
app:layout_constraintTop_toBottomOf="@+id/use_latest_riot" />
<TextView
android:id="@+id/riot_desktop_web"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/riot_desktop_web"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="@id/monitorIcon"
app:layout_constraintStart_toStartOf="@id/monitorIcon"
app:layout_constraintTop_toBottomOf="@id/monitorIcon" />
<TextView
android:id="@+id/riot_ios_android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/riot_ios_android"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="@id/smartphoneIcon"
app:layout_constraintStart_toStartOf="@id/smartphoneIcon"
app:layout_constraintTop_toBottomOf="@id/smartphoneIcon" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/labelsBarrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="riot_ios_android,riot_desktop_web" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center"
android:text="@string/or_other_mx_capabale_client"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/labelsBarrier" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2251,7 +2251,6 @@ Verwahre deinen Wiederherstellungsschlüssel an einem sehr sicheren Ort wie eine
<string name="enter_secret_storage_passphrase">Gib die geheime Speicherpassphrase ein</string>
<string name="enter_secret_storage_passphrase_warning">Warnung:</string>
<string name="enter_secret_storage_passphrase_warning_text">Du solltest nur von einem vertrauenswürdigen Gerät auf den geheimen Speicher zugreifen</string>
<string name="enter_secret_storage_passphrase_reason_verify">Greife auf deinen sicheren Nachrichtenverlauf und deine Cross-Signing-Identität zu, um andere Sitzungen zu überprüfen, indem du deine Passphrase eingibst</string>
<string name="message_action_item_redact">Entfernen…</string>
<string name="share_confirm_room">Möchtest du diesen Anhang an %1$s senden\?</string>

View File

@ -2192,7 +2192,6 @@ Abisua: Fitxategi hau ezabatu daiteke aplikazioa desinstalatzen bada.</string>
<string name="enter_secret_storage_passphrase">Sartu biltegi sekretuko pasa-esaldia</string>
<string name="enter_secret_storage_passphrase_warning">Abisua:</string>
<string name="enter_secret_storage_passphrase_warning_text">Biltegi sekretura gailu fidagarri batetik konektatu beharko zinateke beti</string>
<string name="enter_secret_storage_passphrase_reason_verify">Atzitu zure mezu seguruen historiala eta zeharkako sinatzerako identitatea beste saioak egiaztatzeko zure pasa-esaldia sartuz</string>
<string name="message_action_item_redact">Kendu…</string>
<string name="share_confirm_room">Eranskin hau %1$s gelara bidali nahi duzu\?</string>

View File

@ -2201,7 +2201,6 @@ Si vous navez pas configuré de nouvelle méthode de récupération, un attaq
<string name="enter_secret_storage_passphrase">Saisir la phrase secrète du coffre secret</string>
<string name="enter_secret_storage_passphrase_warning">Attention :</string>
<string name="enter_secret_storage_passphrase_warning_text">Vous devriez accéder à votre coffre secret uniquement depuis un appareil de confiance</string>
<string name="enter_secret_storage_passphrase_reason_verify">Accédez à lhistorique de vos messages sécurisés et à votre identité de signature croisée pour vérifier dautres sessions en saisissant votre phrase secrète</string>
<string name="message_action_item_redact">Supprimer…</string>
<string name="share_confirm_room">Voulez-vous envoyer cette pièce jointe à %1$s \?</string>

View File

@ -2196,7 +2196,6 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
<string name="enter_secret_storage_passphrase">Add meg a jelmondatot a biztonsági tárolóhoz</string>
<string name="enter_secret_storage_passphrase_warning">Figyelem:</string>
<string name="enter_secret_storage_passphrase_warning_text">Csak biztonságos eszközről férj hozzá a biztonsági tárolóhoz</string>
<string name="enter_secret_storage_passphrase_reason_verify">A jelmondat megadásával hozzáférhetsz a biztonságos üzeneteidhez és az eszközök közötti hitelesítéshez használt személyazonosságodhoz, hogy más munkameneteket hitelesíthess</string>
<string name="message_action_item_redact">Töröl…</string>
<string name="share_confirm_room">Ezt a csatolmányt el szeretnéd küldeni ide: %1$s\?</string>

View File

@ -2246,7 +2246,6 @@
<string name="enter_secret_storage_passphrase">Inserisci la password dell\'archivio segreto</string>
<string name="enter_secret_storage_passphrase_warning">Attenzione:</string>
<string name="enter_secret_storage_passphrase_warning_text">Dovresti accedere all\'archivio segreto solo da un dispositivo fidato</string>
<string name="enter_secret_storage_passphrase_reason_verify">Accedi alla cronologia dei messaggi sicuri e all\'identità di firma incrociata per verificare altre sessioni inserendo la tua password</string>
<string name="message_action_item_redact">Rimuovi…</string>
<string name="share_confirm_room">Vuoi inviare questo allegato a %1$s\?</string>

View File

@ -2172,7 +2172,6 @@ Spróbuj uruchomić ponownie aplikację.</string>
<string name="enter_secret_storage_passphrase">Wprowadź hasło tajemnej przestrzeni</string>
<string name="enter_secret_storage_passphrase_warning">Ostrzeżenie:</string>
<string name="enter_secret_storage_passphrase_warning_text">Powinieneś(-nnaś) uzyskać dostęp do tajnej przestrzeni jedynie z zaufanego urządzenia</string>
<string name="enter_secret_storage_passphrase_reason_verify">Uzyskaj dostęp do historii bezpiecznych wiadomości i Twojego(-jej) tożsamości podpisu krzyżowego poprzez weryfikację innej sesji za pomocą hasła</string>
<string name="message_action_item_redact">Usuń…</string>
<string name="share_confirm_room">Czy chcesz wysłać załącznik do %1$s\?</string>

View File

@ -2156,7 +2156,6 @@ Që të garantoni se sju shpëton gjë, thjesht mbajeni të aktivizuar mekani
<string name="enter_secret_storage_passphrase">Jepni frazëkalimin e fshehtë për në depozitim</string>
<string name="enter_secret_storage_passphrase_warning">Kujdes:</string>
<string name="enter_secret_storage_passphrase_warning_text">Duhet të hyni në depozitim të fshehtë vetëm nga një pajisje e besuar</string>
<string name="enter_secret_storage_passphrase_reason_verify">Për verifikim sesionesh të tjerë përmes dhënies së frazëkalimit tuaj, hyni te historiku i mesazheve tuaj të sigurt dhe identiteti juaj për <em>cross-signing</em></string>
<string name="message_action_item_redact">Hiqni…</string>
<string name="share_confirm_room">Doni të dërgohet kjo bashkëngjitje te %1$s\?</string>

View File

@ -2144,7 +2144,6 @@ Matrix 中的消息可見度類似于電子郵件。我們忘記您的郵件意
<string name="enter_secret_storage_passphrase">輸入秘密儲存空間通關密語</string>
<string name="enter_secret_storage_passphrase_warning">警告:</string>
<string name="enter_secret_storage_passphrase_warning_text">您僅能從受信任的裝置存取秘密儲存空間</string>
<string name="enter_secret_storage_passphrase_reason_verify">透過輸入通關密語來存取您的安全訊息歷史與您的交叉簽章身份以驗證其他工作階段</string>
<string name="message_action_item_redact">移除……</string>
<string name="share_confirm_room">您想要傳送此附件到 %1$s 嗎?</string>

View File

@ -2166,7 +2166,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
</plurals>
<string name="poll_item_selected_aria">Selected Option</string>
<string name="command_description_poll">Creates a simple poll</string>
<string name="verification_cannot_access_other_session">Use a recovery method</string>
<string name="verification_cannot_access_other_session">Use a Recovery Passphrase or Key</string>
<string name="verification_use_passphrase">If you cant access an existing session</string>
<string name="new_signin">New Sign In</string>
@ -2175,7 +2175,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="enter_secret_storage_passphrase">Enter secret storage passphrase</string>
<string name="enter_secret_storage_passphrase_warning">Warning:</string>
<string name="enter_secret_storage_passphrase_warning_text">You should only access secret storage from a trusted device</string>
<string name="enter_secret_storage_passphrase_reason_verify">Access your secure message history and your cross-signing identity for verifying other sessions by entering your passphrase</string>
<string name="message_action_item_redact">Remove…</string>
<string name="share_confirm_room">Do you want to send this attachment to %1$s?</string>

View File

@ -6,8 +6,19 @@
<!-- Sections has been created to limit merge conflicts. -->
<!-- BEGIN Strings added by Valere -->
<string name="use_other_session_content_description">Use the latest Riot on your other devices, Riot Web, Riot Desktop, Riot iOS, RiotX for Android, or another cross-signing capable Matrix client</string>
<string name="riot_desktop_web">Riot Web\nRiot Desktop</string>
<string name="riot_ios_android">Riot iOS\nRiot X for Android</string>
<string name="or_other_mx_capabale_client">or another cross-signing capable Matrix client</string>
<string name="use_latest_riot">Use the latest Riot on your other devices:</string>
<string name="command_description_discard_session">Forces the current outbound group session in an encrypted room to be discarded</string>
<string name="command_description_discard_session_not_handled">Only supported in encrypted rooms</string>
<!-- first will be replaced by recovery_passphrase, second will be replaced by recovery_key-->
<string name="enter_secret_storage_passphrase_or_key">Use your %1$s or use your %2$s to continue.</string>
<string name="use_recovery_key">Use Recovery Key</string>
<string name="enter_secret_storage_input_key">Select your Recovery Key, or input it manually by typing it or pasting from your clipboard</string>
<string name="keys_backup_recovery_key_error_decrypt">Backup could not be decrypted with this Recovery Key: please verify that you entered the correct Recovery Key.</string>
<string name="failed_to_access_secure_storage">Failed to access secure storage</string>
<!-- END Strings added by Valere -->