WIP / Verify from passphrase UX

This commit is contained in:
Valere 2020-02-14 19:22:02 +01:00 committed by Benoit Marty
parent cb669ad881
commit 9a08f5ec4e
20 changed files with 400 additions and 95 deletions

View file

@ -42,6 +42,11 @@ interface CrossSigningService {
fun initializeCrossSigning(authParams: UserPasswordAuth?,
callback: MatrixCallback<Unit>? = null)
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?,
callback: MatrixCallback<Unit>? = null) : UserTrustResult
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<MXCrossSigningInfo>>

View file

@ -0,0 +1,23 @@
/*
* 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.android.api.session.crypto.crosssigning
const val MASTER_KEY_SSSS_NAME = "m.cross_signing.master"
const val USER_SIGNING_KEY_SSSS_NAME = "m.cross_signing.user_signing"
const val SELF_SIGNING_KEY_SSSS_NAME = "m.cross_signing.self_signing"

View file

@ -0,0 +1,22 @@
/*
* 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.android.api.session.securestorage
sealed class IntegrityResult {
data class Success(val passphraseBased: Boolean) : IntegrityResult()
data class Error(val cause: SharedSecretStorageError) : IntegrityResult()
}

View file

@ -107,6 +107,7 @@ interface SharedSecretStorageService {
* @param secretKey the secret key to use (@see #Curve25519AesSha2KeySpec)
*
*/
@Throws
fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>)
fun getSecret(name: String, keyId: String?, secretKey: SSSSKeySpec, callback: MatrixCallback<String>)
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
}

View file

@ -292,6 +292,76 @@ internal class DefaultCrossSigningService @Inject constructor(
cryptoStore.clearOtherUserTrust()
}
override fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?, uskKeyPrivateKey: String?, sskPrivateKey: String?, callback: MatrixCallback<Unit>?): UserTrustResult {
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return UserTrustResult.CrossSigningNotConfigured(userId)
var masterKeyIsTrusted = false
var userKeyIsTrusted = false
var selfSignedKeyIsTrusted = false
masterKeyPrivateKey?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
masterKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
uskKeyPrivateKey?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.userKey()?.unpaddedBase64PublicKey) {
userPkSigning?.releaseSigning()
userPkSigning = pkSigning
userKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
sskPrivateKey?.fromBase64NoPadding()
?.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.selfSigningKey()?.unpaddedBase64PublicKey) {
selfSigningPkSigning?.releaseSigning()
selfSigningPkSigning = pkSigning
selfSignedKeyIsTrusted = true
Timber.i("## CrossSigning - Loading master key success")
} else {
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
pkSigning.releaseSigning()
}
}
if (!masterKeyIsTrusted || !userKeyIsTrusted || !selfSignedKeyIsTrusted) {
return UserTrustResult.KeysNotTrusted(mxCrossSigningInfo)
} else {
cryptoStore.markMyMasterKeyAsLocallyTrusted(true)
val checkSelfTrust = checkSelfTrust()
if (checkSelfTrust.isVerified()) {
cryptoStore.storePrivateKeysInfo(masterKeyPrivateKey, uskKeyPrivateKey, sskPrivateKey)
}
return checkSelfTrust
}
}
/**
*
*

View file

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.accountdata.AccountDataService
import im.vector.matrix.android.api.session.events.model.toContent
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.session.securestorage.KeyInfo
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.api.session.securestorage.KeySigner
@ -327,4 +328,37 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
const val ENCRYPTED = "encrypted"
const val DEFAULT_KEY_ID = "m.secret_storage.default_key"
}
override fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult {
if (secretNames.isEmpty()) {
return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret("none"))
}
val keyInfoResult = if (keyId == null) {
getDefaultKey()
} else {
getKey(keyId)
}
val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
if (keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
// Unsupported algorithm
return IntegrityResult.Error(
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")
)
}
secretNames.forEach { secretName ->
val secretEvent = accountDataService.getAccountDataEvent(secretName)
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownSecret(secretName))
if ((secretEvent.content["encrypted"] as? Map<*, *>)?.get(keyInfo.id) == null) {
return IntegrityResult.Error(SharedSecretStorageError.SecretNotEncryptedWithKey(secretName, keyInfo.id))
}
}
return IntegrityResult.Success(keyInfo.content.passphrase != null)
}
}

View file

@ -31,6 +31,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
timelineEventMapper.map(it, buildReadReceipts = false)
}
return RoomSummary(
roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "",

View file

@ -30,7 +30,8 @@ sealed class SharedSecureStorageAction : VectorViewModelAction {
sealed class SharedSecureStorageViewEvent : VectorViewEvents {
object Dismiss : SharedSecureStorageViewEvent()
data class Error(val message: String) : SharedSecureStorageViewEvent()
data class FinishSuccess(val cypherResult: String) : SharedSecureStorageViewEvent()
data class Error(val message: String, val dismiss: Boolean = false) : SharedSecureStorageViewEvent()
data class InlineError(val message: String) : SharedSecureStorageViewEvent()
object ShowModalLoading : SharedSecureStorageViewEvent()
object HideModalLoading : SharedSecureStorageViewEvent()

View file

@ -22,6 +22,7 @@ import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.riotx.R
@ -34,6 +35,7 @@ import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.*
import timber.log.Timber
import javax.inject.Inject
class SharedSecureStorageActivity : SimpleFragmentActivity() {
@ -78,7 +80,7 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
.disposeOnDestroyView()
viewModel.subscribe(this) {
// renderState(it)
// renderState(it)
}
}
@ -88,6 +90,19 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
setResult(Activity.RESULT_CANCELED)
finish()
}
is SharedSecureStorageViewEvent.Error -> {
AlertDialog.Builder(this)
.setTitle(getString(R.string.dialog_title_error))
.setMessage(it.message)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
if (it.dismiss) {
setResult(Activity.RESULT_CANCELED)
finish()
}
}
.show()
}
is SharedSecureStorageViewEvent.ShowModalLoading -> {
showWaitingView()
}
@ -97,6 +112,12 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
is SharedSecureStorageViewEvent.UpdateLoadingState -> {
updateWaitingView(it.waitingData)
}
is SharedSecureStorageViewEvent.FinishSuccess -> {
val dataResult = Intent()
dataResult.putExtra(EXTRA_DATA_RESULT, it.cypherResult)
setResult(Activity.RESULT_OK, dataResult)
finish()
}
}
}
@ -105,6 +126,7 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
companion object {
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
const val RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
fun newIntent(context: Context, keyId: String? = null, requestedSecrets: List<String>, resultKeyStoreAlias: String = RESULT_KEYSTORE_ALIAS): Intent {
require(requestedSecrets.isNotEmpty())

View file

@ -22,33 +22,53 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.io.ByteArrayOutputStream
data class SharedSecureStorageViewState(
val requestedSecrets: List<String> = emptyList(),
val passphraseVisible: Boolean = false
) : MvRxState
class SharedSecureStorageViewModel @AssistedInject constructor(
@Assisted initialState: SharedSecureStorageViewState,
@Assisted val args: SharedSecureStorageActivity.Args,
private val stringProvider: StringProvider,
private val session: Session)
: VectorViewModel<SharedSecureStorageViewState, SharedSecureStorageAction, SharedSecureStorageViewEvent>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: SharedSecureStorageViewState): SharedSecureStorageViewModel
fun create(initialState: SharedSecureStorageViewState, args: SharedSecureStorageActivity.Args): SharedSecureStorageViewModel
}
override fun handle(action: SharedSecureStorageAction) = withState { state ->
init {
val isValid = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(args.requestedSecrets, args.keyId) is IntegrityResult.Success
if (!isValid) {
_viewEvents.post(
SharedSecureStorageViewEvent.Error(
stringProvider.getString(R.string.enter_secret_storage_invalid),
true
)
)
}
}
override fun handle(action: SharedSecureStorageAction) = withState {
when (action) {
is SharedSecureStorageAction.TogglePasswordVisibility -> {
setState {
@ -61,58 +81,68 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
}
is SharedSecureStorageAction.SubmitPassphrase -> {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val passphrase = action.passphrase
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
if (!keyInfoResult.isSuccess()) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key"))
return@withState
}
val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
val decryptedSecretMap = HashMap<String, String>()
GlobalScope.launch(Dispatchers.IO) {
runCatching {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val passphrase = action.passphrase
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
if (!keyInfoResult.isSuccess()) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key"))
return@launch
}
val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
// TODO
// val decryptedSecretMap = HashMap<String, String>()
// val errors = ArrayList<Throwable>()
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = true
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = true
)
))
val keySpec = Curve25519AesSha2KeySpec.fromPassphrase(
passphrase,
keyInfo.content.passphrase?.salt ?: "",
keyInfo.content.passphrase?.iterations ?: 0,
// TODO
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = false,
progress = progress,
progressTotal = total
)
))
}
}
)
))
state.requestedSecrets.forEach {
val keySpec = Curve25519AesSha2KeySpec.fromPassphrase(
passphrase,
keyInfo.content.passphrase?.salt ?: "",
keyInfo.content.passphrase?.iterations ?: 0,
// TODO
object : ProgressListener {
override fun onProgress(progress: Int, total: Int) {
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = false,
progress = progress,
progressTotal = total
)
))
}
}
)
session.sharedSecretStorageService.getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec,
callback = object : MatrixCallback<String> {
override fun onSuccess(data: String) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
}
override fun onFailure(failure: Throwable) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.InlineError(failure.localizedMessage))
withContext(Dispatchers.IO) {
args.requestedSecrets.forEach {
val res = awaitCallback<String> { callback ->
session.sharedSecretStorageService.getSecret(
name = it,
keyId = keyInfo.id,
secretKey = keySpec,
callback = callback)
}
})
decryptedSecretMap[it] = res
}
}
}.fold({
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
val safeForIntentCypher = ByteArrayOutputStream().also {
it.use {
session.securelyStoreObject(decryptedSecretMap as Map<String,String>, args.resultKeyStoreAlias, it)
}
}.toByteArray().toBase64NoPadding()
_viewEvents.post(SharedSecureStorageViewEvent.FinishSuccess(safeForIntentCypher))
}, {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.InlineError(it.localizedMessage))
})
}
}
}
@ -124,11 +154,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? {
val activity: SharedSecureStorageActivity = viewModelContext.activity()
val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
return activity.viewModelFactory.create(
SharedSecureStorageViewState(
requestedSecrets = args.requestedSecrets
)
)
return activity.viewModelFactory.create(state, args)
}
}
}

View file

@ -28,13 +28,15 @@ import butterknife.OnClick
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.textfield.TextInputLayout
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.utils.DebouncedClickListener
import io.reactivex.android.schedulers.AndroidSchedulers
import me.gujun.android.span.span
import timber.log.Timber
import java.util.concurrent.TimeUnit
class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
@ -83,7 +85,8 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
reasonText.text = getString(R.string.enter_secret_storage_passphrase_reason_verify)
mPassphraseTextEdit.editorActionEvents()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
submit()
@ -91,13 +94,6 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
}
.disposeOnDestroyView()
mPassphraseTextEdit.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_NULL) {
submit()
return@setOnEditorActionListener true
}
return@setOnEditorActionListener false
}
mPassphraseTextEdit.textChanges()
.subscribe {
@ -114,17 +110,21 @@ class SharedSecuredStoragePassphraseFragment : VectorBaseFragment() {
}
}
submitButton.setOnClickListener(DebouncedClickListener(
View.OnClickListener {
submitButton.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
submit()
}
))
.disposeOnDestroyView()
cancelButton.setOnClickListener(DebouncedClickListener(
View.OnClickListener {
cancelButton.clicks()
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
sharedViewModel.handle(SharedSecureStorageAction.Cancel)
}
))
.disposeOnDestroyView()
}
fun submit() {

View file

@ -30,4 +30,5 @@ sealed class VerificationAction : VectorViewModelAction {
object GotItConclusion : VerificationAction()
object SkipVerification : VerificationAction()
object VerifyFromPassphrase : VerificationAction()
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
}

View file

@ -15,11 +15,14 @@
*/
package im.vector.riotx.features.crypto.verification
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
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
@ -30,6 +33,9 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
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.crypto.sas.VerificationTxState
import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent
@ -87,15 +93,47 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
viewModel.observeViewEvents {
when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is VerificationBottomSheetViewEvents.AccessSecretStore -> {
startActivity(SharedSecureStorageActivity.newIntent(requireContext(),null, listOf("m.cross_signing.user_signing")))
startActivityForResult(SharedSecureStorageActivity.newIntent(
requireContext(),
null,// use default key
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
SharedSecureStorageActivity.RESULT_KEYSTORE_ALIAS
), SECRET_REQUEST_CODE)
}
is VerificationBottomSheetViewEvents.ModalError -> {
AlertDialog.Builder(requireContext())
.setTitle(getString(R.string.dialog_title_error))
.setMessage(it.errorMessage)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
}
.show()
Unit
}
}.exhaustive
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == SECRET_REQUEST_CODE) {
data?.getStringExtra(SharedSecureStorageActivity.EXTRA_DATA_RESULT)?.let {
viewModel.handle(VerificationAction.GotResultFromSsss(it, SharedSecureStorageActivity.RESULT_KEYSTORE_ALIAS))
}
}
super.onActivityResult(requestCode, resultCode, data)
}
override fun invalidate() = withState(viewModel) { state ->
if (state.selfVerificationMode && state.verifiedFromPrivateKeys) {
showFragment(VerificationConclusionFragment::class, Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationConclusionFragment.Args(true, null, state.isMe))
})
return@withState
}
state.otherUserMxItem?.let { matrixItem ->
if (state.isMe) {
if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) {
@ -235,6 +273,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
companion object {
const val SECRET_REQUEST_CODE = 101
fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
return VerificationBottomSheet().apply {
arguments = Bundle().apply {
@ -248,7 +289,7 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
}
fun forSelfVerification(session: Session) : VerificationBottomSheet{
fun forSelfVerification(session: Session): VerificationBottomSheet {
return VerificationBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(MvRx.KEY_ARG, VerificationArgs(

View file

@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents()
object AccessSecretStore : VerificationBottomSheetViewEvents()
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
}

View file

@ -28,6 +28,9 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
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.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction
@ -39,6 +42,8 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
@ -53,6 +58,7 @@ data class VerificationBottomSheetViewState(
val transactionId: String? = null,
// true when we display the loading and we wait for the other (incoming request)
val selfVerificationMode: Boolean = false,
val verifiedFromPrivateKeys: Boolean = false,
val isMe: Boolean = false
) : MvRxState
@ -250,12 +256,36 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
is VerificationAction.GotItConclusion -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
is VerificationAction.SkipVerification -> {
is VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
is VerificationAction.VerifyFromPassphrase -> {
is VerificationAction.VerifyFromPassphrase -> {
_viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore)
}
is VerificationAction.GotResultFromSsss -> {
try {
action.cypherData.fromBase64NoPadding().inputStream().use { ins ->
val res = session.loadSecureSecret<Map<String, String>>(ins, action.alias)
val trustResult = session.getCrossSigningService().checkTrustFromPrivateKeys(
res?.get(MASTER_KEY_SSSS_NAME),
res?.get(USER_SIGNING_KEY_SSSS_NAME),
res?.get(SELF_SIGNING_KEY_SSSS_NAME)
)
if (trustResult.isVerified()) {
setState {
copy(verifiedFromPrivateKeys = true)
}
} else {
// POP UP something
_viewEvents.post(VerificationBottomSheetViewEvents.ModalError("Failed to import keys"))
}
}
} catch (failure: Throwable) {
_viewEvents.post(VerificationBottomSheetViewEvents.ModalError(failure.localizedMessage))
}
Unit
}
}.exhaustive
}

View file

@ -36,6 +36,7 @@ class CrossSigningEpoxyController @Inject constructor(
interface InteractionListener {
fun onInitializeCrossSigningKeys()
fun onResetCrossSigningKeys()
fun verifySession()
}
var interactionListener: InteractionListener? = null
@ -77,21 +78,31 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.onResetCrossSigningKeys()
}
}
} else if (data.xSigningIsEnableInAccount) {
genericItem {
id("enable")
titleIconResourceId(R.drawable.ic_shield_black)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
}
} else if (data.xSigningIsEnableInAccount) {
genericItem {
id("enable")
titleIconResourceId(R.drawable.ic_shield_black)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
}
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.complete_security))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
listener {
interactionListener?.verifySession()
}
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
interactionListener?.onResetCrossSigningKeys()
}
}
bottomSheetVerificationActionItem {
id("resetkeys")
title("Reset keys")
titleColor(colorProvider.getColor(R.color.riotx_destructive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_destructive_accent))
listener {
interactionListener?.onResetCrossSigningKeys()
}
}
} else {

View file

@ -54,6 +54,11 @@ class CrossSigningSettingsFragment @Inject constructor(
is CrossSigningSettingsViewEvents.RequestPassword -> {
requestPassword()
}
CrossSigningSettingsViewEvents.VerifySession -> {
(requireActivity() as? VectorBaseActivity)?.let { activity ->
activity.navigator.waitSessionVerification(activity)
}
}
}.exhaustive
}
}
@ -93,6 +98,10 @@ class CrossSigningSettingsFragment @Inject constructor(
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
}
override fun verifySession() {
viewModel.handle(CrossSigningAction.VerifySession)
}
override fun onResetCrossSigningKeys() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_confirmation)

View file

@ -25,4 +25,5 @@ sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
object RequestPassword : CrossSigningSettingsViewEvents()
object VerifySession : CrossSigningSettingsViewEvents()
}

View file

@ -45,6 +45,7 @@ data class CrossSigningSettingsViewState(
sealed class CrossSigningAction : VectorViewModelAction {
object InitializeCrossSigning : CrossSigningAction()
object VerifySession : CrossSigningAction()
data class PasswordEntered(val password: String) : CrossSigningAction()
}
@ -88,6 +89,9 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
password = action.password
))
}
CrossSigningAction.VerifySession -> {
_viewEvents.post(CrossSigningSettingsViewEvents.VerifySession)
}
}.exhaustive
}

View file

@ -25,6 +25,8 @@
<string name="new_signin">New Sign In</string>
<string name="enter_secret_storage_invalid">Cannot find secrets in storage</string>
<string name="enter_secret_storage_passphrase">Enter secret storage passphrase</string>
<string name="enter_secret_storage_passphrase_warning">Warning:</string>
<string name="enter_secret_storage_passphrase_warning_text">You should only access secret storage from a trusted device</string>