Merge pull request #1489 from vector-im/feature/cross_sigining_evol

Feature/cross sigining evol
This commit is contained in:
Valere 2020-06-29 17:27:25 +02:00 committed by GitHub
commit c23819bfcf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
68 changed files with 1445 additions and 788 deletions

View File

@ -8,6 +8,7 @@ Features ✨:
Improvements 🙌:
- "Add Matrix app" menu is now always visible (#1495)
- Handle `/op`, `/deop`, and `/nick` commands (#12)
- Prioritising Recovery key over Recovery passphrase (#1463)
Bugfix 🐛:
- Fix dark theme issue on login screen (#1097)

View File

@ -1,3 +1,7 @@
Useful links:
- https://codelabs.developers.google.com/codelabs/webrtc-web/#0
╔════════════════════════════════════════════════╗
║ ║
║A] Placing a call offer ║

View File

@ -20,10 +20,13 @@ import androidx.lifecycle.LiveData
interface InitialSyncProgressService {
fun getInitialSyncProgressStatus() : LiveData<Status?>
fun getInitialSyncProgressStatus(): LiveData<Status>
data class Status(
@StringRes val statusText: Int,
val percentProgress: Int = 0
)
sealed class Status {
object Idle : Status()
data class Progressing(
@StringRes val statusText: Int,
val percentProgress: Int = 0
) : Status()
}
}

View File

@ -99,7 +99,9 @@ interface CryptoService {
fun removeRoomKeysRequestListener(listener: GossipingRequestListener)
fun fetchDevicesList(callback: MatrixCallback<DevicesListResponse>)
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun getDeviceInfo(deviceId: String, callback: MatrixCallback<DeviceInfo>)

View File

@ -41,11 +41,13 @@ interface CrossSigningService {
* Users needs to enter credentials
*/
fun initializeCrossSigning(authParams: UserPasswordAuth?,
callback: MatrixCallback<Unit>? = null)
callback: MatrixCallback<Unit>)
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null
fun checkTrustFromPrivateKeys(masterKeyPrivateKey: String?,
uskKeyPrivateKey: String?,
sskPrivateKey: String?) : UserTrustResult
sskPrivateKey: String?): UserTrustResult
fun getUserCrossSigningKeys(otherUserId: String): MXCrossSigningInfo?
@ -74,6 +76,8 @@ interface CrossSigningService {
otherDeviceId: String,
locallyTrusted: Boolean?): DeviceTrustResult
// FIXME Those method do not have to be in the service
fun onSecretMSKGossip(mskPrivateKey: String)
fun onSecretSSKGossip(sskPrivateKey: String)
fun onSecretUSKGossip(uskPrivateKey: String)
}

View File

@ -18,6 +18,9 @@ package im.vector.matrix.android.api.session.securestorage
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.crosssigning.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
/**
* Some features may require clients to store encrypted data on the server so that it can be shared securely between clients.
@ -111,7 +114,17 @@ interface SharedSecretStorageService {
*/
fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>)
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?) : IntegrityResult
/**
* Return true if SSSS is configured
*/
fun isRecoverySetup(): Boolean {
return checkShouldBeAbleToAccessSecrets(
secretNames = listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
keyId = null
) is IntegrityResult.Success
}
fun checkShouldBeAbleToAccessSecrets(secretNames: List<String>, keyId: String?): IntegrityResult
fun requestSecret(name: String, myOtherDeviceId: String)

View File

@ -29,11 +29,13 @@ import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.NoOpMatrixCallback
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.extensions.tryThis
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
@ -326,35 +328,67 @@ internal class DefaultCryptoService @Inject constructor(
* and, then, if this is the first time, this new device will be announced to all other users
* devices.
*
* @param isInitialSync true if it starts from an initial sync
*/
fun start(isInitialSync: Boolean) {
if (isStarted.get() || isStarting.get()) {
return
}
isStarting.set(true)
fun start() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
internalStart(isInitialSync)
internalStart()
}
// Just update
fetchDevicesList(NoOpMatrixCallback())
}
private suspend fun internalStart(isInitialSync: Boolean) {
// Open the store
cryptoStore.open()
runCatching {
fun ensureDevice() {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
// Open the store
cryptoStore.open()
// TODO why do that everytime? we should mark that it was done
uploadDeviceKeys()
oneTimeKeysUploader.maybeUploadOneTimeKeys()
keysBackupService.checkAndStartKeysBackup()
if (isInitialSync) {
// refresh the devices list for each known room members
deviceListManager.invalidateAllDeviceLists()
deviceListManager.refreshOutdatedDeviceLists()
} else {
incomingGossipingRequestManager.processReceivedGossipingRequests()
// this can throw if no backup
tryThis {
keysBackupService.checkAndStartKeysBackup()
}
}
}
fun onSyncWillProcess(isInitialSync: Boolean) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (isInitialSync) {
try {
// On initial sync, we start all our tracking from
// scratch, so mark everything as untracked. onCryptoEvent will
// be called for all e2e rooms during the processing of the sync,
// at which point we'll start tracking all the users of that room.
deviceListManager.invalidateAllDeviceLists()
// always track my devices?
deviceListManager.startTrackingDeviceList(listOf(userId))
deviceListManager.refreshOutdatedDeviceLists()
} catch (failure: Throwable) {
Timber.e(failure, "## CRYPTO onSyncWillProcess ")
}
}
}
}
private fun internalStart() {
if (isStarted.get() || isStarting.get()) {
return
}
isStarting.set(true)
// Open the store
cryptoStore.open()
runCatching {
// if (isInitialSync) {
// // refresh the devices list for each known room members
// deviceListManager.invalidateAllDeviceLists()
// deviceListManager.refreshOutdatedDeviceLists()
// } else {
// Why would we do that? it will be called at end of syn
incomingGossipingRequestManager.processReceivedGossipingRequests()
// }
}.fold(
{
isStarting.set(false)
@ -623,10 +657,10 @@ internal class DefaultCryptoService @Inject constructor(
roomId: String,
callback: MatrixCallback<MXEncryptEventContentResult>) {
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
internalStart(false)
}
// if (!isStarted()) {
// Timber.v("## CRYPTO | encryptEventContent() : wait after e2e init")
// internalStart(false)
// }
val userIds = getRoomUserIds(roomId)
var alg = roomEncryptorsStore.get(roomId)
if (alg == null) {
@ -835,6 +869,10 @@ internal class DefaultCryptoService @Inject constructor(
*/
private fun handleSDKLevelGossip(secretName: String?, secretValue: String): Boolean {
return when (secretName) {
MASTER_KEY_SSSS_NAME -> {
crossSigningService.onSecretMSKGossip(secretValue)
true
}
SELF_SIGNING_KEY_SSSS_NAME -> {
crossSigningService.onSecretSSKGossip(secretValue)
true
@ -1153,10 +1191,10 @@ internal class DefaultCryptoService @Inject constructor(
}
cryptoCoroutineScope.launch(coroutineDispatchers.crypto) {
if (!isStarted()) {
Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
internalStart(false)
}
// if (!isStarted()) {
// Timber.v("## CRYPTO | requestRoomKeyForEvent() : wait after e2e init")
// internalStart(false)
// }
roomDecryptorProvider
.getOrCreateRoomDecryptor(event.roomId, wireContent.algorithm)
?.requestKeysForEvent(event) ?: run {

View File

@ -156,6 +156,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* @param left the user ids list which left a room
*/
fun handleDeviceListsChanges(changed: Collection<String>, left: Collection<String>) {
Timber.v("## CRYPTO: handleDeviceListsChanges changed:$changed / left:$left")
var isUpdated = false
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
@ -483,6 +484,7 @@ internal class DeviceListManager @Inject constructor(private val cryptoStore: IM
* This method must be called on getEncryptingThreadHandler() thread.
*/
suspend fun refreshOutdatedDeviceLists() {
Timber.v("## CRYPTO | refreshOutdatedDeviceLists()")
val deviceTrackingStatuses = cryptoStore.getDeviceTrackingStatuses().toMutableMap()
val users = deviceTrackingStatuses.keys.filterTo(mutableListOf()) { userId ->

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.crypto
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.keyshare.GossipingRequestListener
@ -310,8 +311,8 @@ internal class IncomingGossipingRequestManager @Inject constructor(
val isDeviceLocallyVerified = cryptoStore.getUserDevice(userId, deviceId)?.trustLevel?.isLocallyVerified()
// Should SDK always Silently reject any request for the master key?
when (secretName) {
MASTER_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.master
SELF_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.selfSigned
USER_SIGNING_KEY_SSSS_NAME -> cryptoStore.getCrossSigningPrivateKeys()?.user
KEYBACKUP_SECRET_SSSS_NAME -> cryptoStore.getKeyBackupRecoveryKeyInfo()?.recoveryKey

View File

@ -140,7 +140,7 @@ internal class DefaultCrossSigningService @Inject constructor(
* - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
*/
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>?) {
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) {
Timber.d("## CrossSigning initializeCrossSigning")
val params = InitializeCrossSigningTask.Params(
@ -150,7 +150,8 @@ internal class DefaultCrossSigningService @Inject constructor(
this.callbackThread = TaskThread.CRYPTO
this.callback = object : MatrixCallback<InitializeCrossSigningTask.Result> {
override fun onFailure(failure: Throwable) {
callback?.onFailure(failure)
Timber.e(failure, "Error in initializeCrossSigning()")
callback.onFailure(failure)
}
override fun onSuccess(data: InitializeCrossSigningTask.Result) {
@ -162,12 +163,39 @@ internal class DefaultCrossSigningService @Inject constructor(
userPkSigning = OlmPkSigning().apply { initWithSeed(data.userKeyPK.fromBase64()) }
selfSigningPkSigning = OlmPkSigning().apply { initWithSeed(data.selfSigningKeyPK.fromBase64()) }
callback?.onSuccess(Unit)
callback.onSuccess(Unit)
}
}
}.executeBy(taskExecutor)
}
override fun onSecretMSKGossip(mskPrivateKey: String) {
Timber.i("## CrossSigning - onSecretSSKGossip")
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {
Timber.e("## CrossSigning - onSecretMSKGossip() received secret but public key is not known")
}
mskPrivateKey.fromBase64()
.let { privateKeySeed ->
val pkSigning = OlmPkSigning()
try {
if (pkSigning.initWithSeed(privateKeySeed) == mxCrossSigningInfo.masterKey()?.unpaddedBase64PublicKey) {
masterPkSigning?.releaseSigning()
masterPkSigning = pkSigning
Timber.i("## CrossSigning - Loading MSK success")
cryptoStore.storeMSKPrivateKey(mskPrivateKey)
return
} else {
Timber.e("## CrossSigning - onSecretMSKGossip() private key do not match public key")
pkSigning.releaseSigning()
}
} catch (failure: Throwable) {
Timber.e("## CrossSigning - onSecretMSKGossip() ${failure.localizedMessage}")
pkSigning.releaseSigning()
}
}
}
override fun onSecretSSKGossip(sskPrivateKey: String) {
Timber.i("## CrossSigning - onSecretSSKGossip")
val mxCrossSigningInfo = getMyCrossSigningKeys() ?: return Unit.also {

View File

@ -32,16 +32,15 @@ internal data class SignatureUploadResponse(
* If a signature is not valid, the homeserver should set the corresponding entry in failures to a JSON object
* with the errcode property set to M_INVALID_SIGNATURE.
*/
val failures: Map<String, Map<String, @JvmSuppressWildcards Any>>? = null
val failures: Map<String, Map<String, UploadResponseFailure>>? = null
)
@JsonClass(generateAdapter = true)
data class UploadResponseFailure(
internal data class UploadResponseFailure(
@Json(name = "status")
val status: Int,
@Json(name = "errCode")
@Json(name = "errcode")
val errCode: String,
@Json(name = "message")

View File

@ -212,7 +212,9 @@ internal interface IMXCryptoStore {
fun getLiveDeviceList(): LiveData<List<CryptoDeviceInfo>>
fun getMyDevicesInfo() : List<DeviceInfo>
fun getLiveMyDevicesInfo() : LiveData<List<DeviceInfo>>
fun saveMyDevicesInfo(info: List<DeviceInfo>)
/**
* Store the crypto algorithm for a room.
@ -397,6 +399,7 @@ internal interface IMXCryptoStore {
fun markMyMasterKeyAsLocallyTrusted(trusted: Boolean)
fun storePrivateKeysInfo(msk: String?, usk: String?, ssk: String?)
fun storeMSKPrivateKey(msk: String?)
fun storeSSKPrivateKey(ssk: String?)
fun storeUSKPrivateKey(usk: String?)

View File

@ -174,7 +174,11 @@ internal class RealmCryptoStore @Inject constructor(
}
override fun open() {
realmLocker = Realm.getInstance(realmConfiguration)
synchronized(this) {
if (realmLocker == null) {
realmLocker = Realm.getInstance(realmConfiguration)
}
}
}
override fun close() {
@ -395,6 +399,14 @@ internal class RealmCryptoStore @Inject constructor(
}
}
override fun storeMSKPrivateKey(msk: String?) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {
xSignMasterPrivateKey = msk
}
}
}
override fun storeSSKPrivateKey(ssk: String?) {
doRealmTransaction(realmConfiguration) { realm ->
realm.where<CryptoMetadataEntity>().findFirst()?.apply {

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.crypto.tasks
import im.vector.matrix.android.api.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.internal.crypto.api.CryptoApi
@ -31,12 +32,21 @@ import javax.inject.Inject
internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Unit> {
data class Params(
// the device keys to send.
// the MSK
val masterKey: CryptoCrossSigningKey,
// the one-time keys to send.
// the USK
val userKey: CryptoCrossSigningKey,
// the explicit device_id to use for upload (default is to use the same as that used during auth).
// the SSK
val selfSignedKey: CryptoCrossSigningKey,
/**
* - If null:
* - no retry will be performed
* - If not null, it may or may not contain a sessionId:
* - If sessionId is null:
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
* - If sessionId is not null:
* - password should not be null as well, and no retry will be performed
*/
val userPasswordAuth: UserPasswordAuth?
)
}
@ -47,42 +57,41 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
private val cryptoApi: CryptoApi,
private val eventBus: EventBus
) : UploadSigningKeysTask {
override suspend fun execute(params: UploadSigningKeysTask.Params) {
val paramsHaveSessionId = params.userPasswordAuth?.session != null
val uploadQuery = UploadSigningKeysBody(
masterKey = params.masterKey.toRest(),
userSigningKey = params.userKey.toRest(),
selfSigningKey = params.selfSignedKey.toRest(),
auth = params.userPasswordAuth.takeIf { params.userPasswordAuth?.session != null }
// If sessionId is provided, use the userPasswordAuth
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
)
try {
// Make a first request to start user-interactive authentication
val request = executeRequest<KeysQueryResponse>(eventBus) {
apiCall = cryptoApi.uploadSigningKeys(uploadQuery)
}
if (request.failures?.isNotEmpty() == true) {
throw UploadSigningKeys(request.failures)
}
return
doRequest(uploadQuery)
} catch (throwable: Throwable) {
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
if (registrationFlowResponse != null
&& params.userPasswordAuth != null
/* Avoid infinite loop */
&& params.userPasswordAuth.session.isNullOrEmpty()
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
&& params.userPasswordAuth?.password != null
&& !paramsHaveSessionId
) {
// Retry with authentication
val req = executeRequest<KeysQueryResponse>(eventBus) {
apiCall = cryptoApi.uploadSigningKeys(
uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session))
)
}
if (req.failures?.isNotEmpty() == true) {
throw UploadSigningKeys(req.failures)
}
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
} else {
// Other error
throw throwable
}
}
}
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {
val keysQueryResponse = executeRequest<KeysQueryResponse>(eventBus) {
apiCall = cryptoApi.uploadSigningKeys(uploadQuery)
}
if (keysQueryResponse.failures?.isNotEmpty() == true) {
throw UploadSigningKeys(keysQueryResponse.failures)
}
}
}

View File

@ -23,6 +23,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
@ -809,6 +810,8 @@ internal class DefaultVerificationService @Inject constructor(
?.let { vt ->
val otherDeviceId = vt.otherDeviceId
if (!crossSigningService.canCrossSign()) {
outgoingGossipingRequestManager.sendSecretShareRequest(MASTER_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
?: "*")))
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
?: "*")))
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
@ -821,7 +824,7 @@ internal class DefaultVerificationService @Inject constructor(
}
private fun handleDoneReceived(senderId: String, doneReq: ValidVerificationDone) {
Timber.v("## SAS Done receieved $doneReq")
Timber.v("## SAS Done received $doneReq")
val existing = getExistingTransaction(senderId, doneReq.transactionId)
if (existing == null) {
Timber.e("## SAS Received invalid Done request")

View File

@ -25,11 +25,11 @@ import javax.inject.Inject
@SessionScope
class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgressService {
private var status = MutableLiveData<InitialSyncProgressService.Status>()
private val status = MutableLiveData<InitialSyncProgressService.Status>()
private var rootTask: TaskInfo? = null
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status?> {
override fun getInitialSyncProgressStatus(): LiveData<InitialSyncProgressService.Status> {
return status
}
@ -63,13 +63,13 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
parent?.setProgress(endedTask.offset + (endedTask.totalProgress * endedTask.parentWeight).toInt())
}
if (endedTask?.parent == null) {
status.postValue(null)
status.postValue(InitialSyncProgressService.Status.Idle)
}
}
fun endAll() {
rootTask = null
status.postValue(null)
status.postValue(InitialSyncProgressService.Status.Idle)
}
private inner class TaskInfo(@StringRes var nameRes: Int,
@ -102,9 +102,7 @@ class DefaultInitialSyncProgressService @Inject constructor() : InitialSyncProgr
it.setProgress(offset + parentProgress)
} ?: run {
Timber.v("--- ${leaf().nameRes}: $currentProgress")
status.postValue(
InitialSyncProgressService.Status(leaf().nameRes, currentProgress)
)
status.postValue(InitialSyncProgressService.Status.Progressing(leaf().nameRes, currentProgress))
}
}
}

View File

@ -145,6 +145,7 @@ internal class DefaultSession @Inject constructor(
override fun open() {
assert(!isOpen)
isOpen = true
cryptoService.get().ensureDevice()
uiHandler.post {
lifecycleObservers.forEach { it.onStart() }
}

View File

@ -51,8 +51,9 @@ internal class SyncResponseHandler @Inject constructor(@SessionDatabase private
measureTimeMillis {
if (!cryptoService.isStarted()) {
Timber.v("Should start cryptoService")
cryptoService.start(isInitialSync)
cryptoService.start()
}
cryptoService.onSyncWillProcess(isInitialSync)
}.also {
Timber.v("Finish handling start cryptoService in $it ms")
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.utils
import org.amshove.kluent.shouldBe
import org.junit.Test
import java.lang.Thread.sleep
class TemporaryStoreTest {
@Test
fun testTemporaryStore() {
// Keep the data 30 millis
val store = TemporaryStore<String>(30)
store.data = "test"
store.data shouldBe "test"
sleep(15)
store.data shouldBe "test"
sleep(20)
store.data shouldBe null
}
}

View File

@ -32,6 +32,7 @@ import im.vector.riotx.features.crypto.recover.BootstrapConfirmPassphraseFragmen
import im.vector.riotx.features.crypto.recover.BootstrapEnterPassphraseFragment
import im.vector.riotx.features.crypto.recover.BootstrapMigrateBackupFragment
import im.vector.riotx.features.crypto.recover.BootstrapSaveRecoveryKeyFragment
import im.vector.riotx.features.crypto.recover.BootstrapSetupRecoveryKeyFragment
import im.vector.riotx.features.crypto.recover.BootstrapWaitingFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationCancelFragment
import im.vector.riotx.features.crypto.verification.cancel.VerificationNotMeFragment
@ -468,6 +469,11 @@ interface FragmentModule {
@FragmentKey(BootstrapWaitingFragment::class)
fun bindBootstrapWaitingFragment(fragment: BootstrapWaitingFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BootstrapSetupRecoveryKeyFragment::class)
fun bindBootstrapSetupRecoveryKeyFragment(fragment: BootstrapSetupRecoveryKeyFragment): Fragment
@Binds
@IntoMap
@FragmentKey(BootstrapSaveRecoveryKeyFragment::class)

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.core.utils
import java.util.Timer
import java.util.TimerTask
const val THREE_MINUTES = 3 * 60_000L
/**
* Store an object T for a specific period of time
*/
open class TemporaryStore<T>(private val delay: Long = THREE_MINUTES) {
private var timer: Timer? = null
var data: T? = null
set(value) {
field = value
timer?.cancel()
timer = Timer().also {
it.schedule(object : TimerTask() {
override fun run() {
field = null
}
}, delay)
}
}
}

View File

@ -44,11 +44,11 @@ class BackupToQuadSMigrationTask @Inject constructor(
sealed class Result {
object Success : Result()
abstract class Failure(val error: String?) : Result()
abstract class Failure(val throwable: Throwable?) : Result()
object InvalidRecoverySecret : Failure(null)
object NoKeyBackupVersion : Failure(null)
object IllegalParams : Failure(null)
class ErrorFailure(throwable: Throwable) : Failure(throwable.localizedMessage)
class ErrorFailure(throwable: Throwable) : Failure(throwable)
}
data class Params(
@ -97,7 +97,7 @@ class BackupToQuadSMigrationTask @Inject constructor(
when {
params.passphrase?.isNotEmpty() == true -> {
reportProgress(params, R.string.bootstrap_progress_generating_ssss)
awaitCallback {
awaitCallback<SsssKeyCreationInfo> {
quadS.generateKeyWithPassphrase(
UUID.randomUUID().toString(),
"ssss_key",

View File

@ -16,7 +16,6 @@
package im.vector.riotx.features.crypto.recover
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.core.platform.VectorViewModelAction
import java.io.OutputStream
@ -29,8 +28,12 @@ sealed class BootstrapActions : VectorViewModelAction {
object GoToCompleted : BootstrapActions()
object GoToEnterAccountPassword : BootstrapActions()
data class DoInitialize(val passphrase: String, val auth: UserPasswordAuth? = null) : BootstrapActions()
data class DoInitializeGeneratedKey(val auth: UserPasswordAuth? = null) : BootstrapActions()
data class Start(val userWantsToEnterPassphrase: Boolean) : BootstrapActions()
object StartKeyBackupMigration : BootstrapActions()
data class DoInitialize(val passphrase: String) : BootstrapActions()
object DoInitializeGeneratedKey : BootstrapActions()
object TogglePasswordVisibility : BootstrapActions()
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()

View File

@ -26,6 +26,7 @@ import android.view.ViewGroup
import android.view.WindowManager
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.fragmentViewModel
@ -44,7 +45,7 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
@Parcelize
data class Args(
val isNewAccount: Boolean
val initCrossSigningOnly: Boolean
) : Parcelable
override val showExpanded = true
@ -76,24 +77,17 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
KeepItSafeDialog().show(requireActivity())
}
is BootstrapViewEvents.SkipBootstrap -> {
promptSkip(event.genKeyOption)
promptSkip()
}
}
}
}
private fun promptSkip(genKeyOption: Boolean) {
private fun promptSkip() {
AlertDialog.Builder(requireContext())
.setTitle(R.string.are_you_sure)
.setMessage(if (genKeyOption) R.string.bootstrap_skip_text else R.string.bootstrap_skip_text_no_gen_key)
.setMessage(R.string.bootstrap_cancel_text)
.setPositiveButton(R.string._continue, null)
.apply {
if (genKeyOption) {
setNeutralButton(R.string.generate_message_key) { _, _ ->
viewModel.handle(BootstrapActions.DoInitializeGeneratedKey())
}
}
}
.setNegativeButton(R.string.skip) { _, _ ->
dismiss()
}
@ -120,49 +114,57 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
}
override fun invalidate() = withState(viewModel) { state ->
when (state.step) {
is BootstrapStep.CheckingMigration -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
bootstrapTitleText.text = getString(R.string.upgrade_security)
bootstrapIcon.isVisible = false
bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title)
showFragment(BootstrapWaitingFragment::class, Bundle())
}
is BootstrapStep.FirstForm -> {
bootstrapIcon.isVisible = false
bootstrapTitleText.text = getString(R.string.bottom_sheet_setup_secure_backup_title)
showFragment(BootstrapSetupRecoveryKeyFragment::class, Bundle())
}
is BootstrapStep.SetupPassphrase -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
bootstrapTitleText.text = getString(R.string.set_recovery_passphrase, getString(R.string.recovery_passphrase))
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_phrase_24dp))
bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
showFragment(BootstrapEnterPassphraseFragment::class, Bundle())
}
is BootstrapStep.ConfirmPassphrase -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_password))
bootstrapTitleText.text = getString(R.string.confirm_recovery_passphrase, getString(R.string.recovery_passphrase))
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_phrase_24dp))
bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
}
is BootstrapStep.AccountPassword -> {
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
bootstrapTitleText.text = getString(R.string.account_password)
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
}
is BootstrapStep.Initializing -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_key_24dp))
bootstrapTitleText.text = getString(R.string.bootstrap_loading_title)
showFragment(BootstrapWaitingFragment::class, Bundle())
}
is BootstrapStep.SaveRecoveryKey -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
bootstrapTitleText.text = getString(R.string.keys_backup_setup_step3_please_make_copy)
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_key_24dp))
bootstrapTitleText.text = getString(R.string.bottom_sheet_save_your_recovery_key_title)
showFragment(BootstrapSaveRecoveryKeyFragment::class, Bundle())
}
is BootstrapStep.DoneSuccess -> {
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_message_key))
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_security_key_24dp))
bootstrapTitleText.text = getString(R.string.bootstrap_finish_title)
showFragment(BootstrapConclusionFragment::class, Bundle())
}
is BootstrapStep.GetBackupSecretForMigration -> {
val isKey = when (state.step) {
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
else -> true
}
val drawableRes = if (isKey) R.drawable.ic_message_key else R.drawable.ic_message_password
val isKey = state.step.useKey()
val drawableRes = if (isKey) R.drawable.ic_security_key_24dp else R.drawable.ic_security_phrase_24dp
bootstrapIcon.isVisible = true
bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(
requireContext(),
drawableRes)
@ -178,10 +180,10 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment() {
const val EXTRA_ARGS = "EXTRA_ARGS"
fun show(fragmentManager: FragmentManager, isAccountCreation: Boolean) {
fun show(fragmentManager: FragmentManager, initCrossSigningOnly: Boolean) {
BootstrapBottomSheet().apply {
isCancelable = false
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(isAccountCreation)) }
arguments = Bundle().apply { this.putParcelable(EXTRA_ARGS, Args(initCrossSigningOnly)) }
}.show(fragmentManager, "BootstrapBottomSheet")
}
}

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.crypto.recover
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.core.text.toSpannable
import androidx.core.view.isGone
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
@ -29,16 +28,12 @@ import im.vector.riotx.R
import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.colorizeMatchingText
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class BootstrapConfirmPassphraseFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment() {
class BootstrapConfirmPassphraseFragment @Inject constructor() : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase
@ -49,12 +44,8 @@ class BootstrapConfirmPassphraseFragment @Inject constructor(
ssss_passphrase_security_progress.isGone = true
val recPassPhrase = getString(R.string.recovery_passphrase)
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_confirm_text, recPassPhrase)
.toSpannable()
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_confirm_passphrase)
bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_again_notice)
ssss_passphrase_enter_edittext.hint = getString(R.string.set_a_security_phrase_hint)
withState(sharedViewModel) {
// set initial value (useful when coming back)

View File

@ -40,12 +40,14 @@ import im.vector.riotx.core.platform.ViewModelTask
import im.vector.riotx.core.platform.WaitingViewData
import im.vector.riotx.core.resources.StringProvider
import timber.log.Timber
import java.lang.IllegalArgumentException
import java.util.UUID
import javax.inject.Inject
sealed class BootstrapResult {
data class Success(val keyInfo: SsssKeyCreationInfo) : BootstrapResult()
object SuccessCrossSigningOnly : BootstrapResult()
abstract class Failure(val error: String?) : BootstrapResult()
@ -58,7 +60,7 @@ sealed class BootstrapResult {
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
object MissingPrivateKey : Failure(null)
data class PasswordAuthFlowMissing(val sessionId: String, val userId: String) : Failure(null)
data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null)
}
interface BootstrapProgressListener {
@ -67,31 +69,45 @@ interface BootstrapProgressListener {
data class Params(
val userPasswordAuth: UserPasswordAuth? = null,
val initOnlyCrossSigning: Boolean = false,
val progressListener: BootstrapProgressListener? = null,
val passphrase: String?,
val keySpec: SsssKeySpec? = null
)
// TODO Rename to CreateServerRecovery
class BootstrapCrossSigningTask @Inject constructor(
private val session: Session,
private val stringProvider: StringProvider
) : ViewModelTask<Params, BootstrapResult> {
override suspend fun execute(params: Params): BootstrapResult {
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
isIndeterminate = true
)
)
val crossSigningService = session.cryptoService().crossSigningService()
try {
awaitCallback<Unit> {
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
// Ensure cross-signing is initialized. Due to migration it is maybe not always correctly initialized
if (!crossSigningService.isCrossSigningInitialized()) {
params.progressListener?.onProgress(
WaitingViewData(
stringProvider.getString(R.string.bootstrap_crosssigning_progress_initializing),
isIndeterminate = true
)
)
try {
awaitCallback<Unit> {
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
}
if (params.initOnlyCrossSigning) {
return BootstrapResult.SuccessCrossSigningOnly
}
} catch (failure: Throwable) {
return handleInitializeXSigningError(failure)
}
} else {
// not sure how this can happen??
if (params.initOnlyCrossSigning) {
return handleInitializeXSigningError(IllegalArgumentException("Cross signing already setup"))
}
} catch (failure: Throwable) {
return handleInitializeXSigningError(failure)
}
val keyInfo: SsssKeyCreationInfo
@ -232,9 +248,11 @@ class BootstrapCrossSigningTask @Inject constructor(
} else {
val registrationFlowResponse = failure.toRegistrationFlowResponse()
if (registrationFlowResponse != null) {
if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } != true) {
return if (registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
BootstrapResult.PasswordAuthFlowMissing(registrationFlowResponse.session ?: "")
} else {
// can't do this from here
return BootstrapResult.UnsupportedAuthFlow()
BootstrapResult.UnsupportedAuthFlow()
}
}
}

View File

@ -19,7 +19,6 @@ package im.vector.riotx.features.crypto.recover
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.core.text.toSpannable
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.editorActionEvents
@ -27,17 +26,13 @@ import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.riotx.R
import im.vector.riotx.core.extensions.showPassword
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.colorizeMatchingText
import im.vector.riotx.features.settings.VectorLocale
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.fragment_bootstrap_enter_passphrase.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class BootstrapEnterPassphraseFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment() {
class BootstrapEnterPassphraseFragment @Inject constructor() : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_bootstrap_enter_passphrase
@ -46,12 +41,9 @@ class BootstrapEnterPassphraseFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recPassPhrase = getString(R.string.recovery_passphrase)
bootstrapDescriptionText.text = getString(R.string.bootstrap_info_text, recPassPhrase)
.toSpannable()
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
bootstrapDescriptionText.text = getString(R.string.set_a_security_phrase_notice)
ssss_passphrase_enter_edittext.hint = getString(R.string.set_a_security_phrase_hint)
ssss_passphrase_enter_edittext.hint = getString(R.string.passphrase_enter_passphrase)
withState(sharedViewModel) {
// set initial value (useful when coming back)
ssss_passphrase_enter_edittext.setText(it.passphrase ?: "")

View File

@ -86,17 +86,12 @@ class BootstrapMigrateBackupFragment @Inject constructor(
}
private fun submit() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.GetBackupSecretForMigration) {
return@withState
}
val isEnteringKey =
when (state.step) {
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
else -> true
}
val getBackupSecretForMigration = state.step as? BootstrapStep.GetBackupSecretForMigration ?: return@withState
val isEnteringKey = getBackupSecretForMigration.useKey()
val secret = bootstrapMigrateEditText.text?.toString()
if (secret.isNullOrBlank()) {
if (secret.isNullOrEmpty()) {
val errRes = if (isEnteringKey) R.string.recovery_key_empty_error_message else R.string.passphrase_empty_error_message
bootstrapRecoveryKeyEnterTil.error = getString(errRes)
} else if (isEnteringKey && !isValidRecoveryKey(secret)) {
@ -112,15 +107,9 @@ class BootstrapMigrateBackupFragment @Inject constructor(
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.GetBackupSecretForMigration) {
return@withState
}
val getBackupSecretForMigration = state.step as? BootstrapStep.GetBackupSecretForMigration ?: return@withState
val isEnteringKey =
when (state.step) {
is BootstrapStep.GetBackupSecretPassForMigration -> state.step.useKey
else -> true
}
val isEnteringKey = getBackupSecretForMigration.useKey()
if (isEnteringKey) {
bootstrapMigrateShowPassword.isVisible = false
@ -128,8 +117,6 @@ class BootstrapMigrateBackupFragment @Inject constructor(
val recKey = getString(R.string.bootstrap_migration_backup_recovery_key)
bootstrapDescriptionText.text = getString(R.string.enter_account_password, recKey)
.toSpannable()
.colorizeMatchingText(recKey, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
bootstrapMigrateEditText.hint = recKey

View File

@ -21,14 +21,12 @@ import android.content.ActivityNotFoundException
import android.content.Intent
import android.os.Bundle
import android.view.View
import androidx.core.text.toSpannable
import androidx.core.view.isVisible
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.utils.colorizeMatchingText
import im.vector.riotx.core.utils.startSharePlainTextIntent
import im.vector.riotx.core.utils.toast
import kotlinx.android.synthetic.main.fragment_bootstrap_save_key.*
@ -48,17 +46,14 @@ class BootstrapSaveRecoveryKeyFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val messageKey = getString(R.string.message_key)
val recoveryPassphrase = getString(R.string.recovery_passphrase)
val color = colorProvider.getColorFromAttribute(R.attr.vctr_toolbar_link_text_color)
bootstrapSaveText.text = getString(R.string.bootstrap_save_key_description, messageKey, recoveryPassphrase)
.toSpannable()
.colorizeMatchingText(messageKey, color)
.colorizeMatchingText(recoveryPassphrase, color)
recoverySave.clickableView.debouncedClicks { downloadRecoveryKey() }
recoveryCopy.clickableView.debouncedClicks { shareRecoveryKey() }
recoveryContinue.clickableView.debouncedClicks { sharedViewModel.handle(BootstrapActions.GoToCompleted) }
recoveryContinue.clickableView.debouncedClicks {
// We do not display the final Fragment anymore
// TODO Do some cleanup
// sharedViewModel.handle(BootstrapActions.GoToCompleted)
sharedViewModel.handle(BootstrapActions.Completed)
}
}
private fun downloadRecoveryKey() = withState(sharedViewModel) { _ ->

View File

@ -0,0 +1,69 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.recover
import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment
import kotlinx.android.synthetic.main.fragment_bootstrap_setup_recovery.*
import javax.inject.Inject
class BootstrapSetupRecoveryKeyFragment @Inject constructor() : VectorBaseFragment() {
override fun getLayoutResId() = R.layout.fragment_bootstrap_setup_recovery
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Actions when a key backup exist
bootstrapSetupSecureSubmit.clickableView.debouncedClicks {
sharedViewModel.handle(BootstrapActions.StartKeyBackupMigration)
}
// Actions when there is no key backup
bootstrapSetupSecureUseSecurityKey.clickableView.debouncedClicks {
sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = false))
}
bootstrapSetupSecureUseSecurityPassphrase.clickableView.debouncedClicks {
sharedViewModel.handle(BootstrapActions.Start(userWantsToEnterPassphrase = true))
}
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step is BootstrapStep.FirstForm) {
if (state.step.keyBackUpExist) {
// Display the set up action
bootstrapSetupSecureSubmit.isVisible = true
bootstrapSetupSecureUseSecurityKey.isVisible = false
bootstrapSetupSecureUseSecurityPassphrase.isVisible = false
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = false
} else {
// Choose between create a passphrase or use a recovery key
bootstrapSetupSecureSubmit.isVisible = false
bootstrapSetupSecureUseSecurityKey.isVisible = true
bootstrapSetupSecureUseSecurityPassphrase.isVisible = true
bootstrapSetupSecureUseSecurityPassphraseSeparator.isVisible = true
}
}
}
}

View File

@ -17,27 +17,25 @@
package im.vector.riotx.features.crypto.recover
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import com.nulabinc.zxcvbn.Strength
import com.nulabinc.zxcvbn.Zxcvbn
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.matrix.android.internal.crypto.keysbackup.model.rest.KeysVersionResult
import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.util.awaitCallback
import im.vector.riotx.R
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.WaitingViewData
@ -47,30 +45,19 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.OutputStream
data class BootstrapViewState(
val step: BootstrapStep = BootstrapStep.SetupPassphrase(false),
val passphrase: String? = null,
val migrationRecoveryKey: String? = null,
val passphraseRepeat: String? = null,
val crossSigningInitialization: Async<Unit> = Uninitialized,
val passphraseStrength: Async<Strength> = Uninitialized,
val passphraseConfirmMatch: Async<Unit> = Uninitialized,
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
val initializationWaitingViewData: WaitingViewData? = null,
val currentReAuth: UserPasswordAuth? = null,
val recoverySaveFileProcess: Async<Unit> = Uninitialized
) : MvRxState
class BootstrapSharedViewModel @AssistedInject constructor(
@Assisted initialState: BootstrapViewState,
@Assisted val args: BootstrapBottomSheet.Args,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter,
private val session: Session,
private val bootstrapTask: BootstrapCrossSigningTask,
private val migrationTask: BackupToQuadSMigrationTask,
private val reAuthHelper: ReAuthHelper
) : VectorViewModel<BootstrapViewState, BootstrapActions, BootstrapViewEvents>(initialState) {
private var doesKeyBackupExist: Boolean = false
private var isBackupCreatedFromPassphrase: Boolean = false
private val zxcvbn = Zxcvbn()
@AssistedInject.Factory
@ -78,13 +65,17 @@ class BootstrapSharedViewModel @AssistedInject constructor(
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
}
private var _pendingSession: String? = null
init {
// need to check if user have an existing keybackup
if (args.isNewAccount) {
if (args.initCrossSigningOnly) {
// Go straight to account password
setState {
copy(step = BootstrapStep.SetupPassphrase(false))
copy(step = BootstrapStep.AccountPassword(false))
}
} else {
// need to check if user have an existing keybackup
setState {
copy(step = BootstrapStep.CheckingMigration)
}
@ -96,8 +87,9 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
if (version == null) {
// we just resume plain bootstrap
doesKeyBackupExist = false
setState {
copy(step = BootstrapStep.SetupPassphrase(false))
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
}
} else {
// we need to get existing backup passphrase/key and convert to SSSS
@ -108,15 +100,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
// strange case... just finish?
_viewEvents.post(BootstrapViewEvents.Dismiss)
} else {
val isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
if (isBackupCreatedFromPassphrase) {
setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
}
} else {
setState {
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
}
doesKeyBackupExist = true
isBackupCreatedFromPassphrase = keyVersion.getAuthDataAsMegolmBackupAuthData()?.privateKeySalt != null
setState {
copy(step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist))
}
}
}
@ -124,6 +111,18 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
private fun handleStartMigratingKeyBackup() {
if (isBackupCreatedFromPassphrase) {
setState {
copy(step = BootstrapStep.GetBackupSecretPassForMigration(isPasswordVisible = false, useKey = false))
}
} else {
setState {
copy(step = BootstrapStep.GetBackupSecretKeyForMigration)
}
}
}
override fun handle(action: BootstrapActions) = withState { state ->
when (action) {
is BootstrapActions.GoBack -> queryBack()
@ -149,11 +148,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
}
else -> {
}
else -> Unit
}
}
BootstrapActions.StartKeyBackupMigration -> {
handleStartMigratingKeyBackup()
}
is BootstrapActions.Start -> {
handleStart(action)
}
is BootstrapActions.UpdateCandidatePassphrase -> {
val strength = zxcvbn.measure(action.pass)
setState {
@ -182,15 +185,15 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
is BootstrapActions.DoInitialize -> {
if (state.passphrase == state.passphraseRepeat) {
val auth = action.auth ?: reAuthHelper.rememberedAuth()
if (auth == null) {
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(action.auth)
startInitializeFlow(userPassword)
}
} else {
setState {
@ -201,8 +204,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
is BootstrapActions.DoInitializeGeneratedKey -> {
val auth = action.auth ?: reAuthHelper.rememberedAuth()
if (auth == null) {
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
passphrase = null,
@ -217,7 +220,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
passphraseRepeat = null
)
}
startInitializeFlow(action.auth)
startInitializeFlow(userPassword)
}
}
BootstrapActions.RecoveryKeySaved -> {
@ -260,10 +263,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} else return@withState
}
is BootstrapActions.ReAuth -> {
startInitializeFlow(
state.currentReAuth?.copy(password = action.pass)
?: UserPasswordAuth(user = session.myUserId, password = action.pass)
)
startInitializeFlow(action.pass)
}
is BootstrapActions.DoMigrateWithPassphrase -> {
startMigrationFlow(state.step, action.passphrase, null)
@ -274,6 +274,18 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}.exhaustive
}
private fun handleStart(action: BootstrapActions.Start) = withState {
if (action.userWantsToEnterPassphrase) {
setState {
copy(
step = BootstrapStep.SetupPassphrase(isPasswordVisible = false)
)
}
} else {
startInitializeFlow(null)
}
}
// =======================================
// Business Logic
// =======================================
@ -299,7 +311,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
private fun startMigrationFlow(prevState: BootstrapStep, passphrase: String?, recoveryKey: String?) {
private fun startMigrationFlow(previousStep: BootstrapStep, passphrase: String?, recoveryKey: String?) { // TODO Rename param
setState {
copy(step = BootstrapStep.Initializing)
}
@ -314,43 +326,44 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
migrationTask.invoke(this, BackupToQuadSMigrationTask.Params(passphrase, recoveryKey, progressListener)) {
if (it is BackupToQuadSMigrationTask.Result.Success) {
setState {
copy(
passphrase = passphrase,
passphraseRepeat = passphrase,
migrationRecoveryKey = recoveryKey
)
}
val auth = reAuthHelper.rememberedAuth()
if (auth == null) {
when (it) {
is BackupToQuadSMigrationTask.Result.Success -> {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
passphrase = passphrase,
passphraseRepeat = passphrase,
migrationRecoveryKey = recoveryKey
)
}
} else {
startInitializeFlow(auth)
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(userPassword)
}
}
} else {
_viewEvents.post(
BootstrapViewEvents.ModalError(
(it as? BackupToQuadSMigrationTask.Result.Failure)?.error
?: stringProvider.getString(R.string.matrix_error
)
)
)
setState {
copy(
step = prevState
is BackupToQuadSMigrationTask.Result.Failure -> {
_viewEvents.post(
BootstrapViewEvents.ModalError(it.toHumanReadable())
)
setState {
copy(
step = previousStep
)
}
}
}
}.exhaustive
}
}
}
private fun startInitializeFlow(auth: UserPasswordAuth?) {
private fun startInitializeFlow(userPassword: String?) = withState { state ->
val previousStep = state.step
setState {
copy(step = BootstrapStep.Initializing)
}
@ -365,60 +378,77 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
withState { state ->
viewModelScope.launch(Dispatchers.IO) {
bootstrapTask.invoke(this, Params(
userPasswordAuth = auth ?: reAuthHelper.rememberedAuth(),
progressListener = progressListener,
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
)) {
when (it) {
is BootstrapResult.Success -> {
viewModelScope.launch(Dispatchers.IO) {
val userPasswordAuth = userPassword?.let {
UserPasswordAuth(
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
session = _pendingSession,
user = session.myUserId,
password = it
)
}
bootstrapTask.invoke(this,
Params(
userPasswordAuth = userPasswordAuth,
initOnlyCrossSigning = args.initCrossSigningOnly,
progressListener = progressListener,
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }
)
) { bootstrapResult ->
when (bootstrapResult) {
is BootstrapResult.SuccessCrossSigningOnly -> {
// TPD
_viewEvents.post(BootstrapViewEvents.Dismiss)
}
is BootstrapResult.Success -> {
setState {
copy(
recoveryKeyCreationInfo = bootstrapResult.keyInfo,
step = BootstrapStep.SaveRecoveryKey(false)
)
}
}
is BootstrapResult.PasswordAuthFlowMissing -> {
// Ask the password to the user
_pendingSession = bootstrapResult.sessionId
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
}
is BootstrapResult.UnsupportedAuthFlow -> {
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
_viewEvents.post(BootstrapViewEvents.Dismiss)
}
is BootstrapResult.InvalidPasswordError -> {
// it's a bad password
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
_pendingSession = null
setState {
copy(
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
)
}
}
is BootstrapResult.Failure -> {
if (bootstrapResult is BootstrapResult.GenericError
&& bootstrapResult.failure is Failure.OtherServerError
&& bootstrapResult.failure.httpCode == 401) {
// Ignore this error
} else {
_viewEvents.post(BootstrapViewEvents.ModalError(bootstrapResult.error ?: stringProvider.getString(R.string.matrix_error)))
// Not sure
setState {
copy(
recoveryKeyCreationInfo = it.keyInfo,
step = BootstrapStep.SaveRecoveryKey(false)
step = previousStep
)
}
}
is BootstrapResult.PasswordAuthFlowMissing -> {
setState {
copy(
currentReAuth = UserPasswordAuth(session = it.sessionId, user = it.userId),
step = BootstrapStep.AccountPassword(false)
)
}
}
is BootstrapResult.UnsupportedAuthFlow -> {
_viewEvents.post(BootstrapViewEvents.ModalError(stringProvider.getString(R.string.auth_flow_not_supported)))
_viewEvents.post(BootstrapViewEvents.Dismiss)
}
is BootstrapResult.InvalidPasswordError -> {
// it's a bad password
setState {
copy(
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
currentReAuth = UserPasswordAuth(session = null, user = session.myUserId),
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param))
)
}
}
is BootstrapResult.Failure -> {
if (it is BootstrapResult.GenericError
&& it.failure is im.vector.matrix.android.api.failure.Failure.OtherServerError
&& it.failure.httpCode == 401) {
} else {
_viewEvents.post(BootstrapViewEvents.ModalError(it.error ?: stringProvider.getString(R.string.matrix_error)))
setState {
copy(
step = BootstrapStep.ConfirmPassphrase(false)
)
}
}
}
}
}
}.exhaustive
}
}
}
@ -441,22 +471,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
} else {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
setState {
copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null,
// Also reset the key
migrationRecoveryKey = null
)
}
}
}
is BootstrapStep.GetBackupSecretKeyForMigration -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}
is BootstrapStep.SetupPassphrase -> {
// do we let you cancel from here?
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
setState {
copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null
)
}
}
is BootstrapStep.ConfirmPassphrase -> {
setState {
copy(
step = BootstrapStep.SetupPassphrase(
isPasswordVisible = (state.step as? BootstrapStep.ConfirmPassphrase)?.isPasswordVisible ?: false
isPasswordVisible = state.step.isPasswordVisible
)
)
}
@ -472,6 +513,32 @@ class BootstrapSharedViewModel @AssistedInject constructor(
BootstrapStep.DoneSuccess -> {
// nop
}
BootstrapStep.CheckingMigration -> Unit
is BootstrapStep.FirstForm -> {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap())
}
is BootstrapStep.GetBackupSecretForMigration -> {
setState {
copy(
step = BootstrapStep.FirstForm(keyBackUpExist = doesKeyBackupExist),
// Also reset the passphrase
passphrase = null,
passphraseRepeat = null,
// Also reset the key
migrationRecoveryKey = null
)
}
}
}.exhaustive
}
private fun BackupToQuadSMigrationTask.Result.Failure.toHumanReadable(): String {
return when (this) {
is BackupToQuadSMigrationTask.Result.InvalidRecoverySecret -> stringProvider.getString(R.string.keys_backup_passphrase_error_decrypt)
is BackupToQuadSMigrationTask.Result.ErrorFailure -> errorFormatter.toHumanReadable(throwable)
// is BackupToQuadSMigrationTask.Result.NoKeyBackupVersion,
// is BackupToQuadSMigrationTask.Result.IllegalParams,
else -> stringProvider.getString(R.string.unexpected_error)
}
}
@ -484,7 +551,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
override fun create(viewModelContext: ViewModelContext, state: BootstrapViewState): BootstrapSharedViewModel? {
val fragment: BootstrapBottomSheet = (viewModelContext as FragmentViewModelContext).fragment()
val args: BootstrapBottomSheet.Args = fragment.arguments?.getParcelable(BootstrapBottomSheet.EXTRA_ARGS)
?: BootstrapBottomSheet.Args(true)
?: BootstrapBottomSheet.Args(initCrossSigningOnly = true)
return fragment.bootstrapViewModelFactory.create(state, args)
}
}

View File

@ -17,6 +17,14 @@
package im.vector.riotx.features.crypto.recover
/**
* TODO The schema is not up to date
*
*
* BootstrapStep.SetupSecureBackup
*
*
*
*
*
* User has signing keys? Account
* Creation ?
@ -77,10 +85,16 @@ package im.vector.riotx.features.crypto.recover
*/
sealed class BootstrapStep {
// This is the first step
object CheckingMigration : BootstrapStep()
// Use will be asked to choose between passphrase or recovery key, or to start process if a key backup exists
data class FirstForm(val keyBackUpExist: Boolean) : BootstrapStep()
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep()
object CheckingMigration : BootstrapStep()
abstract class GetBackupSecretForMigration : BootstrapStep()
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()
@ -90,3 +104,10 @@ sealed class BootstrapStep {
data class SaveRecoveryKey(val isSaved: Boolean) : BootstrapStep()
object DoneSuccess : BootstrapStep()
}
fun BootstrapStep.GetBackupSecretForMigration.useKey(): Boolean {
return when (this) {
is BootstrapStep.GetBackupSecretPassForMigration -> useKey
else -> true
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.crypto.recover
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import com.nulabinc.zxcvbn.Strength
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
import im.vector.riotx.core.platform.WaitingViewData
data class BootstrapViewState(
val step: BootstrapStep = BootstrapStep.CheckingMigration,
val passphrase: String? = null,
val migrationRecoveryKey: String? = null,
val passphraseRepeat: String? = null,
val crossSigningInitialization: Async<Unit> = Uninitialized,
val passphraseStrength: Async<Strength> = Uninitialized,
val passphraseConfirmMatch: Async<Unit> = Uninitialized,
val recoveryKeyCreationInfo: SsssKeyCreationInfo? = null,
val initializationWaitingViewData: WaitingViewData? = null,
val recoverySaveFileProcess: Async<Unit> = Uninitialized
) : MvRxState

View File

@ -44,7 +44,6 @@ import im.vector.matrix.android.api.session.crypto.verification.VerificationTran
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
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.securestorage.IntegrityResult
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64
@ -118,10 +117,6 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
session.cryptoService().verificationService().getExistingTransaction(args.otherUserId, it) as? QrCodeVerificationTransaction
}
val ssssOk = session.sharedSecretStorageService.checkShouldBeAbleToAccessSecrets(
listOf(MASTER_KEY_SSSS_NAME, USER_SIGNING_KEY_SSSS_NAME, SELF_SIGNING_KEY_SSSS_NAME),
null // default key
) is IntegrityResult.Success
setState {
copy(
otherUserMxItem = userItem?.toMatrixItem(),
@ -133,7 +128,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(
roomId = args.roomId,
isMe = args.otherUserId == session.myUserId,
currentDeviceCanCrossSign = session.cryptoService().crossSigningService().canCrossSign(),
quadSContainsSecrets = ssssOk
quadSContainsSecrets = session.sharedSecretStorageService.isRecoverySetup()
)
}

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.os.Parcelable
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar
@ -26,12 +27,10 @@ import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.drawerlayout.widget.DrawerLayout
import androidx.lifecycle.Observer
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@ -41,7 +40,6 @@ import im.vector.riotx.core.extensions.replaceFragment
import im.vector.riotx.core.platform.ToolbarConfigurable
import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.features.crypto.recover.BootstrapBottomSheet
import im.vector.riotx.features.disclaimer.showDisclaimerDialog
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.popup.PopupAlertManager
@ -50,15 +48,25 @@ import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.workers.signout.SignOutViewModel
import im.vector.riotx.push.fcm.FcmHelper
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import timber.log.Timber
import javax.inject.Inject
@Parcelize
data class HomeActivityArgs(
val clearNotification: Boolean,
val accountCreation: Boolean
) : Parcelable
class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDetectorSharedViewModel.Factory {
private lateinit var sharedActionViewModel: HomeSharedActionViewModel
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
@Inject lateinit var viewModelFactory: HomeActivityViewModel.Factory
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
@Inject lateinit var vectorUncaughtExceptionHandler: VectorUncaughtExceptionHandler
@Inject lateinit var pushManager: PushersManager
@ -98,35 +106,40 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
.observe()
.subscribe { sharedAction ->
when (sharedAction) {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
is HomeActivitySharedAction.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)
is HomeActivitySharedAction.CloseDrawer -> drawerLayout.closeDrawer(GravityCompat.START)
is HomeActivitySharedAction.OpenGroup -> {
drawerLayout.closeDrawer(GravityCompat.START)
replaceFragment(R.id.homeDetailFragmentContainer, HomeDetailFragment::class.java)
}
is HomeActivitySharedAction.PromptForSecurityBootstrap -> {
BootstrapBottomSheet.show(supportFragmentManager, true)
}
}.exhaustive
}
.disposeOnDestroy()
if (intent.getBooleanExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, false)) {
val args = intent.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)
if (args?.clearNotification == true) {
notificationDrawerManager.clearAllEvents()
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
}
if (intent.getBooleanExtra(EXTRA_ACCOUNT_CREATION, false)) {
sharedActionViewModel.post(HomeActivitySharedAction.PromptForSecurityBootstrap)
sharedActionViewModel.isAccountCreation = true
intent.removeExtra(EXTRA_ACCOUNT_CREATION)
}
activeSessionHolder.getSafeActiveSession()?.getInitialSyncProgressStatus()?.observe(this, Observer { status ->
if (status == null) {
homeActivityViewModel.observeViewEvents {
when (it) {
is HomeActivityViewEvents.AskPasswordToInitCrossSigning -> handleAskPasswordToInitCrossSigning(it)
is HomeActivityViewEvents.OnNewSession -> handleOnNewSession(it)
}.exhaustive
}
homeActivityViewModel.subscribe(this) { renderState(it) }
shortcutsHandler.observeRoomsAndBuildShortcuts()
.disposeOnDestroy()
}
private fun renderState(state: HomeActivityViewState) {
when (val status = state.initialSyncProgressServiceStatus) {
is InitialSyncProgressService.Status.Idle -> {
waiting_view.isVisible = false
promptCompleteSecurityIfNeeded()
} else {
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = false
}
is InitialSyncProgressService.Status.Progressing -> {
Timber.v("${getString(status.statusText)} ${status.percentProgress}")
waiting_view.setOnClickListener {
// block interactions
@ -143,67 +156,32 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
}
waiting_view.isVisible = true
}
})
// Ask again if the app is relaunched
if (!sharedActionViewModel.hasDisplayedCompleteSecurityPrompt
&& activeSessionHolder.getSafeActiveSession()?.hasAlreadySynced() == true) {
promptCompleteSecurityIfNeeded()
}
shortcutsHandler.observeRoomsAndBuildShortcuts()
.disposeOnDestroy()
}.exhaustive
}
private fun promptCompleteSecurityIfNeeded() {
val session = activeSessionHolder.getSafeActiveSession() ?: return
if (!session.hasAlreadySynced()) return
if (sharedActionViewModel.hasDisplayedCompleteSecurityPrompt) return
// ensure keys are downloaded
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
runOnUiThread {
alertCompleteSecurity(session)
}
}
})
}
private fun alertCompleteSecurity(session: Session) {
val myCrossSigningKeys = session.cryptoService().crossSigningService()
.getMyCrossSigningKeys()
val crossSigningEnabledOnAccount = myCrossSigningKeys != null
if (!crossSigningEnabledOnAccount && !sharedActionViewModel.isAccountCreation) {
// Do not propose for SSO accounts, because we do not support yet confirming account credentials using SSO
if (session.getHomeServerCapabilities().canChangePassword) {
// We need to ask
promptSecurityEvent(
session,
R.string.upgrade_security,
R.string.security_prompt_text
) {
it.navigator.upgradeSessionSecurity(it)
}
} else {
// Do not do it again
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
}
} else if (myCrossSigningKeys?.isTrusted() == false) {
// We need to ask
promptSecurityEvent(
session,
R.string.crosssigning_verify_this_session,
R.string.confirm_your_identity
) {
it.navigator.waitSessionVerification(it)
}
private fun handleAskPasswordToInitCrossSigning(events: HomeActivityViewEvents.AskPasswordToInitCrossSigning) {
// We need to ask
promptSecurityEvent(
events.userItem,
R.string.upgrade_security,
R.string.security_prompt_text
) {
it.navigator.upgradeSessionSecurity(it, true)
}
}
private fun promptSecurityEvent(session: Session, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
sharedActionViewModel.hasDisplayedCompleteSecurityPrompt = true
private fun handleOnNewSession(event: HomeActivityViewEvents.OnNewSession) {
// We need to ask
promptSecurityEvent(
event.userItem,
R.string.crosssigning_verify_this_session,
R.string.confirm_your_identity
) {
it.navigator.waitSessionVerification(it)
}
}
private fun promptSecurityEvent(userItem: MatrixItem.UserItem?, titleRes: Int, descRes: Int, action: ((VectorBaseActivity) -> Unit)) {
popupAlertManager.postVectorAlert(
VerificationVectorAlert(
uid = "upgradeSecurity",
@ -211,7 +189,7 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
description = getString(descRes),
iconId = R.drawable.ic_shield_warning
).apply {
matrixItem = session.getUser(session.myUserId)?.toMatrixItem()
matrixItem = userItem
colorInt = ContextCompat.getColor(this@HomeActivity, R.color.riotx_positive_accent)
contentAction = Runnable {
(weakCurrentActivity?.get() as? VectorBaseActivity)?.let {
@ -225,9 +203,8 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent?.hasExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION) == true) {
if (intent?.getParcelableExtra<HomeActivityArgs>(MvRx.KEY_ARG)?.clearNotification == true) {
notificationDrawerManager.clearAllEvents()
intent.removeExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION)
}
}
@ -290,14 +267,15 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable, UnknownDeviceDet
}
companion object {
private const val EXTRA_CLEAR_EXISTING_NOTIFICATION = "EXTRA_CLEAR_EXISTING_NOTIFICATION"
private const val EXTRA_ACCOUNT_CREATION = "EXTRA_ACCOUNT_CREATION"
fun newIntent(context: Context, clearNotification: Boolean = false, accountCreation: Boolean = false): Intent {
val args = HomeActivityArgs(
clearNotification = clearNotification,
accountCreation = accountCreation
)
return Intent(context, HomeActivity::class.java)
.apply {
putExtra(EXTRA_CLEAR_EXISTING_NOTIFICATION, clearNotification)
putExtra(EXTRA_ACCOUNT_CREATION, accountCreation)
putExtra(MvRx.KEY_ARG, args)
}
}
}

View File

@ -25,5 +25,4 @@ sealed class HomeActivitySharedAction : VectorSharedAction {
object OpenDrawer : HomeActivitySharedAction()
object CloseDrawer : HomeActivitySharedAction()
object OpenGroup : HomeActivitySharedAction()
object PromptForSecurityBootstrap : HomeActivitySharedAction()
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.core.platform.VectorViewEvents
sealed class HomeActivityViewEvents : VectorViewEvents {
data class AskPasswordToInitCrossSigning(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
data class OnNewSession(val userItem: MatrixItem.UserItem?) : HomeActivityViewEvents()
}

View File

@ -0,0 +1,166 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home
import com.airbnb.mvrx.MvRx
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.NoOpMatrixCallback
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.android.internal.crypto.model.CryptoDeviceInfo
import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.rx.asObservable
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.platform.EmptyAction
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.login.ReAuthHelper
import timber.log.Timber
class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState,
@Assisted private val args: HomeActivityArgs,
private val activeSessionHolder: ActiveSessionHolder,
private val reAuthHelper: ReAuthHelper
) : VectorViewModel<HomeActivityViewState, EmptyAction, HomeActivityViewEvents>(initialState) {
@AssistedInject.Factory
interface Factory {
fun create(initialState: HomeActivityViewState, args: HomeActivityArgs): HomeActivityViewModel
}
companion object : MvRxViewModelFactory<HomeActivityViewModel, HomeActivityViewState> {
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeActivityViewState): HomeActivityViewModel? {
val activity: HomeActivity = viewModelContext.activity()
val args: HomeActivityArgs? = activity.intent.getParcelableExtra(MvRx.KEY_ARG)
return args?.let { activity.viewModelFactory.create(state, it) }
}
}
private var checkBootstrap = false
init {
observeInitialSync()
mayBeInitializeCrossSigning()
}
private fun observeInitialSync() {
val session = activeSessionHolder.getSafeActiveSession() ?: return
session.getInitialSyncProgressStatus()
.asObservable()
.subscribe { status ->
when (status) {
is InitialSyncProgressService.Status.Progressing -> {
// Schedule a check of the bootstrap when the init sync will be finished
checkBootstrap = true
}
is InitialSyncProgressService.Status.Idle -> {
if (checkBootstrap) {
checkBootstrap = false
maybeBootstrapCrossSigning()
}
}
}
setState {
copy(
initialSyncProgressServiceStatus = status
)
}
}
.disposeOnClear()
}
private fun mayBeInitializeCrossSigning() {
if (args.accountCreation) {
val password = reAuthHelper.data ?: return Unit.also {
Timber.w("No password to init cross signing")
}
val session = activeSessionHolder.getSafeActiveSession() ?: return Unit.also {
Timber.w("No session to init cross signing")
}
// We do not use the viewModel context because we do not want to cancel this action
Timber.d("Initialize cross signing")
session.cryptoService().crossSigningService().initializeCrossSigning(
authParams = UserPasswordAuth(
session = null,
user = session.myUserId,
password = password
),
callback = NoOpMatrixCallback()
)
}
}
private fun maybeBootstrapCrossSigning() {
// In case of account creation, it is already done before
if (args.accountCreation) return
val session = activeSessionHolder.getSafeActiveSession() ?: return
// Ensure keys of the user are downloaded
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
// Is there already cross signing keys here?
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
if (mxCrossSigningInfo != null) {
// Cross-signing is already set up for this user, is it trusted?
if (!mxCrossSigningInfo.isTrusted()) {
// New session
_viewEvents.post(HomeActivityViewEvents.OnNewSession(session.getUser(session.myUserId)?.toMatrixItem()))
}
} else {
// Initialize cross-signing
val password = reAuthHelper.data
if (password == null) {
// Check this is not an SSO account
if (session.getHomeServerCapabilities().canChangePassword) {
// Ask password to the user: Upgrade security
_viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
}
// Else (SSO) just ignore for the moment
} else {
// We do not use the viewModel context because we do not want to cancel this action
Timber.d("Initialize cross signing")
session.cryptoService().crossSigningService().initializeCrossSigning(
authParams = UserPasswordAuth(
session = null,
user = session.myUserId,
password = password
),
callback = NoOpMatrixCallback()
)
}
}
}
})
}
override fun handle(action: EmptyAction) {
// NA
}
}

View File

@ -0,0 +1,24 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home
import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.session.InitialSyncProgressService
data class HomeActivityViewState(
val initialSyncProgressServiceStatus: InitialSyncProgressService.Status = InitialSyncProgressService.Status.Idle
) : MvRxState

View File

@ -19,7 +19,4 @@ package im.vector.riotx.features.home
import im.vector.riotx.core.platform.VectorSharedActionViewModel
import javax.inject.Inject
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>() {
var hasDisplayedCompleteSecurityPrompt : Boolean = false
var isAccountCreation : Boolean = false
}
class HomeSharedActionViewModel @Inject constructor() : VectorSharedActionViewModel<HomeActivitySharedAction>()

View File

@ -41,7 +41,6 @@ import im.vector.matrix.android.api.auth.registration.Stage
import im.vector.matrix.android.api.auth.wellknown.WellknownResult
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.extensions.configureAndStart
@ -289,7 +288,7 @@ class LoginViewModel @AssistedInject constructor(
private fun handleRegisterWith(action: LoginAction.LoginOrRegister) {
setState { copy(asyncRegistration = Loading()) }
reAuthHelper.rememberAuth(UserPasswordAuth(user = action.username, password = action.password))
reAuthHelper.data = action.password
currentTask = registrationWizard?.createAccount(
action.username,
action.password,
@ -569,6 +568,7 @@ class LoginViewModel @AssistedInject constructor(
action.initialDeviceName,
object : MatrixCallback<Session> {
override fun onSuccess(data: Session) {
reAuthHelper.data = action.password
onSessionCreated(data)
}

View File

@ -16,33 +16,12 @@
package im.vector.riotx.features.login
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import java.util.Timer
import java.util.TimerTask
import im.vector.riotx.core.utils.TemporaryStore
import javax.inject.Inject
import javax.inject.Singleton
const val THREE_MINUTES = 3 * 60_000L
/**
* Will store the account password for 3 minutes
*/
@Singleton
class ReAuthHelper @Inject constructor() {
private var timer: Timer? = null
private var rememberedInfo: UserPasswordAuth? = null
fun rememberAuth(password: UserPasswordAuth?) {
timer?.cancel()
timer = null
rememberedInfo = password
timer = Timer().apply {
schedule(object : TimerTask() {
override fun run() {
rememberedInfo = null
}
}, THREE_MINUTES)
}
}
fun rememberedAuth() = rememberedInfo
}
class ReAuthHelper @Inject constructor() : TemporaryStore<String>()

View File

@ -124,9 +124,9 @@ class DefaultNavigator @Inject constructor(
}
}
override fun upgradeSessionSecurity(context: Context) {
override fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean) {
if (context is VectorBaseActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, false)
BootstrapBottomSheet.show(context.supportFragmentManager, initCrossSigningOnly)
}
}

View File

@ -42,7 +42,7 @@ interface Navigator {
fun waitSessionVerification(context: Context)
fun upgradeSessionSecurity(context: Context)
fun upgradeSessionSecurity(context: Context, initCrossSigningOnly: Boolean)
fun openRoomForSharingAndFinish(activity: Activity, roomId: String, sharedData: SharedData)

View File

@ -119,8 +119,7 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
private fun refreshXSigningStatus() {
val crossSigningKeys = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningIsEnableInAccount = session.cryptoService().crossSigningService().isCrossSigningInitialized()
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()

View File

@ -1,163 +0,0 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.crosssigning
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.loadingItem
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import im.vector.riotx.features.settings.VectorPreferences
import me.gujun.android.span.span
import javax.inject.Inject
class CrossSigningEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
interface InteractionListener {
fun onInitializeCrossSigningKeys()
fun verifySession()
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: CrossSigningSettingsViewState?) {
if (data == null) return
if (data.xSigningKeyCanSign) {
genericItem {
id("can")
titleIconResourceId(R.drawable.ic_shield_trusted)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
}
} else if (data.xSigningKeysAreTrusted) {
genericItem {
id("trusted")
titleIconResourceId(R.drawable.ic_shield_custom)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
}
if (!data.isUploadingKeys) {
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
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()
}
}
}
} 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.crosssigning_verify_this_session))
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()
}
}
} else {
genericItem {
id("not")
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
}
if (vectorPreferences.developerMode() && !data.isUploadingKeys) {
bottomSheetVerificationActionItem {
id("initKeys")
title(stringProvider.getString(R.string.initialize_cross_signing))
titleColor(colorProvider.getColor(R.color.riotx_positive_accent))
iconRes(R.drawable.ic_arrow_right)
iconColor(colorProvider.getColor(R.color.riotx_positive_accent))
listener {
interactionListener?.onInitializeCrossSigningKeys()
}
}
}
}
if (data.isUploadingKeys) {
loadingItem {
id("loading")
}
} else {
val crossSigningKeys = data.crossSigningInfo
crossSigningKeys?.masterKey()?.let {
genericItemWithValue {
id("msk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"Master Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
crossSigningKeys?.userKey()?.let {
genericItemWithValue {
id("usk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"User Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
crossSigningKeys?.selfSigningKey()?.let {
genericItemWithValue {
id("ssk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"Self Signed Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
}
}
}

View File

@ -0,0 +1,25 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.crosssigning
import im.vector.riotx.core.platform.VectorViewModelAction
sealed class CrossSigningSettingsAction : VectorViewModelAction {
object SetUpRecovery : CrossSigningSettingsAction()
object VerifySession : CrossSigningSettingsAction()
object SetupCrossSigning : CrossSigningSettingsAction()
}

View File

@ -0,0 +1,165 @@
/*
* Copyright 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.crosssigning
import com.airbnb.epoxy.TypedEpoxyController
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.ui.list.genericItem
import im.vector.riotx.core.ui.list.genericItemWithValue
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.crypto.verification.epoxy.bottomSheetVerificationActionItem
import me.gujun.android.span.span
import javax.inject.Inject
class CrossSigningSettingsController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val dimensionConverter: DimensionConverter
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
interface InteractionListener {
fun setupRecovery()
fun verifySession()
fun initCrossSigning()
}
var interactionListener: InteractionListener? = null
override fun buildModels(data: CrossSigningSettingsViewState?) {
if (data == null) return
when {
data.xSigningKeyCanSign -> {
genericItem {
id("can")
titleIconResourceId(R.drawable.ic_shield_trusted)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
}
}
data.xSigningKeysAreTrusted -> {
genericItem {
id("trusted")
titleIconResourceId(R.drawable.ic_shield_custom)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
}
}
data.xSigningIsEnableInAccount -> {
genericItem {
id("enable")
titleIconResourceId(R.drawable.ic_shield_black)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
}
}
else -> {
genericItem {
id("not")
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
}
}
}
if (data.recoveryHasToBeSetUp) {
if (data.xSigningIsEnableInAccount) {
bottomSheetVerificationActionItem {
id("setup_recovery")
title(stringProvider.getString(R.string.settings_setup_secure_backup))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
iconRes(R.drawable.ic_arrow_right)
listener {
interactionListener?.setupRecovery()
}
}
} else {
// Propose to setup cross signing
bottomSheetVerificationActionItem {
id("setup_xSgning")
title(stringProvider.getString(R.string.setup_cross_signing))
titleColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_primary))
subTitle(stringProvider.getString(R.string.security_prompt_text))
iconRes(R.drawable.ic_arrow_right)
listener {
interactionListener?.initCrossSigning()
}
}
}
}
if (data.deviceHasToBeVerified) {
bottomSheetVerificationActionItem {
id("verify")
title(stringProvider.getString(R.string.crosssigning_verify_this_session))
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()
}
}
}
val crossSigningKeys = data.crossSigningInfo
crossSigningKeys?.masterKey()?.let {
genericItemWithValue {
id("msk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"Master Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
crossSigningKeys?.userKey()?.let {
genericItemWithValue {
id("usk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"User Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
crossSigningKeys?.selfSigningKey()?.let {
genericItemWithValue {
id("ssk")
titleIconResourceId(R.drawable.key_small)
title(
span {
+"Self Signed Key:\n"
span {
text = it.unpaddedBase64PublicKey ?: ""
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)
textSize = dimensionConverter.spToPx(12)
}
}
)
}
}
}
}

View File

@ -21,7 +21,6 @@ import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.riotx.R
import im.vector.riotx.core.dialogs.PromptPasswordDialog
import im.vector.riotx.core.extensions.cleanup
import im.vector.riotx.core.extensions.configureWith
import im.vector.riotx.core.extensions.exhaustive
@ -31,9 +30,9 @@ import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import javax.inject.Inject
class CrossSigningSettingsFragment @Inject constructor(
private val epoxyController: CrossSigningEpoxyController,
private val controller: CrossSigningSettingsController,
val viewModelFactory: CrossSigningSettingsViewModel.Factory
) : VectorBaseFragment(), CrossSigningEpoxyController.InteractionListener {
) : VectorBaseFragment(), CrossSigningSettingsController.InteractionListener {
override fun getLayoutResId() = R.layout.fragment_generic_recycler
@ -43,7 +42,7 @@ class CrossSigningSettingsFragment @Inject constructor(
super.onActivityCreated(savedInstanceState)
viewModel.observeViewEvents {
when (it) {
is CrossSigningSettingsViewEvents.Failure -> {
is CrossSigningSettingsViewEvents.Failure -> {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(it.throwable))
@ -51,13 +50,14 @@ class CrossSigningSettingsFragment @Inject constructor(
.show()
Unit
}
is CrossSigningSettingsViewEvents.RequestPassword -> {
requestPassword()
CrossSigningSettingsViewEvents.VerifySession -> {
navigator.waitSessionVerification(requireActivity())
}
CrossSigningSettingsViewEvents.VerifySession -> {
(requireActivity() as? VectorBaseActivity)?.let { activity ->
activity.navigator.waitSessionVerification(activity)
}
CrossSigningSettingsViewEvents.SetUpRecovery -> {
navigator.upgradeSessionSecurity(requireActivity(), false)
}
CrossSigningSettingsViewEvents.SetupCrossSigning -> {
navigator.upgradeSessionSecurity(requireActivity(), true)
}
}.exhaustive
}
@ -74,31 +74,29 @@ class CrossSigningSettingsFragment @Inject constructor(
}
override fun invalidate() = withState(viewModel) { state ->
epoxyController.setData(state)
controller.setData(state)
}
private fun setupRecyclerView() {
recyclerView.configureWith(epoxyController, hasFixedSize = false, disableItemAnimation = true)
epoxyController.interactionListener = this
recyclerView.configureWith(controller, hasFixedSize = false, disableItemAnimation = true)
controller.interactionListener = this
}
override fun onDestroyView() {
recyclerView.cleanup()
epoxyController.interactionListener = null
controller.interactionListener = null
super.onDestroyView()
}
private fun requestPassword() {
PromptPasswordDialog().show(requireActivity()) { password ->
viewModel.handle(CrossSigningAction.PasswordEntered(password))
}
}
override fun onInitializeCrossSigningKeys() {
viewModel.handle(CrossSigningAction.InitializeCrossSigning)
override fun setupRecovery() {
viewModel.handle(CrossSigningSettingsAction.SetUpRecovery)
}
override fun verifySession() {
viewModel.handle(CrossSigningAction.VerifySession)
viewModel.handle(CrossSigningSettingsAction.VerifySession)
}
override fun initCrossSigning() {
viewModel.handle(CrossSigningSettingsAction.SetupCrossSigning)
}
}

View File

@ -24,6 +24,7 @@ import im.vector.riotx.core.platform.VectorViewEvents
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
object RequestPassword : CrossSigningSettingsViewEvents()
object SetUpRecovery : CrossSigningSettingsViewEvents()
object VerifySession : CrossSigningSettingsViewEvents()
object SetupCrossSigning : CrossSigningSettingsViewEvents()
}

View File

@ -16,126 +16,70 @@
package im.vector.riotx.features.settings.crosssigning
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxState
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.auth.data.LoginFlowTypes
import im.vector.matrix.android.api.failure.toRegistrationFlowResponse
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.crosssigning.isVerified
import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.core.platform.VectorViewModelAction
data class CrossSigningSettingsViewState(
val crossSigningInfo: MXCrossSigningInfo? = null,
val xSigningIsEnableInAccount: Boolean = false,
val xSigningKeysAreTrusted: Boolean = false,
val xSigningKeyCanSign: Boolean = true,
val isUploadingKeys: Boolean = false
) : MvRxState
sealed class CrossSigningAction : VectorViewModelAction {
object InitializeCrossSigning : CrossSigningAction()
object VerifySession : CrossSigningAction()
data class PasswordEntered(val password: String) : CrossSigningAction()
}
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
private val session: Session)
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningAction, CrossSigningSettingsViewEvents>(initialState) {
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
init {
session.rx().liveCrossSigningInfo(session.myUserId)
.execute {
val crossSigningKeys = it.invoke()?.getOrNull()
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
session.rx().liveMyDeviceInfo(),
session.rx().liveCrossSigningInfo(session.myUserId),
BiFunction { myDeviceInfo, mxCrossSigningInfo ->
(myDeviceInfo to mxCrossSigningInfo)
}
)
.execute { data ->
val crossSigningKeys = data.invoke()?.second?.getOrNull()
val xSigningIsEnableInAccount = crossSigningKeys != null
val xSigningKeysAreTrusted = session.cryptoService().crossSigningService().checkUserTrust(session.myUserId).isVerified()
val xSigningKeyCanSign = session.cryptoService().crossSigningService().canCrossSign()
val hasSeveralDevices = data.invoke()?.first?.size ?: 0 > 1
copy(
crossSigningInfo = crossSigningKeys,
xSigningIsEnableInAccount = xSigningIsEnableInAccount,
xSigningKeysAreTrusted = xSigningKeysAreTrusted,
xSigningKeyCanSign = xSigningKeyCanSign
xSigningKeyCanSign = xSigningKeyCanSign,
deviceHasToBeVerified = hasSeveralDevices && (xSigningIsEnableInAccount && !xSigningKeyCanSign),
recoveryHasToBeSetUp = !session.sharedSecretStorageService.isRecoverySetup()
)
}
}
// Storage when password is required
private var _pendingSession: String? = null
@AssistedInject.Factory
interface Factory {
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
}
override fun handle(action: CrossSigningAction) {
override fun handle(action: CrossSigningSettingsAction) {
when (action) {
is CrossSigningAction.InitializeCrossSigning -> {
initializeCrossSigning(null)
CrossSigningSettingsAction.SetUpRecovery -> {
_viewEvents.post(CrossSigningSettingsViewEvents.SetUpRecovery)
}
is CrossSigningAction.PasswordEntered -> {
initializeCrossSigning(UserPasswordAuth(
session = _pendingSession,
user = session.myUserId,
password = action.password
))
}
CrossSigningAction.VerifySession -> {
CrossSigningSettingsAction.VerifySession -> {
_viewEvents.post(CrossSigningSettingsViewEvents.VerifySession)
}
CrossSigningSettingsAction.SetupCrossSigning -> {
_viewEvents.post(CrossSigningSettingsViewEvents.SetupCrossSigning)
}
}.exhaustive
}
private fun initializeCrossSigning(auth: UserPasswordAuth?) {
_pendingSession = null
setState {
copy(isUploadingKeys = true)
}
session.cryptoService().crossSigningService().initializeCrossSigning(auth, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_pendingSession = null
setState {
copy(isUploadingKeys = false)
}
}
override fun onFailure(failure: Throwable) {
_pendingSession = null
val registrationFlowResponse = failure.toRegistrationFlowResponse()
if (registrationFlowResponse != null) {
// Retry with authentication
if (registrationFlowResponse.flows?.any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true } == true) {
_pendingSession = registrationFlowResponse.session ?: ""
_viewEvents.post(CrossSigningSettingsViewEvents.RequestPassword)
} else {
// can't do this from here
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Throwable("You cannot do that from mobile")))
setState {
copy(isUploadingKeys = false)
}
}
} else {
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(failure))
setState {
copy(isUploadingKeys = false)
}
}
}
})
}
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {
@JvmStatic

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.settings.crosssigning
import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.session.crypto.crosssigning.MXCrossSigningInfo
data class CrossSigningSettingsViewState(
val crossSigningInfo: MXCrossSigningInfo? = null,
val xSigningIsEnableInAccount: Boolean = false,
val xSigningKeysAreTrusted: Boolean = false,
val xSigningKeyCanSign: Boolean = true,
val deviceHasToBeVerified: Boolean = false,
val recoveryHasToBeSetUp: Boolean = false
) : MvRxState

View File

@ -54,7 +54,7 @@ class DeviceVerificationInfoBottomSheetViewModel @AssistedInject constructor(@As
setState {
copy(
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(),
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified()
)
}

View File

@ -89,7 +89,7 @@ class DeviceVerificationInfoEpoxyController @Inject constructor(private val stri
description(stringProvider.getString(R.string.settings_active_sessions_verified_device_desc))
}
} else {
// You need tomcomplete security
// You need to complete security
genericItem {
id("trust${cryptoDeviceInfo.deviceId}")
style(GenericItem.STYLE.BIG_TEXT)

View File

@ -95,7 +95,7 @@ class DevicesViewModel @AssistedInject constructor(
setState {
copy(
hasAccountCrossSigning = session.cryptoService().crossSigningService().getMyCrossSigningKeys() != null,
hasAccountCrossSigning = session.cryptoService().crossSigningService().isCrossSigningInitialized(),
accountCrossSigningIsTrusted = session.cryptoService().crossSigningService().isCrossSigningVerified(),
myDeviceId = session.sessionParams.deviceId ?: ""
)

View File

@ -1,19 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="24dp"
android:viewportWidth="22"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
<path
android:pathData="M19,14C19,16.0333 17.7458,17.9018 16.043,19.4808C14.3615,21.0401 12.4,22.1689 11.3349,22.7219C11.1216,22.8327 10.8784,22.8327 10.6651,22.7219C9.6,22.1689 7.6385,21.0401 5.957,19.4808C4.2542,17.9018 3,16.0333 3,14V3.6043C3,3.1356 3.3255,2.7298 3.7831,2.6282L10.7831,1.0726C10.9259,1.0409 11.0741,1.0409 11.2169,1.0726L18.2169,2.6282C18.6745,2.7298 19,3.1356 19,3.6043V14Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"/>
</group>
<path
android:pathData="M2,8C0.8954,8 0,8.8954 0,10V13C0,14.1046 0.8954,15 2,15H20C21.1046,15 22,14.1046 22,13V10C22,8.8954 21.1046,8 20,8H2ZM4.25,9.5C3.8358,9.5 3.5,9.8358 3.5,10.25C3.5,10.6642 3.8358,11 4.25,11H6.75C7.1642,11 7.5,10.6642 7.5,10.25C7.5,9.8358 7.1642,9.5 6.75,9.5H4.25ZM8.5,10.25C8.5,9.8358 8.8358,9.5 9.25,9.5H9.75C10.1642,9.5 10.5,9.8358 10.5,10.25C10.5,10.6642 10.1642,11 9.75,11H9.25C8.8358,11 8.5,10.6642 8.5,10.25ZM12.25,9.5C11.8358,9.5 11.5,9.8358 11.5,10.25C11.5,10.6642 11.8358,11 12.25,11H14.75C15.1642,11 15.5,10.6642 15.5,10.25C15.5,9.8358 15.1642,9.5 14.75,9.5H12.25ZM16.5,10.25C16.5,9.8358 16.8358,9.5 17.25,9.5H17.75C18.1642,9.5 18.5,9.8358 18.5,10.25C18.5,10.6642 18.1642,11 17.75,11H17.25C16.8358,11 16.5,10.6642 16.5,10.25ZM4.25,12C3.8358,12 3.5,12.3358 3.5,12.75C3.5,13.1642 3.8358,13.5 4.25,13.5H4.75C5.1642,13.5 5.5,13.1642 5.5,12.75C5.5,12.3358 5.1642,12 4.75,12H4.25ZM6.5,12.75C6.5,12.3358 6.8358,12 7.25,12H9.75C10.1642,12 10.5,12.3358 10.5,12.75C10.5,13.1642 10.1642,13.5 9.75,13.5H7.25C6.8358,13.5 6.5,13.1642 6.5,12.75ZM12.25,12C11.8358,12 11.5,12.3358 11.5,12.75C11.5,13.1642 11.8358,13.5 12.25,13.5H12.75C13.1642,13.5 13.5,13.1642 13.5,12.75C13.5,12.3358 13.1642,12 12.75,12H12.25Z"
android:fillColor="#2E2F32"
android:fillType="evenOdd"/>
</vector>

View File

@ -1,19 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="24dp"
android:viewportWidth="22"
android:viewportHeight="24">
<group>
<clip-path
android:pathData="M0,0h22v6h-22zM0,17h22v7h-22z"/>
<path
android:pathData="M11.3349,22.7219C11.1216,22.8327 10.8784,22.8327 10.6651,22.7219C9.6,22.1689 7.6385,21.0401 5.957,19.4808C4.2542,17.9018 3,16.0333 3,14V3.6043C3,3.1356 3.3255,2.7298 3.7831,2.6282L10.7831,1.0726C10.9259,1.0409 11.0741,1.0409 11.2169,1.0726L18.2169,2.6282C18.6745,2.7298 19,3.1356 19,3.6043V14C19,16.0333 17.7458,17.9018 16.043,19.4808C14.3615,21.0401 12.4,22.1689 11.3349,22.7219Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"/>
</group>
<path
android:pathData="M0,10C0,8.8954 0.8954,8 2,8H20C21.1046,8 22,8.8954 22,10V13C22,14.1046 21.1046,15 20,15H2C0.8954,15 0,14.1046 0,13V10ZM5,11.5C5,12.3284 4.3284,13 3.5,13C2.6716,13 2,12.3284 2,11.5C2,10.6716 2.6716,10 3.5,10C4.3284,10 5,10.6716 5,11.5ZM8.5,13C9.3284,13 10,12.3284 10,11.5C10,10.6716 9.3284,10 8.5,10C7.6716,10 7,10.6716 7,11.5C7,12.3284 7.6716,13 8.5,13ZM15,11.5C15,12.3284 14.3284,13 13.5,13C12.6716,13 12,12.3284 12,11.5C12,10.6716 12.6716,10 13.5,10C14.3284,10 15,10.6716 15,11.5ZM18.5,13C19.3284,13 20,12.3284 20,11.5C20,10.6716 19.3284,10 18.5,10C17.6716,10 17,10.6716 17,11.5C17,12.3284 17.6716,13 18.5,13Z"
android:fillColor="#2E2F32"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path android:pathData="M1,2h21.5v5h-21.5zM1,17.7h21.5v5h-21.5z" />
<path
android:fillColor="#2E2F32"
android:pathData="M3.1663,12.4014C3.1663,7.6467 6.9953,3.8177 11.75,3.8177C13.0964,3.8177 14.4429,4.1544 15.6631,4.7435H14.9899C14.5691,4.7435 14.2325,5.0801 14.2325,5.5008C14.2325,5.9216 14.5691,6.2582 14.9899,6.2582H17.3041C17.809,6.2582 18.1877,5.8375 18.1877,5.3746V3.0604C18.1877,2.6396 17.8511,2.303 17.4303,2.303C17.0096,2.303 16.673,2.6396 16.673,3.0604V3.6074C16.6309,3.5653 16.5888,3.5653 16.5467,3.5232C15.074,2.7238 13.433,2.303 11.75,2.303C6.1958,2.303 1.6515,6.8473 1.6515,12.4014C1.6515,14.0845 2.0723,15.7676 2.8717,17.2403C2.998,17.4928 3.2504,17.619 3.545,17.619C3.6712,17.619 3.7974,17.5769 3.9236,17.5348C4.3023,17.3245 4.4286,16.8616 4.2182,16.525C3.5029,15.2627 3.1663,13.8321 3.1663,12.4014Z"
tools:fillColor="#FF0000" />
<path
android:fillColor="#2E2F32"
android:pathData="M20.6281,7.5626C20.4177,7.1839 19.9548,7.0577 19.6182,7.2681C19.2395,7.4785 19.1133,7.9413 19.3237,8.2779C19.9969,9.5402 20.3756,10.9288 20.3756,12.4015C20.3756,17.1562 16.5045,20.9852 11.7919,20.9852C10.4454,20.9852 9.099,20.6486 7.8787,20.0595H8.552C8.9727,20.0595 9.3094,19.7229 9.3094,19.3021C9.3094,18.8813 8.9727,18.5447 8.552,18.5447H6.2377C5.7328,18.5447 5.3541,18.9655 5.3541,19.4283V21.7426C5.3541,22.1633 5.6908,22.4999 6.1115,22.4999C6.5323,22.4999 6.8689,22.1633 6.8689,21.7426V21.1956C6.911,21.2376 6.9531,21.2376 6.9951,21.2797C8.4257,22.0792 10.0667,22.4999 11.7498,22.4999C17.304,22.4999 21.8483,17.9556 21.8483,12.4015C21.8483,10.7184 21.4275,9.0353 20.6281,7.5626Z"
tools:fillColor="#FF0000" />
</group>
<path
android:fillColor="#2E2F32"
android:fillType="evenOdd"
android:pathData="M3,9C1.8954,9 1,9.8954 1,11V14C1,15.1046 1.8954,16 3,16H21C22.1046,16 23,15.1046 23,14V11C23,9.8954 22.1046,9 21,9H3ZM5.25,10.5C4.8358,10.5 4.5,10.8358 4.5,11.25C4.5,11.6642 4.8358,12 5.25,12H7.75C8.1642,12 8.5,11.6642 8.5,11.25C8.5,10.8358 8.1642,10.5 7.75,10.5H5.25ZM9.5,11.25C9.5,10.8358 9.8358,10.5 10.25,10.5H10.75C11.1642,10.5 11.5,10.8358 11.5,11.25C11.5,11.6642 11.1642,12 10.75,12H10.25C9.8358,12 9.5,11.6642 9.5,11.25ZM13.25,10.5C12.8358,10.5 12.5,10.8358 12.5,11.25C12.5,11.6642 12.8358,12 13.25,12H15.75C16.1642,12 16.5,11.6642 16.5,11.25C16.5,10.8358 16.1642,10.5 15.75,10.5H13.25ZM17.5,11.25C17.5,10.8358 17.8358,10.5 18.25,10.5H18.75C19.1642,10.5 19.5,10.8358 19.5,11.25C19.5,11.6642 19.1642,12 18.75,12H18.25C17.8358,12 17.5,11.6642 17.5,11.25ZM5.25,13C4.8358,13 4.5,13.3358 4.5,13.75C4.5,14.1642 4.8358,14.5 5.25,14.5H5.75C6.1642,14.5 6.5,14.1642 6.5,13.75C6.5,13.3358 6.1642,13 5.75,13H5.25ZM7.5,13.75C7.5,13.3358 7.8358,13 8.25,13H10.75C11.1642,13 11.5,13.3358 11.5,13.75C11.5,14.1642 11.1642,14.5 10.75,14.5H8.25C7.8358,14.5 7.5,14.1642 7.5,13.75ZM13.25,13C12.8358,13 12.5,13.3358 12.5,13.75C12.5,14.1642 12.8358,14.5 13.25,14.5H13.75C14.1642,14.5 14.5,14.1642 14.5,13.75C14.5,13.3358 14.1642,13 13.75,13H13.25Z"
tools:fillColor="#FF0000" />
</vector>

View File

@ -0,0 +1,23 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<clip-path android:pathData="M1,2h21.5v5h-21.5zM1,17.7h21.5v5h-21.5z" />
<path
android:fillColor="#2E2F32"
android:pathData="M3.1663,12.4014C3.1663,7.6467 6.9953,3.8177 11.75,3.8177C13.0964,3.8177 14.4429,4.1544 15.6631,4.7435H14.9899C14.5691,4.7435 14.2325,5.0801 14.2325,5.5008C14.2325,5.9216 14.5691,6.2582 14.9899,6.2582H17.3041C17.809,6.2582 18.1877,5.8375 18.1877,5.3746V3.0604C18.1877,2.6396 17.8511,2.303 17.4303,2.303C17.0096,2.303 16.673,2.6396 16.673,3.0604V3.6074C16.6309,3.5653 16.5888,3.5653 16.5467,3.5232C15.074,2.7238 13.433,2.303 11.75,2.303C6.1958,2.303 1.6515,6.8473 1.6515,12.4014C1.6515,14.0845 2.0723,15.7676 2.8717,17.2403C2.998,17.4928 3.2504,17.619 3.545,17.619C3.6712,17.619 3.7974,17.5769 3.9236,17.5348C4.3023,17.3245 4.4286,16.8616 4.2182,16.525C3.5029,15.2627 3.1663,13.8321 3.1663,12.4014Z"
tools:fillColor="#FF0000" />
<path
android:fillColor="#2E2F32"
android:pathData="M20.6281,7.5626C20.4177,7.1839 19.9548,7.0577 19.6182,7.2681C19.2395,7.4785 19.1133,7.9413 19.3237,8.2779C19.9969,9.5402 20.3756,10.9288 20.3756,12.4015C20.3756,17.1562 16.5045,20.9852 11.7919,20.9852C10.4454,20.9852 9.099,20.6486 7.8787,20.0595H8.552C8.9727,20.0595 9.3094,19.7229 9.3094,19.3021C9.3094,18.8813 8.9727,18.5447 8.552,18.5447H6.2377C5.7328,18.5447 5.3541,18.9655 5.3541,19.4283V21.7426C5.3541,22.1633 5.6908,22.4999 6.1115,22.4999C6.5323,22.4999 6.8689,22.1633 6.8689,21.7426V21.1956C6.911,21.2376 6.9531,21.2376 6.9951,21.2797C8.4257,22.0792 10.0667,22.4999 11.7498,22.4999C17.304,22.4999 21.8483,17.9556 21.8483,12.4015C21.8483,10.7184 21.4275,9.0353 20.6281,7.5626Z"
tools:fillColor="#FF0000" />
</group>
<path
android:fillColor="#2E2F32"
android:fillType="evenOdd"
android:pathData="M1,11C1,9.8954 1.8954,9 3,9H21C22.1046,9 23,9.8954 23,11V14C23,15.1046 22.1046,16 21,16H3C1.8954,16 1,15.1046 1,14V11ZM6,12.5C6,13.3284 5.3284,14 4.5,14C3.6716,14 3,13.3284 3,12.5C3,11.6716 3.6716,11 4.5,11C5.3284,11 6,11.6716 6,12.5ZM9.5,14C10.3284,14 11,13.3284 11,12.5C11,11.6716 10.3284,11 9.5,11C8.6716,11 8,11.6716 8,12.5C8,13.3284 8.6716,14 9.5,14ZM16,12.5C16,13.3284 15.3284,14 14.5,14C13.6716,14 13,13.3284 13,12.5C13,11.6716 13.6716,11 14.5,11C15.3284,11 16,11.6716 16,12.5ZM19.5,14C20.3284,14 21,13.3284 21,12.5C21,11.6716 20.3284,11 19.5,11C18.6716,11 18,11.6716 18,12.5C18,13.3284 18.6716,14 19.5,14Z"
tools:fillColor="#FF0000" />
</vector>

View File

@ -1,21 +1,24 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20,21V19C20,16.7909 18.2091,15 16,15H8C5.7909,15 4,16.7909 4,19V21"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:pathData="M12,11C14.2091,11 16,9.2091 16,7C16,4.7909 14.2091,3 12,3C9.7909,3 8,4.7909 8,7C8,9.2091 9.7909,11 12,11Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:fillType="evenOdd"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"/>
<path
android:fillColor="#00000000"
android:pathData="M20,21V19C20,16.7909 18.2091,15 16,15H8C5.7909,15 4,16.7909 4,19V21"
android:strokeWidth="2"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"
android:strokeLineJoin="round"
tools:strokeColor="#FF0000" />
<path
android:fillColor="#00000000"
android:fillType="evenOdd"
android:pathData="M12,11C14.2091,11 16,9.2091 16,7C16,4.7909 14.2091,3 12,3C9.7909,3 8,4.7909 8,7C8,9.2091 9.7909,11 12,11Z"
android:strokeWidth="2"
android:strokeColor="#2E2F32"
android:strokeLineCap="round"
android:strokeLineJoin="round"
tools:strokeColor="#FF0000" />
</vector>

View File

@ -20,9 +20,10 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginStart="16dp"
android:contentDescription="@string/avatar"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
android:src="@drawable/ic_message_password"
android:src="@drawable/ic_security_key_24dp"
android:tint="?riotx_text_primary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
@ -30,7 +31,7 @@
android:id="@+id/bootstrapTitleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:textColor="?riotx_text_primary"
@ -39,7 +40,7 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/bootstrapIcon"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/recovery_passphrase" />
tools:text="@string/bottom_sheet_setup_secure_backup_title" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/bottomSheetFragmentContainer"

View File

@ -27,9 +27,10 @@
android:id="@+id/bootstrapIcon"
android:layout_width="32dp"
android:layout_height="32dp"
android:contentDescription="@string/avatar"
android:importantForAccessibility="no"
android:scaleType="fitCenter"
android:src="@drawable/ic_message_key" />
android:src="@drawable/ic_security_key_24dp"
android:tint="?riotx_text_primary" />
<TextView
android:id="@+id/bootstrapTitleText"
@ -49,6 +50,7 @@
android:id="@+id/keepItSafeText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_secondary"
android:textSize="16sp"
tools:text="@string/bootstrap_crosssigning_save_usb" />

View File

@ -11,7 +11,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/bootstrap_info_text"
android:text="@string/bootstrap_info_text_2"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/ssss_passphrase_enter_til"

View File

@ -15,10 +15,9 @@
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/bootstrap_save_key_description"
android:text="@string/bottom_sheet_save_your_recovery_key_content"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:layout_constraintTop_toTopOf="parent" />
android:textSize="14sp" />
<TextView
android:id="@+id/bootstrapRecoveryKeyText"
@ -44,7 +43,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp"
app:actionTitle="@string/copy_value"
app:actionTitle="@string/action_copy"
app:leftIcon="@drawable/ic_clipboard"
app:rightIcon="@drawable/ic_arrow_right"
app:tint="?colorAccent" />

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="200dp"
android:orientation="vertical">
<TextView
android:id="@+id/bootstrapSetupSecureText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/bottom_sheet_setup_secure_backup_subtitle"
android:textColor="?riotx_text_primary"
android:textSize="14sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:background="?attr/vctr_list_divider_color" />
<im.vector.riotx.core.ui.views.BottomSheetActionButton
android:id="@+id/bootstrapSetupSecureSubmit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp"
app:actionTitle="@string/bottom_sheet_setup_secure_backup_submit"
app:rightIcon="@drawable/ic_arrow_right"
app:tint="?colorAccent" />
<im.vector.riotx.core.ui.views.BottomSheetActionButton
android:id="@+id/bootstrapSetupSecureUseSecurityKey"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp"
android:visibility="gone"
app:actionDescription="@string/bottom_sheet_setup_secure_backup_security_key_subtitle"
app:actionTitle="@string/bottom_sheet_setup_secure_backup_security_key_title"
app:leftIcon="@drawable/ic_security_key_24dp"
app:rightIcon="@drawable/ic_arrow_right"
tools:visibility="visible" />
<View
android:id="@+id/bootstrapSetupSecureUseSecurityPassphraseSeparator"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color"
android:visibility="gone"
tools:visibility="visible" />
<im.vector.riotx.core.ui.views.BottomSheetActionButton
android:id="@+id/bootstrapSetupSecureUseSecurityPassphrase"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp"
android:visibility="gone"
app:actionDescription="@string/bottom_sheet_setup_secure_backup_security_phrase_subtitle"
app:actionTitle="@string/bottom_sheet_setup_secure_backup_security_phrase_title"
app:leftIcon="@drawable/ic_security_phrase_24dp"
app:rightIcon="@drawable/ic_arrow_right"
tools:visibility="visible" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/vctr_list_divider_color" />
</LinearLayout>

View File

@ -19,7 +19,7 @@
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_key"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_key"
android:src="@drawable/ic_message_key" />
android:src="@drawable/ic_security_key_24dp" />
<TextView
android:id="@+id/ssss_restore_with_key"

View File

@ -19,7 +19,7 @@
app:layout_constraintBottom_toBottomOf="@+id/ssss_restore_with_passphrase"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/ssss_restore_with_passphrase"
android:src="@drawable/ic_message_password" />
android:src="@drawable/ic_security_phrase_24dp" />
<TextView
android:id="@+id/ssss_restore_with_passphrase"
@ -98,7 +98,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/use_recovery_key"
app:icon="@drawable/ic_message_key"
app:icon="@drawable/ic_security_key_24dp"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton

View File

@ -2,7 +2,6 @@
<resources>
<!-- use to retrieve the supported languages list -->
<!-- should the same value as the file name -->
<string name="resources_language">en</string>
<string name="resources_country_code">US</string>
<!-- NOTE TO TRANSLATORS: Value MUST have 4 letters and MUST be in this list: https://www.unicode.org/iso15924/iso15924-codes.html. Example: "Arab", "Cyrl", "Latn", etc. -->
@ -35,7 +34,6 @@
<string name="title_activity_verify_device">Verify session</string>
<!-- Signing out screen -->
<string name="keys_backup_is_not_finished_please_wait">Keys backup is not finished, please wait…</string>
<string name="sign_out_bottom_sheet_warning_no_backup">Youll lose your encrypted messages if you sign out now</string>
<string name="sign_out_bottom_sheet_warning_backing_up">Key backup in progress. If you sign out now youll lose access to your encrypted messages.</string>
@ -46,7 +44,6 @@
<string name="are_you_sure">Are you sure?</string>
<string name="backup">Back up</string>
<string name="sign_out_bottom_sheet_will_lose_secure_messages">Youll lose access to your encrypted messages unless you back up your keys before signing out.</string>
<string name="dialog_title_third_party_licences">Third party licences</string>
<!-- splash screen accessibility -->
@ -117,6 +114,7 @@
<string name="action_mark_room_read">Mark as read</string>
<string name="action_open">Open</string>
<string name="action_close">Close</string>
<string name="action_copy">Copy</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="disable">Disable</string>
@ -2078,8 +2076,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="verification_request_you_accepted">You accepted</string>
<string name="verification_sent">Verification Sent</string>
<string name="verification_request">Verification Request</string>
<string name="verification_verify_device">Verify this session</string>
<string name="verification_verify_device_manually">Manually verify</string>
@ -2166,7 +2162,6 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="verification_conclusion_ok_notice">Messages with this user are end-to-end encrypted and can\'t be read by third parties.</string>
<string name="verification_conclusion_ok_self_notice">Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.</string>
<string name="encryption_information_cross_signing_state">Cross-Signing</string>
<string name="encryption_information_dg_xsigning_complete">Cross-Signing is enabled\nPrivate Keys on device.</string>
<string name="encryption_information_dg_xsigning_trusted">Cross-Signing is enabled\nKeys are trusted.\nPrivate keys are not known</string>
@ -2305,8 +2300,9 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="bootstrap_info_text">Secure &amp; unlock encrypted messages and trust with a %s.</string>
<!-- %s will be replaced by recovery_passphrase -->
<string name="bootstrap_info_confirm_text">Enter your %s again to confirm it.</string>
<string name="bootstrap_dont_reuse_pwd">Dont re-use your account password.</string>
<string name="bootstrap_dont_reuse_pwd">Dont use your account password.</string>
<string name="bootstrap_info_text_2">Enter a security phrase only you know, used to secure secrets on your server.</string>
<string name="bootstrap_loading_text">This might take several seconds, please be patient.</string>
<string name="bootstrap_loading_title">Setting up recovery.</string>
@ -2339,7 +2335,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="bootstrap_skip_text">Setting a Recovery Passphrase lets you secure &amp; unlock encrypted messages and trust.\n\nIf you dont want to set a Message Password, generate a Message Key instead.</string>
<string name="bootstrap_skip_text_no_gen_key">Setting a Recovery Passphrase lets you secure &amp; unlock encrypted messages and trust.</string>
<string name="bootstrap_cancel_text">If you cancel now, you may lose encrypted messages &amp; data if you lose access to your logins.\n\nYou can also set up Secure Backup &amp; manage your keys in Settings.</string>
<string name="encryption_enabled">Encryption enabled</string>
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string>
@ -2370,6 +2366,7 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="room_message_placeholder">Message…</string>
<string name="upgrade_security">Encryption upgrade available</string>
<string name="setup_cross_signing">Enable Cross Signing</string>
<string name="security_prompt_text">Verify yourself &amp; others to keep your chats safe</string>
<!-- %s will be replaced by recovery_key -->
@ -2478,4 +2475,25 @@ Not all features in Riot are implemented in RiotX yet. Main missing (and coming
<string name="a11y_stop_camera">Stop the camera</string>
<string name="a11y_start_camera">Start the camera</string>
</resources>
<string name="settings_setup_secure_backup">Set up Secure Backup</string>
<string name="bottom_sheet_setup_secure_backup_title">Secure backup</string>
<string name="bottom_sheet_setup_secure_backup_subtitle">Safeguard against losing access to encrypted messages &amp; data by backing up encryption keys on your server.</string>
<string name="bottom_sheet_setup_secure_backup_submit">Set up</string>
<string name="bottom_sheet_setup_secure_backup_security_key_title">Use a Security Key</string>
<string name="bottom_sheet_setup_secure_backup_security_key_subtitle">Generate a security key to store somewhere safe like a password manager or a safe.</string>
<string name="bottom_sheet_setup_secure_backup_security_phrase_title">Use a Security Phrase</string>
<string name="bottom_sheet_setup_secure_backup_security_phrase_subtitle">Enter a secret phrase only you know, and generate a key for backup.</string>
<string name="bottom_sheet_save_your_recovery_key_title">Save your Security Key</string>
<string name="bottom_sheet_save_your_recovery_key_content">Store your Security Key somewhere safe, like a password manager or a safe.</string>
<string name="set_a_security_phrase_title">Set a Security Phrase</string>
<string name="set_a_security_phrase_notice">Enter a security phrase only you know, used to secure secrets on your server.</string>
<string name="set_a_security_phrase_hint">Security Phrase</string>
<string name="set_a_security_phrase_again_notice">Enter your Security Phrase again to confirm it.</string>
<string name="save_your_security_key_title">Save your Security Key</string>
<string name="save_your_security_key_notice">Store your Security Key somewhere safe, like a password manager or a safe.</string>
</resources>