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?, fun initializeCrossSigning(authParams: UserPasswordAuth?,
callback: MatrixCallback<Unit>? = null) callback: MatrixCallback<Unit>? = null)
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?,
callback: MatrixCallback<Unit>? = null) : UserTrustResult
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo? fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
fun getLiveCrossSigningKeys(userId: String): LiveData<Optional<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) * @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() 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.events.model.toContent
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec 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.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.KeyInfo
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.api.session.securestorage.KeySigner import im.vector.matrix.android.api.session.securestorage.KeySigner
@ -327,4 +328,37 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
const val ENCRYPTED = "encrypted" const val ENCRYPTED = "encrypted"
const val DEFAULT_KEY_ID = "m.secret_storage.default_key" 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 { val latestEvent = roomSummaryEntity.latestPreviewableEvent?.let {
timelineEventMapper.map(it, buildReadReceipts = false) timelineEventMapper.map(it, buildReadReceipts = false)
} }
return RoomSummary( return RoomSummary(
roomId = roomSummaryEntity.roomId, roomId = roomSummaryEntity.roomId,
displayName = roomSummaryEntity.displayName ?: "", displayName = roomSummaryEntity.displayName ?: "",

View File

@ -30,7 +30,8 @@ sealed class SharedSecureStorageAction : VectorViewModelAction {
sealed class SharedSecureStorageViewEvent : VectorViewEvents { sealed class SharedSecureStorageViewEvent : VectorViewEvents {
object Dismiss : SharedSecureStorageViewEvent() 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() data class InlineError(val message: String) : SharedSecureStorageViewEvent()
object ShowModalLoading : SharedSecureStorageViewEvent() object ShowModalLoading : SharedSecureStorageViewEvent()
object HideModalLoading : SharedSecureStorageViewEvent() object HideModalLoading : SharedSecureStorageViewEvent()

View File

@ -22,6 +22,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import im.vector.riotx.R import im.vector.riotx.R
@ -34,6 +35,7 @@ import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity.* import kotlinx.android.synthetic.main.activity.*
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
class SharedSecureStorageActivity : SimpleFragmentActivity() { class SharedSecureStorageActivity : SimpleFragmentActivity() {
@ -88,6 +90,19 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
setResult(Activity.RESULT_CANCELED) setResult(Activity.RESULT_CANCELED)
finish() 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 -> { is SharedSecureStorageViewEvent.ShowModalLoading -> {
showWaitingView() showWaitingView()
} }
@ -97,6 +112,12 @@ class SharedSecureStorageActivity : SimpleFragmentActivity() {
is SharedSecureStorageViewEvent.UpdateLoadingState -> { is SharedSecureStorageViewEvent.UpdateLoadingState -> {
updateWaitingView(it.waitingData) 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 { companion object {
const val EXTRA_DATA_RESULT = "EXTRA_DATA_RESULT"
const val RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity" const val RESULT_KEYSTORE_ALIAS = "SharedSecureStorageActivity"
fun newIntent(context: Context, keyId: String? = null, requestedSecrets: List<String>, resultKeyStoreAlias: String = RESULT_KEYSTORE_ALIAS): Intent { fun newIntent(context: Context, keyId: String? = null, requestedSecrets: List<String>, resultKeyStoreAlias: String = RESULT_KEYSTORE_ALIAS): Intent {
require(requestedSecrets.isNotEmpty()) require(requestedSecrets.isNotEmpty())

View File

@ -22,33 +22,53 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject 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.listeners.ProgressListener
import im.vector.matrix.android.api.session.Session 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.Curve25519AesSha2KeySpec
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult 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.R
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider 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( data class SharedSecureStorageViewState(
val requestedSecrets: List<String> = emptyList(),
val passphraseVisible: Boolean = false val passphraseVisible: Boolean = false
) : MvRxState ) : MvRxState
class SharedSecureStorageViewModel @AssistedInject constructor( class SharedSecureStorageViewModel @AssistedInject constructor(
@Assisted initialState: SharedSecureStorageViewState, @Assisted initialState: SharedSecureStorageViewState,
@Assisted val args: SharedSecureStorageActivity.Args,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val session: Session) private val session: Session)
: VectorViewModel<SharedSecureStorageViewState, SharedSecureStorageAction, SharedSecureStorageViewEvent>(initialState) { : VectorViewModel<SharedSecureStorageViewState, SharedSecureStorageAction, SharedSecureStorageViewEvent>(initialState) {
@AssistedInject.Factory @AssistedInject.Factory
interface 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) { when (action) {
is SharedSecureStorageAction.TogglePasswordVisibility -> { is SharedSecureStorageAction.TogglePasswordVisibility -> {
setState { setState {
@ -61,26 +81,25 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
_viewEvents.post(SharedSecureStorageViewEvent.Dismiss) _viewEvents.post(SharedSecureStorageViewEvent.Dismiss)
} }
is SharedSecureStorageAction.SubmitPassphrase -> { is SharedSecureStorageAction.SubmitPassphrase -> {
val decryptedSecretMap = HashMap<String, String>()
GlobalScope.launch(Dispatchers.IO) {
runCatching {
_viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading) _viewEvents.post(SharedSecureStorageViewEvent.ShowModalLoading)
val passphrase = action.passphrase val passphrase = action.passphrase
val keyInfoResult = session.sharedSecretStorageService.getDefaultKey() val keyInfoResult = session.sharedSecretStorageService.getDefaultKey()
if (!keyInfoResult.isSuccess()) { if (!keyInfoResult.isSuccess()) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading) _viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key")) _viewEvents.post(SharedSecureStorageViewEvent.Error("Cannot find ssss key"))
return@withState return@launch
} }
val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo val keyInfo = (keyInfoResult as KeyInfoResult.Success).keyInfo
// TODO
// val decryptedSecretMap = HashMap<String, String>()
// val errors = ArrayList<Throwable>()
_viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState( _viewEvents.post(SharedSecureStorageViewEvent.UpdateLoadingState(
WaitingViewData( WaitingViewData(
message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message), message = stringProvider.getString(R.string.keys_backup_restoring_computing_key_waiting_message),
isIndeterminate = true isIndeterminate = true
) )
)) ))
state.requestedSecrets.forEach {
val keySpec = Curve25519AesSha2KeySpec.fromPassphrase( val keySpec = Curve25519AesSha2KeySpec.fromPassphrase(
passphrase, passphrase,
keyInfo.content.passphrase?.salt ?: "", keyInfo.content.passphrase?.salt ?: "",
@ -99,19 +118,30 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
} }
} }
) )
withContext(Dispatchers.IO) {
args.requestedSecrets.forEach {
val res = awaitCallback<String> { callback ->
session.sharedSecretStorageService.getSecret( session.sharedSecretStorageService.getSecret(
name = it, name = it,
keyId = keyInfo.id, keyId = keyInfo.id,
secretKey = keySpec, secretKey = keySpec,
callback = object : MatrixCallback<String> { callback = callback)
override fun onSuccess(data: String) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
} }
decryptedSecretMap[it] = res
override fun onFailure(failure: Throwable) {
_viewEvents.post(SharedSecureStorageViewEvent.HideModalLoading)
_viewEvents.post(SharedSecureStorageViewEvent.InlineError(failure.localizedMessage))
} }
}
}.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? { override fun create(viewModelContext: ViewModelContext, state: SharedSecureStorageViewState): SharedSecureStorageViewModel? {
val activity: SharedSecureStorageActivity = viewModelContext.activity() val activity: SharedSecureStorageActivity = viewModelContext.activity()
val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG) val args: SharedSecureStorageActivity.Args = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
return activity.viewModelFactory.create( return activity.viewModelFactory.create(state, args)
SharedSecureStorageViewState(
requestedSecrets = args.requestedSecrets
)
)
} }
} }
} }

View File

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

View File

@ -30,4 +30,5 @@ sealed class VerificationAction : VectorViewModelAction {
object GotItConclusion : VerificationAction() object GotItConclusion : VerificationAction()
object SkipVerification : VerificationAction() object SkipVerification : VerificationAction()
object VerifyFromPassphrase : 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 package im.vector.riotx.features.crypto.verification
import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -30,6 +33,9 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.matrix.android.api.session.Session 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.matrix.android.api.session.crypto.sas.VerificationTxState
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -89,13 +95,45 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
when (it) { when (it) {
is VerificationBottomSheetViewEvents.Dismiss -> dismiss() is VerificationBottomSheetViewEvents.Dismiss -> dismiss()
is VerificationBottomSheetViewEvents.AccessSecretStore -> { 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 }.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 -> 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 -> state.otherUserMxItem?.let { matrixItem ->
if (state.isMe) { if (state.isMe) {
if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) { if (state.sasTransactionState == VerificationTxState.Verified || state.qrTransactionState == VerificationTxState.Verified) {
@ -235,6 +273,9 @@ class VerificationBottomSheet : VectorBaseBottomSheetDialogFragment() {
} }
companion object { companion object {
const val SECRET_REQUEST_CODE = 101
fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet { fun withArgs(roomId: String?, otherUserId: String, transactionId: String? = null): VerificationBottomSheet {
return VerificationBottomSheet().apply { return VerificationBottomSheet().apply {
arguments = Bundle().apply { arguments = Bundle().apply {

View File

@ -24,4 +24,5 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class VerificationBottomSheetViewEvents : VectorViewEvents { sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents() object Dismiss : VerificationBottomSheetViewEvents()
object AccessSecretStore : 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 com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session 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.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction
import im.vector.matrix.android.api.session.crypto.sas.SasVerificationTransaction 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.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.util.MatrixItem import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem 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.matrix.android.internal.crypto.verification.PendingVerificationRequest
import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -53,6 +58,7 @@ data class VerificationBottomSheetViewState(
val transactionId: String? = null, val transactionId: String? = null,
// true when we display the loading and we wait for the other (incoming request) // true when we display the loading and we wait for the other (incoming request)
val selfVerificationMode: Boolean = false, val selfVerificationMode: Boolean = false,
val verifiedFromPrivateKeys: Boolean = false,
val isMe: Boolean = false val isMe: Boolean = false
) : MvRxState ) : MvRxState
@ -256,6 +262,30 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini
is VerificationAction.VerifyFromPassphrase -> { is VerificationAction.VerifyFromPassphrase -> {
_viewEvents.post(VerificationBottomSheetViewEvents.AccessSecretStore) _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 }.exhaustive
} }

View File

@ -36,6 +36,7 @@ class CrossSigningEpoxyController @Inject constructor(
interface InteractionListener { interface InteractionListener {
fun onInitializeCrossSigningKeys() fun onInitializeCrossSigningKeys()
fun onResetCrossSigningKeys() fun onResetCrossSigningKeys()
fun verifySession()
} }
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
@ -77,12 +78,23 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.onResetCrossSigningKeys() interactionListener?.onResetCrossSigningKeys()
} }
} }
}
} else if (data.xSigningIsEnableInAccount) { } else if (data.xSigningIsEnableInAccount) {
genericItem { genericItem {
id("enable") id("enable")
titleIconResourceId(R.drawable.ic_shield_black) titleIconResourceId(R.drawable.ic_shield_black)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) 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 { bottomSheetVerificationActionItem {
id("resetkeys") id("resetkeys")
title("Reset keys") title("Reset keys")
@ -93,7 +105,6 @@ class CrossSigningEpoxyController @Inject constructor(
interactionListener?.onResetCrossSigningKeys() interactionListener?.onResetCrossSigningKeys()
} }
} }
}
} else { } else {
genericItem { genericItem {
id("not") id("not")

View File

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

View File

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

View File

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

View File

@ -25,6 +25,8 @@
<string name="new_signin">New Sign In</string> <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">Enter secret storage passphrase</string>
<string name="enter_secret_storage_passphrase_warning">Warning:</string> <string name="enter_secret_storage_passphrase_warning">Warning:</string>
<string name="enter_secret_storage_passphrase_warning_text">You should only access secret storage from a trusted device</string> <string name="enter_secret_storage_passphrase_warning_text">You should only access secret storage from a trusted device</string>