WIP / Verify from passphrase UX
This commit is contained in:
parent
cb669ad881
commit
9a08f5ec4e
@ -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>>
|
||||
|
@ -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"
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ┏━━━━━━━━┓ ┏━━━━━━━━┓
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 ?: "",
|
||||
|
@ -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()
|
||||
|
@ -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())
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -25,4 +25,5 @@ sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
|
||||
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
|
||||
|
||||
object RequestPassword : CrossSigningSettingsViewEvents()
|
||||
object VerifySession : CrossSigningSettingsViewEvents()
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user