mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-12-25 09:10:53 +01:00
Merge pull request #1198 from vector-im/feature/crosssigning_bootstrap
Feature/crosssigning bootstrap
This commit is contained in:
commit
0164f94047
@ -4,16 +4,18 @@ Changes in RiotX 0.19.0 (2020-XX-XX)
|
||||
Features ✨:
|
||||
- Cross-Signing | Support SSSS secret sharing (#944)
|
||||
- Cross-Signing | Verify new session from existing session (#1134)
|
||||
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||
|
||||
Improvements 🙌:
|
||||
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||
- CrossSigning / Update Shield Logic for DM (#963)
|
||||
- Xsigning | Complete security new session design update (#1135)
|
||||
- Cross-Signing | Update Shield Logic for DM (#963)
|
||||
- Cross-Signing | Complete security new session design update (#1135)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Missing avatar/displayname after verification request message (#841)
|
||||
- Crypto | RiotX sometimes rotate the current device keys (#1170)
|
||||
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
|
||||
- Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191)
|
||||
- Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925)
|
||||
|
||||
Translations 🗣:
|
||||
|
@ -22,6 +22,7 @@ import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.DeviceTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.UserTrustResult
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
|
||||
interface CrossSigningService {
|
||||
|
||||
@ -52,6 +53,8 @@ interface CrossSigningService {
|
||||
|
||||
fun getMyCrossSigningKeys(): MXCrossSigningInfo?
|
||||
|
||||
fun getCrossSigningPrivateKeys(): PrivateKeysInfo?
|
||||
|
||||
fun canCrossSign(): Boolean
|
||||
|
||||
fun trustUser(otherUserId: String,
|
||||
|
@ -19,3 +19,7 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
interface KeySigner {
|
||||
fun sign(canonicalJson: String): Map<String, Map<String, String>>?
|
||||
}
|
||||
|
||||
class EmptyKeySigner : KeySigner {
|
||||
override fun sign(canonicalJson: String): Map<String, Map<String, String>>? = null
|
||||
}
|
||||
|
@ -19,5 +19,6 @@ package im.vector.matrix.android.api.session.securestorage
|
||||
data class SsssKeyCreationInfo(
|
||||
val keyId: String = "",
|
||||
var content: SecretStorageKeyContent?,
|
||||
val recoveryKey: String = ""
|
||||
val recoveryKey: String = "",
|
||||
val keySpec: SsssKeySpec
|
||||
)
|
||||
|
@ -641,7 +641,7 @@ internal class MXOlmDevice @Inject constructor(
|
||||
throw MXCryptoError.OlmError(e)
|
||||
}
|
||||
|
||||
if (null != timeline) {
|
||||
if (timeline?.isNotBlank() == true) {
|
||||
val timelineSet = inboundGroupSessionMessageIndexes.getOrPut(timeline) { mutableSetOf() }
|
||||
|
||||
val messageIndexKey = senderKey + "|" + sessionId + "|" + decryptResult.mIndex
|
||||
|
@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.crypto.model.KeyUsage
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UploadSignatureQueryBuilder
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
|
||||
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSignaturesTask
|
||||
import im.vector.matrix.android.internal.crypto.tasks.UploadSigningKeysTask
|
||||
import im.vector.matrix.android.internal.di.UserId
|
||||
@ -595,6 +596,10 @@ internal class DefaultCrossSigningService @Inject constructor(
|
||||
return cryptoStore.getMyCrossSigningInfo()
|
||||
}
|
||||
|
||||
override fun getCrossSigningPrivateKeys(): PrivateKeysInfo? {
|
||||
return cryptoStore.getCrossSigningPrivateKeys()
|
||||
}
|
||||
|
||||
override fun canCrossSign(): Boolean {
|
||||
return checkSelfTrust().isVerified() && cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
|
||||
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
|
||||
|
@ -28,7 +28,7 @@ import com.squareup.moshi.JsonClass
|
||||
* The user_signing_keys property will only be included when a user requests their own keys.
|
||||
*/
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class KeysQueryResponse(
|
||||
internal data class KeysQueryResponse(
|
||||
/**
|
||||
* The device keys per devices per users.
|
||||
* Map from userId to map from deviceId to MXDeviceInfo
|
||||
|
@ -102,7 +102,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
callback.onSuccess(SsssKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(key)
|
||||
recoveryKey = computeRecoveryKey(key),
|
||||
keySpec = RawBytesKeySpec(key)
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -142,7 +143,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
callback.onSuccess(SsssKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(privatePart.privateKey)
|
||||
recoveryKey = computeRecoveryKey(privatePart.privateKey),
|
||||
keySpec = RawBytesKeySpec(privatePart.privateKey)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ import im.vector.matrix.android.internal.di.UserId
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import timber.log.Timber
|
||||
import java.util.ArrayList
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface RoomVerificationUpdateTask : Task<RoomVerificationUpdateTask.Params, Unit> {
|
||||
@ -75,7 +74,7 @@ internal class DefaultRoomVerificationUpdateTask @Inject constructor(
|
||||
// TODO use a global event decryptor? attache to session and that listen to new sessionId?
|
||||
// for now decrypt sync
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId + UUID.randomUUID().toString())
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
|
@ -230,7 +230,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
|
||||
private fun decryptIfNeeded(event: Event) {
|
||||
if (event.mxDecryptionResult == null) {
|
||||
try {
|
||||
val result = cryptoService.decryptEvent(event, event.roomId ?: "")
|
||||
val result = cryptoService.decryptEvent(event, "")
|
||||
event.mxDecryptionResult = OlmDecryptionResult(
|
||||
payload = result.clearEvent,
|
||||
senderKey = result.senderCurve25519Key,
|
||||
|
@ -368,7 +368,7 @@ dependencies {
|
||||
|
||||
implementation "androidx.emoji:emoji-appcompat:1.0.0"
|
||||
|
||||
implementation 'com.github.BillCarsonFr:JsonViewer:0.4'
|
||||
implementation 'com.github.BillCarsonFr:JsonViewer:0.5'
|
||||
|
||||
// QR-code
|
||||
// Stick to 3.3.3 because of https://github.com/zxing/zxing/issues/1170
|
||||
|
@ -26,6 +26,12 @@ 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.recover.BootstrapAccountPasswordFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapConclusionFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
|
||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
|
||||
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
|
||||
import im.vector.riotx.features.crypto.verification.choose.VerificationChooseMethodFragment
|
||||
@ -78,8 +84,8 @@ import im.vector.riotx.features.settings.devices.VectorSettingsDevicesFragment
|
||||
import im.vector.riotx.features.settings.devtools.AccountDataFragment
|
||||
import im.vector.riotx.features.settings.devtools.GossipingEventsPaperTrailFragment
|
||||
import im.vector.riotx.features.settings.devtools.IncomingKeyRequestListFragment
|
||||
import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment
|
||||
import im.vector.riotx.features.settings.devtools.KeyRequestsFragment
|
||||
import im.vector.riotx.features.settings.devtools.OutgoingKeyRequestListFragment
|
||||
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
|
||||
import im.vector.riotx.features.settings.push.PushGatewaysFragment
|
||||
import im.vector.riotx.features.share.IncomingShareFragment
|
||||
@ -402,4 +408,34 @@ interface FragmentModule {
|
||||
@IntoMap
|
||||
@FragmentKey(GossipingEventsPaperTrailFragment::class)
|
||||
fun bindGossipingEventsPaperTrailFragment(fragment: GossipingEventsPaperTrailFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapEnterPassphraseFragment::class)
|
||||
fun bindBootstrapEnterPassphraseFragment(fragment: BootstrapEnterPassphraseFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapConfirmPassphraseFragment::class)
|
||||
fun bindBootstrapConfirmPassphraseFragment(fragment: BootstrapConfirmPassphraseFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapWaitingFragment::class)
|
||||
fun bindBootstrapWaitingFragment(fragment: BootstrapWaitingFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapSaveRecoveryKeyFragment::class)
|
||||
fun bindBootstrapSaveRecoveryKeyFragment(fragment: BootstrapSaveRecoveryKeyFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapConclusionFragment::class)
|
||||
fun bindBootstrapConclusionFragment(fragment: BootstrapConclusionFragment): Fragment
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@FragmentKey(BootstrapAccountPasswordFragment::class)
|
||||
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.createdirect.CreateDirectRoomActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
import im.vector.riotx.features.crypto.quads.SharedSecureStorageActivity
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.riotx.features.crypto.verification.VerificationBottomSheet
|
||||
import im.vector.riotx.features.debug.DebugMenuActivity
|
||||
import im.vector.riotx.features.home.HomeActivity
|
||||
@ -128,6 +129,7 @@ interface ScreenComponent {
|
||||
fun inject(bottomSheet: VerificationBottomSheet)
|
||||
fun inject(bottomSheet: DeviceVerificationInfoBottomSheet)
|
||||
fun inject(bottomSheet: DeviceListBottomSheet)
|
||||
fun inject(bottomSheet: BootstrapBottomSheet)
|
||||
|
||||
/* ==========================================================================================
|
||||
* Others
|
||||
|
@ -39,6 +39,7 @@ import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.HomeRoomListDataSource
|
||||
import im.vector.riotx.features.html.EventHtmlRenderer
|
||||
import im.vector.riotx.features.html.VectorHtmlCompressor
|
||||
import im.vector.riotx.features.login.ReAuthHelper
|
||||
import im.vector.riotx.features.navigation.Navigator
|
||||
import im.vector.riotx.features.notifications.NotifiableEventResolver
|
||||
import im.vector.riotx.features.notifications.NotificationBroadcastReceiver
|
||||
@ -131,6 +132,8 @@ interface VectorComponent {
|
||||
|
||||
fun alertManager() : PopupAlertManager
|
||||
|
||||
fun reAuthHelper() : ReAuthHelper
|
||||
|
||||
@Component.Factory
|
||||
interface Factory {
|
||||
fun create(@BindsInstance context: Context): VectorComponent
|
||||
|
@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.core.ui.views
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.isVisible
|
||||
import butterknife.BindView
|
||||
import butterknife.ButterKnife
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.setTextOrHide
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
|
||||
class BottomSheetActionButton @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
@BindView(R.id.itemVerificationActionTitle)
|
||||
lateinit var actionTextView: TextView
|
||||
|
||||
@BindView(R.id.itemVerificationActionSubTitle)
|
||||
lateinit var descriptionTextView: TextView
|
||||
|
||||
@BindView(R.id.itemVerificationLeftIcon)
|
||||
lateinit var leftIconImageView: ImageView
|
||||
|
||||
@BindView(R.id.itemVerificationActionIcon)
|
||||
lateinit var rightIconImageView: ImageView
|
||||
|
||||
@BindView(R.id.itemVerificationClickableZone)
|
||||
lateinit var clickableView: View
|
||||
|
||||
var title: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
actionTextView.setTextOrHide(value)
|
||||
}
|
||||
|
||||
var subTitle: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
descriptionTextView.setTextOrHide(value)
|
||||
}
|
||||
|
||||
var forceStartPadding: Boolean? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (leftIcon == null) {
|
||||
if (forceStartPadding == true) {
|
||||
leftIconImageView.isInvisible = true
|
||||
} else {
|
||||
leftIconImageView.isGone = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var leftIcon: Drawable? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (value == null) {
|
||||
if (forceStartPadding == true) {
|
||||
leftIconImageView.isInvisible = true
|
||||
} else {
|
||||
leftIconImageView.isGone = true
|
||||
}
|
||||
leftIconImageView.setImageDrawable(null)
|
||||
} else {
|
||||
leftIconImageView.isVisible = true
|
||||
leftIconImageView.setImageDrawable(value)
|
||||
}
|
||||
}
|
||||
|
||||
var rightIcon: Drawable? = null
|
||||
set(value) {
|
||||
field = value
|
||||
rightIconImageView.setImageDrawable(value)
|
||||
}
|
||||
|
||||
var tint: Int? = null
|
||||
set(value) {
|
||||
field = value
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
leftIconImageView.imageTintList = value?.let { ColorStateList.valueOf(value) }
|
||||
} else {
|
||||
leftIcon?.let {
|
||||
leftIcon = ThemeUtils.tintDrawable(context, it, value ?: ThemeUtils.getColor(context, android.R.attr.textColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.item_verification_action, this)
|
||||
ButterKnife.bind(this)
|
||||
|
||||
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BottomSheetActionButton, 0, 0)
|
||||
title = typedArray.getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""
|
||||
subTitle = typedArray.getString(R.styleable.BottomSheetActionButton_actionDescription) ?: ""
|
||||
forceStartPadding = typedArray.getBoolean(R.styleable.BottomSheetActionButton_forceStartPadding, false)
|
||||
leftIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_leftIcon)
|
||||
|
||||
rightIcon = typedArray.getDrawable(R.styleable.BottomSheetActionButton_rightIcon)
|
||||
|
||||
tint = typedArray.getColor(R.styleable.BottomSheetActionButton_tint, ThemeUtils.getColor(context, android.R.attr.textColor))
|
||||
|
||||
typedArray.recycle()
|
||||
}
|
||||
}
|
@ -153,7 +153,7 @@ fun startAddGoogleAccountIntent(context: AppCompatActivity, requestCode: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null) {
|
||||
fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: String, subject: String? = null, requestCode: Int? = null) {
|
||||
val share = Intent(Intent.ACTION_SEND)
|
||||
share.type = "text/plain"
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
@ -165,7 +165,11 @@ fun startSharePlainTextIntent(fragment: Fragment, chooserTitle: String?, text: S
|
||||
share.putExtra(Intent.EXTRA_SUBJECT, subject)
|
||||
share.putExtra(Intent.EXTRA_TEXT, text)
|
||||
try {
|
||||
fragment.startActivity(Intent.createChooser(share, chooserTitle))
|
||||
if (requestCode != null) {
|
||||
fragment.startActivityForResult(Intent.createChooser(share, chooserTitle), requestCode)
|
||||
} else {
|
||||
fragment.startActivity(Intent.createChooser(share, chooserTitle))
|
||||
}
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
fragment.activity?.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
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.riotx.R
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
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_bootstrap_enter_account_password.*
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.bootstrapDescriptionText
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.ssss_view_show_password
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapAccountPasswordFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_account_password
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val recPassPhrase = getString(R.string.account_password)
|
||||
bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
bootstrapAccountPasswordEditText.hint = getString(R.string.account_password)
|
||||
|
||||
bootstrapAccountPasswordEditText.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
bootstrapAccountPasswordEditText.textChanges()
|
||||
.distinct()
|
||||
.subscribe {
|
||||
if (!it.isNullOrBlank()) {
|
||||
bootstrapAccountPasswordTil.error = null
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_view_show_password.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
bootstrapPasswordButton.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
submit()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
withState(sharedViewModel) { state ->
|
||||
(state.step as? BootstrapStep.AccountPassword)?.failure?.let {
|
||||
bootstrapAccountPasswordTil.error = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.AccountPassword) {
|
||||
return@withState
|
||||
}
|
||||
val accountPassword = bootstrapAccountPasswordEditText.text?.toString()
|
||||
if (accountPassword.isNullOrBlank()) {
|
||||
bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
|
||||
} else {
|
||||
view?.hideKeyboard()
|
||||
sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.step is BootstrapStep.AccountPassword) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false)
|
||||
ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.core.platform.VectorViewModelAction
|
||||
import java.io.OutputStream
|
||||
|
||||
sealed class BootstrapActions : VectorViewModelAction {
|
||||
|
||||
// Navigation
|
||||
|
||||
object GoBack : BootstrapActions()
|
||||
data class GoToConfirmPassphrase(val passphrase: String) : BootstrapActions()
|
||||
object GoToCompleted : BootstrapActions()
|
||||
object GoToEnterAccountPassword : BootstrapActions()
|
||||
|
||||
data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions()
|
||||
data class DoInitializeGeneratedKey(val auth: UserPasswordAuth? = null) : BootstrapActions()
|
||||
object TogglePasswordVisibility : BootstrapActions()
|
||||
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
|
||||
data class ReAuth(val pass: String) : BootstrapActions()
|
||||
object RecoveryKeySaved : BootstrapActions()
|
||||
object Completed : BootstrapActions()
|
||||
object SaveReqQueryStarted : BootstrapActions()
|
||||
data class SaveKeyToUri(val os: OutputStream) : BootstrapActions()
|
||||
object SaveReqFailed : BootstrapActions()
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.WindowManager
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.airbnb.mvrx.fragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.extensions.commitTransaction
|
||||
import im.vector.riotx.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import kotlinx.android.synthetic.main.bottom_sheet_bootstrap.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
|
||||
override val showExpanded = true
|
||||
|
||||
@Inject
|
||||
lateinit var bootstrapViewModelFactory: BootstrapSharedViewModel.Factory
|
||||
|
||||
private val viewModel by fragmentViewModel(BootstrapSharedViewModel::class)
|
||||
|
||||
override fun injectWith(injector: ScreenComponent) {
|
||||
injector.inject(this)
|
||||
}
|
||||
|
||||
override fun getLayoutResId() = R.layout.bottom_sheet_bootstrap
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
viewModel.observeViewEvents { event ->
|
||||
when (event) {
|
||||
is BootstrapViewEvents.Dismiss -> dismiss()
|
||||
is BootstrapViewEvents.ModalError -> {
|
||||
AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.dialog_title_error)
|
||||
.setMessage(event.error)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
BootstrapViewEvents.RecoveryKeySaved -> {
|
||||
KeepItSafeDialog().show(requireActivity())
|
||||
}
|
||||
is BootstrapViewEvents.SkipBootstrap -> {
|
||||
promptSkip(event.genKeyOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun promptSkip(genKeyOption: Boolean) {
|
||||
AlertDialog.Builder(requireContext())
|
||||
.setTitle(R.string.are_you_sure)
|
||||
.setMessage(if (genKeyOption) R.string.bootstrap_skip_text else R.string.bootstrap_skip_text_no_gen_key)
|
||||
.setPositiveButton(R.string._continue, null)
|
||||
.apply {
|
||||
if (genKeyOption) {
|
||||
setNeutralButton(R.string.generate_message_key) { _, _ ->
|
||||
viewModel.handle(BootstrapActions.DoInitializeGeneratedKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.skip) { _, _ ->
|
||||
dismiss()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
val rootView = super.onCreateView(inflater, container, savedInstanceState)
|
||||
dialog?.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||
return rootView
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
return super.onCreateDialog(savedInstanceState).apply {
|
||||
setOnKeyListener { _, keyCode, keyEvent ->
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) {
|
||||
viewModel.handle(BootstrapActions.GoBack)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { state ->
|
||||
|
||||
when (state.step) {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
|
||||
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
|
||||
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.AccountPassword -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
|
||||
bootstrapTitleText.text = getString(R.string.account_password)
|
||||
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.Initializing -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
|
||||
showFragment(BootstrapWaitingFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.SaveRecoveryKey -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
|
||||
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
|
||||
}
|
||||
is BootstrapStep.DoneSuccess -> {
|
||||
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
|
||||
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
|
||||
showFragment(BootstrapConclusionFragment::class, Bundle())
|
||||
}
|
||||
}
|
||||
super.invalidate()
|
||||
}
|
||||
|
||||
private fun showFragment(fragmentClass: KClass<out Fragment>, bundle: Bundle) {
|
||||
if (childFragmentManager.findFragmentByTag(fragmentClass.simpleName) == null) {
|
||||
childFragmentManager.commitTransaction {
|
||||
replace(R.id.bottomSheetFragmentContainer,
|
||||
fragmentClass.java,
|
||||
bundle,
|
||||
fragmentClass.simpleName
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
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.colorizeMatchingText
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_conclusion.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapConclusionFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_conclusion
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
bootstrapConclusionContinue.clickableView.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.Completed)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.DoneSuccess) return@withState
|
||||
|
||||
bootstrapConclusionText.text = getString(
|
||||
R.string.bootstrap_cross_signing_success,
|
||||
getString(R.string.recovery_passphrase),
|
||||
getString(R.string.message_key)
|
||||
)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isGone
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
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.riotx.R
|
||||
import im.vector.riotx.core.extensions.hideKeyboard
|
||||
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_bootstrap_enter_passphrase.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapConfirmPassphraseFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
ssss_passphrase_security_progress.isGone = true
|
||||
|
||||
val recPassPhrase = getString(R.string.recovery_passphrase)
|
||||
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_confirm_text, recPassPhrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_confirm_passphrase)
|
||||
|
||||
withState(sharedViewModel) {
|
||||
// set initial value (useful when coming back)
|
||||
ssss_passphrase_enter_edittext.setText(it.passphraseRepeat ?: "")
|
||||
ssss_passphrase_enter_edittext.requestFocus()
|
||||
}
|
||||
|
||||
ssss_passphrase_enter_edittext.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_passphrase_enter_edittext.textChanges()
|
||||
.subscribe {
|
||||
ssss_passphrase_enter_til.error = null
|
||||
sharedViewModel.handle(BootstrapActions.UpdateConfirmCandidatePassphrase(it?.toString() ?: ""))
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
// when (it) {
|
||||
// is SharedSecureStorageViewEvent.InlineError -> {
|
||||
// ssss_passphrase_enter_til.error = it.message
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
ssss_view_show_password.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.ConfirmPassphrase) {
|
||||
return@withState
|
||||
}
|
||||
val passphrase = ssss_passphrase_enter_edittext.text?.toString()
|
||||
if (passphrase.isNullOrBlank()) {
|
||||
ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message)
|
||||
} else if (passphrase != state.passphrase) {
|
||||
ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_does_not_match)
|
||||
} else {
|
||||
view?.hideKeyboard()
|
||||
sharedViewModel.handle(BootstrapActions.DoInitialize(passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.step is BootstrapStep.ConfirmPassphrase) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)
|
||||
ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,222 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||
import im.vector.matrix.android.api.session.securestorage.EmptyKeySigner
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.util.awaitCallback
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.WaitingViewData
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
sealed class BootstrapResult {
|
||||
|
||||
data class Success(val keyInfo: SsssKeyCreationInfo) : BootstrapResult()
|
||||
|
||||
abstract class Failure(val error: String?) : BootstrapResult()
|
||||
|
||||
class UnsupportedAuthFlow : Failure(null)
|
||||
|
||||
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
|
||||
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
|
||||
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
|
||||
object MissingPrivateKey : Failure(null)
|
||||
|
||||
data class PasswordAuthFlowMissing(val sessionId: String, val userId: String) : Failure(null)
|
||||
}
|
||||
|
||||
interface BootstrapProgressListener {
|
||||
fun onProgress(data: WaitingViewData)
|
||||
}
|
||||
|
||||
data class Params(
|
||||
val userPasswordAuth: UserPasswordAuth? = null,
|
||||
val progressListener: BootstrapProgressListener? = null,
|
||||
val passphrase: String?
|
||||
)
|
||||
|
||||
class BootstrapCrossSigningTask @Inject constructor(
|
||||
private val session: Session,
|
||||
private val stringProvider: StringProvider
|
||||
) {
|
||||
|
||||
operator fun invoke(
|
||||
scope: CoroutineScope,
|
||||
params: Params,
|
||||
onResult: (BootstrapResult) -> Unit = {}
|
||||
) {
|
||||
val backgroundJob = scope.async { execute(params) }
|
||||
scope.launch { onResult(backgroundJob.await()) }
|
||||
}
|
||||
|
||||
suspend fun execute(params: Params): BootstrapResult {
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
|
||||
isIndeterminate = true
|
||||
)
|
||||
)
|
||||
val crossSigningService = session.cryptoService().crossSigningService()
|
||||
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
return handleInitializeXSigningError(failure)
|
||||
}
|
||||
|
||||
val keyInfo: SsssKeyCreationInfo
|
||||
|
||||
val ssssService = session.sharedSecretStorageService
|
||||
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_pbkdf2),
|
||||
isIndeterminate = true)
|
||||
)
|
||||
try {
|
||||
keyInfo = awaitCallback {
|
||||
params.passphrase?.let { passphrase ->
|
||||
ssssService.generateKeyWithPassphrase(
|
||||
UUID.randomUUID().toString(),
|
||||
"ssss_key",
|
||||
passphrase,
|
||||
EmptyKeySigner(),
|
||||
null,
|
||||
it
|
||||
)
|
||||
} ?: kotlin.run {
|
||||
ssssService.generateKey(
|
||||
UUID.randomUUID().toString(),
|
||||
"ssss_key",
|
||||
EmptyKeySigner(),
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (failure: Failure) {
|
||||
return BootstrapResult.FailedToCreateSSSSKey(failure)
|
||||
}
|
||||
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_default_key),
|
||||
isIndeterminate = true)
|
||||
)
|
||||
try {
|
||||
awaitCallback<Unit> {
|
||||
ssssService.setDefaultKey(keyInfo.keyId, it)
|
||||
}
|
||||
} catch (failure: Failure) {
|
||||
// Maybe we could just ignore this error?
|
||||
return BootstrapResult.FailedToSetDefaultSSSSKey(failure)
|
||||
}
|
||||
|
||||
val xKeys = crossSigningService.getCrossSigningPrivateKeys()
|
||||
val mskPrivateKey = xKeys?.master ?: return BootstrapResult.MissingPrivateKey
|
||||
val sskPrivateKey = xKeys.selfSigned ?: return BootstrapResult.MissingPrivateKey
|
||||
val uskPrivateKey = xKeys.user ?: return BootstrapResult.MissingPrivateKey
|
||||
|
||||
try {
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_msk),
|
||||
isIndeterminate = true
|
||||
)
|
||||
)
|
||||
awaitCallback<Unit> {
|
||||
ssssService.storeSecret(
|
||||
MASTER_KEY_SSSS_NAME,
|
||||
mskPrivateKey,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it
|
||||
)
|
||||
}
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_usk),
|
||||
isIndeterminate = true
|
||||
)
|
||||
)
|
||||
awaitCallback<Unit> {
|
||||
ssssService.storeSecret(
|
||||
USER_SIGNING_KEY_SSSS_NAME,
|
||||
uskPrivateKey,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)),
|
||||
it
|
||||
)
|
||||
}
|
||||
params.progressListener?.onProgress(
|
||||
WaitingViewData(
|
||||
stringProvider.getString(R.string.bootstrap_crosssigning_progress_save_ssk), isIndeterminate = true
|
||||
)
|
||||
)
|
||||
awaitCallback<Unit> {
|
||||
ssssService.storeSecret(
|
||||
SELF_SIGNING_KEY_SSSS_NAME,
|
||||
sskPrivateKey,
|
||||
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it
|
||||
)
|
||||
}
|
||||
} catch (failure: Failure) {
|
||||
// Maybe we could just ignore this error?
|
||||
return BootstrapResult.FailedToStorePrivateKeyInSSSS(failure)
|
||||
}
|
||||
|
||||
// TODO configure key backup?
|
||||
|
||||
return BootstrapResult.Success(keyInfo)
|
||||
}
|
||||
|
||||
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
|
||||
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
|
||||
return BootstrapResult.InvalidPasswordError(failure.error)
|
||||
} else if (failure is Failure.OtherServerError && failure.httpCode == 401) {
|
||||
try {
|
||||
MoshiProvider.providesMoshi()
|
||||
.adapter(RegistrationFlowResponse::class.java)
|
||||
.fromJson(failure.errorBody)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}?.let { flowResponse ->
|
||||
if (flowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
|
||||
// can't do this from here
|
||||
return BootstrapResult.UnsupportedAuthFlow()
|
||||
}
|
||||
}
|
||||
}
|
||||
return BootstrapResult.GenericError(failure)
|
||||
}
|
||||
}
|
@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import androidx.core.text.toSpannable
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
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.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 im.vector.riotx.features.settings.VectorLocale
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapEnterPassphraseFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val recPassPhrase = getString(R.string.recovery_passphrase)
|
||||
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_text, recPassPhrase)
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_enter_passphrase)
|
||||
withState(sharedViewModel) {
|
||||
// set initial value (usefull when coming back)
|
||||
ssss_passphrase_enter_edittext.setText(it.passphrase ?: "")
|
||||
}
|
||||
ssss_passphrase_enter_edittext.editorActionEvents()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
|
||||
submit()
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
ssss_passphrase_enter_edittext.textChanges()
|
||||
.subscribe {
|
||||
// ssss_passphrase_enter_til.error = null
|
||||
sharedViewModel.handle(BootstrapActions.UpdateCandidatePassphrase(it?.toString() ?: ""))
|
||||
// ssss_passphrase_submit.isEnabled = it.isNotBlank()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
sharedViewModel.observeViewEvents {
|
||||
// when (it) {
|
||||
// is SharedSecureStorageViewEvent.InlineError -> {
|
||||
// ssss_passphrase_enter_til.error = it.message
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
ssss_view_show_password.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun submit() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.SetupPassphrase) {
|
||||
return@withState
|
||||
}
|
||||
val score = state.passphraseStrength.invoke()?.score
|
||||
val passphrase = ssss_passphrase_enter_edittext.text?.toString()
|
||||
if (passphrase.isNullOrBlank()) {
|
||||
ssss_passphrase_enter_til.error = getString(R.string.passphrase_empty_error_message)
|
||||
} else if (score != 4) {
|
||||
ssss_passphrase_enter_til.error = getString(R.string.passphrase_passphrase_too_weak)
|
||||
} else {
|
||||
sharedViewModel.handle(BootstrapActions.GoToConfirmPassphrase(passphrase))
|
||||
}
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
super.invalidate()
|
||||
|
||||
if (state.step is BootstrapStep.SetupPassphrase) {
|
||||
val isPasswordVisible = state.step.isPasswordVisible
|
||||
ssss_passphrase_enter_edittext.showPassword(isPasswordVisible, updateCursor = false)
|
||||
ssss_view_show_password.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed_black else R.drawable.ic_eye_black)
|
||||
|
||||
state.passphraseStrength.invoke()?.let { strength ->
|
||||
val score = strength.score
|
||||
ssss_passphrase_security_progress.strength = score
|
||||
if (score in 1..3) {
|
||||
val hint =
|
||||
strength.feedback?.getWarning(VectorLocale.applicationLocale)?.takeIf { it.isNotBlank() }
|
||||
?: strength.feedback?.getSuggestions(VectorLocale.applicationLocale)?.firstOrNull()
|
||||
if (hint != null && hint != ssss_passphrase_enter_til.error.toString()) {
|
||||
ssss_passphrase_enter_til.error = hint
|
||||
}
|
||||
} else {
|
||||
ssss_passphrase_enter_til.error = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.jakewharton.rxbinding3.view.clicks
|
||||
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.colorizeMatchingText
|
||||
import im.vector.riotx.core.utils.startSharePlainTextIntent
|
||||
import im.vector.riotx.core.utils.toast
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapSaveRecoveryKeyFragment @Inject constructor(
|
||||
private val colorProvider: ColorProvider
|
||||
) : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_save_key
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, getString(R.string.message_key), getString(R.string.recovery_passphrase))
|
||||
.toSpannable()
|
||||
.colorizeMatchingText(getString(R.string.recovery_passphrase), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
.colorizeMatchingText(getString(R.string.message_key), colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
|
||||
|
||||
recoverySave.clickableView.clicks()
|
||||
.debounce(600, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
downloadRecoveryKey()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
recoveryCopy.clickableView.clicks()
|
||||
.debounce(600, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
shareRecoveryKey()
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
||||
recoveryContinue.clickableView.clicks()
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
sharedViewModel.handle(BootstrapActions.GoToCompleted)
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
private fun downloadRecoveryKey() = withState(sharedViewModel) { _ ->
|
||||
|
||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_TITLE, "riot-recovery-key.txt")
|
||||
|
||||
try {
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqQueryStarted)
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step3_please_make_copy)), REQUEST_CODE_SAVE)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
requireActivity().toast(R.string.error_no_external_application_found)
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == REQUEST_CODE_SAVE) {
|
||||
val uri = data?.data
|
||||
if (resultCode == RESULT_OK && uri != null) {
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
sharedViewModel.handle(BootstrapActions.SaveKeyToUri(context!!.contentResolver!!.openOutputStream(uri)!!))
|
||||
} catch (failure: Throwable) {
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// result code seems to be always cancelled here.. so act as if it was saved
|
||||
sharedViewModel.handle(BootstrapActions.SaveReqFailed)
|
||||
}
|
||||
return
|
||||
} else if (requestCode == REQUEST_CODE_COPY) {
|
||||
sharedViewModel.handle(BootstrapActions.RecoveryKeySaved)
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
|
||||
private fun shareRecoveryKey() = withState(sharedViewModel) { state ->
|
||||
val recoveryKey = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey()
|
||||
?: return@withState
|
||||
|
||||
startSharePlainTextIntent(this,
|
||||
context?.getString(R.string.keys_backup_setup_step3_share_intent_chooser_title),
|
||||
recoveryKey,
|
||||
context?.getString(R.string.recovery_key), REQUEST_CODE_COPY)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
val step = state.step
|
||||
if (step !is BootstrapStep.SaveRecoveryKey) return@withState
|
||||
|
||||
recoveryContinue.isVisible = step.isSaved
|
||||
bootstrapRecoveryKeyText.text = state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_CODE_SAVE = 123
|
||||
const val REQUEST_CODE_COPY = 124
|
||||
}
|
||||
}
|
@ -0,0 +1,350 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.airbnb.mvrx.Async
|
||||
import com.airbnb.mvrx.Fail
|
||||
import com.airbnb.mvrx.FragmentViewModelContext
|
||||
import com.airbnb.mvrx.Loading
|
||||
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.nulabinc.zxcvbn.Strength
|
||||
import com.nulabinc.zxcvbn.Zxcvbn
|
||||
import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
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
|
||||
import im.vector.riotx.features.login.ReAuthHelper
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.OutputStream
|
||||
|
||||
data class BootstrapViewState(
|
||||
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
|
||||
val passphrase: String? = null,
|
||||
val passphraseRepeat: String? = null,
|
||||
val crossSigningInitialization: Async<Unit> = Uninitialized,
|
||||
val passphraseStrength: Async<Strength> = Uninitialized,
|
||||
val passphraseConfirmMatch: Async<Unit> = Uninitialized,
|
||||
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
|
||||
val initializationWaitingViewData: WaitingViewData? = null,
|
||||
val currentReAuth: UserPasswordAuth? = null,
|
||||
val recoverySaveFileProcess: Async<Unit> = Uninitialized
|
||||
) : MvRxState
|
||||
|
||||
sealed class BootstrapStep {
|
||||
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
|
||||
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
|
||||
object Initializing : BootstrapStep()
|
||||
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
|
||||
object DoneSuccess : BootstrapStep()
|
||||
}
|
||||
|
||||
class BootstrapSharedViewModel @AssistedInject constructor(
|
||||
@Assisted initialState: BootstrapViewState,
|
||||
private val stringProvider: StringProvider,
|
||||
private val session: Session,
|
||||
private val bootstrapTask: BootstrapCrossSigningTask,
|
||||
private val reAuthHelper: ReAuthHelper
|
||||
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
|
||||
|
||||
private val zxcvbn = Zxcvbn()
|
||||
|
||||
@AssistedInject.Factory
|
||||
interface Factory {
|
||||
fun create(initialState: BootstrapViewState): BootstrapSharedViewModel
|
||||
}
|
||||
|
||||
override fun handle(action: BootstrapActions) = withState { state ->
|
||||
when (action) {
|
||||
is BootstrapActions.GoBack -> queryBack()
|
||||
BootstrapActions.TogglePasswordVisibility -> {
|
||||
when (state.step) {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
setState {
|
||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||
}
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
setState {
|
||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||
}
|
||||
}
|
||||
|
||||
is BootstrapStep.AccountPassword -> {
|
||||
setState {
|
||||
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is BootstrapActions.UpdateCandidatePassphrase -> {
|
||||
val strength = zxcvbn.measure(action.pass)
|
||||
setState {
|
||||
copy(
|
||||
passphrase = action.pass,
|
||||
passphraseStrength = Success(strength)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapActions.GoToConfirmPassphrase -> {
|
||||
setState {
|
||||
copy(
|
||||
passphrase = action.passphrase,
|
||||
step = BootstrapStep.ConfirmPassphrase(
|
||||
isPasswordVisible = (state.step as? BootstrapStep.SetupPassphrase)?.isPasswordVisible ?: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapActions.UpdateConfirmCandidatePassphrase -> {
|
||||
setState {
|
||||
copy(
|
||||
passphraseRepeat = action.pass
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapActions.DoInitialize -> {
|
||||
if (state.passphrase == state.passphraseRepeat) {
|
||||
val auth = action.auth ?: reAuthHelper.rememberedAuth()
|
||||
if (auth == null) {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(action.auth)
|
||||
}
|
||||
} else {
|
||||
setState {
|
||||
copy(
|
||||
passphraseConfirmMatch = Fail(Throwable(stringProvider.getString(R.string.passphrase_passphrase_does_not_match)))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is BootstrapActions.DoInitializeGeneratedKey -> {
|
||||
val auth = action.auth ?: reAuthHelper.rememberedAuth()
|
||||
if (auth == null) {
|
||||
setState {
|
||||
copy(
|
||||
passphrase = null,
|
||||
passphraseRepeat = null,
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
startInitializeFlow(action.auth)
|
||||
}
|
||||
}
|
||||
BootstrapActions.RecoveryKeySaved -> {
|
||||
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
|
||||
setState {
|
||||
copy(step = BootstrapStep.SaveRecoveryKey(true))
|
||||
}
|
||||
}
|
||||
BootstrapActions.Completed -> {
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
BootstrapActions.GoToCompleted -> {
|
||||
setState {
|
||||
copy(step = BootstrapStep.DoneSuccess)
|
||||
}
|
||||
}
|
||||
BootstrapActions.SaveReqQueryStarted -> {
|
||||
setState {
|
||||
copy(recoverySaveFileProcess = Loading())
|
||||
}
|
||||
}
|
||||
is BootstrapActions.SaveKeyToUri -> {
|
||||
saveRecoveryKeyToUri(action.os)
|
||||
}
|
||||
BootstrapActions.SaveReqFailed -> {
|
||||
setState {
|
||||
copy(recoverySaveFileProcess = Uninitialized)
|
||||
}
|
||||
}
|
||||
BootstrapActions.GoToEnterAccountPassword -> {
|
||||
setState {
|
||||
copy(step = BootstrapStep.AccountPassword(false))
|
||||
}
|
||||
}
|
||||
is BootstrapActions.ReAuth -> {
|
||||
startInitializeFlow(
|
||||
state.currentReAuth?.copy(password = action.pass)
|
||||
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
|
||||
)
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// Business Logic
|
||||
// =======================================
|
||||
private fun saveRecoveryKeyToUri(os: OutputStream) = withState { state ->
|
||||
viewModelScope.launch {
|
||||
kotlin.runCatching {
|
||||
os.use {
|
||||
os.write((state.recoveryKeyCreationInfo?.recoveryKey?.formatRecoveryKey() ?: "").toByteArray())
|
||||
}
|
||||
}.fold({
|
||||
setState {
|
||||
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
|
||||
copy(
|
||||
recoverySaveFileProcess = Success(Unit),
|
||||
step = BootstrapStep.SaveRecoveryKey(isSaved = true)
|
||||
)
|
||||
}
|
||||
}, {
|
||||
setState {
|
||||
copy(recoverySaveFileProcess = Fail(it))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInitializeFlow(auth: UserPasswordAuth?) {
|
||||
setState {
|
||||
copy(step = BootstrapStep.Initializing)
|
||||
}
|
||||
|
||||
val progressListener = object : BootstrapProgressListener {
|
||||
override fun onProgress(data: WaitingViewData) {
|
||||
setState {
|
||||
copy(
|
||||
initializationWaitingViewData = data
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withState { state ->
|
||||
viewModelScope.launch {
|
||||
bootstrapTask.invoke(this, Params(
|
||||
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
|
||||
progressListener = progressListener,
|
||||
passphrase = state.passphrase
|
||||
)) {
|
||||
when (it) {
|
||||
is BootstrapResult.Success -> {
|
||||
setState {
|
||||
copy(
|
||||
recoveryKeyCreationInfo = it.keyInfo,
|
||||
step = BootstrapStep.SaveRecoveryKey(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.PasswordAuthFlowMissing -> {
|
||||
setState {
|
||||
copy(
|
||||
currentReAuth = UserPasswordAuth(session = it.sessionId, user = it.userId),
|
||||
step = BootstrapStep.AccountPassword(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.UnsupportedAuthFlow -> {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
|
||||
_viewEvents.post(BootstrapViewEvents.Dismiss)
|
||||
}
|
||||
is BootstrapResult.InvalidPasswordError -> {
|
||||
// it's a bad password
|
||||
setState {
|
||||
copy(
|
||||
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
|
||||
currentReAuth = UserPasswordAuth(session = null, user = session.myUserId),
|
||||
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapResult.Failure -> {
|
||||
if (it is BootstrapResult.GenericError
|
||||
&& it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError
|
||||
&& it.failure.httpCode == 401) {
|
||||
} else {
|
||||
_viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error)))
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.ConfirmPassphrase(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =======================================
|
||||
// Fragment interaction
|
||||
// =======================================
|
||||
|
||||
private fun queryBack() = withState { state ->
|
||||
when (state.step) {
|
||||
is BootstrapStep.SetupPassphrase -> {
|
||||
// do we let you cancel from here?
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
|
||||
}
|
||||
is BootstrapStep.ConfirmPassphrase -> {
|
||||
setState {
|
||||
copy(
|
||||
step = BootstrapStep.SetupPassphrase(
|
||||
isPasswordVisible = (state.step as? BootstrapStep.ConfirmPassphrase)?.isPasswordVisible ?: false
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
is BootstrapStep.AccountPassword -> {
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
||||
}
|
||||
BootstrapStep.Initializing -> {
|
||||
// do we let you cancel from here?
|
||||
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
|
||||
}
|
||||
is BootstrapStep.SaveRecoveryKey,
|
||||
BootstrapStep.DoneSuccess -> {
|
||||
// nop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================
|
||||
// Companion, view model assisted creation
|
||||
// ======================================
|
||||
|
||||
companion object : MvRxViewModelFactory<BootstrapSharedViewModel, BootstrapViewState> {
|
||||
|
||||
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
|
||||
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
|
||||
return fragment.bootstrapViewModelFactory.create(state)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import im.vector.riotx.core.platform.VectorViewEvents
|
||||
|
||||
sealed class BootstrapViewEvents : VectorViewEvents {
|
||||
object Dismiss : BootstrapViewEvents()
|
||||
data class ModalError(val error: String) : BootstrapViewEvents()
|
||||
object RecoveryKeySaved: BootstrapViewEvents()
|
||||
data class SkipBootstrap(val genKeyOption: Boolean = true): BootstrapViewEvents()
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.airbnb.mvrx.parentFragmentViewModel
|
||||
import com.airbnb.mvrx.withState
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.platform.VectorBaseFragment
|
||||
import kotlinx.android.synthetic.main.fragment_bootstrap_waiting.*
|
||||
import javax.inject.Inject
|
||||
|
||||
class BootstrapWaitingFragment @Inject constructor() : VectorBaseFragment() {
|
||||
|
||||
override fun getLayoutResId() = R.layout.fragment_bootstrap_waiting
|
||||
|
||||
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(sharedViewModel) { state ->
|
||||
if (state.step !is BootstrapStep.Initializing) return@withState
|
||||
bootstrapLoadingStatusText.text = state.initializationWaitingViewData?.message
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.view.KeyEvent
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.content.ContextCompat
|
||||
import im.vector.riotx.R
|
||||
import me.gujun.android.span.image
|
||||
import me.gujun.android.span.span
|
||||
|
||||
class KeepItSafeDialog {
|
||||
|
||||
fun show(activity: Activity) {
|
||||
val dialogLayout = activity.layoutInflater.inflate(R.layout.dialog_recovery_key_saved_info, null)
|
||||
|
||||
val descriptionText = dialogLayout.findViewById<TextView>(R.id.keepItSafeText)
|
||||
|
||||
descriptionText.text = span {
|
||||
span {
|
||||
image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!)
|
||||
+" "
|
||||
+activity.getString(R.string.bootstrap_crosssigning_print_it)
|
||||
+"\n\n"
|
||||
image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!)
|
||||
+" "
|
||||
+activity.getString(R.string.bootstrap_crosssigning_save_usb)
|
||||
+"\n\n"
|
||||
image(ContextCompat.getDrawable(activity, R.drawable.ic_check_on)!!)
|
||||
+" "
|
||||
+activity.getString(R.string.bootstrap_crosssigning_save_cloud)
|
||||
+"\n\n"
|
||||
}
|
||||
}
|
||||
|
||||
AlertDialog.Builder(activity)
|
||||
// .setIcon(android.R.drawable.ic_dialog_alert)
|
||||
// .setTitle(R.string.devices_delete_dialog_title)
|
||||
.setView(dialogLayout)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
|
||||
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
dialog.cancel()
|
||||
return@OnKeyListener true
|
||||
}
|
||||
false
|
||||
})
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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.recover
|
||||
|
||||
fun String.formatRecoveryKey(): String {
|
||||
return this.replace(" ", "")
|
||||
.chunked(16)
|
||||
.joinToString("\n") {
|
||||
it
|
||||
.chunked(4)
|
||||
.joinToString(" ")
|
||||
}
|
||||
}
|
@ -25,7 +25,6 @@ import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import butterknife.BindView
|
||||
@ -352,11 +351,11 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
var current = this as? View
|
||||
while (current != null) {
|
||||
if (current is CoordinatorLayout) return current
|
||||
current = current.parent as? View
|
||||
}
|
||||
return null
|
||||
}
|
||||
// fun View.getParentCoordinatorLayout(): CoordinatorLayout? {
|
||||
// var current = this as? View
|
||||
// while (current != null) {
|
||||
// if (current is CoordinatorLayout) return current
|
||||
// current = current.parent as? View
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
|
@ -40,6 +40,7 @@ import im.vector.riotx.core.extensions.replaceFragment
|
||||
import im.vector.riotx.core.platform.ToolbarConfigurable
|
||||
import im.vector.riotx.core.platform.VectorBaseActivity
|
||||
import im.vector.riotx.core.pushers.PushersManager
|
||||
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
|
||||
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
|
||||
import im.vector.riotx.features.notifications.NotificationDrawerManager
|
||||
import im.vector.riotx.features.popup.PopupAlertManager
|
||||
@ -95,6 +96,9 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
drawerLayout.closeDrawer(GravityCompat.START)
|
||||
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
|
||||
}
|
||||
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
|
||||
BootstrapBottomSheet().apply { isCancelable = false }.show(supportFragmentManager, "BootstrapBottomSheet")
|
||||
}
|
||||
}
|
||||
}
|
||||
.disposeOnDestroy()
|
||||
@ -103,6 +107,10 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
notificationDrawerManager.clearAllEvents()
|
||||
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
|
||||
}
|
||||
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
|
||||
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
|
||||
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
|
||||
}
|
||||
|
||||
activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
|
||||
if (status == null) {
|
||||
@ -246,11 +254,13 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"
|
||||
private const val EXTRA_ACCOUNT_CREATION = "EXTRA_ACCOUNT_CREATION"
|
||||
|
||||
fun newIntent(context: Context, clearNotification: Boolean = false): Intent {
|
||||
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
|
||||
return Intent(context, HomeActivity::class.java)
|
||||
.apply {
|
||||
putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification)
|
||||
putExtra(EXTRA_ACCOUNT_CREATION, accountCreation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorSharedAction
|
||||
sealed class HomeActivitySharedAction : VectorSharedAction {
|
||||
object OpenDrawer : HomeActivitySharedAction()
|
||||
object OpenGroup : HomeActivitySharedAction()
|
||||
object PromptForSecurityBootstrap : HomeActivitySharedAction()
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import im.vector.matrix.android.api.session.events.model.RelationType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.session.room.VerificationState
|
||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||
@ -71,7 +70,7 @@ class VerificationItemFactory @Inject constructor(
|
||||
?: return ignoredConclusion(event, highlight, callback)
|
||||
|
||||
// If it's not a request ignore this event
|
||||
if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
|
||||
// if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
|
||||
|
||||
val referenceInformationData = messageInformationDataFactory.create(refEvent, null)
|
||||
|
||||
|
@ -217,7 +217,10 @@ open class LoginActivity : VectorBaseActivity(), ToolbarConfigurable {
|
||||
|
||||
private fun updateWithState(loginViewState: LoginViewState) {
|
||||
if (loginViewState.isUserLogged()) {
|
||||
val intent = HomeActivity.newIntent(this)
|
||||
val intent = HomeActivity.newIntent(
|
||||
this,
|
||||
accountCreation = loginViewState.signMode == SignMode.SignUp
|
||||
)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
return
|
||||
|
@ -38,6 +38,7 @@ import im.vector.matrix.android.api.auth.registration.Stage
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.auth.data.LoginFlowTypes
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||
import im.vector.riotx.core.extensions.configureAndStart
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
@ -56,7 +57,8 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
private val pushRuleTriggerListener: PushRuleTriggerListener,
|
||||
private val homeServerConnectionConfigFactory: HomeServerConnectionConfigFactory,
|
||||
private val sessionListener: SessionListener)
|
||||
private val sessionListener: SessionListener,
|
||||
private val reAuthHelper: ReAuthHelper)
|
||||
: VectorViewModel<LoginViewState, LoginAction, LoginViewEvents>(initialState) {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@ -240,6 +242,7 @@ class LoginViewModel @AssistedInject constructor(@Assisted initialState: LoginVi
|
||||
|
||||
private fun handleRegisterWith(action: LoginAction.LoginOrRegister) {
|
||||
setState { copy(asyncRegistration = Loading()) }
|
||||
reAuthHelper.rememberAuth(UserPasswordAuth(user = action.username, password = action.password))
|
||||
currentTask = registrationWizard?.createAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
|
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.login
|
||||
|
||||
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
|
||||
import java.util.Timer
|
||||
import java.util.TimerTask
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
const val THREE_MINUTES = 3 * 60_000L
|
||||
|
||||
@Singleton
|
||||
class ReAuthHelper @Inject constructor() {
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
private var rememberedInfo: UserPasswordAuth? = null
|
||||
|
||||
fun rememberAuth(password: UserPasswordAuth?) {
|
||||
timer?.cancel()
|
||||
timer = null
|
||||
rememberedInfo = password
|
||||
timer = Timer().apply {
|
||||
schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
rememberedInfo = null
|
||||
}
|
||||
}, THREE_MINUTES)
|
||||
}
|
||||
}
|
||||
|
||||
fun rememberedAuth() = rememberedInfo
|
||||
}
|
@ -174,7 +174,9 @@ class VectorFileLogger @Inject constructor(val context: Context, private val vec
|
||||
|
||||
companion object {
|
||||
private val LINE_SEPARATOR = System.getProperty("line.separator") ?: "\n"
|
||||
private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
|
||||
// private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
|
||||
private val DATE_FORMAT = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss*SSSZZZZ", Locale.US)
|
||||
|
||||
private var mIsTimeZoneSet = false
|
||||
}
|
||||
}
|
||||
|
21
vector/src/main/res/drawable/ic_clipboard.xml
Normal file
21
vector/src/main/res/drawable/ic_clipboard.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16,4H18C19.1046,4 20,4.8954 20,6V20C20,21.1046 19.1046,22 18,22H6C4.8954,22 4,21.1046 4,20V6C4,4.8954 4.8954,4 6,4H8"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,3C8,2.4477 8.4477,2 9,2H15C15.5523,2 16,2.4477 16,3V5C16,5.5523 15.5523,6 15,6H9C8.4477,6 8,5.5523 8,5V3Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
27
vector/src/main/res/drawable/ic_download.xml
Normal file
27
vector/src/main/res/drawable/ic_download.xml
Normal file
@ -0,0 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3,17V20C3,21.1046 3.8954,22 5,22H19C20.1046,22 21,21.1046 21,20V17"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8,12L12,16L16,12"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,2V16"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_message_key.xml
Normal file
22
vector/src/main/res/drawable/ic_message_key.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||
<path
|
||||
android:pathData="M21,11.4445C21.0038,12.911 20.6612,14.3577 20,15.6667C18.401,18.8659 15.1321,20.8875 11.5555,20.8889C10.089,20.8927 8.6423,20.5501 7.3333,19.8889L1,22L3.1111,15.6667C2.4499,14.3577 2.1073,12.911 2.1111,11.4445C2.1125,7.8679 4.1341,4.599 7.3333,3C8.6423,2.3388 10.089,1.9962 11.5555,2H12.1111C16.9064,2.2646 20.7354,6.0936 21,10.8889V11.4445V11.4445Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M2,8C0.8954,8 0,8.8954 0,10V13C0,14.1046 0.8954,15 2,15H20C21.1046,15 22,14.1046 22,13V10C22,8.8954 21.1046,8 20,8H2ZM4.25,9.5C3.8358,9.5 3.5,9.8358 3.5,10.25C3.5,10.6642 3.8358,11 4.25,11H6.75C7.1642,11 7.5,10.6642 7.5,10.25C7.5,9.8358 7.1642,9.5 6.75,9.5H4.25ZM8.5,10.25C8.5,9.8358 8.8358,9.5 9.25,9.5H9.75C10.1642,9.5 10.5,9.8358 10.5,10.25C10.5,10.6642 10.1642,11 9.75,11H9.25C8.8358,11 8.5,10.6642 8.5,10.25ZM12.25,9.5C11.8358,9.5 11.5,9.8358 11.5,10.25C11.5,10.6642 11.8358,11 12.25,11H14.75C15.1642,11 15.5,10.6642 15.5,10.25C15.5,9.8358 15.1642,9.5 14.75,9.5H12.25ZM16.5,10.25C16.5,9.8358 16.8358,9.5 17.25,9.5H17.75C18.1642,9.5 18.5,9.8358 18.5,10.25C18.5,10.6642 18.1642,11 17.75,11H17.25C16.8358,11 16.5,10.6642 16.5,10.25ZM4.25,12C3.8358,12 3.5,12.3358 3.5,12.75C3.5,13.1642 3.8358,13.5 4.25,13.5H4.75C5.1642,13.5 5.5,13.1642 5.5,12.75C5.5,12.3358 5.1642,12 4.75,12H4.25ZM6.5,12.75C6.5,12.3358 6.8358,12 7.25,12H9.75C10.1642,12 10.5,12.3358 10.5,12.75C10.5,13.1642 10.1642,13.5 9.75,13.5H7.25C6.8358,13.5 6.5,13.1642 6.5,12.75ZM12.25,12C11.8358,12 11.5,12.3358 11.5,12.75C11.5,13.1642 11.8358,13.5 12.25,13.5H12.75C13.1642,13.5 13.5,13.1642 13.5,12.75C13.5,12.3358 13.1642,12 12.75,12H12.25Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
22
vector/src/main/res/drawable/ic_message_password.xml
Normal file
22
vector/src/main/res/drawable/ic_message_password.xml
Normal file
@ -0,0 +1,22 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="24">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
|
||||
<path
|
||||
android:pathData="M21,11.4445C21.0038,12.911 20.6612,14.3577 20,15.6667C18.401,18.8659 15.1321,20.8875 11.5555,20.8889C10.089,20.8927 8.6423,20.5501 7.3333,19.8889L1,22L3.1111,15.6667C2.4499,14.3577 2.1073,12.911 2.1111,11.4445C2.1125,7.8679 4.1341,4.599 7.3333,3C8.6423,2.3388 10.089,1.9962 11.5555,2H12.1111C16.9064,2.2646 20.7354,6.0936 21,10.8889V11.4445V11.4445Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
<path
|
||||
android:pathData="M0,10C0,8.8954 0.8954,8 2,8H20C21.1046,8 22,8.8954 22,10V13C22,14.1046 21.1046,15 20,15H2C0.8954,15 0,14.1046 0,13V10ZM5,11.5C5,12.3284 4.3284,13 3.5,13C2.6716,13 2,12.3284 2,11.5C2,10.6716 2.6716,10 3.5,10C4.3284,10 5,10.6716 5,11.5ZM8.5,13C9.3284,13 10,12.3284 10,11.5C10,10.6716 9.3284,10 8.5,10C7.6716,10 7,10.6716 7,11.5C7,12.3284 7.6716,13 8.5,13ZM15,11.5C15,12.3284 14.3284,13 13.5,13C12.6716,13 12,12.3284 12,11.5C12,10.6716 12.6716,10 13.5,10C14.3284,10 15,10.6716 15,11.5ZM18.5,13C19.3284,13 20,12.3284 20,11.5C20,10.6716 19.3284,10 18.5,10C17.6716,10 17,10.6716 17,11.5C17,12.3284 17.6716,13 18.5,13Z"
|
||||
android:fillColor="#2E2F32"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
@ -1,22 +1,21 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="15dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="15"
|
||||
android:viewportHeight="16">
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14,15v-1.556c0,-1.718 -1.455,-3.11 -3.25,-3.11h-6.5c-1.795,0 -3.25,1.392 -3.25,3.11L1,15"
|
||||
android:pathData="M20,21V19C20,16.7909 18.2091,15 16,15H8C5.7909,15 4,16.7909 4,19V21"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.167"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#7E899C"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M4.25,4.111a3.25,3.111 0,1 0,6.5 0a3.25,3.111 0,1 0,-6.5 0z"
|
||||
android:pathData="M12,11C14.2091,11 16,9.2091 16,7C16,4.7909 14.2091,3 12,3C9.7909,3 8,4.7909 8,7C8,9.2091 9.7909,11 12,11Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.167"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#7E899C"
|
||||
android:strokeColor="#2E2F32"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
|
60
vector/src/main/res/layout/bottom_sheet_bootstrap.xml
Normal file
60
vector/src/main/res/layout/bottom_sheet_bootstrap.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.core.widget.NestedScrollView 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/bottomSheetScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
android:fadeScrollbars="false"
|
||||
android:scrollbars="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bootstrapIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_message_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapTitleText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/bootstrapIcon"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/bootstrapIcon"
|
||||
app:layout_constraintTop_toTopOf="@+id/bootstrapIcon"
|
||||
tools:text="@string/recovery_passphrase" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/bottomSheetFragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapTitleText" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
@ -58,11 +58,10 @@
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/bottomSheetFragmentContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/verificationRequestAvatar" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView 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="wrap_content"
|
||||
android:paddingStart="?dialogPreferredPadding"
|
||||
android:paddingLeft="?dialogPreferredPadding"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="?dialogPreferredPadding"
|
||||
android:paddingRight="?dialogPreferredPadding"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/bootstrapIcon"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:contentDescription="@string/avatar"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="@drawable/ic_message_key" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapTitleText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="1"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:text="@string/keep_it_safe"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/keepItSafeText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
tools:text="@string/bootstrap_crosssigning_save_usb" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
50
vector/src/main/res/layout/fragment_bootstrap_conclusion.xml
Normal file
50
vector/src/main/res/layout/fragment_bootstrap_conclusion.xml
Normal file
@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapConclusionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bootstrap_cross_signing_success"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<Space
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/bootstrapConclusionContinue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/finish"
|
||||
app:forceStartPadding="false"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
</LinearLayout>
|
@ -0,0 +1,70 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapDescriptionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
tools:text="@string/enter_account_password"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/bootstrapAccountPasswordTil"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/bootstrapAccountPasswordTil"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/bootstrapPasswordButton"
|
||||
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/bootstrapAccountPasswordEditText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="3"
|
||||
android:singleLine="false"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:hint="@string/passphrase_enter_passphrase"
|
||||
tools:inputType="textPassword" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ssss_view_show_password"
|
||||
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="?colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/bootstrapAccountPasswordTil"
|
||||
app:layout_constraintTop_toTopOf="@+id/bootstrapAccountPasswordTil" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/bootstrapPasswordButton"
|
||||
style="@style/VectorButtonStyleText"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="@dimen/layout_vertical_margin"
|
||||
android:padding="8dp"
|
||||
android:text="@string/_continue"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/bootstrapAccountPasswordTil" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapDescriptionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bootstrap_info_text"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/ssss_passphrase_enter_til"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/ssss_passphrase_enter_til"
|
||||
style="@style/VectorTextInputLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:errorEnabled="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/ssss_view_show_password"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapDescriptionText">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/ssss_passphrase_enter_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:hint="@string/passphrase_enter_passphrase"
|
||||
android:imeOptions="actionDone"
|
||||
android:maxLines="3"
|
||||
android:singleLine="false"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
tools:inputType="textPassword" />
|
||||
|
||||
<!-- This is inside the TIL, if not the keyboard will hide it when in bottomsheet -->
|
||||
|
||||
<im.vector.riotx.core.ui.views.PasswordStrengthBar
|
||||
android:id="@+id/ssss_passphrase_security_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_height="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapWarningInfo"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textSize="12sp"
|
||||
android:gravity="center_vertical"
|
||||
android:drawableStart="@drawable/ic_alert_triangle"
|
||||
android:drawableTint="@color/riotx_destructive_accent"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/bootstrap_dont_reuse_pwd" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ssss_view_show_password"
|
||||
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="?colorAccent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/ssss_passphrase_enter_til"
|
||||
app:layout_constraintTop_toTopOf="@+id/ssss_passphrase_enter_til" />
|
||||
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/bootstrapWarningInfo"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:layout_marginTop="8dp"-->
|
||||
<!-- android:drawableStart="@drawable/e2e_warning"-->
|
||||
<!-- android:drawablePadding="4dp"-->
|
||||
<!-- android:text="@string/bootstrap_dont_reuse_pwd"-->
|
||||
<!-- app:layout_constraintBottom_toBottomOf="parent"-->
|
||||
<!-- app:layout_constraintTop_toBottomOf="@id/ssss_passphrase_enter_til" />-->
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
88
vector/src/main/res/layout/fragment_bootstrap_save_key.xml
Normal file
88
vector/src/main/res/layout/fragment_bootstrap_save_key.xml
Normal file
@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout 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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapSaveText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/bootstrap_save_key_description"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapRecoveryKeyText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layout_margin="@dimen/layout_horizontal_margin"
|
||||
android:fontFamily="monospace"
|
||||
android:gravity="center_horizontal"
|
||||
android:padding="4dp"
|
||||
android:textColor="?vctr_notice_secondary"
|
||||
android:textSize="15sp"
|
||||
tools:text="HHWJ Y8DK RDR4\nBQEN FQ4V M4F8\nBQEN FQ4V M4A8" />
|
||||
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/recoveryCopy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/copy_value"
|
||||
app:leftIcon="@drawable/ic_clipboard"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/recoverySave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/keys_backup_setup_step3_save_button_title"
|
||||
app:leftIcon="@drawable/ic_download"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
<im.vector.riotx.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/recoveryContinue"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="50dp"
|
||||
app:actionTitle="@string/_continue"
|
||||
app:forceStartPadding="true"
|
||||
app:rightIcon="@drawable/ic_arrow_right"
|
||||
app:tint="?colorAccent" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="?attr/vctr_list_divider_color" />
|
||||
|
||||
|
||||
</LinearLayout>
|
41
vector/src/main/res/layout/fragment_bootstrap_waiting.xml
Normal file
41
vector/src/main/res/layout/fragment_bootstrap_waiting.xml
Normal file
@ -0,0 +1,41 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="200dp"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapDescriptionText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/bootstrap_loading_text"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/bootstrapWaitingProgress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/bootstrapDescriptionText" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bootstrapLoadingStatusText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:textColor="?riotx_text_primary"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintTop_toBottomOf="@id/bootstrapWaitingProgress"
|
||||
tools:text="Bending the spoon..." />
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -2,6 +2,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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/itemVerificationClickableZone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
@ -13,18 +14,34 @@
|
||||
android:paddingRight="@dimen/layout_horizontal_margin"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemVerificationLeftIcon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:scaleType="center"
|
||||
android:tint="?riotx_text_primary"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/ic_share"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemVerificationActionTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:textColor="@color/riotx_accent"
|
||||
android:textSize="16sp"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/itemVerificationActionSubTitle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/itemVerificationLeftIcon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
app:layout_goneMarginStart="0dp"
|
||||
tools:text="@string/start_verification" />
|
||||
|
||||
<TextView
|
||||
@ -38,7 +55,7 @@
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@+id/itemVerificationActionTitle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/itemVerificationActionTitle"
|
||||
tools:text="For maximum security, do this in person"
|
||||
tools:visibility="visible" />
|
||||
|
@ -105,4 +105,13 @@
|
||||
<attr name="optionIsWinner" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="BottomSheetActionButton">
|
||||
<attr name="tint" format="color" />
|
||||
<attr name="actionTitle" format="string" />
|
||||
<attr name="actionDescription" format="string" />
|
||||
<attr name="leftIcon" format="reference" />
|
||||
<attr name="rightIcon" format="reference" />
|
||||
<attr name="forceStartPadding" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
@ -32,6 +32,60 @@
|
||||
<string name="verify_cancelled_notice">Verify your devices from Settings.</string>
|
||||
<string name="verification_cancelled">Verification Cancelled</string>
|
||||
|
||||
<string name="recovery_passphrase">Message Password</string>
|
||||
<string name="message_key">Message Key</string>
|
||||
<string name="account_password">Account Password</string>
|
||||
|
||||
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="set_recovery_passphrase">Set a %s</string>
|
||||
<string name="generate_message_key">Generate a Message Key</string>
|
||||
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="confirm_recovery_passphrase">Confirm %s</string>
|
||||
|
||||
|
||||
<!-- %s will be replaced by account_password -->
|
||||
<string name="enter_account_password">Enter your %s to continue.</string>
|
||||
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="bootstrap_info_text">Secure & unlock encrypted messages and trust with a %s.</string>
|
||||
<!-- %s will be replaced by recovery_passphrase -->
|
||||
<string name="bootstrap_info_confirm_text">Enter your %s again to confirm it.</string>
|
||||
<string name="bootstrap_dont_reuse_pwd">Don’t re-use your account password.</string>
|
||||
|
||||
|
||||
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
|
||||
<string name="bootstrap_loading_title">Setting up recovery.</string>
|
||||
<string name="your_recovery_key">Your recovery key</string>
|
||||
<string name="bootstrap_finish_title">You‘re done!</string>
|
||||
<string name="keep_it_safe">Keep it safe</string>
|
||||
<string name="finish">Finish</string>
|
||||
|
||||
<!-- %1$s is replaced by message_key and %2$s by recovery_passphrase -->
|
||||
<string name="bootstrap_save_key_description">Use this %1$s as a safety net in case you forget your %2$s.</string>
|
||||
|
||||
<string name="bootstrap_crosssigning_progress_initializing">Publishing created identity keys</string>
|
||||
<string name="bootstrap_crosssigning_progress_pbkdf2">Generating secure key from passphrase</string>
|
||||
<string name="bootstrap_crosssigning_progress_default_key">Defining SSSS default Key</string>
|
||||
<string name="bootstrap_crosssigning_progress_save_msk">Synchronizing Master key</string>
|
||||
<string name="bootstrap_crosssigning_progress_save_usk">Synchronizing User key</string>
|
||||
<string name="bootstrap_crosssigning_progress_save_ssk">Synchronizing Self Signing key</string>
|
||||
|
||||
|
||||
<!-- %1$s is replaced by message_key and %2$s by recovery_passphrase -->
|
||||
<string name="bootstrap_cross_signing_success">Your %2$s & %1$s are now set.\n\nKeep them safe! You’ll need them to unlock encrypted messages and secure information if you lose all of your active sessions.</string>
|
||||
|
||||
<!-- the %s will be replaced by a check mark on screen-->
|
||||
<string name="bootstrap_crosssigning_print_it">Print it and store it somewhere safe</string>
|
||||
<string name="bootstrap_crosssigning_save_usb">Save it on a USB key or backup drive</string>
|
||||
<string name="bootstrap_crosssigning_save_cloud">Copy it to your personal cloud storage</string>
|
||||
|
||||
<string name="auth_flow_not_supported">You cannot do that from mobile</string>
|
||||
|
||||
<string name="bootstrap_skip_text">Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
||||
<string name="bootstrap_skip_text_no_gen_key">Setting a Message Password lets you secure & unlock encrypted messages and trust.</string>
|
||||
|
||||
<!-- END Strings added by Valere -->
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user