Merge branch 'develop' into feature/fga/voip_v1_start

This commit is contained in:
ganfra 2021-02-04 16:52:06 +01:00
commit 6cd462f852
112 changed files with 2571 additions and 916 deletions

View File

@ -1,4 +1,4 @@
Changes in Element 1.0.16 (2020-XX-XX) Changes in Element 1.X.X (2020-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
@ -25,14 +25,22 @@ Test:
Other changes: Other changes:
- -
Changes in Element 1.0.15 (2020-XX-XX) Changes in Element 1.0.16 (2020-02-04)
===================================================
Bugfix 🐛:
- Fix crash on API < 30 and light theme (#2774)
Changes in Element 1.0.15 (2020-02-03)
=================================================== ===================================================
Features ✨: Features ✨:
- - Social Login support
Improvements 🙌: Improvements 🙌:
- - SSO support for cross signing (#1062)
- Deactivate account when logged in with SSO (#1264)
- SSO UIA doesn't work (#2754)
Bugfix 🐛: Bugfix 🐛:
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started. - Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.
@ -40,9 +48,9 @@ Bugfix 🐛:
- UrlPreview should be updated when the url is edited and changed (#2678) - UrlPreview should be updated when the url is edited and changed (#2678)
- When receiving a new pepper from identity server, use it on the next hash lookup (#2708) - When receiving a new pepper from identity server, use it on the next hash lookup (#2708)
- Crashes reported by PlayStore (new in 1.0.14) (#2707) - Crashes reported by PlayStore (new in 1.0.14) (#2707)
- Widgets: Support $matrix_widget_id parameter (#2748)
Translations 🗣: - Data for Worker overload (#2721)
- - Fix multiple tasks
SDK API changes ⚠️: SDK API changes ⚠️:
- Increase targetSdkVersion to 30 (#2600) - Increase targetSdkVersion to 30 (#2600)
@ -50,9 +58,6 @@ SDK API changes ⚠️:
Build 🧱: Build 🧱:
- Compile with Android SDK 30 (Android 11) - Compile with Android SDK 30 (Android 11)
Test:
-
Other changes: Other changes:
- Update Dagger to 2.31 version so we can use the embedded AssistedInject feature - Update Dagger to 2.31 version so we can use the embedded AssistedInject feature

View File

@ -0,0 +1,2 @@
Main changes in this version: Social Login support.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.15

View File

@ -0,0 +1,2 @@
Main changes in this version: Social Login support.
Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16

View File

@ -16,8 +16,18 @@
package org.matrix.android.sdk.account package org.matrix.android.sdk.account
import org.junit.Assert.assertTrue
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.data.LoginFlowResult import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
@ -25,12 +35,8 @@ import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback import org.matrix.android.sdk.common.TestMatrixCallback
import org.junit.Assert.assertTrue import kotlin.coroutines.Continuation
import org.junit.FixMethodOrder import kotlin.coroutines.resume
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.junit.runners.MethodSorters
@RunWith(JUnit4::class) @RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -44,7 +50,18 @@ class DeactivateAccountTest : InstrumentedTest {
// Deactivate the account // Deactivate the account
commonTestHelper.runBlockingTest { commonTestHelper.runBlockingTest {
session.deactivateAccount(TestConstants.PASSWORD, false) session.deactivateAccount(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, false)
} }
// Try to login on the previous account, it will fail (M_USER_DEACTIVATED) // Try to login on the previous account, it will fail (M_USER_DEACTIVATED)

View File

@ -19,6 +19,18 @@ package org.matrix.android.sdk.common
import android.os.SystemClock import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.OutgoingSasVerificationTransaction
@ -36,17 +48,10 @@ import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM_BACKUP
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupAuthData
import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreationInfo
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import java.util.UUID import java.util.UUID
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -304,10 +309,18 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun initializeCrossSigning(session: Session) { fun initializeCrossSigning(session: Session) {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
session.cryptoService().crossSigningService() session.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = session.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
} }
} }

View File

@ -17,7 +17,18 @@
package org.matrix.android.sdk.internal.crypto package org.matrix.android.sdk.internal.crypto
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
@ -30,19 +41,13 @@ import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.TestConstants import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper import org.matrix.android.sdk.internal.crypto.model.OlmSessionWrapper
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm import org.matrix.android.sdk.internal.crypto.store.db.deserializeFromRealm
import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm import org.matrix.android.sdk.internal.crypto.store.db.serializeForRealm
import org.amshove.kluent.shouldBe
import org.junit.Assert
import org.junit.Before
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.olm.OlmSession import org.matrix.olm.OlmSession
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
/** /**
* Ref: * Ref:
@ -202,10 +207,18 @@ class UnwedgingTest : InstrumentedTest {
// It's a trick to force key request on fail to decrypt // It's a trick to force key request on fail to decrypt
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = bobSession.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
} }
// Wait until we received back the key // Wait until we received back the key

View File

@ -17,14 +17,6 @@
package org.matrix.android.sdk.internal.crypto.crosssigning package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull import org.junit.Assert.assertNotNull
@ -35,6 +27,19 @@ import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ -49,10 +54,17 @@ class XSigningTest : InstrumentedTest {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(object : UserInteractiveAuthInterceptor {
user = aliceSession.myUserId, override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
password = TestConstants.PASSWORD promise.resume(
), it) UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
} }
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys() val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
@ -86,8 +98,18 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
) )
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } mTestHelper.doSync<Unit> {
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it)
}
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it) }
// Check that alice can see bob keys // Check that alice can see bob keys
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD password = TestConstants.PASSWORD
) )
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(aliceAuthParams)
}
}, it) }
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(bobAuthParams)
}
}, it) }
// Check that alice can see bob keys // Check that alice can see bob keys
val bobUserId = bobSession.myUserId val bobUserId = bobSession.myUserId

View File

@ -18,7 +18,21 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.IncomingSasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
@ -28,6 +42,7 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxStat
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.common.CommonTestHelper import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.SessionTestParams import org.matrix.android.sdk.common.SessionTestParams
@ -40,19 +55,9 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import junit.framework.TestCase.fail
import org.junit.Assert
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -200,10 +205,17 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession1.cryptoService().crossSigningService() aliceSession1.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = aliceSession1.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
)
)
}
}, it)
} }
// Also bootstrap keybackup on first session // Also bootstrap keybackup on first session
@ -305,10 +317,18 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = aliceSession.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
} }
// Create an encrypted room and send a couple of messages // Create an encrypted room and send a couple of messages
@ -332,10 +352,18 @@ class KeyShareTests : InstrumentedTest {
val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true)) val bobSession = mTestHelper.createAccount(TestConstants.USER_BOB, SessionTestParams(true))
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = bobSession.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
} }
// Let alice invite bob // Let alice invite bob
@ -356,7 +384,7 @@ class KeyShareTests : InstrumentedTest {
val roomRoomBobPov = aliceSession.getRoom(roomId) val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId) val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId)
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") } var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
assert(dRes == null) assert(dRes == null)
@ -367,7 +395,7 @@ class KeyShareTests : InstrumentedTest {
Thread.sleep(3_000) Thread.sleep(3_000)
// With the bug the first session would have improperly reshare that key :/ // With the bug the first session would have improperly reshare that key :/
dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") } dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin.root, "") }
Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}") Log.d("#TEST", "KS: sgould not decrypt that ${beforeJoin.root.getClearContent().toModel<MessageContent>()?.body}")
assert(dRes?.clearEvent == null) assert(dRes?.clearEvent == null)
} }

View File

@ -17,20 +17,25 @@
package org.matrix.android.sdk.internal.crypto.verification.qrcode package org.matrix.android.sdk.internal.crypto.verification.qrcode
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.TestConstants
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.amshove.kluent.shouldBe import org.amshove.kluent.shouldBe
import org.junit.FixMethodOrder import org.junit.FixMethodOrder
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.runners.MethodSorters import org.junit.runners.MethodSorters
import org.matrix.android.sdk.InstrumentedTest
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.common.CommonTestHelper
import org.matrix.android.sdk.common.CryptoTestHelper
import org.matrix.android.sdk.common.TestConstants
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM) @FixMethodOrder(MethodSorters.JVM)
@ -157,18 +162,34 @@ class VerificationTest : InstrumentedTest {
mTestHelper.doSync<Unit> { callback -> mTestHelper.doSync<Unit> { callback ->
aliceSession.cryptoService().crossSigningService() aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = aliceSession.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), callback) promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, callback)
} }
mTestHelper.doSync<Unit> { callback -> mTestHelper.doSync<Unit> { callback ->
bobSession.cryptoService().crossSigningService() bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = bobSession.myUserId, object : UserInteractiveAuthInterceptor {
password = TestConstants.PASSWORD override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), callback) promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, callback)
} }
val aliceVerificationService = aliceSession.cryptoService().verificationService() val aliceVerificationService = aliceSession.cryptoService().verificationService()

View File

@ -0,0 +1,69 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
/**
* This class provides the authentication data by using user and password
*/
@JsonClass(generateAdapter = true)
data class TokenBasedAuth(
/**
* This is a session identifier that the client must pass back to the homeserver,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
@Json(name = "session")
override val session: String? = null,
/**
* A client may receive a login token via some external service, such as email or SMS.
* Note that a login token is separate from an access token, the latter providing general authentication to various API endpoints.
*/
@Json(name = "token")
val token: String? = null,
/**
* The txn_id should be a random string generated by the client for the request.
* The same txn_id should be used if retrying the request.
* The txn_id may be used by the server to disallow other devices from using the token,
* thus providing "single use" tokens while still allowing the device to retry the request.
* This would be done by tying the token to the txn_id server side, as well as potentially invalidating
* the token completely once the device has successfully logged in
* (e.g. when we receive a request from the newly provisioned access_token).
*/
@Json(name = "txn_id")
val transactionId: String? = null,
// registration information
@Json(name = "type")
val type: String? = LoginFlowTypes.TOKEN
) : UIABaseAuth {
override fun hasAuthInfo() = token != null
override fun copyWithSession(session: String) = this.copy(session = session)
override fun asMap(): Map<String, *> = mapOf(
"session" to session,
"token" to token,
"transactionId" to transactionId,
"type" to type
)
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 2020 New Vector Ltd * Copyright 2020 The Matrix.org Foundation C.I.C.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,6 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.error package org.matrix.android.sdk.api.auth
class SsoFlowNotSupportedYet : Throwable() interface UIABaseAuth {
/**
* This is a session identifier that the client must pass back to the homeserver,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
val session: String?
fun hasAuthInfo(): Boolean
fun copyWithSession(session: String): UIABaseAuth
fun asMap() : Map<String, *>
}

View File

@ -0,0 +1,47 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.api.auth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation
/**
* Some API endpoints require authentication that interacts with the user.
* The homeserver may provide many different ways of authenticating, such as user/password auth, login via a social network (OAuth2),
* login by confirming a token sent to their email address, etc.
*
* The process takes the form of one or more 'stages'.
* At each stage the client submits a set of data for a given authentication type and awaits a response from the server,
* which will either be a final success or a request to perform an additional stage.
* This exchange continues until the final success.
*
* For each endpoint, a server offers one or more 'flows' that the client can use to authenticate itself.
* Each flow comprises a series of stages, as described above.
* The client is free to choose which flow it follows, however the flow's stages must be completed in order.
* Failing to follow the flows in order must result in an HTTP 401 response.
* When all stages in a flow are complete, authentication is complete and the API call succeeds.
*/
interface UserInteractiveAuthInterceptor {
/**
* When the API needs additional auth, this will be called.
* Implementation should check the flows from flow response and act accordingly.
* Updated auth should be provided using promise.resume, this allow implementation to perform
* an async operation (prompt for user password, open sso fallback) and then resume initial API call when done.
*/
fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>)
}

View File

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.crypto.model.rest package org.matrix.android.sdk.api.auth
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
@ -27,7 +27,7 @@ data class UserPasswordAuth(
// device device session id // device device session id
@Json(name = "session") @Json(name = "session")
val session: String? = null, override val session: String? = null,
// registration information // registration information
@Json(name = "type") @Json(name = "type")
@ -38,4 +38,16 @@ data class UserPasswordAuth(
@Json(name = "password") @Json(name = "password")
val password: String? = null val password: String? = null
) ) : UIABaseAuth {
override fun hasAuthInfo() = password != null
override fun copyWithSession(session: String) = this.copy(session = session)
override fun asMap(): Map<String, *> = mapOf(
"session" to session,
"user" to user,
"password" to password,
"type" to type
)
}

View File

@ -38,15 +38,24 @@ data class SsoIdentityProvider(
* If present then it must be an HTTPS URL to an image resource. * If present then it must be an HTTPS URL to an image resource.
* This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily. * This should be hosted by the homeserver service provider to not leak the client's IP address unnecessarily.
*/ */
@Json(name = "icon") val iconUrl: String? @Json(name = "icon") val iconUrl: String?,
/**
* The `brand` field is **optional**. It allows the client to style the login
* button to suit a particular brand. It should be a string matching the
* "Common namespaced identifier grammar" as defined in
* [MSC2758](https://github.com/matrix-org/matrix-doc/pull/2758).
*/
@Json(name = "brand") val brand: String?
) : Parcelable { ) : Parcelable {
companion object { companion object {
// Not really defined by the spec, but we may define some ids here const val BRAND_GOOGLE = "org.matrix.google"
const val ID_GOOGLE = "google" const val BRAND_GITHUB = "org.matrix.github"
const val ID_GITHUB = "github" const val BRAND_APPLE = "org.matrix.apple"
const val ID_APPLE = "apple" const val BRAND_FACEBOOK = "org.matrix.facebook"
const val ID_FACEBOOK = "facebook" const val BRAND_TWITTER = "org.matrix.twitter"
const val ID_TWITTER = "twitter" const val BRAND_GITLAB = "org.matrix.gitlab"
} }
} }

View File

@ -14,14 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package org.matrix.android.sdk.internal.auth.registration package org.matrix.android.sdk.api.auth.registration
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.FlowResult
import org.matrix.android.sdk.api.auth.registration.Stage
import org.matrix.android.sdk.api.auth.registration.TermPolicies
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
@ -109,3 +106,8 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
return FlowResult(missingStage, completedStage) return FlowResult(missingStage, completedStage)
} }
fun RegistrationFlowResponse.nextUncompletedStage(flowIndex: Int = 0): String? {
val completed = completedStages ?: emptyList()
return flows?.getOrNull(flowIndex)?.stages?.firstOrNull { completed.contains(it).not() }
}

View File

@ -16,8 +16,8 @@
package org.matrix.android.sdk.api.failure package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import java.io.IOException import java.io.IOException
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
&& error.message == "Invalid password" && error.message == "Invalid password"
} }
fun Throwable.isInvalidUIAAuth(): Boolean {
return this is Failure.ServerError
&& error.code == MatrixError.M_FORBIDDEN
&& error.flows != null
}
/** /**
* Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible * Try to convert to a RegistrationFlowResponse. Return null in the cases it's not possible
*/ */
@ -53,6 +59,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
.adapter(RegistrationFlowResponse::class.java) .adapter(RegistrationFlowResponse::class.java)
.fromJson(this.errorBody) .fromJson(this.errorBody)
} }
} else if (this is Failure.ServerError && this.httpCode == 401 && this.error.code == MatrixError.M_FORBIDDEN) {
// This happens when the submission for this stage was bad (like bad password)
if (this.error.session != null && this.error.flows != null) {
RegistrationFlowResponse(
flows = this.error.flows,
session = this.error.session,
completedStages = this.error.completedStages,
params = this.error.params
)
} else null
} else { } else {
null null
} }

View File

@ -16,8 +16,8 @@
package org.matrix.android.sdk.api.failure package org.matrix.android.sdk.api.failure
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.crypto.MXCryptoError import org.matrix.android.sdk.api.session.crypto.MXCryptoError
import org.matrix.android.sdk.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.network.ssl.Fingerprint import org.matrix.android.sdk.internal.network.ssl.Fingerprint
import java.io.IOException import java.io.IOException

View File

@ -18,6 +18,8 @@ package org.matrix.android.sdk.api.failure
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.util.JsonDict
import org.matrix.android.sdk.internal.auth.data.InteractiveAuthenticationFlow
/** /**
* This data class holds the error defined by the matrix specifications. * This data class holds the error defined by the matrix specifications.
@ -42,7 +44,17 @@ data class MatrixError(
@Json(name = "soft_logout") val isSoftLogout: Boolean = false, @Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For M_INVALID_PEPPER // For M_INVALID_PEPPER
// {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"} // {"error": "pepper does not match 'erZvr'", "lookup_pepper": "pQgMS", "algorithm": "sha256", "errcode": "M_INVALID_PEPPER"}
@Json(name = "lookup_pepper") val newLookupPepper: String? = null @Json(name = "lookup_pepper") val newLookupPepper: String? = null,
// For M_FORBIDDEN UIA
@Json(name = "session")
val session: String? = null,
@Json(name = "completed")
val completedStages: List<String>? = null,
@Json(name = "flows")
val flows: List<InteractiveAuthenticationFlow>? = null,
@Json(name = "params")
val params: JsonDict? = null
) { ) {
companion object { companion object {

View File

@ -251,6 +251,8 @@ interface Session :
val sharedSecretStorageService: SharedSecretStorageService val sharedSecretStorageService: SharedSecretStorageService
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
/** /**
* Maintenance API, allows to print outs info on DB size to logcat * Maintenance API, allows to print outs info on DB size to logcat
*/ */

View File

@ -16,6 +16,8 @@
package org.matrix.android.sdk.api.session.account package org.matrix.android.sdk.api.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
/** /**
* This interface defines methods to manage the account. It's implemented at the session level. * This interface defines methods to manage the account. It's implemented at the session level.
*/ */
@ -43,5 +45,5 @@ interface AccountService {
* @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see * @param eraseAllData set to true to forget all messages that have been sent. Warning: this will cause future users to see
* an incomplete view of conversations * an incomplete view of conversations
*/ */
suspend fun deactivateAccount(password: String, eraseAllData: Boolean) suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean)
} }

View File

@ -20,6 +20,7 @@ import android.content.Context
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.paging.PagedList import androidx.paging.PagedList
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.listeners.ProgressListener import org.matrix.android.sdk.api.listeners.ProgressListener
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupService
@ -53,7 +54,7 @@ interface CryptoService {
fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>) fun setDeviceName(deviceId: String, deviceName: String, callback: MatrixCallback<Unit>)
fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>)
fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>) fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback<Unit>)

View File

@ -18,10 +18,10 @@ package org.matrix.android.sdk.api.session.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustResult
import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult import org.matrix.android.sdk.internal.crypto.crosssigning.UserTrustResult
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
interface CrossSigningService { interface CrossSigningService {
@ -40,7 +40,7 @@ interface CrossSigningService {
* Initialize cross signing for this user. * Initialize cross signing for this user.
* Users needs to enter credentials * Users needs to enter credentials
*/ */
fun initializeCrossSigning(authParams: UserPasswordAuth?, fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?,
callback: MatrixCallback<Unit>) callback: MatrixCallback<Unit>)
fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null

View File

@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.profile
import android.net.Uri import android.net.Uri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.JsonDict import org.matrix.android.sdk.api.util.JsonDict
@ -107,8 +108,7 @@ interface ProfileService {
* Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid * Finalize adding a 3Pids. Call this method once the user has validated that he owns the ThreePid
*/ */
fun finalizeAddingThreePid(threePid: ThreePid, fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable matrixCallback: MatrixCallback<Unit>): Cancelable
/** /**

View File

@ -36,3 +36,6 @@ internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect"
internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect"
internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl" internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl"
// Ref: https://matrix.org/docs/spec/client_server/r0.6.1#single-sign-on
internal const val SSO_UIA_FALLBACK_PATH = "/_matrix/client/r0/auth/m.login.sso/fallback/web"

View File

@ -43,5 +43,6 @@ internal data class LoginFlow(
* See MSC #2858 * See MSC #2858
*/ */
@Json(name = "org.matrix.msc2858.identity_providers") @Json(name = "org.matrix.msc2858.identity_providers")
val ssoIdentityProvider: List<SsoIdentityProvider>? val ssoIdentityProvider: List<SsoIdentityProvider>? = null
) )

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
import org.matrix.android.sdk.api.auth.registration.toFlowResult
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError import org.matrix.android.sdk.api.failure.Failure.RegistrationFlowError
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable

View File

@ -0,0 +1,53 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.auth.registration
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.auth.UIABaseAuth
import timber.log.Timber
import kotlin.coroutines.suspendCoroutine
internal suspend fun handleUIA(failure: Throwable, interceptor: UserInteractiveAuthInterceptor, retryBlock: suspend (UIABaseAuth) -> Unit): Boolean {
Timber.d("## UIA: check error ${failure.message}")
val flowResponse = failure.toRegistrationFlowResponse()
?: return false.also {
Timber.d("## UIA: not a UIA error")
}
Timber.d("## UIA: error can be passed to interceptor")
Timber.d("## UIA: type = ${flowResponse.flows}")
Timber.d("## UIA: delegate to interceptor...")
val authUpdate = try {
suspendCoroutine<UIABaseAuth> { continuation ->
interceptor.performStage(flowResponse, (failure as? Failure.ServerError)?.error?.code, continuation)
}
} catch (failure: Throwable) {
Timber.w(failure, "## UIA: failed to participate")
return false
}
Timber.d("## UIA: updated auth $authUpdate")
return try {
retryBlock(authUpdate)
true
} catch (failure: Throwable) {
handleUIA(failure, interceptor, retryBlock)
}
}

View File

@ -30,6 +30,7 @@ import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.crypto.MXCryptoConfig import org.matrix.android.sdk.api.crypto.MXCryptoConfig
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) { override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDeviceTask deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId)) { .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
this.executionThread = TaskThread.CRYPTO this.executionThread = TaskThread.CRYPTO
this.callback = callback this.callback = callback
} }

View File

@ -19,30 +19,30 @@ package org.matrix.android.sdk.internal.crypto.crosssigning
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.work.BackoffPolicy import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService import org.matrix.android.sdk.api.session.crypto.crosssigning.CrossSigningService
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.DeviceListManager import org.matrix.android.sdk.internal.crypto.DeviceListManager
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore import org.matrix.android.sdk.internal.crypto.store.IMXCryptoStore
import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo import org.matrix.android.sdk.internal.crypto.store.PrivateKeysInfo
import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask import org.matrix.android.sdk.internal.crypto.tasks.InitializeCrossSigningTask
import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskExecutor
import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.TaskThread
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.di.SessionId
import org.matrix.android.sdk.internal.di.WorkManagerProvider
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmPkSigning
import org.matrix.olm.OlmUtility import org.matrix.olm.OlmUtility
@ -61,7 +61,10 @@ internal class DefaultCrossSigningService @Inject constructor(
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val cryptoCoroutineScope: CoroutineScope, private val cryptoCoroutineScope: CoroutineScope,
private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { private val workManagerProvider: WorkManagerProvider,
private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
) : CrossSigningService,
DeviceListManager.UserDevicesUpdateListener {
private var olmUtility: OlmUtility? = null private var olmUtility: OlmUtility? = null
@ -147,11 +150,11 @@ internal class DefaultCrossSigningService @Inject constructor(
* - Sign the keys and upload them * - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures * - 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(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
Timber.d("## CrossSigning initializeCrossSigning") Timber.d("## CrossSigning initializeCrossSigning")
val params = InitializeCrossSigningTask.Params( val params = InitializeCrossSigningTask.Params(
authParams = authParams interactiveAuthInterceptor = uiaInterceptor
) )
initializeCrossSigningTask.configureWith(params) { initializeCrossSigningTask.configureWith(params) {
this.callbackThread = TaskThread.CRYPTO this.callbackThread = TaskThread.CRYPTO
@ -689,7 +692,7 @@ internal class DefaultCrossSigningService @Inject constructor(
return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted))
} }
fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult { fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult {
val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified()
myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId))
@ -747,8 +750,11 @@ internal class DefaultCrossSigningService @Inject constructor(
} }
override fun onUsersDeviceUpdate(userIds: List<String>) { override fun onUsersDeviceUpdate(userIds: List<String>) {
Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds") Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users: $userIds")
val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds) val workerParams = UpdateTrustWorker.Params(
sessionId = sessionId,
filename = updateTrustWorkerDataRepository.createParam(userIds)
)
val workerData = WorkerParamsFactory.toData(workerParams) val workerData = WorkerParamsFactory.toData(workerParams)
val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>() val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder<UpdateTrustWorker>()

View File

@ -55,7 +55,11 @@ internal class UpdateTrustWorker(context: Context,
internal data class Params( internal data class Params(
override val sessionId: String, override val sessionId: String,
override val lastFailureMessage: String? = null, override val lastFailureMessage: String? = null,
val updatedUserIds: List<String> // Kept for compatibility, but not used anymore (can be used for pending Worker)
val updatedUserIds: List<String>? = null,
// Passing a long list of userId can break the Work Manager due to data size limitation.
// so now we use a temporary file to store the data
val filename: String? = null
) : SessionWorkerParams ) : SessionWorkerParams
@Inject lateinit var crossSigningService: DefaultCrossSigningService @Inject lateinit var crossSigningService: DefaultCrossSigningService
@ -64,6 +68,7 @@ internal class UpdateTrustWorker(context: Context,
@CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration
@UserId @Inject lateinit var myUserId: String @UserId @Inject lateinit var myUserId: String
@Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper
@Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository
@SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration
// @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater
@ -74,7 +79,17 @@ internal class UpdateTrustWorker(context: Context,
} }
override suspend fun doSafeWork(params: Params): Result { override suspend fun doSafeWork(params: Params): Result {
var userList = params.updatedUserIds var userList = params.filename
?.let { updateTrustWorkerDataRepository.getParam(it) }
?.userIds
?: params.updatedUserIds.orEmpty()
if (userList.isEmpty()) {
// This should not happen, but let's avoid go further in case of empty list
cleanup(params)
return Result.success()
}
// Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user,
// or a new device?) So we check all again :/ // or a new device?) So we check all again :/
@ -213,9 +228,15 @@ internal class UpdateTrustWorker(context: Context,
} }
} }
cleanup(params)
return Result.success() return Result.success()
} }
private fun cleanup(params: Params) {
params.filename
?.let { updateTrustWorkerDataRepository.delete(it) }
}
private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) { private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) {
val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java)
.equalTo(CrossSigningInfoEntityFields.USER_ID, userId) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId)

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.crypto.crosssigning
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.internal.di.MoshiProvider
import org.matrix.android.sdk.internal.di.SessionFilesDirectory
import java.io.File
import java.util.UUID
import javax.inject.Inject
@JsonClass(generateAdapter = true)
internal data class UpdateTrustWorkerData(
@Json(name = "userIds")
val userIds: List<String>
)
internal class UpdateTrustWorkerDataRepository @Inject constructor(
@SessionFilesDirectory parentDir: File
) {
private val workingDirectory = File(parentDir, "tw")
private val jsonAdapter = MoshiProvider.providesMoshi().adapter(UpdateTrustWorkerData::class.java)
// Return the path of the created file
fun createParam(userIds: List<String>): String {
val filename = "${UUID.randomUUID()}.json"
workingDirectory.mkdirs()
val file = File(workingDirectory, filename)
UpdateTrustWorkerData(userIds = userIds)
.let { jsonAdapter.toJson(it) }
.let { file.writeText(it) }
return filename
}
fun getParam(filename: String): UpdateTrustWorkerData? {
return File(workingDirectory, filename)
.takeIf { it.exists() }
?.readText()
?.let { jsonAdapter.fromJson(it) }
}
fun delete(filename: String) {
tryOrNull("Unable to delete $filename") {
File(workingDirectory, filename).delete()
}
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.crypto.model.rest
import org.matrix.android.sdk.api.auth.UIABaseAuth
data class DefaultBaseAuth(
/**
* This is a session identifier that the client must pass back to the homeserver,
* if one is provided, in subsequent attempts to authenticate in the same API call.
*/
override val session: String? = null
) : UIABaseAuth {
override fun hasAuthInfo() = true
override fun copyWithSession(session: String) = this.copy(session = session)
override fun asMap(): Map<String, *> = mapOf("session" to session)
}

View File

@ -24,5 +24,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeleteDeviceParams( internal data class DeleteDeviceParams(
@Json(name = "auth") @Json(name = "auth")
val userPasswordAuth: UserPasswordAuth? = null val auth: Map<String, *>? = null
) )

View File

@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody(
val userSigningKey: RestKeyInfo? = null, val userSigningKey: RestKeyInfo? = null,
@Json(name = "auth") @Json(name = "auth")
val auth: UserPasswordAuth? = null val auth: Map<String, *>? = null
) )

View File

@ -16,18 +16,22 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> { internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
data class Params( data class Params(
val deviceId: String val deviceId: String,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val userAuthParam: UIABaseAuth?
) )
} }
@ -39,12 +43,17 @@ internal class DefaultDeleteDeviceTask @Inject constructor(
override suspend fun execute(params: DeleteDeviceTask.Params) { override suspend fun execute(params: DeleteDeviceTask.Params) {
try { try {
executeRequest<Unit>(globalErrorReceiver) { executeRequest<Unit>(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams()) apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse() if (params.userInteractiveAuthInterceptor == null
?.let { Failure.RegistrationFlowError(it) } || !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
?: throwable execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable
}
} }
} }
} }

View File

@ -19,7 +19,7 @@ package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams import org.matrix.android.sdk.internal.crypto.model.rest.DeleteDeviceParams
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
return executeRequest(globalErrorReceiver) { return executeRequest(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId, apiCall = cryptoApi.deleteDevice(params.deviceId,
DeleteDeviceParams( DeleteDeviceParams(
userPasswordAuth = UserPasswordAuth( auth = UserPasswordAuth(
type = LoginFlowTypes.PASSWORD, type = LoginFlowTypes.PASSWORD,
session = params.authSession, session = params.authSession,
user = userId, user = userId,
password = params.password password = params.password
) ).asMap()
) )
) )
} }

View File

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import dagger.Lazy import dagger.Lazy
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.internal.crypto.MXOlmDevice import org.matrix.android.sdk.internal.crypto.MXOlmDevice
import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder import org.matrix.android.sdk.internal.crypto.MyDeviceInfoHolder
import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable import org.matrix.android.sdk.internal.crypto.crosssigning.canonicalSignable
@ -24,7 +26,6 @@ import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
import org.matrix.android.sdk.internal.crypto.model.KeyUsage import org.matrix.android.sdk.internal.crypto.model.KeyUsage
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder import org.matrix.android.sdk.internal.crypto.model.rest.UploadSignatureQueryBuilder
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.JsonCanonicalizer
@ -34,7 +35,7 @@ import javax.inject.Inject
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> { internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
data class Params( data class Params(
val authParams: UserPasswordAuth? val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
) )
data class Result( data class Result(
@ -117,10 +118,21 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
.key(sskPublicKey) .key(sskPublicKey)
.signature(userId, masterPublicKey, signedSSK) .signature(userId, masterPublicKey, signedSSK)
.build(), .build(),
userPasswordAuth = params.authParams userAuthParam = null
// userAuthParam = params.authParams
) )
uploadSigningKeysTask.execute(uploadSigningKeysParams) try {
uploadSigningKeysTask.execute(uploadSigningKeysParams)
} catch (failure: Throwable) {
if (params.interactiveAuthInterceptor == null
|| !handleUIA(failure, params.interactiveAuthInterceptor) { authUpdate ->
uploadSigningKeysTask.execute(uploadSigningKeysParams.copy(userAuthParam = authUpdate))
}) {
Timber.d("## UIA: propagate failure")
throw failure
}
}
// Sign the current device with SSK // Sign the current device with SSK
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder() val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()

View File

@ -16,14 +16,12 @@
package org.matrix.android.sdk.internal.crypto.tasks package org.matrix.android.sdk.internal.crypto.tasks
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.api.CryptoApi import org.matrix.android.sdk.internal.crypto.api.CryptoApi
import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey import org.matrix.android.sdk.internal.crypto.model.CryptoCrossSigningKey
import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse import org.matrix.android.sdk.internal.crypto.model.rest.KeysQueryResponse
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody import org.matrix.android.sdk.internal.crypto.model.rest.UploadSigningKeysBody
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.model.toRest import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
// the SSK // the SSK
val selfSignedKey: CryptoCrossSigningKey, val selfSignedKey: CryptoCrossSigningKey,
/** /**
* - If null: * Authorisation info (User Interactive flow)
* - 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? val userAuthParam: UIABaseAuth?
) )
} }
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
) : UploadSigningKeysTask { ) : UploadSigningKeysTask {
override suspend fun execute(params: UploadSigningKeysTask.Params) { override suspend fun execute(params: UploadSigningKeysTask.Params) {
val paramsHaveSessionId = params.userPasswordAuth?.session != null
val uploadQuery = UploadSigningKeysBody( val uploadQuery = UploadSigningKeysBody(
masterKey = params.masterKey.toRest(), masterKey = params.masterKey.toRest(),
userSigningKey = params.userKey.toRest(), userSigningKey = params.userKey.toRest(),
selfSigningKey = params.selfSignedKey.toRest(), selfSigningKey = params.selfSignedKey.toRest(),
// If sessionId is provided, use the userPasswordAuth auth = params.userAuthParam?.asMap()
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
) )
try { doRequest(uploadQuery)
doRequest(uploadQuery)
} catch (throwable: Throwable) {
val registrationFlowResponse = throwable.toRegistrationFlowResponse()
if (registrationFlowResponse != null
&& registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }
&& params.userPasswordAuth?.password != null
&& !paramsHaveSessionId
) {
// Retry with authentication
doRequest(uploadQuery.copy(auth = params.userPasswordAuth.copy(session = registrationFlowResponse.session)))
} else {
// Other error
throw throwable
}
}
} }
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) { private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {

View File

@ -53,6 +53,8 @@ import org.matrix.android.sdk.api.session.thirdparty.ThirdPartyService
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.api.session.user.UserService import org.matrix.android.sdk.api.session.user.UserService
import org.matrix.android.sdk.api.session.widgets.WidgetService import org.matrix.android.sdk.api.session.widgets.WidgetService
import org.matrix.android.sdk.api.util.appendParamToUrl
import org.matrix.android.sdk.internal.auth.SSO_UIA_FALLBACK_PATH
import org.matrix.android.sdk.internal.auth.SessionParamsStore import org.matrix.android.sdk.internal.auth.SessionParamsStore
import org.matrix.android.sdk.internal.crypto.DefaultCryptoService import org.matrix.android.sdk.internal.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
@ -277,6 +279,18 @@ internal class DefaultSession @Inject constructor(
return "$myUserId - ${sessionParams.deviceId}" return "$myUserId - ${sessionParams.deviceId}"
} }
override fun getUiaSsoFallbackUrl(authenticationSessionId: String): String {
val hsBas = sessionParams.homeServerConnectionConfig
.homeServerUri
.toString()
.trim { it == '/' }
return buildString {
append(hsBas)
append(SSO_UIA_FALLBACK_PATH)
appendParamToUrl("session", authenticationSessionId)
}
}
override fun logDbUsageInfo() { override fun logDbUsageInfo() {
RealmDebugTools(realmConfiguration).logInfo("Session") RealmDebugTools(realmConfiguration).logInfo("Session")
} }

View File

@ -18,7 +18,7 @@ package org.matrix.android.sdk.internal.session.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
/** /**
* Class to pass request parameters to update the password. * Class to pass request parameters to update the password.

View File

@ -18,21 +18,21 @@ package org.matrix.android.sdk.internal.session.account
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class DeactivateAccountParams( internal data class DeactivateAccountParams(
@Json(name = "auth")
val auth: UserPasswordAuth? = null,
// Set to true to erase all data of the account // Set to true to erase all data of the account
@Json(name = "erase") @Json(name = "erase")
val erase: Boolean val erase: Boolean,
@Json(name = "auth")
val auth: Map<String, *>? = null
) { ) {
companion object { companion object {
fun create(userId: String, password: String, erase: Boolean): DeactivateAccountParams { fun create(auth: UIABaseAuth?, erase: Boolean): DeactivateAccountParams {
return DeactivateAccountParams( return DeactivateAccountParams(
auth = UserPasswordAuth(user = userId, password = password), auth = auth?.asMap(),
erase = erase erase = erase
) )
} }

View File

@ -16,6 +16,9 @@
package org.matrix.android.sdk.internal.session.account package org.matrix.android.sdk.internal.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
@ -27,8 +30,9 @@ import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> { internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
data class Params( data class Params(
val password: String, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val eraseAllData: Boolean val eraseAllData: Boolean,
val userAuthParam: UIABaseAuth? = null
) )
} }
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
) : DeactivateAccountTask { ) : DeactivateAccountTask {
override suspend fun execute(params: DeactivateAccountTask.Params) { override suspend fun execute(params: DeactivateAccountTask.Params) {
val deactivateAccountParams = DeactivateAccountParams.create(userId, params.password, params.eraseAllData) val deactivateAccountParams = DeactivateAccountParams.create(params.userAuthParam, params.eraseAllData)
executeRequest<Unit>(globalErrorReceiver) { try {
apiCall = accountAPI.deactivate(deactivateAccountParams) executeRequest<Unit>(globalErrorReceiver) {
apiCall = accountAPI.deactivate(deactivateAccountParams)
}
} catch (throwable: Throwable) {
if (!handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable
}
} }
// Logout from identity server if any, ignoring errors // Logout from identity server if any, ignoring errors
runCatching { identityDisconnectTask.execute(Unit) } runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") } .onFailure { Timber.w(it, "Unable to disconnect identity server") }

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.account package org.matrix.android.sdk.internal.session.account
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.account.AccountService import org.matrix.android.sdk.api.session.account.AccountService
import javax.inject.Inject import javax.inject.Inject
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
} }
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) { override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData)) deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
} }
} }

View File

@ -28,9 +28,8 @@ import java.io.File
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
internal class ImageCompressor @Inject constructor() { internal class ImageCompressor @Inject constructor(private val context: Context) {
suspend fun compress( suspend fun compress(
context: Context,
imageFile: File, imageFile: File,
desiredWidth: Int, desiredWidth: Int,
desiredHeight: Int, desiredHeight: Int,
@ -46,7 +45,7 @@ internal class ImageCompressor @Inject constructor() {
} }
} ?: return@withContext imageFile } ?: return@withContext imageFile
val destinationFile = createDestinationFile(context) val destinationFile = createDestinationFile()
runCatching { runCatching {
destinationFile.outputStream().use { destinationFile.outputStream().use {
@ -118,7 +117,7 @@ internal class ImageCompressor @Inject constructor() {
} }
} }
private fun createDestinationFile(context: Context): File { private fun createDestinationFile(): File {
return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir)
} }
} }

View File

@ -156,7 +156,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
// Do not compress gif // Do not compress gif
&& attachment.mimeType != MimeTypes.Gif && attachment.mimeType != MimeTypes.Gif
&& params.compressBeforeSending) { && params.compressBeforeSending) {
fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE)
.also { compressedFile -> .also { compressedFile ->
// Get new Bitmap size // Get new Bitmap size
compressedFile.inputStream().use { compressedFile.inputStream().use {

View File

@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where import io.realm.kotlin.where
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.profile.ProfileService import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
@ -170,14 +171,12 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
} }
override fun finalizeAddingThreePid(threePid: ThreePid, override fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
accountPassword: String?,
matrixCallback: MatrixCallback<Unit>): Cancelable { matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params( .configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid, threePid = threePid,
session = uiaSession, userInteractiveAuthInterceptor = userInteractiveAuthInterceptor,
accountPassword = accountPassword,
userWantsToCancel = false userWantsToCancel = false
)) { )) {
callback = alsoRefresh(matrixCallback) callback = alsoRefresh(matrixCallback)
@ -189,8 +188,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
return finalizeAddingThreePidTask return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params( .configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid, threePid = threePid,
session = null, userInteractiveAuthInterceptor = null,
accountPassword = null,
userWantsToCancel = true userWantsToCancel = true
)) { )) {
callback = alsoRefresh(matrixCallback) callback = alsoRefresh(matrixCallback)

View File

@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.session.profile
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody( internal data class FinalizeAddThreePidBody(
@ -37,5 +36,5 @@ internal data class FinalizeAddThreePidBody(
* Additional authentication information for the user-interactive authentication API. * Additional authentication information for the user-interactive authentication API.
*/ */
@Json(name = "auth") @Json(name = "auth")
val auth: UserPasswordAuth? val auth: Map<String, *>? = null
) )

View File

@ -17,10 +17,12 @@
package org.matrix.android.sdk.internal.session.profile package org.matrix.android.sdk.internal.session.profile
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.auth.registration.handleUIA
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity import org.matrix.android.sdk.internal.database.model.PendingThreePidEntity
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.di.SessionDatabase
@ -29,13 +31,14 @@ import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> { internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params( data class Params(
val threePid: ThreePid, val threePid: ThreePid,
val session: String?, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val accountPassword: String?, val userAuthParam: UIABaseAuth? = null,
val userWantsToCancel: Boolean val userWantsToCancel: Boolean
) )
} }
@ -62,20 +65,21 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
val body = FinalizeAddThreePidBody( val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret, clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid, sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) { auth = params.userAuthParam?.asMap()
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
) )
apiCall = profileAPI.finalizeAddThreePid(body) apiCall = profileAPI.finalizeAddThreePid(body)
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse() if (params.userInteractiveAuthInterceptor == null
?.let { Failure.RegistrationFlowError(it) } || !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
?: throwable execute(params.copy(userAuthParam = auth))
}
) {
Timber.d("## UIA: propagate failure")
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
}
} }
} }

View File

@ -143,9 +143,11 @@ internal class CreateRoomBodyBuilder @Inject constructor(
} }
private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean { private suspend fun canEnableEncryption(params: CreateRoomParams): Boolean {
return (params.enableEncryptionIfInvitedUsersSupportIt return params.enableEncryptionIfInvitedUsersSupportIt
&& crossSigningService.isCrossSigningVerified() // Parity with web, enable if users have encryption ready devices
&& params.invite3pids.isEmpty()) // for now remove checks on cross signing and 3pid invites
// && crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isEmpty()
&& params.invitedUserIds.isNotEmpty() && params.invitedUserIds.isNotEmpty()
&& params.invitedUserIds.let { userIds -> && params.invitedUserIds.let { userIds ->
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false) val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)

View File

@ -140,14 +140,13 @@ internal class RoomSummaryUpdater @Inject constructor(
.queryActiveRoomMembersEvent() .queryActiveRoomMembersEvent()
.notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId)
.findAll() .findAll()
.asSequence()
.map { it.userId } .map { it.userId }
roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.clear()
roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers)
if (roomSummaryEntity.isEncrypted) { if (roomSummaryEntity.isEncrypted) {
// mmm maybe we could only refresh shield instead of checking trust also? // mmm maybe we could only refresh shield instead of checking trust also?
crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList()) crossSigningService.onUsersDeviceUpdate(otherRoomMembers)
} }
} }
} }

View File

@ -53,7 +53,7 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use
} }
} }
val isAddedByMe = widgetEvent.senderId == userId val isAddedByMe = widgetEvent.senderId == userId
val computedUrl = widgetContent.computeURL(widgetEvent.roomId) val computedUrl = widgetContent.computeURL(widgetEvent.roomId, widgetId)
return Widget( return Widget(
widgetContent = widgetContent, widgetContent = widgetContent,
event = widgetEvent, event = widgetEvent,
@ -65,13 +65,14 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use
) )
} }
private fun WidgetContent.computeURL(roomId: String?): String? { private fun WidgetContent.computeURL(roomId: String?, widgetId: String): String? {
var computedUrl = url ?: return null var computedUrl = url ?: return null
val myUser = userDataSource.getUser(userId) val myUser = userDataSource.getUser(userId)
computedUrl = computedUrl computedUrl = computedUrl
.replace("\$matrix_user_id", userId) .replace("\$matrix_user_id", userId)
.replace("\$matrix_display_name", myUser?.displayName ?: userId) .replace("\$matrix_display_name", myUser?.displayName ?: userId)
.replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "") .replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "")
.replace("\$matrix_widget_id", widgetId)
if (roomId != null) { if (roomId != null) {
computedUrl = computedUrl.replace("\$matrix_room_id", roomId) computedUrl = computedUrl.replace("\$matrix_room_id", roomId)

View File

@ -13,7 +13,7 @@ kapt {
// Note: 2 digits max for each value // Note: 2 digits max for each value
ext.versionMajor = 1 ext.versionMajor = 1
ext.versionMinor = 0 ext.versionMinor = 0
ext.versionPatch = 15 ext.versionPatch = 17
static def getGitTimestamp() { static def getGitTimestamp() {
def cmd = 'git show -s --format=%ct' def cmd = 'git show -s --format=%ct'

View File

@ -42,13 +42,18 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
@ -67,10 +72,18 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> { doSync<Unit> {
existingSession!!.cryptoService().crossSigningService() existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = existingSession!!.myUserId, object : UserInteractiveAuthInterceptor {
password = "password" override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password",
session = flowResponse.session
)
)
}
}, it)
} }
} }

View File

@ -46,8 +46,13 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
@LargeTest @LargeTest
@ -67,17 +72,35 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true) existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> { doSync<Unit> {
existingSession!!.cryptoService().crossSigningService() existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth( .initializeCrossSigning(
user = existingSession!!.myUserId, object : UserInteractiveAuthInterceptor {
password = "password" override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
), it) promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password",
session = flowResponse.session
)
)
}
}, it)
} }
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources)) val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
runBlocking { runBlocking {
task.execute(Params( task.execute(Params(
userPasswordAuth = UserPasswordAuth(password = password), userInteractiveAuthInterceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = password,
session = flowResponse.session
)
)
}
},
passphrase = passphrase, passphrase = passphrase,
setupMode = SetupMode.NORMAL setupMode = SetupMode.NORMAL
)) ))

View File

@ -63,7 +63,6 @@
<activity <activity
android:name=".features.MainActivity" android:name=".features.MainActivity"
android:taskAffinity=""
android:theme="@style/AppTheme.Launcher" /> android:theme="@style/AppTheme.Launcher" />
<!-- Activity alias for the launcher Activity (must be declared after the Activity it targets) --> <!-- Activity alias for the launcher Activity (must be declared after the Activity it targets) -->
@ -244,6 +243,27 @@
<activity android:name=".features.usercode.UserCodeActivity" /> <activity android:name=".features.usercode.UserCodeActivity" />
<activity android:name=".features.call.transfer.CallTransferActivity" /> <activity android:name=".features.call.transfer.CallTransferActivity" />
<!-- Single instance is very important for the custom scheme callback-->
<activity android:name=".features.auth.ReAuthActivity"
android:launchMode="singleInstance"
android:exported="false">
<!-- XXX: UIA SSO has only web fallback, i.e no url redirect, so for now we comment this out
hopefully, we would use it when finally available
-->
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data-->
<!-- android:host="reauth"-->
<!-- android:scheme="element" />-->
<!-- </intent-filter>-->
</activity>
<!-- Services --> <!-- Services -->
<service <service

View File

@ -28,7 +28,7 @@ import im.vector.app.features.crypto.keysbackup.settings.KeysBackupSettingsFragm
import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment import im.vector.app.features.crypto.quads.SharedSecuredStorageKeyFragment
import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment import im.vector.app.features.crypto.quads.SharedSecuredStoragePassphraseFragment
import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment import im.vector.app.features.crypto.quads.SharedSecuredStorageResetAllFragment
import im.vector.app.features.crypto.recover.BootstrapAccountPasswordFragment import im.vector.app.features.crypto.recover.BootstrapReAuthFragment
import im.vector.app.features.crypto.recover.BootstrapConclusionFragment import im.vector.app.features.crypto.recover.BootstrapConclusionFragment
import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment import im.vector.app.features.crypto.recover.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
@ -522,8 +522,8 @@ interface FragmentModule {
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class) @FragmentKey(BootstrapReAuthFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment fun bindBootstrapReAuthFragment(fragment: BootstrapReAuthFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap

View File

@ -25,6 +25,7 @@ import im.vector.app.core.dialogs.UnrecognizedCertificateDialog
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.preference.UserAvatarPreference import im.vector.app.core.preference.UserAvatarPreference
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.call.CallControlsBottomSheet import im.vector.app.features.call.CallControlsBottomSheet
import im.vector.app.features.call.VectorCallActivity import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.conference.VectorJitsiActivity import im.vector.app.features.call.conference.VectorJitsiActivity
@ -147,6 +148,7 @@ interface ScreenComponent {
fun inject(activity: SearchActivity) fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity) fun inject(activity: UserCodeActivity)
fun inject(activity: CallTransferActivity) fun inject(activity: CallTransferActivity)
fun inject(activity: ReAuthActivity)
/* ========================================================================================== /* ==========================================================================================
* BottomSheets * BottomSheets

View File

@ -106,12 +106,13 @@ class DefaultErrorFormatter @Inject constructor(
HttpURLConnection.HTTP_NOT_FOUND -> HttpURLConnection.HTTP_NOT_FOUND ->
// homeserver not found // homeserver not found
stringProvider.getString(R.string.login_error_no_homeserver_found) stringProvider.getString(R.string.login_error_no_homeserver_found)
HttpURLConnection.HTTP_UNAUTHORIZED ->
// uia errors?
stringProvider.getString(R.string.error_unauthorized)
else -> else ->
throwable.localizedMessage throwable.localizedMessage
} }
} }
is SsoFlowNotSupportedYet ->
stringProvider.getString(R.string.error_sso_flow_not_supported_yet)
is DialPadLookup.Failure -> is DialPadLookup.Failure ->
stringProvider.getString(R.string.call_dial_pad_lookup_error) stringProvider.getString(R.string.call_dial_pad_lookup_error)
else -> throwable.localizedMessage else -> throwable.localizedMessage

View File

@ -200,6 +200,7 @@ abstract class VectorBaseFragment<VB: ViewBinding> : BaseMvRxFragment(), HasScre
} }
protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) { protected fun showLoadingDialog(message: CharSequence? = null, cancelable: Boolean = false) {
progress?.dismiss()
progress = ProgressDialog(requireContext()).apply { progress = ProgressDialog(requireContext()).apply {
setCancelable(cancelable) setCancelable(cancelable)
setMessage(message ?: getString(R.string.please_wait)) setMessage(message ?: getString(R.string.please_wait))

View File

@ -0,0 +1,63 @@
/*
* 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.app.core.ui.list
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.button.MaterialButton
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
/**
* A generic button list item.
*/
@EpoxyModelClass(layout = R.layout.item_positive_button)
abstract class GenericPositiveButtonItem : VectorEpoxyModel<GenericPositiveButtonItem.Holder>() {
@EpoxyAttribute
var text: String? = null
@EpoxyAttribute
var buttonClickAction: View.OnClickListener? = null
@EpoxyAttribute
@ColorInt
var textColor: Int? = null
@EpoxyAttribute
@DrawableRes
var iconRes: Int? = null
override fun bind(holder: Holder) {
super.bind(holder)
holder.button.text = text
if (iconRes != null) {
holder.button.setIconResource(iconRes!!)
} else {
holder.button.icon = null
}
buttonClickAction?.let { holder.button.setOnClickListener(it) }
}
class Holder : VectorEpoxyHolder() {
val button by bind<MaterialButton>(R.id.itemGenericItemButton)
}
}

View File

@ -22,15 +22,13 @@ import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.viewModel
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.startSyncing import im.vector.app.core.extensions.startSyncing
import im.vector.app.core.platform.EmptyViewModel
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.deleteAllFiles import im.vector.app.core.utils.deleteAllFiles
import im.vector.app.databinding.FragmentLoadingBinding import im.vector.app.databinding.FragmentLoadingBinding
@ -83,8 +81,6 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
} }
} }
private val emptyViewModel: EmptyViewModel by viewModel()
override fun getBinding() = FragmentLoadingBinding.inflate(layoutInflater) override fun getBinding() = FragmentLoadingBinding.inflate(layoutInflater)
private lateinit var args: MainActivityArgs private lateinit var args: MainActivityArgs
@ -150,7 +146,7 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
} }
when { when {
args.isAccountDeactivated -> { args.isAccountDeactivated -> {
emptyViewModel.viewModelScope.launch { lifecycleScope.launch {
// Just do the local cleanup // Just do the local cleanup
Timber.w("Account deactivated, start app") Timber.w("Account deactivated, start app")
sessionHolder.clearActiveSession() sessionHolder.clearActiveSession()
@ -159,7 +155,7 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
} }
} }
args.clearCredentials -> { args.clearCredentials -> {
emptyViewModel.viewModelScope.launch { lifecycleScope.launch {
try { try {
session.signOut(!args.isUserLoggedOut) session.signOut(!args.isUserLoggedOut)
Timber.w("SIGN_OUT: success, start app") Timber.w("SIGN_OUT: success, start app")
@ -172,7 +168,7 @@ class MainActivity : VectorBaseActivity<FragmentLoadingBinding>(), UnlockedActiv
} }
} }
args.clearCache -> { args.clearCache -> {
emptyViewModel.viewModelScope.launch { lifecycleScope.launch {
try { try {
session.clearCache() session.clearCache()
doLocalCleanup(clearPreferences = false) doLocalCleanup(clearPreferences = false)

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) 2021 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.app.features.auth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentReauthConfirmBinding
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
class PromptFragment : VectorBaseFragment<FragmentReauthConfirmBinding>() {
private val viewModel: ReAuthViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentReauthConfirmBinding.inflate(layoutInflater, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.reAuthConfirmButton.debouncedClicks {
onButtonClicked()
}
views.passwordReveal.debouncedClicks {
viewModel.handle(ReAuthActions.StartSSOFallback)
}
views.passwordReveal.debouncedClicks {
viewModel.handle(ReAuthActions.TogglePassVisibility)
}
}
private fun onButtonClicked() = withState(viewModel) { state ->
when (state.flowType) {
LoginFlowTypes.SSO -> {
viewModel.handle(ReAuthActions.StartSSOFallback)
}
LoginFlowTypes.PASSWORD -> {
val password = views.passwordField.text.toString()
if (password.isBlank()) {
// Prompt to enter something
views.passwordFieldTil.error = getString(R.string.error_empty_field_your_password)
} else {
views.passwordFieldTil.error = null
viewModel.handle(ReAuthActions.ReAuthWithPass(password))
}
}
else -> {
// not supported
}
}
}
override fun invalidate() = withState(viewModel) {
when (it.flowType) {
LoginFlowTypes.SSO -> {
views.passwordContainer.isVisible = false
views.reAuthConfirmButton.text = getString(R.string.auth_login_sso)
}
LoginFlowTypes.PASSWORD -> {
views.passwordContainer.isVisible = true
views.reAuthConfirmButton.text = getString(R.string._continue)
}
else -> {
// This login flow is not supported, you should use web?
}
}
views.passwordField.showPassword(it.passwordVisible)
if (it.passwordVisible) {
views.passwordReveal.setImageResource(R.drawable.ic_eye_closed)
views.passwordReveal.contentDescription = getString(R.string.a11y_hide_password)
} else {
views.passwordReveal.setImageResource(R.drawable.ic_eye)
views.passwordReveal.contentDescription = getString(R.string.a11y_show_password)
}
if (it.lastErrorCode != null) {
when (it.flowType) {
LoginFlowTypes.SSO -> {
views.genericErrorText.isVisible = true
views.genericErrorText.text = getString(R.string.authentication_error)
}
LoginFlowTypes.PASSWORD -> {
views.passwordFieldTil.error = getString(R.string.authentication_error)
}
else -> {
// nop
}
}
} else {
views.passwordFieldTil.error = null
views.genericErrorText.isVisible = false
}
}
}

View File

@ -14,10 +14,14 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.platform package im.vector.app.features.auth
import com.airbnb.mvrx.MvRxState import im.vector.app.core.platform.VectorViewModelAction
data class EmptyState( sealed class ReAuthActions : VectorViewModelAction {
val dummy: Int = 0 object StartSSOFallback : ReAuthActions()
) : MvRxState object FallBackPageLoaded : ReAuthActions()
object FallBackPageClosed : ReAuthActions()
object TogglePassVisibility : ReAuthActions()
data class ReAuthWithPass(val password: String) : ReAuthActions()
}

View File

@ -0,0 +1,228 @@
/*
* Copyright (c) 2021 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.app.features.auth
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Parcelable
import androidx.browser.customtabs.CustomTabsCallback
import androidx.browser.customtabs.CustomTabsClient
import androidx.browser.customtabs.CustomTabsServiceConnection
import androidx.browser.customtabs.CustomTabsSession
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment
import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.core.utils.openUrlInChromeCustomTab
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.AuthenticationService
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import timber.log.Timber
import javax.inject.Inject
class ReAuthActivity : SimpleFragmentActivity(), ReAuthViewModel.Factory {
@Parcelize
data class Args(
val flowType: String?,
val title: String?,
val session: String?,
val lastErrorCode: String?,
val resultKeyStoreAlias: String
) : Parcelable
// For sso
private var customTabsServiceConnection: CustomTabsServiceConnection? = null
private var customTabsClient: CustomTabsClient? = null
private var customTabsSession: CustomTabsSession? = null
@Inject lateinit var authenticationService: AuthenticationService
@Inject lateinit var reAuthViewModelFactory: ReAuthViewModel.Factory
override fun create(initialState: ReAuthState) = reAuthViewModelFactory.create(initialState)
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
private val sharedViewModel: ReAuthViewModel by viewModel()
// override fun getTitleRes() = R.string.re_authentication_activity_title
override fun initUiAndData() {
super.initUiAndData()
val title = intent.extras?.getString(EXTRA_REASON_TITLE) ?: getString(R.string.re_authentication_activity_title)
supportActionBar?.setTitle(title) ?: run { setTitle(title) }
// val authArgs = intent.getParcelableExtra<Args>(MvRx.KEY_ARG)
// For the sso flow we can for now only rely on the fallback flow, that handles all
// the UI, due to the sandbox nature of CCT (chrome custom tab) we cannot get much information
// on how the process did go :/
// so we assume that after the user close the tab we return success and let caller retry the UIA flow :/
if (isFirstCreation()) {
addFragment(
R.id.container,
PromptFragment::class.java
)
}
sharedViewModel.observeViewEvents {
when (it) {
is ReAuthEvents.OpenSsoURl -> {
openInCustomTab(it.url)
}
ReAuthEvents.Dismiss -> {
setResult(RESULT_CANCELED)
finish()
}
is ReAuthEvents.PasswordFinishSuccess -> {
setResult(RESULT_OK, Intent().apply {
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.PASSWORD)
putExtra(RESULT_VALUE, it.passwordSafeForIntent)
})
finish()
}
}
}
}
override fun onResume() {
super.onResume()
// It's the only way we have to know if sso falback flow was successful
withState(sharedViewModel) {
if (it.ssoFallbackPageWasShown) {
Timber.d("## UIA ssoFallbackPageWasShown tentative success")
setResult(RESULT_OK, Intent().apply {
putExtra(RESULT_FLOW_TYPE, LoginFlowTypes.SSO)
})
finish()
}
}
}
override fun onStart() {
super.onStart()
withState(sharedViewModel) { state ->
if (state.ssoFallbackPageWasShown) {
sharedViewModel.handle(ReAuthActions.FallBackPageClosed)
return@withState
}
}
val packageName = CustomTabsClient.getPackageName(this, null)
// packageName can be null if there are 0 or several CustomTabs compatible browsers installed on the device
if (packageName != null) {
customTabsServiceConnection = object : CustomTabsServiceConnection() {
override fun onCustomTabsServiceConnected(name: ComponentName, client: CustomTabsClient) {
Timber.d("## CustomTab onCustomTabsServiceConnected($name)")
customTabsClient = client
.also { it.warmup(0L) }
customTabsSession = customTabsClient?.newSession(object : CustomTabsCallback() {
// override fun onPostMessage(message: String, extras: Bundle?) {
// Timber.v("## CustomTab onPostMessage($message)")
// }
//
// override fun onMessageChannelReady(extras: Bundle?) {
// Timber.v("## CustomTab onMessageChannelReady()")
// }
override fun onNavigationEvent(navigationEvent: Int, extras: Bundle?) {
Timber.v("## CustomTab onNavigationEvent($navigationEvent), $extras")
super.onNavigationEvent(navigationEvent, extras)
if (navigationEvent == NAVIGATION_FINISHED) {
// sharedViewModel.handle(ReAuthActions.FallBackPageLoaded)
}
}
override fun onRelationshipValidationResult(relation: Int, requestedOrigin: Uri, result: Boolean, extras: Bundle?) {
Timber.v("## CustomTab onRelationshipValidationResult($relation), $requestedOrigin")
super.onRelationshipValidationResult(relation, requestedOrigin, result, extras)
}
})
}
override fun onServiceDisconnected(name: ComponentName?) {
Timber.d("## CustomTab onServiceDisconnected($name)")
}
}.also {
CustomTabsClient.bindCustomTabsService(
this,
// Despite the API, packageName cannot be null
packageName,
it
)
}
}
}
override fun onStop() {
super.onStop()
customTabsServiceConnection?.let { this.unbindService(it) }
customTabsServiceConnection = null
customTabsSession = null
}
private fun openInCustomTab(ssoUrl: String) {
openUrlInChromeCustomTab(this, customTabsSession, ssoUrl)
val channelOpened = customTabsSession?.requestPostMessageChannel(Uri.parse("https://element.io"))
Timber.d("## CustomTab channelOpened: $channelOpened")
}
companion object {
const val EXTRA_AUTH_TYPE = "EXTRA_AUTH_TYPE"
const val EXTRA_REASON_TITLE = "EXTRA_REASON_TITLE"
const val RESULT_FLOW_TYPE = "RESULT_FLOW_TYPE"
const val RESULT_VALUE = "RESULT_VALUE"
const val DEFAULT_RESULT_KEYSTORE_ALIAS = "ReAuthActivity"
fun newIntent(context: Context,
fromError: RegistrationFlowResponse,
lastErrorCode: String?,
reasonTitle: String?,
resultKeyStoreAlias: String = DEFAULT_RESULT_KEYSTORE_ALIAS): Intent {
val authType = when (fromError.nextUncompletedStage()) {
LoginFlowTypes.PASSWORD -> {
LoginFlowTypes.PASSWORD
}
LoginFlowTypes.SSO -> {
LoginFlowTypes.SSO
}
else -> {
// TODO, support more auth type?
null
}
}
return Intent(context, ReAuthActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(authType, reasonTitle, fromError.session, lastErrorCode, resultKeyStoreAlias))
}
}
}
}

View File

@ -14,13 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.app.core.platform package im.vector.app.features.auth
/** import im.vector.app.core.platform.VectorViewEvents
* Mainly used to get a viewModelScope
*/ sealed class ReAuthEvents : VectorViewEvents {
class EmptyViewModel(initialState: EmptyState) : VectorViewModel<EmptyState, EmptyAction, EmptyViewEvents>(initialState) { data class OpenSsoURl(val url: String) : ReAuthEvents()
override fun handle(action: EmptyAction) { object Dismiss : ReAuthEvents()
// N/A data class PasswordFinishSuccess(val passwordSafeForIntent: String) : ReAuthEvents()
}
} }

View File

@ -0,0 +1,39 @@
/*
* Copyright (c) 2021 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.app.features.auth
import com.airbnb.mvrx.MvRxState
data class ReAuthState(
val title: String? = null,
val session: String? = null,
val flowType: String? = null,
val ssoFallbackPageWasShown: Boolean = false,
val passwordVisible: Boolean = false,
val lastErrorCode: String? = null,
val resultKeyStoreAlias: String = ""
) : MvRxState {
constructor(args: ReAuthActivity.Args) : this(
args.title,
args.session,
args.flowType,
lastErrorCode = args.lastErrorCode,
resultKeyStoreAlias = args.resultKeyStoreAlias
)
constructor() : this(null, null)
}

View File

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021 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.app.features.auth
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.platform.VectorViewModel
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.internal.crypto.crosssigning.toBase64NoPadding
import java.io.ByteArrayOutputStream
class ReAuthViewModel @AssistedInject constructor(
@Assisted val initialState: ReAuthState,
private val session: Session
) : VectorViewModel<ReAuthState, ReAuthActions, ReAuthEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: ReAuthState): ReAuthViewModel
}
companion object : MvRxViewModelFactory<ReAuthViewModel, ReAuthState> {
override fun create(viewModelContext: ViewModelContext, state: ReAuthState): ReAuthViewModel? {
val factory = when (viewModelContext) {
is FragmentViewModelContext -> viewModelContext.fragment as? Factory
is ActivityViewModelContext -> viewModelContext.activity as? Factory
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
}
override fun handle(action: ReAuthActions) = withState { state ->
when (action) {
ReAuthActions.StartSSOFallback -> {
if (state.flowType == LoginFlowTypes.SSO) {
setState { copy(ssoFallbackPageWasShown = true) }
val ssoURL = session.getUiaSsoFallbackUrl(initialState.session ?: "")
_viewEvents.post(ReAuthEvents.OpenSsoURl(ssoURL))
}
}
ReAuthActions.FallBackPageLoaded -> {
setState { copy(ssoFallbackPageWasShown = true) }
}
ReAuthActions.FallBackPageClosed -> {
// Should we do something here?
}
ReAuthActions.TogglePassVisibility -> {
setState {
copy(
passwordVisible = !state.passwordVisible
)
}
}
is ReAuthActions.ReAuthWithPass -> {
val safeForIntentCypher = ByteArrayOutputStream().also {
it.use {
session.securelyStoreObject(action.password, initialState.resultKeyStoreAlias, it)
}
}.toByteArray().toBase64NoPadding()
_viewEvents.post(ReAuthEvents.PasswordFinishSuccess(safeForIntentCypher))
}
}
}
}

View File

@ -1,110 +0,0 @@
/*
* 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.app.features.crypto.recover
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.showPassword
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.colorizeMatchingText
import im.vector.app.databinding.FragmentBootstrapEnterAccountPasswordBinding
import io.reactivex.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class BootstrapAccountPasswordFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentBootstrapEnterAccountPasswordBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapEnterAccountPasswordBinding {
return FragmentBootstrapEnterAccountPasswordBinding.inflate(inflater, container, false)
}
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val recPassPhrase = getString(R.string.account_password)
views.bootstrapDescriptionText.text = getString(R.string.enter_account_password, recPassPhrase)
.toSpannable()
.colorizeMatchingText(recPassPhrase, colorProvider.getColorFromAttribute(android.R.attr.textColorLink))
views.bootstrapAccountPasswordEditText.hint = getString(R.string.account_password)
views.bootstrapAccountPasswordEditText.editorActionEvents()
.throttleFirst(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
if (it.actionId == EditorInfo.IME_ACTION_DONE) {
submit()
}
}
.disposeOnDestroyView()
views.bootstrapAccountPasswordEditText.textChanges()
.distinctUntilChanged()
.subscribe {
if (!it.isNullOrBlank()) {
views.bootstrapAccountPasswordTil.error = null
}
}
.disposeOnDestroyView()
views.ssssViewShowPassword.debouncedClicks { sharedViewModel.handle(BootstrapActions.TogglePasswordVisibility) }
views.bootstrapPasswordButton.debouncedClicks { submit() }
withState(sharedViewModel) { state ->
(state.step as? BootstrapStep.AccountPassword)?.failure?.let {
views.bootstrapAccountPasswordTil.error = it
}
}
}
private fun submit() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountPassword) {
return@withState
}
val accountPassword = views.bootstrapAccountPasswordEditText.text?.toString()
if (accountPassword.isNullOrBlank()) {
views.bootstrapAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
} else {
view?.hideKeyboard()
sharedViewModel.handle(BootstrapActions.ReAuth(accountPassword))
}
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step is BootstrapStep.AccountPassword) {
val isPasswordVisible = state.step.isPasswordVisible
views.bootstrapAccountPasswordEditText.showPassword(isPasswordVisible, updateCursor = false)
views.ssssViewShowPassword.setImageResource(if (isPasswordVisible) R.drawable.ic_eye_closed else R.drawable.ic_eye)
}
}
}

View File

@ -37,7 +37,7 @@ sealed class BootstrapActions : VectorViewModelAction {
object TogglePasswordVisibility : BootstrapActions() object TogglePasswordVisibility : BootstrapActions()
data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateCandidatePassphrase(val pass: String) : BootstrapActions()
data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions() data class UpdateConfirmCandidatePassphrase(val pass: String) : BootstrapActions()
data class ReAuth(val pass: String) : BootstrapActions() // data class ReAuth(val pass: String) : BootstrapActions()
object RecoveryKeySaved : BootstrapActions() object RecoveryKeySaved : BootstrapActions()
object Completed : BootstrapActions() object Completed : BootstrapActions()
object SaveReqQueryStarted : BootstrapActions() object SaveReqQueryStarted : BootstrapActions()
@ -47,4 +47,8 @@ sealed class BootstrapActions : VectorViewModelAction {
object HandleForgotBackupPassphrase : BootstrapActions() object HandleForgotBackupPassphrase : BootstrapActions()
data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions() data class DoMigrateWithPassphrase(val passphrase: String) : BootstrapActions()
data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions() data class DoMigrateWithRecoveryKey(val recoveryKey: String) : BootstrapActions()
object SsoAuthDone: BootstrapActions()
data class PasswordAuthDone(val password: String): BootstrapActions()
object ReAuthCancelled: BootstrapActions()
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.crypto.recover package im.vector.app.features.crypto.recover
import android.app.Activity
import android.app.Dialog import android.app.Dialog
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -36,9 +37,12 @@ import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetBootstrapBinding import im.vector.app.databinding.BottomSheetBootstrapBinding
import im.vector.app.features.auth.ReAuthActivity
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject import javax.inject.Inject
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -64,6 +68,25 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
return BottomSheetBootstrapBinding.inflate(inflater, container, false) return BottomSheetBootstrapBinding.inflate(inflater, container, false)
} }
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(BootstrapActions.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(BootstrapActions.PasswordAuthDone(password))
}
else -> {
viewModel.handle(BootstrapActions.ReAuthCancelled)
}
}
} else {
viewModel.handle(BootstrapActions.ReAuthCancelled)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
viewModel.observeViewEvents { event -> viewModel.observeViewEvents { event ->
@ -85,6 +108,14 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
is BootstrapViewEvents.SkipBootstrap -> { is BootstrapViewEvents.SkipBootstrap -> {
promptSkip() promptSkip()
} }
is BootstrapViewEvents.RequestReAuth -> {
ReAuthActivity.newIntent(requireContext(),
event.flowResponse,
event.lastErrorCode,
getString(R.string.initialize_cross_signing)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
} }
} }
} }
@ -149,11 +180,11 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
views.bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title) views.bootstrapTitleText.text = getString(R.string.set_a_security_phrase_title)
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle()) showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
} }
is BootstrapStep.AccountPassword -> { is BootstrapStep.AccountReAuth -> {
views.bootstrapIcon.isVisible = true views.bootstrapIcon.isVisible = true
views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user)) views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
views.bootstrapTitleText.text = getString(R.string.account_password) views.bootstrapTitleText.text = getString(R.string.re_authentication_activity_title)
showFragment(BootstrapAccountPasswordFragment::class, Bundle()) showFragment(BootstrapReAuthFragment::class, Bundle())
} }
is BootstrapStep.Initializing -> { is BootstrapStep.Initializing -> {
views.bootstrapIcon.isVisible = true views.bootstrapIcon.isVisible = true

View File

@ -20,10 +20,9 @@ import im.vector.app.R
import im.vector.app.core.platform.ViewModelTask import im.vector.app.core.platform.ViewModelTask
import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.MatrixError import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -38,7 +37,6 @@ import org.matrix.android.sdk.internal.crypto.keysbackup.model.MegolmBackupCreat
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersion
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
import timber.log.Timber import timber.log.Timber
import java.util.UUID import java.util.UUID
@ -51,16 +49,12 @@ sealed class BootstrapResult {
abstract class Failure(val error: String?) : BootstrapResult() abstract class Failure(val error: String?) : BootstrapResult()
class UnsupportedAuthFlow : Failure(null)
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage) data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null) data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage) class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage) class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
object MissingPrivateKey : Failure(null) object MissingPrivateKey : Failure(null)
data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null)
} }
interface BootstrapProgressListener { interface BootstrapProgressListener {
@ -68,7 +62,7 @@ interface BootstrapProgressListener {
} }
data class Params( data class Params(
val userPasswordAuth: UserPasswordAuth? = null, val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val progressListener: BootstrapProgressListener? = null, val progressListener: BootstrapProgressListener? = null,
val passphrase: String?, val passphrase: String?,
val keySpec: SsssKeySpec? = null, val keySpec: SsssKeySpec? = null,
@ -101,7 +95,10 @@ class BootstrapCrossSigningTask @Inject constructor(
try { try {
awaitCallback<Unit> { awaitCallback<Unit> {
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it) crossSigningService.initializeCrossSigning(
params.userInteractiveAuthInterceptor,
it
)
} }
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) { if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
return BootstrapResult.SuccessCrossSigningOnly return BootstrapResult.SuccessCrossSigningOnly
@ -312,16 +309,6 @@ class BootstrapCrossSigningTask @Inject constructor(
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult { private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) { if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
return BootstrapResult.InvalidPasswordError(failure.error) return BootstrapResult.InvalidPasswordError(failure.error)
} else {
val registrationFlowResponse = failure.toRegistrationFlowResponse()
if (registrationFlowResponse != null) {
return if (registrationFlowResponse.flows.orEmpty().any { it.stages?.contains(LoginFlowTypes.PASSWORD) == true }) {
BootstrapResult.PasswordAuthFlowMissing(registrationFlowResponse.session ?: "")
} else {
// can't do this from here
BootstrapResult.UnsupportedAuthFlow()
}
}
} }
return BootstrapResult.GenericError(failure) return BootstrapResult.GenericError(failure)
} }

View File

@ -0,0 +1,84 @@
/*
* 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.app.features.crypto.recover
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.airbnb.mvrx.parentFragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.FragmentBootstrapReauthBinding
import javax.inject.Inject
class BootstrapReAuthFragment @Inject constructor(
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentBootstrapReauthBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentBootstrapReauthBinding {
return FragmentBootstrapReauthBinding.inflate(inflater, container, false)
}
val sharedViewModel: BootstrapSharedViewModel by parentFragmentViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.bootstrapRetryButton.debouncedClicks { submit() }
views.bootstrapCancelButton.debouncedClicks { cancel() }
}
private fun submit() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountReAuth) {
return@withState
}
if (state.passphrase != null) {
sharedViewModel.handle(BootstrapActions.DoInitialize(state.passphrase))
} else {
sharedViewModel.handle(BootstrapActions.DoInitializeGeneratedKey)
}
}
private fun cancel() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountReAuth) {
return@withState
}
sharedViewModel.handle(BootstrapActions.GoBack)
}
override fun invalidate() = withState(sharedViewModel) { state ->
if (state.step !is BootstrapStep.AccountReAuth) {
return@withState
}
val failure = state.step.failure
views.reAuthFailureText.setTextOrHide(failure)
if (failure == null) {
views.waitingProgress.isVisible = true
views.bootstrapCancelButton.isVisible = false
views.bootstrapRetryButton.isVisible = false
} else {
views.waitingProgress.isVisible = false
views.bootstrapCancelButton.isVisible = true
views.bootstrapRetryButton.isVisible = true
}
}
}

View File

@ -26,25 +26,35 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.nulabinc.zxcvbn.Zxcvbn import com.nulabinc.zxcvbn.Zxcvbn
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.WaitingViewData import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec import org.matrix.android.sdk.api.session.securestorage.RawBytesKeySpec
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult import org.matrix.android.sdk.internal.crypto.keysbackup.model.rest.KeysVersionResult
import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey import org.matrix.android.sdk.internal.crypto.keysbackup.util.extractCurveKeyFromRecoveryKey
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
import java.io.OutputStream import java.io.OutputStream
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class BootstrapSharedViewModel @AssistedInject constructor( class BootstrapSharedViewModel @AssistedInject constructor(
@Assisted initialState: BootstrapViewState, @Assisted initialState: BootstrapViewState,
@ -66,7 +76,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel fun create(initialState: BootstrapViewState, args: BootstrapBottomSheet.Args): BootstrapSharedViewModel
} }
private var _pendingSession: String? = null // private var _pendingSession: String? = null
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
init { init {
@ -81,7 +94,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
SetupMode.CROSS_SIGNING_ONLY -> { SetupMode.CROSS_SIGNING_ONLY -> {
// Go straight to account password // Go straight to account password
setState { setState {
copy(step = BootstrapStep.AccountPassword(false)) copy(step = BootstrapStep.AccountReAuth())
} }
} }
SetupMode.NORMAL -> { SetupMode.NORMAL -> {
@ -149,10 +162,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible)) copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
} }
} }
is BootstrapStep.AccountPassword -> { is BootstrapStep.AccountReAuth -> {
setState { // nop
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
} }
is BootstrapStep.GetBackupSecretPassForMigration -> { is BootstrapStep.GetBackupSecretPassForMigration -> {
setState { setState {
@ -196,16 +207,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
is BootstrapActions.DoInitialize -> { is BootstrapActions.DoInitialize -> {
if (state.passphrase == state.passphraseRepeat) { if (state.passphrase == state.passphraseRepeat) {
val userPassword = reAuthHelper.data startInitializeFlow(state)
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(userPassword)
}
} else { } else {
setState { setState {
copy( copy(
@ -215,24 +217,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
} }
is BootstrapActions.DoInitializeGeneratedKey -> { is BootstrapActions.DoInitializeGeneratedKey -> {
val userPassword = reAuthHelper.data startInitializeFlow(state)
if (userPassword == null) {
setState {
copy(
passphrase = null,
passphraseRepeat = null,
step = BootstrapStep.AccountPassword(false)
)
}
} else {
setState {
copy(
passphrase = null,
passphraseRepeat = null
)
}
startInitializeFlow(userPassword)
}
} }
BootstrapActions.RecoveryKeySaved -> { BootstrapActions.RecoveryKeySaved -> {
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved) _viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
@ -263,7 +248,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
BootstrapActions.GoToEnterAccountPassword -> { BootstrapActions.GoToEnterAccountPassword -> {
setState { setState {
copy(step = BootstrapStep.AccountPassword(false)) copy(step = BootstrapStep.AccountReAuth())
} }
} }
BootstrapActions.HandleForgotBackupPassphrase -> { BootstrapActions.HandleForgotBackupPassphrase -> {
@ -273,15 +258,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
} else return@withState } else return@withState
} }
is BootstrapActions.ReAuth -> { // is BootstrapActions.ReAuth -> {
startInitializeFlow(action.pass) // startInitializeFlow(action.pass)
} // }
is BootstrapActions.DoMigrateWithPassphrase -> { is BootstrapActions.DoMigrateWithPassphrase -> {
startMigrationFlow(state.step, action.passphrase, null) startMigrationFlow(state.step, action.passphrase, null)
} }
is BootstrapActions.DoMigrateWithRecoveryKey -> { is BootstrapActions.DoMigrateWithRecoveryKey -> {
startMigrationFlow(state.step, null, action.recoveryKey) startMigrationFlow(state.step, null, action.recoveryKey)
} }
BootstrapActions.SsoAuthDone -> {
uiaContinuation?.resume(DefaultBaseAuth(session = pendingAuth?.session ?: ""))
}
is BootstrapActions.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = decryptedPass,
user = session.myUserId
)
)
}
BootstrapActions.ReAuthCancelled -> {
setState {
copy(step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.authentication_error)))
}
}
}.exhaustive }.exhaustive
} }
@ -293,7 +296,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
) )
} }
} else { } else {
startInitializeFlow(null) startInitializeFlow(it)
} }
} }
@ -346,16 +349,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
migrationRecoveryKey = recoveryKey migrationRecoveryKey = recoveryKey
) )
} }
val userPassword = reAuthHelper.data // val userPassword = reAuthHelper.data
if (userPassword == null) { // if (userPassword == null) {
setState { // setState {
copy( // copy(
step = BootstrapStep.AccountPassword(false) // step = BootstrapStep.AccountPassword(false)
) // )
} // }
} else { // } else {
startInitializeFlow(userPassword) withState { startInitializeFlow(it) }
} // }
} }
is BackupToQuadSMigrationTask.Result.Failure -> { is BackupToQuadSMigrationTask.Result.Failure -> {
_viewEvents.post( _viewEvents.post(
@ -372,7 +375,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
} }
private fun startInitializeFlow(userPassword: String?) = withState { state -> private fun startInitializeFlow(state: BootstrapViewState) {
val previousStep = state.step val previousStep = state.step
setState { setState {
@ -389,19 +392,45 @@ class BootstrapSharedViewModel @AssistedInject constructor(
} }
} }
viewModelScope.launch(Dispatchers.IO) { val interceptor = object : UserInteractiveAuthInterceptor {
val userPasswordAuth = userPassword?.let { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
UserPasswordAuth( when (flowResponse.nextUncompletedStage()) {
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task LoginFlowTypes.PASSWORD -> {
session = _pendingSession, pendingAuth = UserPasswordAuth(
user = session.myUserId, // Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
password = it session = flowResponse.session,
) user = session.myUserId,
password = null
)
uiaContinuation = promise
setState {
copy(
step = BootstrapStep.AccountReAuth()
)
}
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
}
LoginFlowTypes.SSO -> {
pendingAuth = DefaultBaseAuth(flowResponse.session)
uiaContinuation = promise
setState {
copy(
step = BootstrapStep.AccountReAuth()
)
}
_viewEvents.post(BootstrapViewEvents.RequestReAuth(flowResponse, errCode))
}
else -> {
promise.resumeWith(Result.failure(UnsupportedOperationException()))
}
}
} }
}
viewModelScope.launch(Dispatchers.IO) {
bootstrapTask.invoke(this, bootstrapTask.invoke(this,
Params( Params(
userPasswordAuth = userPasswordAuth, userInteractiveAuthInterceptor = interceptor,
progressListener = progressListener, progressListener = progressListener,
passphrase = state.passphrase, passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } }, keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
@ -410,7 +439,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
) { bootstrapResult -> ) { bootstrapResult ->
when (bootstrapResult) { when (bootstrapResult) {
is BootstrapResult.SuccessCrossSigningOnly -> { is BootstrapResult.SuccessCrossSigningOnly -> {
// TPD
_viewEvents.post(BootstrapViewEvents.Dismiss(true)) _viewEvents.post(BootstrapViewEvents.Dismiss(true))
} }
is BootstrapResult.Success -> { is BootstrapResult.Success -> {
@ -424,26 +452,11 @@ class BootstrapSharedViewModel @AssistedInject constructor(
) )
} }
} }
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(false))
}
is BootstrapResult.InvalidPasswordError -> { is BootstrapResult.InvalidPasswordError -> {
// it's a bad password // it's a bad password / auth
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
_pendingSession = null
setState { setState {
copy( copy(
step = BootstrapStep.AccountPassword(false, stringProvider.getString(R.string.auth_invalid_login_param)) step = BootstrapStep.AccountReAuth(stringProvider.getString(R.string.auth_invalid_login_param))
) )
} }
} }
@ -516,7 +529,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
) )
} }
} }
is BootstrapStep.AccountPassword -> { is BootstrapStep.AccountReAuth -> {
_viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null)) _viewEvents.post(BootstrapViewEvents.SkipBootstrap(state.passphrase != null))
} }
BootstrapStep.Initializing -> { BootstrapStep.Initializing -> {

View File

@ -52,11 +52,11 @@ package im.vector.app.features.crypto.recover
* BootstrapStep.ConfirmPassphrase * BootstrapStep.ConfirmPassphrase
* *
* *
* is password needed? * is password/reauth needed?
* *
* *
* *
* BootstrapStep.AccountPassword * BootstrapStep.AccountReAuth
* *
* *
* *
@ -94,7 +94,7 @@ sealed class BootstrapStep {
data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() data class SetupPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep() data class ConfirmPassphrase(val isPasswordVisible: Boolean) : BootstrapStep()
data class AccountPassword(val isPasswordVisible: Boolean, val failure: String? = null) : BootstrapStep() data class AccountReAuth(val failure: String? = null) : BootstrapStep()
abstract class GetBackupSecretForMigration : BootstrapStep() abstract class GetBackupSecretForMigration : BootstrapStep()
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration() data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()

View File

@ -17,10 +17,12 @@
package im.vector.app.features.crypto.recover package im.vector.app.features.crypto.recover
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
sealed class BootstrapViewEvents : VectorViewEvents { sealed class BootstrapViewEvents : VectorViewEvents {
data class Dismiss(val success: Boolean) : BootstrapViewEvents() data class Dismiss(val success: Boolean) : BootstrapViewEvents()
data class ModalError(val error: String) : BootstrapViewEvents() data class ModalError(val error: String) : BootstrapViewEvents()
object RecoveryKeySaved : BootstrapViewEvents() object RecoveryKeySaved : BootstrapViewEvents()
data class SkipBootstrap(val genKeyOption: Boolean = true) : BootstrapViewEvents() data class SkipBootstrap(val genKeyOption: Boolean = true) : BootstrapViewEvents()
data class RequestReAuth(val flowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : BootstrapViewEvents()
} }

View File

@ -21,29 +21,37 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.login.ReAuthHelper import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds import org.matrix.android.sdk.api.pushrules.RuleIds
import org.matrix.android.sdk.api.session.InitialSyncProgressService import org.matrix.android.sdk.api.session.InitialSyncProgressService
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.asObservable import org.matrix.android.sdk.rx.asObservable
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class HomeActivityViewModel @AssistedInject constructor( class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState, @Assisted initialState: HomeActivityViewState,
@ -74,7 +82,6 @@ class HomeActivityViewModel @AssistedInject constructor(
init { init {
cleanupFiles() cleanupFiles()
observeInitialSync() observeInitialSync()
mayBeInitializeCrossSigning()
checkSessionPushIsOn() checkSessionPushIsOn()
observeCrossSigningReset() observeCrossSigningReset()
} }
@ -122,10 +129,10 @@ class HomeActivityViewModel @AssistedInject constructor(
// Schedule a check of the bootstrap when the init sync will be finished // Schedule a check of the bootstrap when the init sync will be finished
checkBootstrap = true checkBootstrap = true
} }
is InitialSyncProgressService.Status.Idle -> { is InitialSyncProgressService.Status.Idle -> {
if (checkBootstrap) { if (checkBootstrap) {
checkBootstrap = false checkBootstrap = false
maybeBootstrapCrossSigning() maybeBootstrapCrossSigningAfterInitialSync()
} }
} }
} }
@ -139,29 +146,6 @@ class HomeActivityViewModel @AssistedInject constructor(
.disposeOnClear() .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()
)
}
}
/** /**
* After migration from riot to element some users reported that their * After migration from riot to element some users reported that their
* push setting for the session was set to off * push setting for the session was set to off
@ -197,56 +181,66 @@ class HomeActivityViewModel @AssistedInject constructor(
} }
} }
private fun maybeBootstrapCrossSigning() { private fun maybeBootstrapCrossSigningAfterInitialSync() {
// In case of account creation, it is already done before // We do not use the viewModel context because we do not want to tie this action to activity view model
if (args.accountCreation) return GlobalScope.launch(Dispatchers.IO) {
val session = activeSessionHolder.getSafeActiveSession() ?: return@launch
val session = activeSessionHolder.getSafeActiveSession() ?: return tryOrNull("## MaybeBootstrapCrossSigning: Failed to download keys") {
awaitCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
session.cryptoService().downloadKeys(listOf(session.myUserId), true, it)
}
}
// Ensure keys of the user are downloaded // From there we are up to date with server
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> { // Is there already cross signing keys here?
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) { val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
// Is there already cross signing keys here? if (mxCrossSigningInfo != null) {
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys() // Cross-signing is already set up for this user, is it trusted?
if (mxCrossSigningInfo != null) { if (!mxCrossSigningInfo.isTrusted()) {
// Cross-signing is already set up for this user, is it trusted? // New session
if (!mxCrossSigningInfo.isTrusted()) { _viewEvents.post(
// New session HomeActivityViewEvents.OnNewSession(
_viewEvents.post( session.getUser(session.myUserId)?.toMatrixItem(),
HomeActivityViewEvents.OnNewSession( // If it's an old unverified, we should send requests
session.getUser(session.myUserId)?.toMatrixItem(), // instead of waiting for an incoming one
// If it's an old unverified, we should send requests reAuthHelper.data != null
// instead of waiting for an incoming one )
reAuthHelper.data != null )
) }
) } else {
} // Try to initialize cross signing in background if possible
} else { Timber.d("Initialize cross signing...")
// Initialize cross-signing awaitCallback<Unit> {
val password = reAuthHelper.data try {
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( session.cryptoService().crossSigningService().initializeCrossSigning(
authParams = UserPasswordAuth( object : UserInteractiveAuthInterceptor {
session = null, override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
user = session.myUserId, // We missed server grace period or it's not setup, see if we remember locally password
password = password if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD
), && errCode == null
callback = NoOpMatrixCallback() && reAuthHelper.data != null) {
promise.resume(
UserPasswordAuth(
session = flowResponse.session,
user = session.myUserId,
password = reAuthHelper.data
)
)
} else {
promise.resumeWith(Result.failure(Exception("Cannot silently initialize cross signing, UIA missing")))
}
}
},
callback = it
) )
Timber.d("Initialize cross signing SUCCESS")
} catch (failure: Throwable) {
Timber.e(failure, "Failed to initialize cross signing")
} }
} }
} }
}) }
} }
override fun handle(action: HomeActivityViewActions) { override fun handle(action: HomeActivityViewActions) {

View File

@ -19,13 +19,11 @@ package im.vector.app.features.link
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.viewModelScope import androidx.lifecycle.lifecycleScope
import com.airbnb.mvrx.viewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.platform.EmptyViewModel
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.toast import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityProgressBinding import im.vector.app.databinding.ActivityProgressBinding
@ -48,8 +46,6 @@ class LinkHandlerActivity : VectorBaseActivity<ActivityProgressBinding>() {
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@Inject lateinit var permalinkHandler: PermalinkHandler @Inject lateinit var permalinkHandler: PermalinkHandler
private val emptyViewModel: EmptyViewModel by viewModel()
override fun injectWith(injector: ScreenComponent) { override fun injectWith(injector: ScreenComponent) {
injector.inject(this) injector.inject(this)
} }
@ -155,7 +151,7 @@ class LinkHandlerActivity : VectorBaseActivity<ActivityProgressBinding>() {
// Should not happen // Should not happen
startLoginActivity(uri) startLoginActivity(uri)
} else { } else {
emptyViewModel.viewModelScope.launch { lifecycleScope.launch {
try { try {
session.signOut(true) session.signOut(true)
Timber.d("## displayAlreadyLoginPopup(): logout succeeded") Timber.d("## displayAlreadyLoginPopup(): logout succeeded")

View File

@ -83,25 +83,28 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
ssoIdentityProviders?.forEach { identityProvider -> ssoIdentityProviders?.forEach { identityProvider ->
// Use some heuristic to render buttons according to branding guidelines // Use some heuristic to render buttons according to branding guidelines
val button: MaterialButton = cachedViews[identityProvider.id] val button: MaterialButton = cachedViews[identityProvider.id]
?: when (identityProvider.id) { ?: when (identityProvider.brand) {
SsoIdentityProvider.ID_GOOGLE -> { SsoIdentityProvider.BRAND_GOOGLE -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_google_style) MaterialButton(context, null, R.attr.vctr_social_login_button_google_style)
} }
SsoIdentityProvider.ID_GITHUB -> { SsoIdentityProvider.BRAND_GITHUB -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_github_style) MaterialButton(context, null, R.attr.vctr_social_login_button_github_style)
} }
SsoIdentityProvider.ID_APPLE -> { SsoIdentityProvider.BRAND_APPLE -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style) MaterialButton(context, null, R.attr.vctr_social_login_button_apple_style)
} }
SsoIdentityProvider.ID_FACEBOOK -> { SsoIdentityProvider.BRAND_FACEBOOK -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style) MaterialButton(context, null, R.attr.vctr_social_login_button_facebook_style)
} }
SsoIdentityProvider.ID_TWITTER -> { SsoIdentityProvider.BRAND_TWITTER -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style) MaterialButton(context, null, R.attr.vctr_social_login_button_twitter_style)
} }
SsoIdentityProvider.BRAND_GITLAB -> {
MaterialButton(context, null, R.attr.vctr_social_login_button_gitlab_style)
}
else -> { else -> {
// TODO Use iconUrl // TODO Use iconUrl
MaterialButton(context, null, R.attr.materialButtonStyle).apply { MaterialButton(context, null, R.attr.materialButtonOutlinedStyle).apply {
transformationMethod = null transformationMethod = null
textAlignment = View.TEXT_ALIGNMENT_CENTER textAlignment = View.TEXT_ALIGNMENT_CENTER
} }
@ -131,12 +134,13 @@ class SocialLoginButtonsView @JvmOverloads constructor(context: Context, attrs:
clipChildren = false clipChildren = false
if (isInEditMode) { if (isInEditMode) {
ssoIdentityProviders = listOf( ssoIdentityProviders = listOf(
SsoIdentityProvider(SsoIdentityProvider.ID_GOOGLE, "Google", null), SsoIdentityProvider("Google", "Google", null, SsoIdentityProvider.BRAND_GOOGLE),
SsoIdentityProvider(SsoIdentityProvider.ID_FACEBOOK, "Facebook", null), SsoIdentityProvider("Facebook", "Facebook", null, SsoIdentityProvider.BRAND_FACEBOOK),
SsoIdentityProvider(SsoIdentityProvider.ID_APPLE, "Apple", null), SsoIdentityProvider("Apple", "Apple", null, SsoIdentityProvider.BRAND_APPLE),
SsoIdentityProvider(SsoIdentityProvider.ID_GITHUB, "GitHub", null), SsoIdentityProvider("GitHub", "GitHub", null, SsoIdentityProvider.BRAND_GITHUB),
SsoIdentityProvider(SsoIdentityProvider.ID_TWITTER, "Twitter", null), SsoIdentityProvider("Twitter", "Twitter", null, SsoIdentityProvider.BRAND_TWITTER),
SsoIdentityProvider("Custom_pro", "SSO", null) SsoIdentityProvider("Gitlab", "Gitlab", null, SsoIdentityProvider.BRAND_GITLAB),
SsoIdentityProvider("Custom_pro", "SSO", null, null)
) )
} }
val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0) val typedArray = context.theme.obtainStyledAttributes(attrs, R.styleable.SocialLoginButtonsView, 0, 0)

View File

@ -230,10 +230,13 @@ class DefaultNavigator @Inject constructor(
} }
override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) { override fun openKeysBackupSetup(context: Context, showManualExport: Boolean) {
// if cross signing is enabled we should propose full 4S // if cross signing is enabled and trusted or not set up at all we should propose full 4S
sessionHolder.getSafeActiveSession()?.let { session -> sessionHolder.getSafeActiveSession()?.let { session ->
if (session.cryptoService().crossSigningService().canCrossSign() && context is AppCompatActivity) { if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL) || session.cryptoService().crossSigningService().canCrossSign()) {
(context as? AppCompatActivity)?.let {
BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL)
}
} else { } else {
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport)) context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
} }

View File

@ -15,11 +15,11 @@
*/ */
package im.vector.app.features.popup package im.vector.app.features.popup
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.view.View
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import com.tapadoo.alerter.Alerter import com.tapadoo.alerter.Alerter
import im.vector.app.R import im.vector.app.R
@ -171,28 +171,38 @@ class PopupAlertManager @Inject constructor() {
} }
} }
@SuppressLint("InlinedApi")
private fun clearLightStatusBar() { private fun clearLightStatusBar() {
weakCurrentActivity?.get() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
?.takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } weakCurrentActivity?.get()
// Do not change anything on Dark themes // Do not change anything on Dark themes
?.takeIf { ThemeUtils.isLightTheme(it) } ?.takeIf { ThemeUtils.isLightTheme(it) }
?.let { it.window?.decorView } ?.window?.decorView
?.let { view -> ?.let { view ->
view.windowInsetsController?.setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
} view.windowInsetsController?.setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS)
} else {
@Suppress("DEPRECATION")
view.systemUiVisibility = view.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
}
}
}
} }
@SuppressLint("InlinedApi")
private fun setLightStatusBar() { private fun setLightStatusBar() {
weakCurrentActivity?.get() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
?.takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } weakCurrentActivity?.get()
// Do not change anything on Dark themes // Do not change anything on Dark themes
?.takeIf { ThemeUtils.isLightTheme(it) } ?.takeIf { ThemeUtils.isLightTheme(it) }
?.let { it.window?.decorView } ?.window?.decorView
?.let { view -> ?.let { view ->
view.windowInsetsController?.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
} view.windowInsetsController?.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS)
} else {
@Suppress("DEPRECATION")
view.systemUiVisibility = view.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
}
} }
private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) { private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) {

View File

@ -80,8 +80,6 @@ class RoomMemberProfileController @Inject constructor(
action = { callback?.onIgnoreClicked() } action = { callback?.onIgnoreClicked() }
) )
if (!state.isMine) { if (!state.isMine) {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_more))
buildProfileAction( buildProfileAction(
id = "direct", id = "direct",
editable = false, editable = false,

View File

@ -311,10 +311,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
} }
mCrossSigningStatePreference.isVisible = true mCrossSigningStatePreference.isVisible = true
if (!vectorPreferences.developerMode()) {
// When not in developer mode, intercept click on this preference
mCrossSigningStatePreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { true }
}
} }
private val saveMegolmStartForActivityResult = registerStartForActivityResult { private val saveMegolmStartForActivityResult = registerStartForActivityResult {

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) 2021 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.app.features.settings.account.deactivation
import im.vector.app.core.platform.VectorViewModelAction
sealed class DeactivateAccountAction : VectorViewModelAction {
object TogglePassword : DeactivateAccountAction()
data class DeactivateAccount(val eraseAllData: Boolean) : DeactivateAccountAction()
object SsoAuthDone: DeactivateAccountAction()
data class PasswordAuthDone(val password: String): DeactivateAccountAction()
object ReAuthCancelled: DeactivateAccountAction()
}

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.account.deactivation package im.vector.app.features.settings.account.deactivation
import android.app.Activity
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -23,16 +24,16 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.widget.textChanges
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.showPassword import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDeactivateAccountBinding import im.vector.app.databinding.FragmentDeactivateAccountBinding
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs import im.vector.app.features.MainActivityArgs
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.settings.VectorSettingsActivity import im.vector.app.features.settings.VectorSettingsActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject import javax.inject.Inject
@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor(
return FragmentDeactivateAccountBinding.inflate(inflater, container, false) return FragmentDeactivateAccountBinding.inflate(inflater, container, false)
} }
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(DeactivateAccountAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(DeactivateAccountAction.PasswordAuthDone(password))
}
else -> {
viewModel.handle(DeactivateAccountAction.ReAuthCancelled)
}
}
} else {
viewModel.handle(DeactivateAccountAction.ReAuthCancelled)
}
}
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
(activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title) (activity as? AppCompatActivity)?.supportActionBar?.setTitle(R.string.deactivate_account_title)
@ -66,59 +86,46 @@ class DeactivateAccountFragment @Inject constructor(
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupUi()
setupViewListeners() setupViewListeners()
observeViewEvents() observeViewEvents()
} }
private fun setupUi() {
views.deactivateAccountPassword.textChanges()
.subscribe {
views.deactivateAccountPasswordTil.error = null
views.deactivateAccountSubmit.isEnabled = it.isNotEmpty()
}
.disposeOnDestroyView()
}
private fun setupViewListeners() { private fun setupViewListeners() {
views.deactivateAccountPasswordReveal.setOnClickListener {
viewModel.handle(DeactivateAccountAction.TogglePassword)
}
views.deactivateAccountSubmit.debouncedClicks { views.deactivateAccountSubmit.debouncedClicks {
viewModel.handle(DeactivateAccountAction.DeactivateAccount( viewModel.handle(DeactivateAccountAction.DeactivateAccount(
views.deactivateAccountPassword.text.toString(), views.deactivateAccountEraseCheckbox.isChecked)
views.deactivateAccountEraseCheckbox.isChecked)) )
} }
} }
private fun observeViewEvents() { private fun observeViewEvents() {
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is DeactivateAccountViewEvents.Loading -> { is DeactivateAccountViewEvents.Loading -> {
settingsActivity?.ignoreInvalidTokenError = true settingsActivity?.ignoreInvalidTokenError = true
showLoadingDialog(it.message) showLoadingDialog(it.message)
} }
DeactivateAccountViewEvents.EmptyPassword -> { DeactivateAccountViewEvents.InvalidAuth -> {
dismissLoadingDialog()
settingsActivity?.ignoreInvalidTokenError = false settingsActivity?.ignoreInvalidTokenError = false
views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
} }
DeactivateAccountViewEvents.InvalidPassword -> { is DeactivateAccountViewEvents.OtherFailure -> {
settingsActivity?.ignoreInvalidTokenError = false
views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
}
is DeactivateAccountViewEvents.OtherFailure -> {
settingsActivity?.ignoreInvalidTokenError = false settingsActivity?.ignoreInvalidTokenError = false
dismissLoadingDialog()
displayErrorDialog(it.throwable) displayErrorDialog(it.throwable)
} }
DeactivateAccountViewEvents.Done -> DeactivateAccountViewEvents.Done -> {
MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true)) MainActivity.restartApp(requireActivity(), MainActivityArgs(clearCredentials = true, isAccountDeactivated = true))
}
is DeactivateAccountViewEvents.RequestReAuth -> {
ReAuthActivity.newIntent(requireContext(),
it.registrationFlowResponse,
it.lastErrorCode,
getString(R.string.deactivate_account_title)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
}.exhaustive }.exhaustive
} }
} }
override fun invalidate() = withState(viewModel) { state ->
views.deactivateAccountPassword.showPassword(state.passwordShown)
views.deactivateAccountPasswordReveal.setImageResource(if (state.passwordShown) R.drawable.ic_eye_closed else R.drawable.ic_eye)
}
} }

View File

@ -17,14 +17,15 @@
package im.vector.app.features.settings.account.deactivation package im.vector.app.features.settings.account.deactivation
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
/** /**
* Transient events for deactivate account settings screen * Transient events for deactivate account settings screen
*/ */
sealed class DeactivateAccountViewEvents : VectorViewEvents { sealed class DeactivateAccountViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents() data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
object EmptyPassword : DeactivateAccountViewEvents() object InvalidAuth : DeactivateAccountViewEvents()
object InvalidPassword : DeactivateAccountViewEvents()
data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents() data class OtherFailure(val throwable: Throwable) : DeactivateAccountViewEvents()
object Done : DeactivateAccountViewEvents() object Done : DeactivateAccountViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DeactivateAccountViewEvents()
} }

View File

@ -21,25 +21,28 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.features.auth.ReAuthActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.failure.isInvalidPassword import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.isInvalidUIAAuth
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import java.lang.Exception import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
data class DeactivateAccountViewState( data class DeactivateAccountViewState(
val passwordShown: Boolean = false val passwordShown: Boolean = false
) : MvRxState ) : MvRxState
sealed class DeactivateAccountAction : VectorViewModelAction {
object TogglePassword : DeactivateAccountAction()
data class DeactivateAccount(val password: String, val eraseAllData: Boolean) : DeactivateAccountAction()
}
class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState, class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private val initialState: DeactivateAccountViewState,
private val session: Session) private val session: Session)
: VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) { : VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
} }
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
override fun handle(action: DeactivateAccountAction) { override fun handle(action: DeactivateAccountAction) {
when (action) { when (action) {
DeactivateAccountAction.TogglePassword -> handleTogglePassword() DeactivateAccountAction.TogglePassword -> handleTogglePassword()
is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action) is DeactivateAccountAction.DeactivateAccount -> handleDeactivateAccount(action)
DeactivateAccountAction.SsoAuthDone -> {
Timber.d("## UIA - FallBack success")
if (pendingAuth != null) {
uiaContinuation?.resume(pendingAuth!!)
} else {
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
}
}
is DeactivateAccountAction.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = decryptedPass,
user = session.myUserId
)
)
}
DeactivateAccountAction.ReAuthCancelled -> {
Timber.d("## UIA - Reauth cancelled")
uiaContinuation?.resumeWith(Result.failure((Exception())))
uiaContinuation = null
pendingAuth = null
}
}.exhaustive }.exhaustive
} }
@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
} }
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) { private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
if (action.password.isEmpty()) {
_viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
return
}
_viewEvents.post(DeactivateAccountViewEvents.Loading()) _viewEvents.post(DeactivateAccountViewEvents.Loading())
viewModelScope.launch { viewModelScope.launch {
val event = try { val event = try {
session.deactivateAccount(action.password, action.eraseAllData) session.deactivateAccount(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
_viewEvents.post(DeactivateAccountViewEvents.RequestReAuth(flowResponse, errCode))
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
uiaContinuation = promise
}
}, action.eraseAllData)
DeactivateAccountViewEvents.Done DeactivateAccountViewEvents.Done
} catch (failure: Exception) { } catch (failure: Exception) {
if (failure.isInvalidPassword()) { if (failure.isInvalidUIAAuth()) {
DeactivateAccountViewEvents.InvalidPassword DeactivateAccountViewEvents.InvalidAuth
} else { } else {
DeactivateAccountViewEvents.OtherFailure(failure) DeactivateAccountViewEvents.OtherFailure(failure)
} }

View File

@ -18,4 +18,9 @@ package im.vector.app.features.settings.crosssigning
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class CrossSigningSettingsAction : VectorViewModelAction sealed class CrossSigningSettingsAction : VectorViewModelAction {
object InitializeCrossSigning: CrossSigningSettingsAction()
object SsoAuthDone: CrossSigningSettingsAction()
data class PasswordAuthDone(val password: String): CrossSigningSettingsAction()
object ReAuthCancelled: CrossSigningSettingsAction()
}

View File

@ -19,8 +19,11 @@ import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericButtonItem
import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItem
import im.vector.app.core.ui.list.genericItemWithValue import im.vector.app.core.ui.list.genericItemWithValue
import im.vector.app.core.ui.list.genericPositiveButtonItem
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import me.gujun.android.span.span import me.gujun.android.span.span
import javax.inject.Inject import javax.inject.Inject
@ -31,7 +34,9 @@ class CrossSigningSettingsController @Inject constructor(
private val dimensionConverter: DimensionConverter private val dimensionConverter: DimensionConverter
) : TypedEpoxyController<CrossSigningSettingsViewState>() { ) : TypedEpoxyController<CrossSigningSettingsViewState>() {
interface InteractionListener interface InteractionListener {
fun didTapInitializeCrossSigning()
}
var interactionListener: InteractionListener? = null var interactionListener: InteractionListener? = null
@ -44,6 +49,13 @@ class CrossSigningSettingsController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_trusted) titleIconResourceId(R.drawable.ic_shield_trusted)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_complete))
} }
genericButtonItem {
id("Reset")
text(stringProvider.getString(R.string.reset_cross_signing))
buttonClickAction(DebouncedClickListener({
interactionListener?.didTapInitializeCrossSigning()
}))
}
} }
data.xSigningKeysAreTrusted -> { data.xSigningKeysAreTrusted -> {
genericItem { genericItem {
@ -51,6 +63,13 @@ class CrossSigningSettingsController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_custom) titleIconResourceId(R.drawable.ic_shield_custom)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_trusted))
} }
genericButtonItem {
id("Reset")
text(stringProvider.getString(R.string.reset_cross_signing))
buttonClickAction(DebouncedClickListener({
interactionListener?.didTapInitializeCrossSigning()
}))
}
} }
data.xSigningIsEnableInAccount -> { data.xSigningIsEnableInAccount -> {
genericItem { genericItem {
@ -58,12 +77,27 @@ class CrossSigningSettingsController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_black) titleIconResourceId(R.drawable.ic_shield_black)
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_not_trusted))
} }
genericButtonItem {
id("Reset")
text(stringProvider.getString(R.string.reset_cross_signing))
buttonClickAction(DebouncedClickListener({
interactionListener?.didTapInitializeCrossSigning()
}))
}
} }
else -> { else -> {
genericItem { genericItem {
id("not") id("not")
title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled)) title(stringProvider.getString(R.string.encryption_information_dg_xsigning_disabled))
} }
genericPositiveButtonItem {
id("Initialize")
text(stringProvider.getString(R.string.initialize_cross_signing))
buttonClickAction(DebouncedClickListener({
interactionListener?.didTapInitializeCrossSigning()
}))
}
} }
} }

View File

@ -15,20 +15,26 @@
*/ */
package im.vector.app.features.settings.crosssigning package im.vector.app.features.settings.crosssigning
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject import javax.inject.Inject
@ -47,19 +53,55 @@ class CrossSigningSettingsFragment @Inject constructor(
private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel() private val viewModel: CrossSigningSettingsViewModel by fragmentViewModel()
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(CrossSigningSettingsAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(CrossSigningSettingsAction.PasswordAuthDone(password))
}
else -> {
viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
}
}
// activityResult.data?.extras?.getString(ReAuthActivity.RESULT_TOKEN)?.let { token ->
// }
} else {
viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
setupRecyclerView() setupRecyclerView()
viewModel.observeViewEvents { viewModel.observeViewEvents { event ->
when (it) { when (event) {
is CrossSigningSettingsViewEvents.Failure -> { is CrossSigningSettingsViewEvents.Failure -> {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(it.throwable)) .setMessage(errorFormatter.toHumanReadable(event.throwable))
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
Unit Unit
} }
is CrossSigningSettingsViewEvents.RequestReAuth -> {
ReAuthActivity.newIntent(requireContext(),
event.registrationFlowResponse,
event.lastErrorCode,
getString(R.string.initialize_cross_signing)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
is CrossSigningSettingsViewEvents.ShowModalWaitingView -> {
views.waitingView.waitingView.isVisible = true
views.waitingView.waitingStatusText.setTextOrHide(event.status)
}
CrossSigningSettingsViewEvents.HideModalWaitingView -> {
views.waitingView.waitingView.isVisible = false
}
}.exhaustive }.exhaustive
} }
} }
@ -83,4 +125,8 @@ class CrossSigningSettingsFragment @Inject constructor(
controller.interactionListener = null controller.interactionListener = null
super.onDestroyView() super.onDestroyView()
} }
override fun didTapInitializeCrossSigning() {
viewModel.handle(CrossSigningSettingsAction.InitializeCrossSigning)
}
} }

View File

@ -17,10 +17,14 @@
package im.vector.app.features.settings.crosssigning package im.vector.app.features.settings.crosssigning
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
/** /**
* Transient events for cross signing settings screen * Transient events for cross signing settings screen
*/ */
sealed class CrossSigningSettingsViewEvents : VectorViewEvents { sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents() data class Failure(val throwable: Throwable) : CrossSigningSettingsViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : CrossSigningSettingsViewEvents()
data class ShowModalWaitingView(val status: String?) : CrossSigningSettingsViewEvents()
object HideModalWaitingView : CrossSigningSettingsViewEvents()
} }

View File

@ -15,25 +15,48 @@
*/ */
package im.vector.app.features.settings.crosssigning package im.vector.app.features.settings.crosssigning
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo import org.matrix.android.sdk.api.session.crypto.crosssigning.MXCrossSigningInfo
import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.Optional
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified import org.matrix.android.sdk.internal.crypto.crosssigning.isVerified
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState, class CrossSigningSettingsViewModel @AssistedInject constructor(
private val session: Session) @Assisted private val initialState: CrossSigningSettingsViewState,
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) { private val session: Session,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
init { init {
Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>( Observable.combineLatest<List<DeviceInfo>, Optional<MXCrossSigningInfo>, Pair<List<DeviceInfo>, Optional<MXCrossSigningInfo>>>(
@ -58,15 +81,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat
} }
} }
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
} }
override fun handle(action: CrossSigningSettingsAction) { override fun handle(action: CrossSigningSettingsAction) {
// No op for the moment when (action) {
// when (action) { CrossSigningSettingsAction.InitializeCrossSigning -> {
// }.exhaustive _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback<Unit> {
session.cryptoService().crossSigningService().initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse,
errCode: String?,
promise: Continuation<UIABaseAuth>) {
Timber.d("## UIA : initializeCrossSigning UIA")
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD
&& reAuthHelper.data != null && errCode == null) {
UserPasswordAuth(
session = null,
user = session.myUserId,
password = reAuthHelper.data
).let { promise.resume(it) }
} else {
Timber.d("## UIA : initializeCrossSigning UIA > start reauth activity")
_viewEvents.post(CrossSigningSettingsViewEvents.RequestReAuth(flowResponse, errCode))
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
uiaContinuation = promise
}
}
}, it)
}
} catch (failure: Throwable) {
handleInitializeXSigningError(failure)
} finally {
_viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView)
}
}
Unit
}
is CrossSigningSettingsAction.SsoAuthDone -> {
Timber.d("## UIA - FallBack success")
if (pendingAuth != null) {
uiaContinuation?.resume(pendingAuth!!)
} else {
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
}
}
is CrossSigningSettingsAction.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = decryptedPass,
user = session.myUserId
)
)
}
CrossSigningSettingsAction.ReAuthCancelled -> {
Timber.d("## UIA - Reauth cancelled")
_viewEvents.post(CrossSigningSettingsViewEvents.HideModalWaitingView)
uiaContinuation?.resumeWith(Result.failure((Exception())))
uiaContinuation = null
pendingAuth = null
}
}.exhaustive
}
private fun handleInitializeXSigningError(failure: Throwable) {
Timber.e(failure, "## CrossSigning - Failed to initialize cross signing")
_viewEvents.post(CrossSigningSettingsViewEvents.Failure(Exception(stringProvider.getString(R.string.failed_to_initialize_cross_signing))))
} }
companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> { companion object : MvRxViewModelFactory<CrossSigningSettingsViewModel, CrossSigningSettingsViewState> {

View File

@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
sealed class DevicesAction : VectorViewModelAction { sealed class DevicesAction : VectorViewModelAction {
object Refresh : DevicesAction() object Refresh : DevicesAction()
data class Delete(val deviceId: String) : DevicesAction() data class Delete(val deviceId: String) : DevicesAction()
data class Password(val password: String) : DevicesAction() // data class Password(val password: String) : DevicesAction()
data class Rename(val deviceId: String, val newName: String) : DevicesAction() data class Rename(val deviceId: String, val newName: String) : DevicesAction()
data class PromptRename(val deviceId: String) : DevicesAction() data class PromptRename(val deviceId: String) : DevicesAction()
@ -30,4 +30,8 @@ sealed class DevicesAction : VectorViewModelAction {
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction() data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
object CompleteSecurity : DevicesAction() object CompleteSecurity : DevicesAction()
data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction() data class MarkAsManuallyVerified(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesAction()
object SsoAuthDone: DevicesAction()
data class PasswordAuthDone(val password: String): DevicesAction()
object ReAuthCancelled: DevicesAction()
} }

View File

@ -19,6 +19,7 @@ package im.vector.app.features.settings.devices
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
@ -27,9 +28,12 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
*/ */
sealed class DevicesViewEvents : VectorViewEvents { sealed class DevicesViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DevicesViewEvents() data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
// object HideLoading : DevicesViewEvents()
data class Failure(val throwable: Throwable) : DevicesViewEvents() data class Failure(val throwable: Throwable) : DevicesViewEvents()
object RequestPassword : DevicesViewEvents() // object RequestPassword : DevicesViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : DevicesViewEvents()
data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents() data class PromptRenameDevice(val deviceInfo: DeviceInfo) : DevicesViewEvents()

View File

@ -27,16 +27,21 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import im.vector.app.core.error.SsoFlowNotSupportedYet import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.NoOpMatrixCallback
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -44,13 +49,22 @@ import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.VerificationService import org.matrix.android.sdk.api.session.crypto.verification.VerificationService
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.VerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState import org.matrix.android.sdk.api.session.crypto.verification.VerificationTxState
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.nextUncompletedStage
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.internal.util.awaitCallback
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
@ -70,9 +84,14 @@ data class DeviceFullInfo(
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
private val session: Session private val session: Session,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider
) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener { ) : VectorViewModel<DevicesViewState, DevicesAction, DevicesViewEvents>(initialState), VerificationService.Listener {
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
fun create(initialState: DevicesViewState): DevicesViewModel fun create(initialState: DevicesViewState): DevicesViewModel
@ -87,10 +106,6 @@ class DevicesViewModel @AssistedInject constructor(
} }
} }
// temp storage when we ask for the user password
private var _currentDeviceId: String? = null
private var _currentSession: String? = null
private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create() private val refreshPublisher: PublishSubject<Unit> = PublishSubject.create()
init { init {
@ -189,13 +204,43 @@ class DevicesViewModel @AssistedInject constructor(
return when (action) { return when (action) {
is DevicesAction.Refresh -> queryRefreshDevicesList() is DevicesAction.Refresh -> queryRefreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action) is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action) is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
is DevicesAction.CompleteSecurity -> handleCompleteSecurity() is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action) is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(action)
is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action) is DevicesAction.VerifyMyDeviceManually -> handleShowDeviceCryptoInfo(action)
is DevicesAction.SsoAuthDone -> {
// we should use token based auth
// _viewEvents.post(CrossSigningSettingsViewEvents.ShowModalWaitingView(null))
// will release the interactive auth interceptor
Timber.d("## UIA - FallBack success $pendingAuth , continuation: $uiaContinuation")
if (pendingAuth != null) {
uiaContinuation?.resume(pendingAuth!!)
} else {
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
}
Unit
}
is DevicesAction.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = decryptedPass,
user = session.myUserId
)
)
Unit
}
DevicesAction.ReAuthCancelled -> {
Timber.d("## UIA - Reauth cancelled")
// _viewEvents.post(DevicesViewEvents.Loading)
uiaContinuation?.resumeWith(Result.failure((Exception())))
uiaContinuation = null
pendingAuth = null
Unit
}
} }
} }
@ -285,95 +330,48 @@ class DevicesViewModel @AssistedInject constructor(
) )
} }
session.cryptoService().deleteDevice(deviceId, object : MatrixCallback<Unit> { viewModelScope.launch(Dispatchers.IO) {
override fun onFailure(failure: Throwable) { try {
var isPasswordRequestFound = false awaitCallback<Unit> {
session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor {
if (failure is Failure.RegistrationFlowError) { override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
// We only support LoginFlowTypes.PASSWORD Timber.d("## UIA : deleteDevice UIA")
// Check if we can provide the user password if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD && reAuthHelper.data != null && errCode == null) {
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow -> UserPasswordAuth(
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true session = null,
} user = session.myUserId,
password = reAuthHelper.data
if (isPasswordRequestFound) { ).let { promise.resume(it) }
_currentDeviceId = deviceId } else {
_currentSession = failure.registrationFlowResponse.session Timber.d("## UIA : deleteDevice UIA > start reauth activity")
_viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode))
setState { pendingAuth = DefaultBaseAuth(session = flowResponse.session)
copy( uiaContinuation = promise
request = Success(Unit) }
)
} }
}, it)
_viewEvents.post(DevicesViewEvents.RequestPassword)
}
} }
if (!isPasswordRequestFound) {
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
setState {
copy(
request = Fail(failure)
)
}
_viewEvents.post(DevicesViewEvents.Failure(SsoFlowNotSupportedYet()))
}
}
override fun onSuccess(data: Unit) {
setState { setState {
copy( copy(
request = Success(data) request = Success(Unit)
) )
} }
// force settings update // force settings update
queryRefreshDevicesList() queryRefreshDevicesList()
} } catch (failure: Throwable) {
})
}
private fun handlePassword(action: DevicesAction.Password) {
val currentDeviceId = _currentDeviceId
if (currentDeviceId.isNullOrBlank()) {
// Abort
return
}
setState {
copy(
request = Loading()
)
}
session.cryptoService().deleteDeviceWithUserPassword(currentDeviceId, _currentSession, action.password, object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
_currentDeviceId = null
_currentSession = null
setState {
copy(
request = Success(data)
)
}
// force settings update
queryRefreshDevicesList()
}
override fun onFailure(failure: Throwable) {
_currentDeviceId = null
_currentSession = null
// Password is maybe not good
setState { setState {
copy( copy(
request = Fail(failure) request = Fail(failure)
) )
} }
if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
_viewEvents.post(DevicesViewEvents.Failure(failure)) _viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.authentication_error))))
} else {
_viewEvents.post(DevicesViewEvents.Failure(Exception(stringProvider.getString(R.string.matrix_error))))
}
// ...
Timber.e(failure, "failed to delete session")
} }
}) }
} }
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.devices package im.vector.app.features.settings.devices
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.ManuallyVerifyDialog import im.vector.app.core.dialogs.ManuallyVerifyDialog
import im.vector.app.core.dialogs.PromptPasswordDialog
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import javax.inject.Inject import javax.inject.Inject
@ -52,7 +55,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
// used to avoid requesting to enter the password for each deletion // used to avoid requesting to enter the password for each deletion
// Note: Sonar does not like to use password for member name. // Note: Sonar does not like to use password for member name.
private var mAccountPass: String = "" // private var mAccountPass: String = ""
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentGenericRecyclerBinding {
return FragmentGenericRecyclerBinding.inflate(inflater, container, false) return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
@ -71,7 +74,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
when (it) { when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message) is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable) is DevicesViewEvents.Failure -> showFailure(it.throwable)
is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog() is DevicesViewEvents.RequestReAuth -> askForReAuthentication(it)
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo) is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
is DevicesViewEvents.ShowVerifyDevice -> { is DevicesViewEvents.ShowVerifyDevice -> {
VerificationBottomSheet.withArgs( VerificationBottomSheet.withArgs(
@ -93,13 +96,6 @@ class VectorSettingsDevicesFragment @Inject constructor(
} }
} }
override fun showFailure(throwable: Throwable) {
super.showFailure(throwable)
// Password is maybe not good, for safety measure, reset it here
mAccountPass = ""
}
override fun onDestroyView() { override fun onDestroyView() {
devicesController.callback = null devicesController.callback = null
views.genericRecyclerView.cleanup() views.genericRecyclerView.cleanup()
@ -119,14 +115,6 @@ class VectorSettingsDevicesFragment @Inject constructor(
) )
} }
// override fun onDeleteDevice(deviceInfo: DeviceInfo) {
// devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
// }
//
// override fun onRenameDevice(deviceInfo: DeviceInfo) {
// displayDeviceRenameDialog(deviceInfo)
// }
override fun retry() { override fun retry() {
viewModel.handle(DevicesAction.Refresh) viewModel.handle(DevicesAction.Refresh)
} }
@ -154,17 +142,34 @@ class VectorSettingsDevicesFragment @Inject constructor(
.show() .show()
} }
/** private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
* Show a dialog to ask for user password, or use a previously entered password. if (activityResult.resultCode == Activity.RESULT_OK) {
*/ when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
private fun maybeShowDeleteDeviceWithPasswordDialog() { LoginFlowTypes.SSO -> {
if (mAccountPass.isNotEmpty()) { viewModel.handle(DevicesAction.SsoAuthDone)
viewModel.handle(DevicesAction.Password(mAccountPass)) }
} else { LoginFlowTypes.PASSWORD -> {
PromptPasswordDialog().show(requireActivity()) { password -> val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
mAccountPass = password viewModel.handle(DevicesAction.PasswordAuthDone(password))
viewModel.handle(DevicesAction.Password(mAccountPass)) }
else -> {
viewModel.handle(DevicesAction.ReAuthCancelled)
}
} }
} else {
viewModel.handle(DevicesAction.ReAuthCancelled)
}
}
/**
* Launch the re auth activity to get credentials
*/
private fun askForReAuthentication(reAuthReq: DevicesViewEvents.RequestReAuth) {
ReAuthActivity.newIntent(requireContext(),
reAuthReq.registrationFlowResponse,
reAuthReq.lastErrorCode,
getString(R.string.devices_delete_dialog_title)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
} }
} }

View File

@ -25,6 +25,11 @@ sealed class ThreePidsSettingsAction : VectorViewModelAction {
data class SubmitCode(val threePid: ThreePid.Msisdn, val code: String) : ThreePidsSettingsAction() data class SubmitCode(val threePid: ThreePid.Msisdn, val code: String) : ThreePidsSettingsAction()
data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction() data class ContinueThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction() data class CancelThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
data class AccountPassword(val password: String) : ThreePidsSettingsAction()
// data class AccountPassword(val password: String) : ThreePidsSettingsAction()
data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction() data class DeleteThreePid(val threePid: ThreePid) : ThreePidsSettingsAction()
object SsoAuthDone : ThreePidsSettingsAction()
data class PasswordAuthDone(val password: String) : ThreePidsSettingsAction()
object ReAuthCancelled : ThreePidsSettingsAction()
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.settings.threepids package im.vector.app.features.settings.threepids
import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -26,7 +27,6 @@ import androidx.appcompat.app.AppCompatActivity
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.PromptPasswordDialog
import im.vector.app.core.dialogs.withColoredButton import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
@ -35,10 +35,12 @@ import im.vector.app.core.extensions.getFormattedValue
import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.isMsisdn import im.vector.app.core.extensions.isMsisdn
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import javax.inject.Inject import javax.inject.Inject
@ -64,15 +66,42 @@ class ThreePidsSettingsFragment @Inject constructor(
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable) is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
ThreePidsSettingsViewEvents.RequestPassword -> askUserPassword() is ThreePidsSettingsViewEvents.RequestReAuth -> askAuthentication(it)
}.exhaustive }.exhaustive
} }
} }
private fun askUserPassword() { // private fun askUserPassword() {
PromptPasswordDialog().show(requireActivity()) { password -> // PromptPasswordDialog().show(requireActivity()) { password ->
viewModel.handle(ThreePidsSettingsAction.AccountPassword(password)) // viewModel.handle(ThreePidsSettingsAction.AccountPassword(password))
// }
// }
private fun askAuthentication(event: ThreePidsSettingsViewEvents.RequestReAuth) {
ReAuthActivity.newIntent(requireContext(),
event.registrationFlowResponse,
event.lastErrorCode,
getString(R.string.settings_add_email_address)).let { intent ->
reAuthActivityResultLauncher.launch(intent)
}
}
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(ThreePidsSettingsAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(ThreePidsSettingsAction.PasswordAuthDone(password))
}
else -> {
viewModel.handle(ThreePidsSettingsAction.ReAuthCancelled)
}
}
} else {
viewModel.handle(ThreePidsSettingsAction.ReAuthCancelled)
} }
} }

View File

@ -17,8 +17,10 @@
package im.vector.app.features.settings.threepids package im.vector.app.features.settings.threepids
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
sealed class ThreePidsSettingsViewEvents : VectorViewEvents { sealed class ThreePidsSettingsViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents() data class Failure(val throwable: Throwable) : ThreePidsSettingsViewEvents()
object RequestPassword : ThreePidsSettingsViewEvents() // object RequestPassword : ThreePidsSettingsViewEvents()
data class RequestReAuth(val registrationFlowResponse: RegistrationFlowResponse, val lastErrorCode: String?) : ThreePidsSettingsViewEvents()
} }

View File

@ -24,21 +24,28 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.SsoFlowNotSupportedYet
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ReadOnceTrue import im.vector.app.core.utils.ReadOnceTrue
import im.vector.app.features.auth.ReAuthActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.internal.crypto.crosssigning.fromBase64
import org.matrix.android.sdk.internal.crypto.model.rest.DefaultBaseAuth
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.rx
import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class ThreePidsSettingsViewModel @AssistedInject constructor( class ThreePidsSettingsViewModel @AssistedInject constructor(
@Assisted initialState: ThreePidsSettingsViewState, @Assisted initialState: ThreePidsSettingsViewState,
@ -48,36 +55,16 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
// UIA session // UIA session
private var pendingThreePid: ThreePid? = null private var pendingThreePid: ThreePid? = null
private var pendingSession: String? = null // private var pendingSession: String? = null
private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> { private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
isLoading(false) isLoading(false)
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
if (failure is Failure.RegistrationFlowError) {
var isPasswordRequestFound = false
// We only support LoginFlowTypes.PASSWORD
// Check if we can provide the user password
failure.registrationFlowResponse.flows?.forEach { interactiveAuthenticationFlow ->
isPasswordRequestFound = isPasswordRequestFound || interactiveAuthenticationFlow.stages?.any { it == LoginFlowTypes.PASSWORD } == true
}
if (isPasswordRequestFound) {
pendingSession = failure.registrationFlowResponse.session
_viewEvents.post(ThreePidsSettingsViewEvents.RequestPassword)
} else {
// LoginFlowTypes.PASSWORD not supported, and this is the only one Element supports so far...
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(SsoFlowNotSupportedYet()))
}
} else {
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
}
} }
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
pendingThreePid = null pendingThreePid = null
pendingSession = null
isLoading(false) isLoading(false)
} }
} }
@ -142,16 +129,50 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
override fun handle(action: ThreePidsSettingsAction) { override fun handle(action: ThreePidsSettingsAction) {
when (action) { when (action) {
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action) is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action) is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action) is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action)
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action) is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action) is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action) is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action)
is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action) ThreePidsSettingsAction.SsoAuthDone -> {
Timber.d("## UIA - FallBack success")
if (pendingAuth != null) {
uiaContinuation?.resume(pendingAuth!!)
} else {
uiaContinuation?.resumeWith(Result.failure((IllegalArgumentException())))
}
}
is ThreePidsSettingsAction.PasswordAuthDone -> {
val decryptedPass = session.loadSecureSecret<String>(action.password.fromBase64().inputStream(), ReAuthActivity.DEFAULT_RESULT_KEYSTORE_ALIAS)
uiaContinuation?.resume(
UserPasswordAuth(
session = pendingAuth?.session,
password = decryptedPass,
user = session.myUserId
)
)
}
ThreePidsSettingsAction.ReAuthCancelled -> {
Timber.d("## UIA - Reauth cancelled")
uiaContinuation?.resumeWith(Result.failure((Exception())))
uiaContinuation = null
pendingAuth = null
}
}.exhaustive }.exhaustive
} }
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
private val uiaInterceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
_viewEvents.post(ThreePidsSettingsViewEvents.RequestReAuth(flowResponse, errCode))
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
uiaContinuation = promise
}
}
private fun handleSubmitCode(action: ThreePidsSettingsAction.SubmitCode) { private fun handleSubmitCode(action: ThreePidsSettingsAction.SubmitCode) {
isLoading(true) isLoading(true)
setState { setState {
@ -168,7 +189,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
override fun onSuccess(data: Unit) { override fun onSuccess(data: Unit) {
// then finalize // then finalize
pendingThreePid = action.threePid pendingThreePid = action.threePid
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback) session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback)
} }
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
@ -232,7 +253,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
isLoading(true) isLoading(true)
pendingThreePid = action.threePid pendingThreePid = action.threePid
viewModelScope.launch { viewModelScope.launch {
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback) session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback)
} }
} }
@ -243,16 +264,14 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
} }
} }
private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) { // private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) {
val safeSession = pendingSession ?: return Unit // val safeThreePid = pendingThreePid ?: return Unit
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending session"))) } // .also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
val safeThreePid = pendingThreePid ?: return Unit // isLoading(true)
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) } // viewModelScope.launch {
isLoading(true) // session.finalizeAddingThreePid(safeThreePid, uiaInterceptor, loadingCallback)
viewModelScope.launch { // }
session.finalizeAddingThreePid(safeThreePid, safeSession, action.password, loadingCallback) // }
}
}
private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) { private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) {
isLoading(true) isLoading(true)

View File

@ -115,8 +115,10 @@ class ServerBackupStatusViewModel @AssistedInject constructor(@Assisted initialS
// So recovery is not setup // So recovery is not setup
// Check if cross signing is enabled and local secrets known // Check if cross signing is enabled and local secrets known
if (crossSigningInfo.getOrNull()?.isTrusted() == true if (
&& pInfo.getOrNull()?.allKnown().orFalse() crossSigningInfo.getOrNull() == null
|| (crossSigningInfo.getOrNull()?.isTrusted() == true
&& pInfo.getOrNull()?.allKnown().orFalse())
) { ) {
// So 4S is not setup and we have local secrets, // So 4S is not setup and we have local secrets,
return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup()) return@Function4 BannerState.Setup(numberOfKeys = getNumberOfKeysToBackup())

Some files were not shown because too many files have changed in this diff Show More