Merge pull request #1198 from vector-im/feature/crosssigning_bootstrap

Feature/crosssigning bootstrap
This commit is contained in:
Valere 2020-04-07 15:14:54 +02:00 committed by GitHub
commit 0164f94047
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 2462 additions and 45 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
}

View File

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

View File

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

View File

@ -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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}

View File

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

View File

@ -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()
}
}

View File

@ -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(" ")
}
}

View File

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

View File

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

View File

@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorSharedAction
sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
object OpenGroup : HomeActivitySharedAction()
object PromptForSecurityBootstrap : HomeActivitySharedAction()
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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>

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

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

View File

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

View File

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

View File

@ -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 &amp; 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">Dont 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">Youre 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 &amp; %1$s are now set.\n\nKeep them safe! Youll 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 &amp; unlock encrypted messages and trust.\n\nIf you dont 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 &amp; unlock encrypted messages and trust.</string>
<!-- END Strings added by Valere -->