Merge branch 'develop' into feature/fix_small_issues

This commit is contained in:
Benoit Marty 2020-07-11 21:56:07 +02:00 committed by GitHub
commit a35749964c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 415 additions and 154 deletions

View file

@ -9,6 +9,8 @@ Improvements 🙌:
- Creating and listening to EventInsertEntity. (#1634)
- Handling (almost) properly the groups fetching (#1634)
- Improve fullscreen media display (#327)
- Setup server recovery banner (#1648)
- Set up SSSS from security settings (#1567)
Bugfix 🐛:
- Integration Manager: Wrong URL to review terms if URL in config contains path (#1606)
@ -18,6 +20,7 @@ Bugfix 🐛:
- Leave room only leaves the current version (#1656)
- Regression | Share action menu do not work (#1647)
- verification issues on transition (#1555)
- Fix issue when restoring keys backup using recovery key
Translations 🗣:
-
@ -30,6 +33,7 @@ Build 🧱:
- Revert to build-tools 3.5.3
Other changes:
- Use Intent.ACTION_CREATE_DOCUMENT to save megolm key or recovery key in a txt file
- Use `Context#withStyledAttributes` extension function (#1546)
Changes in Riot.imX 0.91.4 (2020-07-06)

View file

@ -17,9 +17,14 @@
package im.vector.matrix.rx
import androidx.paging.PagedList
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.query.QueryStringValue
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
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.group.GroupSummaryQueryParams
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.identity.ThreePid
@ -37,9 +42,11 @@ import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.store.PrivateKeysInfo
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountData
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function3
class RxSession(private val session: Session) {
@ -170,6 +177,38 @@ class RxSession(private val session: Session) {
fun liveRoomChangeMembershipState(): Observable<Map<String, ChangeMembershipState>> {
return session.getChangeMembershipsLive().asObservable()
}
fun liveSecretSynchronisationInfo(): Observable<SecretsSynchronisationInfo> {
return Observable.combineLatest<List<UserAccountData>, Optional<MXCrossSigningInfo>, Optional<PrivateKeysInfo>, SecretsSynchronisationInfo>(
liveAccountData(setOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME, KEYBACKUP_SECRET_SSSS_NAME)),
liveCrossSigningInfo(session.myUserId),
liveCrossSigningPrivateKeys(),
Function3 { _, crossSigningInfo, pInfo ->
// first check if 4S is already setup
val is4SSetup = session.sharedSecretStorageService.isRecoverySetup()
val isCrossSigningEnabled = crossSigningInfo.getOrNull() != null
val isCrossSigningTrusted = crossSigningInfo.getOrNull()?.isTrusted() == true
val allPrivateKeysKnown = pInfo.getOrNull()?.allKnown().orFalse()
val keysBackupService = session.cryptoService().keysBackupService()
val currentBackupVersion = keysBackupService.currentBackupVersion
val megolmBackupAvailable = currentBackupVersion != null
val savedBackupKey = keysBackupService.getKeyBackupRecoveryKeyInfo()
val megolmKeyKnown = savedBackupKey?.version == currentBackupVersion
SecretsSynchronisationInfo(
isBackupSetup = is4SSetup,
isCrossSigningEnabled = isCrossSigningEnabled,
isCrossSigningTrusted = isCrossSigningTrusted,
allPrivateKeysKnown = allPrivateKeysKnown,
megolmBackupAvailable = megolmBackupAvailable,
megolmSecretKnown = megolmKeyKnown,
isMegolmKeyIn4S = session.sharedSecretStorageService.isMegolmKeyInBackup()
)
}
)
.distinctUntilChanged()
}
}
fun Session.rx(): RxSession {

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.matrix.rx
data class SecretsSynchronisationInfo(
val isBackupSetup: Boolean,
val isCrossSigningEnabled: Boolean,
val isCrossSigningTrusted: Boolean,
val allPrivateKeysKnown: Boolean,
val megolmBackupAvailable: Boolean,
val megolmSecretKnown: Boolean,
val isMegolmKeyIn4S: Boolean
)

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.api.session.securestorage
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
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
@ -124,6 +125,13 @@ interface SharedSecretStorageService {
) is IntegrityResult.Success
}
fun isMegolmKeyInBackup(): Boolean {
return checkShouldBeAbleToAccessSecrets(
secretNames = listOf(KEYBACKUP_SECRET_SSSS_NAME),
keyId = null
) is IntegrityResult.Success
}
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
fun requestSecret(name: String, myOtherDeviceId: String)

View file

@ -71,8 +71,8 @@ internal class OutgoingGossipingRequestManager @Inject constructor(
delay(1500)
cryptoStore.getOrAddOutgoingSecretShareRequest(secretName, recipients)?.let {
// TODO check if there is already one that is being sent?
if (it.state == OutgoingGossipingRequestState.SENDING || it.state == OutgoingGossipingRequestState.SENT) {
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we already request for that session: $it")
if (it.state == OutgoingGossipingRequestState.SENDING /**|| it.state == OutgoingGossipingRequestState.SENT*/) {
Timber.v("## CRYPTO - GOSSIP sendSecretShareRequest() : we are already sending for that session: $it")
return@launch
}

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.crypto.crosssigning
import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.extensions.orFalse
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
@ -509,9 +510,7 @@ internal class DefaultCrossSigningService @Inject constructor(
override fun allPrivateKeysKnown(): Boolean {
return checkSelfTrust().isVerified()
&& cryptoStore.getCrossSigningPrivateKeys()?.selfSigned != null
&& cryptoStore.getCrossSigningPrivateKeys()?.user != null
&& cryptoStore.getCrossSigningPrivateKeys()?.master != null
&& cryptoStore.getCrossSigningPrivateKeys()?.allKnown().orFalse()
}
override fun trustUser(otherUserId: String, callback: MatrixCallback<Unit>) {

View file

@ -20,4 +20,6 @@ data class PrivateKeysInfo(
val master: String? = null,
val selfSigned: String? = null,
val user: String? = null
)
) {
fun allKnown() = master != null && selfSigned != null && user != null
}

View file

@ -16,13 +16,12 @@
package im.vector.riotx.core.extensions
import android.content.ActivityNotFoundException
import android.content.Intent
import android.app.Activity
import android.os.Parcelable
import androidx.fragment.app.Fragment
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast
import im.vector.riotx.core.utils.selectTxtFileToWrite
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@ -98,27 +97,25 @@ fun Fragment.getAllChildFragments(): List<Fragment> {
const val POP_BACK_STACK_EXCLUSIVE = 0
fun Fragment.queryExportKeys(userId: String, requestCode: Int) {
// We need WRITE_EXTERNAL permission
// if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
// this,
// PERMISSION_REQUEST_CODE_EXPORT_KEYS,
// R.string.permissions_rationale_msg_keys_backup_export)) {
// WRITE permissions are not needed
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).let {
it.format(Date())
}
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(
Intent.EXTRA_TITLE,
"riot-megolm-export-$userId-$timestamp.txt"
)
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
try {
startActivityForResult(Intent.createChooser(intent, getString(R.string.keys_backup_setup_step1_manual_export)), requestCode)
} catch (activityNotFoundException: ActivityNotFoundException) {
activity?.toast(R.string.error_no_external_application_found)
}
// }
selectTxtFileToWrite(
activity = requireActivity(),
fragment = this,
defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
requestCode = requestCode
)
}
fun Activity.queryExportKeys(userId: String, requestCode: Int) {
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
selectTxtFileToWrite(
activity = this,
fragment = null,
defaultFileName = "riot-megolm-export-$userId-$timestamp.txt",
chooserHint = getString(R.string.keys_backup_setup_step1_manual_export),
requestCode = requestCode
)
}

View file

@ -162,9 +162,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
return this
}
protected fun Disposable.disposeOnDestroy(): Disposable {
protected fun Disposable.disposeOnDestroy() {
uiDisposables.add(this)
return this
}
override fun onCreate(savedInstanceState: Bundle?) {

View file

@ -234,9 +234,8 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
private val uiDisposables = CompositeDisposable()
protected fun Disposable.disposeOnDestroyView(): Disposable {
protected fun Disposable.disposeOnDestroyView() {
uiDisposables.add(this)
return this
}
/* ==========================================================================================

View file

@ -424,6 +424,33 @@ fun openPlayStore(activity: Activity, appId: String = BuildConfig.APPLICATION_ID
}
}
/**
* Ask the user to select a location and a file name to write in
*/
fun selectTxtFileToWrite(
activity: Activity,
fragment: Fragment?,
defaultFileName: String,
chooserHint: String,
requestCode: Int
) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, defaultFileName)
try {
val chooserIntent = Intent.createChooser(intent, chooserHint)
if (fragment != null) {
fragment.startActivityForResult(chooserIntent, requestCode)
} else {
activity.startActivityForResult(chooserIntent, requestCode)
}
} catch (activityNotFoundException: ActivityNotFoundException) {
activity.toast(R.string.error_no_external_application_found)
}
}
// ==============================================================================================================
// Media utils
// ==============================================================================================================

View file

@ -63,7 +63,6 @@ const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA = 569
const val PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA = 570
const val PERMISSION_REQUEST_CODE_AUDIO_CALL = 571
const val PERMISSION_REQUEST_CODE_VIDEO_CALL = 572
const val PERMISSION_REQUEST_CODE_EXPORT_KEYS = 573
const val PERMISSION_REQUEST_CODE_CHANGE_AVATAR = 574
const val PERMISSION_REQUEST_CODE_DOWNLOAD_FILE = 575
const val PERMISSION_REQUEST_CODE_PICK_ATTACHMENT = 576

View file

@ -49,7 +49,7 @@ class KeysBackupRestoreFromKeyViewModel @Inject constructor(
viewModelScope.launch(Dispatchers.IO) {
val recoveryKey = recoveryCode.value!!
try {
sharedViewModel.recoverUsingBackupPass(recoveryKey)
sharedViewModel.recoverUsingBackupRecoveryKey(recoveryKey)
} catch (failure: Throwable) {
recoveryCodeErrorText.postValue(stringProvider.getString(R.string.keys_backup_recovery_code_error_decrypt))
}

View file

@ -16,7 +16,6 @@
package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import androidx.appcompat.app.AlertDialog
@ -27,12 +26,9 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.ExportKeysDialog
import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.extensions.queryExportKeys
import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.SimpleFragmentActivity
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
@ -97,7 +93,7 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
.show()
}
KeysBackupSetupSharedViewModel.NAVIGATE_MANUAL_EXPORT -> {
exportKeysManually()
queryExportKeys(session.myUserId, REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
@ -129,38 +125,6 @@ class KeysBackupSetupActivity : SimpleFragmentActivity() {
})
}
private fun exportKeysManually() {
if (checkPermissions(PERMISSIONS_FOR_WRITING_FILES,
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export)) {
try {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "text/plain"
intent.putExtra(Intent.EXTRA_TITLE, "riot-megolm-export-${session.myUserId}-${System.currentTimeMillis()}.txt")
startActivityForResult(
Intent.createChooser(
intent,
getString(R.string.keys_backup_setup_step1_manual_export)
),
REQUEST_CODE_SAVE_MEGOLM_EXPORT
)
} catch (activityNotFoundException: ActivityNotFoundException) {
toast(R.string.error_no_external_application_found)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
exportKeysManually()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {
val uri = data?.data

View file

@ -48,6 +48,9 @@ class KeysBackupSetupSharedViewModel @Inject constructor() : ViewModel() {
lateinit var session: Session
val userId: String
get() = session.myUserId
var showManualExport: MutableLiveData<Boolean> = MutableLiveData()
var navigateEvent: MutableLiveData<LiveEvent<String>> = MutableLiveData()

View file

@ -15,8 +15,10 @@
*/
package im.vector.riotx.features.crypto.keysbackup.setup
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.view.View
import android.widget.Button
import android.widget.TextView
@ -29,25 +31,27 @@ import butterknife.BindView
import butterknife.OnClick
import com.google.android.material.bottomsheet.BottomSheetDialog
import im.vector.riotx.R
import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.files.writeToFile
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.LiveEvent
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.selectTxtFileToWrite
import im.vector.riotx.core.utils.startSharePlainTextIntent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject
class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment() {
companion object {
private const val SAVE_RECOVERY_KEY_REQUEST_CODE = 2754
}
override fun getLayoutResId() = R.layout.fragment_keys_backup_setup_step3
@BindView(R.id.keys_backup_setup_step3_button)
@ -130,15 +134,15 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
dialog.findViewById<View>(R.id.keys_backup_setup_save)?.setOnClickListener {
val permissionsChecked = checkPermissions(
PERMISSIONS_FOR_WRITING_FILES,
this,
PERMISSION_REQUEST_CODE_EXPORT_KEYS,
R.string.permissions_rationale_msg_keys_backup_export
val userId = viewModel.userId
val timestamp = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
selectTxtFileToWrite(
activity = requireActivity(),
fragment = this,
defaultFileName = "recovery-key-$userId-$timestamp.txt",
chooserHint = getString(R.string.save_recovery_key_chooser_hint),
requestCode = SAVE_RECOVERY_KEY_REQUEST_CODE
)
if (permissionsChecked) {
exportRecoveryKeyToFile(recoveryKey)
}
dialog.dismiss()
}
@ -163,34 +167,32 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
}
private fun exportRecoveryKeyToFile(data: String) {
private fun exportRecoveryKeyToFile(uri: Uri, data: String) {
GlobalScope.launch(Dispatchers.Main) {
Try {
withContext(Dispatchers.IO) {
val parentDir = context?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
val file = File(parentDir, "recovery-key-" + System.currentTimeMillis() + ".txt")
writeToFile(data, file)
addEntryToDownloadManager(requireContext(), file, "text/plain")
file.absolutePath
requireContext().contentResolver.openOutputStream(uri)
?.use { os ->
os.write(data.toByteArray())
os.flush()
}
}
?: throw IOException("Unable to write the file")
}
.fold(
{ throwable ->
context?.let {
activity?.let {
AlertDialog.Builder(it)
.setTitle(R.string.dialog_title_error)
.setMessage(throwable.localizedMessage)
.setMessage(errorFormatter.toHumanReadable(throwable))
}
},
{ path ->
{
viewModel.copyHasBeenMade = true
context?.let {
activity?.let {
AlertDialog.Builder(it)
.setMessage(getString(R.string.recovery_key_export_saved_as_warning, path))
.setTitle(R.string.dialog_title_success)
.setMessage(R.string.recovery_key_export_saved)
}
}
)
@ -200,11 +202,14 @@ class KeysBackupSetupStep3Fragment @Inject constructor() : VectorBaseFragment()
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
viewModel.recoveryKey.value?.let {
exportRecoveryKeyToFile(it)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
SAVE_RECOVERY_KEY_REQUEST_CODE -> {
val uri = data?.data
if (resultCode == Activity.RESULT_OK && uri != null) {
viewModel.recoveryKey.value?.let {
exportRecoveryKeyToFile(uri, it)
}
}
}
}

View file

@ -45,7 +45,8 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class Args(
val initCrossSigningOnly: Boolean
val initCrossSigningOnly: Boolean,
val forceReset4S: Boolean
) : Parcelable
override val showExpanded = true
@ -180,10 +181,15 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
const val EXTRA_ARGS = "EXTRA_ARGS"
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean) {
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean, forceReset4S: Boolean) {
BootstrapBottomSheet().apply {
isCancelable = false
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(initCrossSigningOnly)) }
arguments = Bundle().apply {
this.putParcelable(EXTRA_ARGS, Args(
initCrossSigningOnly,
forceReset4S
))
}
}.show(fragmentManager, "BootstrapBottomSheet")
}
}

View file

@ -258,6 +258,30 @@ class BootstrapCrossSigningTask @Inject constructor(
)
}
}
} else {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Existing megolm backup found")
// ensure we store existing backup secret if we have it!
val knownSecret = session.cryptoService().keysBackupService().getKeyBackupRecoveryKeyInfo()
if (knownSecret != null && knownSecret.version == serverVersion.version) {
// check it matches
val isValid = awaitCallback<Boolean> {
session.cryptoService().keysBackupService().isValidRecoveryKeyForCurrentVersion(knownSecret.recoveryKey, it)
}
if (isValid) {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key valid and known")
awaitCallback<Unit> {
extractCurveKeyFromRecoveryKey(knownSecret.recoveryKey)?.toBase64NoPadding()?.let { secret ->
ssssService.storeSecret(
KEYBACKUP_SECRET_SSSS_NAME,
secret,
listOf(SharedSecretStorageService.KeyRef(keyInfo.keyId, keyInfo.keySpec)), it
)
}
}
} else {
Timber.d("## BootstrapCrossSigningTask: Creating 4S - Megolm key is unknown by this session")
}
}
}
} catch (failure: Throwable) {
Timber.e("## BootstrapCrossSigningTask: Failed to init keybackup")

View file

@ -58,6 +58,13 @@ class BootstrapSetupRecoveryKeyFragment @Inject constructor() : VectorBaseFragme
bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
} else {
if (state.step.reset) {
bootstrapSetupSecureText.text = getString(R.string.reset_secure_backup_title)
bootstrapSetupWarningTextView.isVisible = true
} else {
bootstrapSetupSecureText.text = getString(R.string.bottom_sheet_setup_secure_backup_subtitle)
bootstrapSetupWarningTextView.isVisible = false
}
// Choose between create a passphrase or use a recovery key
bootstrapSetupSecureSubmit.isVisible = false
bootstrapSetupSecureUseSecurityKey.isVisible = true

View file

@ -69,7 +69,11 @@ class BootstrapSharedViewModel @AssistedInject constructor(
init {
if (args.initCrossSigningOnly) {
if (args.forceReset4S) {
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = false, reset = true))
}
} else if (args.initCrossSigningOnly) {
// Go straight to account password
setState {
copy(step = BootstrapStep.AccountPassword(false))
@ -554,7 +558,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true)
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true, forceReset4S = false)
return fragment.bootstrapViewModelFactory.create(state, args)
}
}

View file

@ -89,7 +89,7 @@ sealed class BootstrapStep {
object CheckingMigration : BootstrapStep()
// Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
data class FirstForm(val keyBackUpExist: Boolean) : BootstrapStep()
data class FirstForm(val keyBackUpExist: Boolean, val reset: Boolean = false) : BootstrapStep()
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()

View file

@ -146,7 +146,7 @@ class VerificationRequestController @Inject constructor(
}
}
if (state.isMe && state.currentDeviceCanCrossSign) {
if (state.isMe && state.currentDeviceCanCrossSign && !state.selfVerificationMode) {
dividerItem {
id("sep_notMe")
}

View file

@ -193,14 +193,15 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupKeysBackupBanner() {
serverBackupStatusViewModel.subscribe(this) {
when (val banState = it.bannerState.invoke()) {
is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
null,
BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
}
}.disposeOnDestroyView()
serverBackupStatusViewModel
.subscribe(this) {
when (val banState = it.bannerState.invoke()) {
is BannerState.Setup -> homeKeysBackupBanner.render(KeysBackupBanner.State.Setup(banState.numberOfKeys), false)
BannerState.BackingUp -> homeKeysBackupBanner.render(KeysBackupBanner.State.BackingUp, false)
null,
BannerState.Hidden -> homeKeysBackupBanner.render(KeysBackupBanner.State.Hidden, false)
}
}
homeKeysBackupBanner.delegate = this
}

View file

@ -146,7 +146,7 @@ class DefaultNavigator @Inject constructor(
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
if (context is VectorBaseActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly)
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly, false)
}
}
@ -222,7 +222,7 @@ class DefaultNavigator @Inject constructor(
// if cross signing is enabled we should propose full 4S
sessionHolder.getSafeActiveSession()?.let { session ->
if (session.cryptoService().crossSigningService().canCrossSign() && context is VectorBaseActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, false)
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
} else {
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
}

View file

@ -72,6 +72,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
const val SETTINGS_ALLOW_INTEGRATIONS_KEY = "SETTINGS_ALLOW_INTEGRATIONS_KEY"
const val SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY = "SETTINGS_INTEGRATION_MANAGER_UI_URL_KEY"
const val SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY = "SETTINGS_SECURE_MESSAGE_RECOVERY_PREFERENCE_KEY"
// const val SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY = "SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY"
// user
const val SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY = "SETTINGS_PROFILE_PICTURE_PREFERENCE_KEY"

View file

@ -23,6 +23,7 @@ import android.content.Intent
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
@ -33,6 +34,8 @@ import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.ImportRoomKeysResult
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.android.internal.crypto.model.rest.DevicesListResponse
import im.vector.matrix.rx.SecretsSynchronisationInfo
import im.vector.matrix.rx.rx
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.dialogs.ExportKeysDialog
@ -42,13 +45,16 @@ import im.vector.riotx.core.intent.analyseIntent
import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.SimpleTextWatcher
import im.vector.riotx.core.preference.VectorPreference
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_EXPORT_KEYS
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.preference.VectorPreferenceCategory
import im.vector.riotx.core.utils.openFileSelection
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.crypto.keys.KeysExporter
import im.vector.riotx.features.crypto.keys.KeysImporter
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import javax.inject.Inject
class VectorSettingsSecurityPrivacyFragment @Inject constructor(
@ -58,6 +64,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
override var titleRes = R.string.settings_security_and_privacy
override val preferenceXmlRes = R.xml.vector_settings_security_privacy
private var disposables = mutableListOf<Disposable>()
// cryptography
private val mCryptographyCategory by lazy {
@ -94,6 +101,97 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
// My device name may have been updated
refreshMyDevice()
refreshXSigningStatus()
session.rx().liveSecretSynchronisationInfo()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
refresh4SSection(it)
refreshXSigningStatus()
}.also {
disposables.add(it)
}
}
private val secureBackupCategory by lazy {
findPreference<VectorPreferenceCategory>("SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY")!!
}
private val secureBackupPreference by lazy {
findPreference<VectorPreference>("SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY")!!
}
// private val secureBackupResetPreference by lazy {
// findPreference<VectorPreference>(VectorPreferences.SETTINGS_SECURE_BACKUP_RESET_PREFERENCE_KEY)
// }
override fun onPause() {
super.onPause()
disposables.forEach {
it.dispose()
}
disposables.clear()
}
private fun refresh4SSection(info: SecretsSynchronisationInfo) {
// it's a lot of if / else if / else
// But it's not yet clear how to manage all cases
if (!info.isCrossSigningEnabled) {
// There is not cross signing, so we can remove the section
secureBackupCategory.isVisible = false
} else {
if (!info.isBackupSetup) {
if (info.isCrossSigningEnabled && info.allPrivateKeysKnown) {
// You can setup recovery!
secureBackupCategory.isVisible = true
secureBackupPreference.title = getString(R.string.settings_secure_backup_setup)
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
true
}
} else {
// just hide all, you can't setup from here
// you should synchronize to get gossips
secureBackupCategory.isVisible = false
}
} else {
// so here we know that 4S is setup
if (info.isCrossSigningTrusted && info.allPrivateKeysKnown) {
// Looks like we have all cross signing secrets and session is trusted
// Let's see if there is a megolm backup
if (!info.megolmBackupAvailable || info.megolmSecretKnown) {
// Only option here is to create a new backup if you want?
// aka reset
secureBackupCategory.isVisible = true
secureBackupPreference.title = getString(R.string.settings_secure_backup_reset)
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = true)
true
}
} else if (!info.megolmSecretKnown) {
// megolm backup is available but we don't have key
// you could try to synchronize to get missing megolm key ?
secureBackupCategory.isVisible = true
secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
vectorActivity.let {
it.navigator.requestSelfSessionVerification(it)
}
true
}
} else {
secureBackupCategory.isVisible = false
}
} else {
// there is a backup, but this session is not trusted, or is missing some secrets
// you should enter passphrase to get them or verify against another session
secureBackupCategory.isVisible = true
secureBackupPreference.title = getString(R.string.settings_secure_backup_enter_to_setup)
secureBackupPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
vectorActivity.let {
it.navigator.requestSelfSessionVerification(it)
}
true
}
}
}
}
}
override fun bindPref() {
@ -117,39 +215,42 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
refreshXSigningStatus()
secureBackupPreference.icon = activity?.let {
ThemeUtils.tintDrawable(it,
ContextCompat.getDrawable(it, R.drawable.ic_secure_backup)!!, R.attr.vctr_settings_icon_tint_color)
}
}
// Todo this should be refactored and use same state as 4S section
private fun refreshXSigningStatus() {
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
if (xSigningKeyCanSign) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
} else if (xSigningKeysAreTrusted) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
} else if (xSigningIsEnableInAccount) {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
} else {
mCrossSigningStatePreference.setIcon(android.R.color.transparent)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
when {
xSigningKeyCanSign -> {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_trusted)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_complete)
}
xSigningKeysAreTrusted -> {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_custom)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_trusted)
}
xSigningIsEnableInAccount -> {
mCrossSigningStatePreference.setIcon(R.drawable.ic_shield_black)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_not_trusted)
}
else -> {
mCrossSigningStatePreference.setIcon(android.R.color.transparent)
mCrossSigningStatePreference.summary = getString(R.string.encryption_information_dg_xsigning_disabled)
}
}
mCrossSigningStatePreference.isVisible = true
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
if (allGranted(grantResults)) {
if (requestCode == PERMISSION_REQUEST_CODE_EXPORT_KEYS) {
queryExportKeys(activeSessionHolder.getSafeActiveSession()?.myUserId ?: "", REQUEST_CODE_SAVE_MEGOLM_EXPORT)
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_SAVE_MEGOLM_EXPORT) {

View file

@ -26,6 +26,7 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.extensions.orFalse
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.MXCrossSigningInfo
@ -114,9 +115,7 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
// So recovery is not setup
// Check if cross signing is enabled and local secrets known
if (crossSigningInfo.getOrNull()?.isTrusted() == true
&& pInfo.getOrNull()?.master != null
&& pInfo.getOrNull()?.selfSigned != null
&& pInfo.getOrNull()?.user != null
&& pInfo.getOrNull()?.allKnown().orFalse()
) {
// So 4S is not setup and we have local secrets,
return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())

View file

@ -121,7 +121,7 @@ class SignOutBottomSheetDialogFragment : VectorBaseBottomSheetDialogFragment(),
super.onActivityCreated(savedInstanceState)
setupRecoveryButton.action = {
BootstrapBottomSheet.show(parentFragmentManager, false)
BootstrapBottomSheet.show(parentFragmentManager, initCrossSigningOnly = false, forceReset4S = false)
}
exitAnywayButton.action = {

View file

@ -43,6 +43,7 @@
app:actionDescription="@string/bottom_sheet_setup_secure_backup_security_key_subtitle"
app:actionTitle="@string/bottom_sheet_setup_secure_backup_security_key_title"
app:leftIcon="@drawable/ic_security_key_24dp"
app:tint="?attr/riotx_text_primary"
app:rightIcon="@drawable/ic_arrow_right"
tools:visibility="visible" />
@ -63,6 +64,7 @@
app:actionDescription="@string/bottom_sheet_setup_secure_backup_security_phrase_subtitle"
app:actionTitle="@string/bottom_sheet_setup_secure_backup_security_phrase_title"
app:leftIcon="@drawable/ic_security_phrase_24dp"
app:tint="?attr/riotx_text_primary"
app:rightIcon="@drawable/ic_arrow_right"
tools:visibility="visible" />
@ -71,4 +73,18 @@
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />
<TextView
android:id="@+id/bootstrapSetupWarningTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:text="@string/reset_secure_backup_warning"
android:textColor="@color/riotx_destructive_accent"
android:drawableStart="@drawable/ic_warning_small"
android:drawablePadding="4dp"
android:textSize="14sp" />
</LinearLayout>

View file

@ -126,6 +126,7 @@
<string name="dialog_title_confirmation">Confirmation</string>
<string name="dialog_title_warning">Warning</string>
<string name="dialog_title_error">Error</string>
<string name="dialog_title_success">Success</string>
<!-- Bottom navigation buttons -->
<string name="bottom_action_home">Home</string>
@ -842,6 +843,15 @@
<string name="settings_send_message_with_enter">Send message with enter</string>
<string name="settings_send_message_with_enter_summary">Enter button of the soft keyboard will send message instead of adding a line break</string>
<string name="settings_secure_backup_section_title">Secure Backup</string>
<string name="settings_secure_backup_manage">Manage</string>
<string name="settings_secure_backup_setup">Set up Secure Backup</string>
<string name="settings_secure_backup_reset">Reset Secure Backup</string>
<string name="settings_secure_backup_enter_to_setup">Set up on this device</string>
<string name="settings_secure_backup_section_info">Safeguard against losing access to encrypted messages &amp; data by backing up encryption keys on your server.</string>
<string name="reset_secure_backup_title">Generate a new Security Key or set a new Security Phrase for your existing backup.</string>
<string name="reset_secure_backup_warning">This will replace your current Key or Phrase.</string>
<string name="settings_deactivate_account_section">Deactivate account</string>
<string name="settings_deactivate_my_account">Deactivate my account</string>
<string name="settings_discovery_category">Discovery</string>
@ -1413,6 +1423,7 @@ Why choose Riot.im?
<string name="keys_backup_setup_step3_share_recovery_file">Share</string>
<string name="keys_backup_setup_step3_save_button_title">Save as File</string>
<string name="recovery_key_export_saved_as_warning">The recovery key has been saved to \'%s\'.\n\nWarning: this file may be deleted if the application is uninstalled.</string>
<string name="recovery_key_export_saved">The recovery key has been saved.</string>
<string name="keys_backup_setup_override_backup_prompt_tile">A backup already exist on your HomeServer</string>
<string name="keys_backup_setup_override_backup_prompt_description">It looks like you already have setup key backup from another session. Do you want to replace it with the one youre creating?</string>
@ -2526,4 +2537,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="crypto_error_withheld_unverified">You cannot access this message because your session is not trusted by the sender</string>
<string name="crypto_error_withheld_generic">You cannot access this message because the sender purposely did not send the keys</string>
<string name="notice_crypto_unable_to_decrypt_merged">Waiting for encryption history</string>
<string name="save_recovery_key_chooser_hint">Save recovery key in</string>
</resources>

View file

@ -48,6 +48,23 @@
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_4S_CATEGORY_KEY"
android:title="@string/settings_secure_backup_section_title">
<im.vector.riotx.core.preference.VectorPreference
android:key="SETTINGS_SECURE_BACKUP_RECOVERY_PREFERENCE_KEY"
android:persistent="false"
android:icon="@drawable/ic_secure_backup"
android:title="@string/settings_secure_backup_setup" />
<im.vector.riotx.core.preference.VectorPreference
android:focusable="false"
android:persistent="false"
android:summary="@string/settings_secure_backup_section_info" />
</im.vector.riotx.core.preference.VectorPreferenceCategory>
<im.vector.riotx.core.preference.VectorPreferenceCategory
android:key="SETTINGS_CRYPTOGRAPHY_MANAGE_PREFERENCE_KEY"
android:title="@string/settings_cryptography_manage_keys">