Merge pull request #2753 from vector-im/feature/bca/sso_reauth

Feature/bca/sso reauth
This commit is contained in:
Benoit Marty 2021-02-03 16:12:54 +01:00 committed by GitHub
commit bc22647b48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 2446 additions and 837 deletions

View file

@ -2,10 +2,12 @@ Changes in Element 1.0.15 (2020-XX-XX)
===================================================
Features ✨:
-
- Social Login support
Improvements 🙌:
-
- SSO support for cross signing (#1062)
- Deactivate account when logged in with SSO (#1264)
- SSO UIA doesn't work (#2754)
Bugfix 🐛:
- Fix clear cache issue: sometimes, after a clear cache, there is still a token, so the init sync service is not started.

View file

@ -16,8 +16,18 @@
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.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.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.failure.Failure
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.TestConstants
import org.matrix.android.sdk.common.TestMatrixCallback
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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(JUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -44,7 +50,18 @@ class DeactivateAccountTest : InstrumentedTest {
// Deactivate the account
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)

View file

@ -19,6 +19,18 @@ package org.matrix.android.sdk.common
import android.os.SystemClock
import android.util.Log
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.crypto.verification.IncomingSasVerificationTransaction
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.keysbackup.model.MegolmBackupAuthData
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.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
@ -304,10 +309,18 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
fun initializeCrossSigning(session: Session) {
mTestHelper.doSync<Unit> {
session.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = session.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
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
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.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.MXCryptoError
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.internal.crypto.model.OlmSessionWrapper
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.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 timber.log.Timber
import java.util.concurrent.CountDownLatch
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
/**
* Ref:
@ -202,10 +207,18 @@ class UnwedgingTest : InstrumentedTest {
// It's a trick to force key request on fail to decrypt
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
// Wait until we received back the key

View file

@ -17,14 +17,6 @@
package org.matrix.android.sdk.internal.crypto.crosssigning
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.assertFalse
import org.junit.Assert.assertNotNull
@ -35,6 +27,19 @@ 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.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)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@ -49,10 +54,17 @@ class XSigningTest : InstrumentedTest {
mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
val myCrossSigningKeys = aliceSession.cryptoService().crossSigningService().getMyCrossSigningKeys()
@ -86,8 +98,18 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) }
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) }
mTestHelper.doSync<Unit> {
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
mTestHelper.doSync<MXUsersDevicesMap<CryptoDeviceInfo>> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) }
@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest {
password = TestConstants.PASSWORD
)
mTestHelper.doSync<Unit> { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) }
mTestHelper.doSync<Unit> { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) }
mTestHelper.doSync<Unit> { 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
val bobUserId = bobSession.myUserId

View file

@ -18,7 +18,21 @@ package org.matrix.android.sdk.internal.crypto.gossiping
import android.util.Log
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.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.SasVerificationTransaction
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.room.model.RoomDirectoryVisibility
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.CryptoTestHelper
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.MXUsersDevicesMap
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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -200,10 +205,17 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> {
aliceSession1.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession1.myUserId,
password = TestConstants.PASSWORD
)
)
}
}, it)
}
// Also bootstrap keybackup on first session
@ -305,10 +317,18 @@ class KeyShareTests : InstrumentedTest {
mTestHelper.doSync<Unit> {
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
// 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))
mTestHelper.doSync<Unit> {
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, it)
}
// Let alice invite bob
@ -356,7 +384,7 @@ class KeyShareTests : InstrumentedTest {
val roomRoomBobPov = aliceSession.getRoom(roomId)
val beforeJoin = roomRoomBobPov!!.getTimeLineEvent(secondEventId)
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
var dRes = tryOrNull { bobSession.cryptoService().decryptEvent(beforeJoin!!.root, "") }
assert(dRes == null)
@ -367,7 +395,7 @@ class KeyShareTests : InstrumentedTest {
Thread.sleep(3_000)
// 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}")
assert(dRes?.clearEvent == null)
}

View file

@ -17,20 +17,25 @@
package org.matrix.android.sdk.internal.crypto.verification.qrcode
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.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.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 kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@FixMethodOrder(MethodSorters.JVM)
@ -157,18 +162,34 @@ class VerificationTest : InstrumentedTest {
mTestHelper.doSync<Unit> { callback ->
aliceSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD
), callback)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = aliceSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, callback)
}
mTestHelper.doSync<Unit> { callback ->
bobSession.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD
), callback)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = bobSession.myUserId,
password = TestConstants.PASSWORD,
session = flowResponse.session
)
)
}
}, callback)
}
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

@ -0,0 +1,31 @@
/*
* 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
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
* 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.JsonClass
@ -27,7 +27,7 @@ data class UserPasswordAuth(
// device device session id
@Json(name = "session")
val session: String? = null,
override val session: String? = null,
// registration information
@Json(name = "type")
@ -38,4 +38,16 @@ data class UserPasswordAuth(
@Json(name = "password")
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.
* 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 {
companion object {
// Not really defined by the spec, but we may define some ids here
const val ID_GOOGLE = "google"
const val ID_GITHUB = "github"
const val ID_APPLE = "apple"
const val ID_FACEBOOK = "facebook"
const val ID_TWITTER = "twitter"
const val BRAND_GOOGLE = "org.matrix.google"
const val BRAND_GITHUB = "org.matrix.github"
const val BRAND_APPLE = "org.matrix.apple"
const val BRAND_FACEBOOK = "org.matrix.facebook"
const val BRAND_TWITTER = "org.matrix.twitter"
const val BRAND_GITLAB = "org.matrix.gitlab"
}
}

View file

@ -14,14 +14,11 @@
* 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.JsonClass
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.internal.auth.data.InteractiveAuthenticationFlow
@ -109,3 +106,8 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
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
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
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 java.io.IOException
import javax.net.ssl.HttpsURLConnection
@ -43,6 +43,12 @@ fun Throwable.isInvalidPassword(): Boolean {
&& 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
*/
@ -53,6 +59,16 @@ fun Throwable.toRegistrationFlowResponse(): RegistrationFlowResponse? {
.adapter(RegistrationFlowResponse::class.java)
.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 {
null
}

View file

@ -16,8 +16,8 @@
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.internal.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.internal.network.ssl.Fingerprint
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.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.
@ -42,7 +44,17 @@ data class MatrixError(
@Json(name = "soft_logout") val isSoftLogout: Boolean = false,
// For 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 {

View file

@ -245,6 +245,8 @@ interface Session :
val sharedSecretStorageService: SharedSecretStorageService
fun getUiaSsoFallbackUrl(authenticationSessionId: String): String
/**
* 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
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
/**
* 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
* 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.paging.PagedList
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.session.crypto.crosssigning.CrossSigningService
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 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>)

View file

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

View file

@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.profile
import android.net.Uri
import androidx.lifecycle.LiveData
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.util.Cancelable
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
*/
fun finalizeAddingThreePid(threePid: ThreePid,
uiaSession: String?,
accountPassword: String?,
userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
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 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
*/
@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.RegistrationResult
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.RegistrationFlowError
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 org.matrix.android.sdk.api.MatrixCallback
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.extensions.tryOrNull
import org.matrix.android.sdk.api.failure.Failure
@ -207,9 +208,9 @@ internal class DefaultCryptoService @Inject constructor(
.executeBy(taskExecutor)
}
override fun deleteDevice(deviceId: String, callback: MatrixCallback<Unit>) {
override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback<Unit>) {
deleteDeviceTask
.configureWith(DeleteDeviceTask.Params(deviceId)) {
.configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) {
this.executionThread = TaskThread.CRYPTO
this.callback = callback
}

View file

@ -22,6 +22,7 @@ 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.auth.UserInteractiveAuthInterceptor
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.MXCrossSigningInfo
@ -29,7 +30,6 @@ import org.matrix.android.sdk.api.util.Optional
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.UserPasswordAuth
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.tasks.InitializeCrossSigningTask
@ -150,11 +150,11 @@ internal class DefaultCrossSigningService @Inject constructor(
* - Sign the keys and upload them
* - Sign the current device with SSK and sign MSK with device key (migration) and upload signatures
*/
override fun initializeCrossSigning(authParams: UserPasswordAuth?, callback: MatrixCallback<Unit>) {
override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback<Unit>) {
Timber.d("## CrossSigning initializeCrossSigning")
val params = InitializeCrossSigningTask.Params(
authParams = authParams
interactiveAuthInterceptor = uiaInterceptor
)
initializeCrossSigningTask.configureWith(params) {
this.callbackThread = TaskThread.CRYPTO

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)
internal data class DeleteDeviceParams(
@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,
@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
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
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.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.executeRequest
import org.matrix.android.sdk.internal.task.Task
import timber.log.Timber
import javax.inject.Inject
internal interface DeleteDeviceTask : Task<DeleteDeviceTask.Params, Unit> {
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) {
try {
executeRequest<Unit>(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams())
apiCall = cryptoApi.deleteDevice(params.deviceId, DeleteDeviceParams(params.userAuthParam?.asMap()))
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
if (params.userInteractiveAuthInterceptor == null
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
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.internal.crypto.api.CryptoApi
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.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -44,12 +44,12 @@ internal class DefaultDeleteDeviceWithUserPasswordTask @Inject constructor(
return executeRequest(globalErrorReceiver) {
apiCall = cryptoApi.deleteDevice(params.deviceId,
DeleteDeviceParams(
userPasswordAuth = UserPasswordAuth(
auth = UserPasswordAuth(
type = LoginFlowTypes.PASSWORD,
session = params.authSession,
user = userId,
password = params.password
)
).asMap()
)
)
}

View file

@ -17,6 +17,8 @@
package org.matrix.android.sdk.internal.crypto.tasks
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.MyDeviceInfoHolder
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.KeyUsage
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.task.Task
import org.matrix.android.sdk.internal.util.JsonCanonicalizer
@ -34,7 +35,7 @@ import javax.inject.Inject
internal interface InitializeCrossSigningTask : Task<InitializeCrossSigningTask.Params, InitializeCrossSigningTask.Result> {
data class Params(
val authParams: UserPasswordAuth?
val interactiveAuthInterceptor: UserInteractiveAuthInterceptor?
)
data class Result(
@ -117,10 +118,21 @@ internal class DefaultInitializeCrossSigningTask @Inject constructor(
.key(sskPublicKey)
.signature(userId, masterPublicKey, signedSSK)
.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
val uploadSignatureQueryBuilder = UploadSignatureQueryBuilder()

View file

@ -16,14 +16,12 @@
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.toRegistrationFlowResponse
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.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.UserPasswordAuth
import org.matrix.android.sdk.internal.crypto.model.toRest
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -39,15 +37,9 @@ internal interface UploadSigningKeysTask : Task<UploadSigningKeysTask.Params, Un
// the SSK
val selfSignedKey: CryptoCrossSigningKey,
/**
* - If null:
* - no retry will be performed
* - If not null, it may or may not contain a sessionId:
* - If sessionId is null:
* - password should not be null: the task will perform a first request to get a sessionId, and then a second one
* - If sessionId is not null:
* - password should not be null as well, and no retry will be performed
* Authorisation info (User Interactive flow)
*/
val userPasswordAuth: UserPasswordAuth?
val userAuthParam: UIABaseAuth?
)
}
@ -59,31 +51,13 @@ internal class DefaultUploadSigningKeysTask @Inject constructor(
) : UploadSigningKeysTask {
override suspend fun execute(params: UploadSigningKeysTask.Params) {
val paramsHaveSessionId = params.userPasswordAuth?.session != null
val uploadQuery = UploadSigningKeysBody(
masterKey = params.masterKey.toRest(),
userSigningKey = params.userKey.toRest(),
selfSigningKey = params.selfSignedKey.toRest(),
// If sessionId is provided, use the userPasswordAuth
auth = params.userPasswordAuth.takeIf { paramsHaveSessionId }
auth = params.userAuthParam?.asMap()
)
try {
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
}
}
doRequest(uploadQuery)
}
private suspend fun doRequest(uploadQuery: UploadSigningKeysBody) {

View file

@ -52,6 +52,8 @@ import org.matrix.android.sdk.api.session.terms.TermsService
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.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.crypto.DefaultCryptoService
import org.matrix.android.sdk.internal.database.tools.RealmDebugTools
@ -273,6 +275,18 @@ internal class DefaultSession @Inject constructor(
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() {
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.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.

View file

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

View file

@ -16,6 +16,9 @@
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.network.GlobalErrorReceiver
import org.matrix.android.sdk.internal.network.executeRequest
@ -27,8 +30,9 @@ import javax.inject.Inject
internal interface DeactivateAccountTask : Task<DeactivateAccountTask.Params, Unit> {
data class Params(
val password: String,
val eraseAllData: Boolean
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val eraseAllData: Boolean,
val userAuthParam: UIABaseAuth? = null
)
}
@ -41,12 +45,21 @@ internal class DefaultDeactivateAccountTask @Inject constructor(
) : DeactivateAccountTask {
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) {
apiCall = accountAPI.deactivate(deactivateAccountParams)
try {
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
runCatching { identityDisconnectTask.execute(Unit) }
.onFailure { Timber.w(it, "Unable to disconnect identity server") }

View file

@ -16,6 +16,7 @@
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 javax.inject.Inject
@ -26,7 +27,7 @@ internal class DefaultAccountService @Inject constructor(private val changePassw
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword))
}
override suspend fun deactivateAccount(password: String, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(password, eraseAllData))
override suspend fun deactivateAccount(userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, eraseAllData: Boolean) {
deactivateAccountTask.execute(DeactivateAccountTask.Params(userInteractiveAuthInterceptor, eraseAllData))
}
}

View file

@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import io.realm.kotlin.where
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.profile.ProfileService
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,
uiaSession: String?,
accountPassword: String?,
userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
matrixCallback: MatrixCallback<Unit>): Cancelable {
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = uiaSession,
accountPassword = accountPassword,
userInteractiveAuthInterceptor = userInteractiveAuthInterceptor,
userWantsToCancel = false
)) {
callback = alsoRefresh(matrixCallback)
@ -189,8 +188,7 @@ internal class DefaultProfileService @Inject constructor(private val taskExecuto
return finalizeAddingThreePidTask
.configureWith(FinalizeAddingThreePidTask.Params(
threePid = threePid,
session = null,
accountPassword = null,
userInteractiveAuthInterceptor = null,
userWantsToCancel = true
)) {
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.JsonClass
import org.matrix.android.sdk.internal.crypto.model.rest.UserPasswordAuth
@JsonClass(generateAdapter = true)
internal data class FinalizeAddThreePidBody(
@ -37,5 +36,5 @@ internal data class FinalizeAddThreePidBody(
* Additional authentication information for the user-interactive authentication API.
*/
@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
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.toRegistrationFlowResponse
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.PendingThreePidEntityFields
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.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import timber.log.Timber
import javax.inject.Inject
internal abstract class FinalizeAddingThreePidTask : Task<FinalizeAddingThreePidTask.Params, Unit> {
data class Params(
val threePid: ThreePid,
val session: String?,
val accountPassword: String?,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor?,
val userAuthParam: UIABaseAuth? = null,
val userWantsToCancel: Boolean
)
}
@ -62,20 +65,21 @@ internal class DefaultFinalizeAddingThreePidTask @Inject constructor(
val body = FinalizeAddThreePidBody(
clientSecret = pendingThreePids.clientSecret,
sid = pendingThreePids.sid,
auth = if (params.session != null && params.accountPassword != null) {
UserPasswordAuth(
session = params.session,
user = userId,
password = params.accountPassword
)
} else null
auth = params.userAuthParam?.asMap()
)
apiCall = profileAPI.finalizeAddThreePid(body)
}
} catch (throwable: Throwable) {
throw throwable.toRegistrationFlowResponse()
?.let { Failure.RegistrationFlowError(it) }
?: throwable
if (params.userInteractiveAuthInterceptor == null
|| !handleUIA(throwable, params.userInteractiveAuthInterceptor) { auth ->
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 {
return (params.enableEncryptionIfInvitedUsersSupportIt
&& crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isEmpty())
return params.enableEncryptionIfInvitedUsersSupportIt
// Parity with web, enable if users have encryption ready devices
// for now remove checks on cross signing and 3pid invites
// && crossSigningService.isCrossSigningVerified()
&& params.invite3pids.isEmpty()
&& params.invitedUserIds.isNotEmpty()
&& params.invitedUserIds.let { userIds ->
val keys = deviceListManager.downloadKeys(userIds, forceDownload = false)

View file

@ -42,13 +42,18 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
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.crypto.verification.SasVerificationTransaction
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.VerificationTransaction
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)
@LargeTest
@ -67,10 +72,18 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password"
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
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.runner.RunWith
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.internal.crypto.model.rest.UserPasswordAuth
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
@RunWith(AndroidJUnit4::class)
@LargeTest
@ -67,17 +72,35 @@ class VerifySessionPassphraseTest : VerificationTestBase() {
existingSession = createAccountAndSync(matrix, userName, password, true)
doSync<Unit> {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password"
), it)
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password",
session = flowResponse.session
)
)
}
}, it)
}
val task = BootstrapCrossSigningTask(existingSession!!, StringProvider(context.resources))
runBlocking {
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,
setupMode = SetupMode.NORMAL
))

View file

@ -241,6 +241,27 @@
<activity android:name=".features.home.room.detail.search.SearchActivity" />
<activity android:name=".features.usercode.UserCodeActivity" />
<!-- 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 -->
<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.SharedSecuredStoragePassphraseFragment
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.BootstrapConfirmPassphraseFragment
import im.vector.app.features.crypto.recover.BootstrapEnterPassphraseFragment
@ -522,8 +522,8 @@ interface FragmentModule {
@Binds
@IntoMap
@FragmentKey(BootstrapAccountPasswordFragment::class)
fun bindBootstrapAccountPasswordFragment(fragment: BootstrapAccountPasswordFragment): Fragment
@FragmentKey(BootstrapReAuthFragment::class)
fun bindBootstrapReAuthFragment(fragment: BootstrapReAuthFragment): Fragment
@Binds
@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.preference.UserAvatarPreference
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.VectorCallActivity
import im.vector.app.features.call.conference.VectorJitsiActivity
@ -145,6 +146,7 @@ interface ScreenComponent {
fun inject(activity: VectorJitsiActivity)
fun inject(activity: SearchActivity)
fun inject(activity: UserCodeActivity)
fun inject(activity: ReAuthActivity)
/* ==========================================================================================
* BottomSheets

View file

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

View file

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

@ -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

@ -0,0 +1,27 @@
/*
* 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 im.vector.app.core.platform.VectorViewModelAction
sealed class ReAuthActions : VectorViewModelAction {
object StartSSOFallback : ReAuthActions()
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

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020 New Vector Ltd
* 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.
@ -14,6 +14,12 @@
* limitations under the License.
*/
package im.vector.app.core.error
package im.vector.app.features.auth
class SsoFlowNotSupportedYet : Throwable()
import im.vector.app.core.platform.VectorViewEvents
sealed class ReAuthEvents : VectorViewEvents {
data class OpenSsoURl(val url: String) : ReAuthEvents()
object Dismiss : ReAuthEvents()
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()
data class UpdateCandidatePassphrase(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 Completed : BootstrapActions()
object SaveReqQueryStarted : BootstrapActions()
@ -47,4 +47,8 @@ sealed class BootstrapActions : VectorViewModelAction {
object HandleForgotBackupPassphrase : BootstrapActions()
data class DoMigrateWithPassphrase(val passphrase: 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
import android.app.Activity
import android.app.Dialog
import android.os.Build
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.extensions.commitTransaction
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.databinding.BottomSheetBootstrapBinding
import im.vector.app.features.auth.ReAuthActivity
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject
import kotlin.reflect.KClass
@ -64,6 +68,25 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
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?) {
super.onViewCreated(view, savedInstanceState)
viewModel.observeViewEvents { event ->
@ -85,6 +108,14 @@ class BootstrapBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetBoot
is BootstrapViewEvents.SkipBootstrap -> {
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)
showFragment(BootstrapConfirmPassphraseFragment::class, Bundle())
}
is BootstrapStep.AccountPassword -> {
is BootstrapStep.AccountReAuth -> {
views.bootstrapIcon.isVisible = true
views.bootstrapIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_user))
views.bootstrapTitleText.text = getString(R.string.account_password)
showFragment(BootstrapAccountPasswordFragment::class, Bundle())
views.bootstrapTitleText.text = getString(R.string.re_authentication_activity_title)
showFragment(BootstrapReAuthFragment::class, Bundle())
}
is BootstrapStep.Initializing -> {
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.WaitingViewData
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.MatrixError
import org.matrix.android.sdk.api.failure.toRegistrationFlowResponse
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.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.KeysVersionResult
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 timber.log.Timber
import java.util.UUID
@ -51,16 +49,12 @@ sealed class BootstrapResult {
abstract class Failure(val error: String?) : BootstrapResult()
class UnsupportedAuthFlow : Failure(null)
data class GenericError(val failure: Throwable) : Failure(failure.localizedMessage)
data class InvalidPasswordError(val matrixError: MatrixError) : Failure(null)
class FailedToCreateSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
class FailedToSetDefaultSSSSKey(failure: Throwable) : Failure(failure.localizedMessage)
class FailedToStorePrivateKeyInSSSS(failure: Throwable) : Failure(failure.localizedMessage)
object MissingPrivateKey : Failure(null)
data class PasswordAuthFlowMissing(val sessionId: String) : Failure(null)
}
interface BootstrapProgressListener {
@ -68,7 +62,7 @@ interface BootstrapProgressListener {
}
data class Params(
val userPasswordAuth: UserPasswordAuth? = null,
val userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor,
val progressListener: BootstrapProgressListener? = null,
val passphrase: String?,
val keySpec: SsssKeySpec? = null,
@ -101,7 +95,10 @@ class BootstrapCrossSigningTask @Inject constructor(
try {
awaitCallback<Unit> {
crossSigningService.initializeCrossSigning(params.userPasswordAuth, it)
crossSigningService.initializeCrossSigning(
params.userInteractiveAuthInterceptor,
it
)
}
if (params.setupMode == SetupMode.CROSS_SIGNING_ONLY) {
return BootstrapResult.SuccessCrossSigningOnly
@ -312,16 +309,6 @@ class BootstrapCrossSigningTask @Inject constructor(
private fun handleInitializeXSigningError(failure: Throwable): BootstrapResult {
if (failure is Failure.ServerError && failure.error.code == MatrixError.M_FORBIDDEN) {
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)
}

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.nulabinc.zxcvbn.Zxcvbn
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.WaitingViewData
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper
import kotlinx.coroutines.Dispatchers
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.session.Session
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.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 java.io.OutputStream
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class BootstrapSharedViewModel @AssistedInject constructor(
@Assisted initialState: BootstrapViewState,
@ -66,7 +76,10 @@ class BootstrapSharedViewModel @AssistedInject constructor(
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 {
@ -81,7 +94,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
SetupMode.CROSS_SIGNING_ONLY -> {
// Go straight to account password
setState {
copy(step = BootstrapStep.AccountPassword(false))
copy(step = BootstrapStep.AccountReAuth())
}
}
SetupMode.NORMAL -> {
@ -149,10 +162,8 @@ class BootstrapSharedViewModel @AssistedInject constructor(
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
}
is BootstrapStep.AccountPassword -> {
setState {
copy(step = state.step.copy(isPasswordVisible = !state.step.isPasswordVisible))
}
is BootstrapStep.AccountReAuth -> {
// nop
}
is BootstrapStep.GetBackupSecretPassForMigration -> {
setState {
@ -196,16 +207,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
is BootstrapActions.DoInitialize -> {
if (state.passphrase == state.passphraseRepeat) {
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(userPassword)
}
startInitializeFlow(state)
} else {
setState {
copy(
@ -215,24 +217,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
is BootstrapActions.DoInitializeGeneratedKey -> {
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
passphrase = null,
passphraseRepeat = null,
step = BootstrapStep.AccountPassword(false)
)
}
} else {
setState {
copy(
passphrase = null,
passphraseRepeat = null
)
}
startInitializeFlow(userPassword)
}
startInitializeFlow(state)
}
BootstrapActions.RecoveryKeySaved -> {
_viewEvents.post(BootstrapViewEvents.RecoveryKeySaved)
@ -263,7 +248,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
BootstrapActions.GoToEnterAccountPassword -> {
setState {
copy(step = BootstrapStep.AccountPassword(false))
copy(step = BootstrapStep.AccountReAuth())
}
}
BootstrapActions.HandleForgotBackupPassphrase -> {
@ -273,15 +258,33 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
} else return@withState
}
is BootstrapActions.ReAuth -> {
startInitializeFlow(action.pass)
}
// is BootstrapActions.ReAuth -> {
// startInitializeFlow(action.pass)
// }
is BootstrapActions.DoMigrateWithPassphrase -> {
startMigrationFlow(state.step, action.passphrase, null)
}
is BootstrapActions.DoMigrateWithRecoveryKey -> {
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
}
@ -293,7 +296,7 @@ class BootstrapSharedViewModel @AssistedInject constructor(
)
}
} else {
startInitializeFlow(null)
startInitializeFlow(it)
}
}
@ -346,16 +349,16 @@ class BootstrapSharedViewModel @AssistedInject constructor(
migrationRecoveryKey = recoveryKey
)
}
val userPassword = reAuthHelper.data
if (userPassword == null) {
setState {
copy(
step = BootstrapStep.AccountPassword(false)
)
}
} else {
startInitializeFlow(userPassword)
}
// val userPassword = reAuthHelper.data
// if (userPassword == null) {
// setState {
// copy(
// step = BootstrapStep.AccountPassword(false)
// )
// }
// } else {
withState { startInitializeFlow(it) }
// }
}
is BackupToQuadSMigrationTask.Result.Failure -> {
_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
setState {
@ -389,19 +392,45 @@ class BootstrapSharedViewModel @AssistedInject constructor(
}
}
viewModelScope.launch(Dispatchers.IO) {
val userPasswordAuth = userPassword?.let {
UserPasswordAuth(
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
session = _pendingSession,
user = session.myUserId,
password = it
)
val interceptor = object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
when (flowResponse.nextUncompletedStage()) {
LoginFlowTypes.PASSWORD -> {
pendingAuth = UserPasswordAuth(
// Note that _pendingSession may or may not be null, this is OK, it will be managed by the task
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,
Params(
userPasswordAuth = userPasswordAuth,
userInteractiveAuthInterceptor = interceptor,
progressListener = progressListener,
passphrase = state.passphrase,
keySpec = state.migrationRecoveryKey?.let { extractCurveKeyFromRecoveryKey(it)?.let { RawBytesKeySpec(it) } },
@ -410,7 +439,6 @@ class BootstrapSharedViewModel @AssistedInject constructor(
) { bootstrapResult ->
when (bootstrapResult) {
is BootstrapResult.SuccessCrossSigningOnly -> {
// TPD
_viewEvents.post(BootstrapViewEvents.Dismiss(true))
}
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 -> {
// it's a bad password
// We clear the auth session, to avoid 'Requested operation has changed during the UI authentication session' error
_pendingSession = null
// it's a bad password / auth
setState {
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))
}
BootstrapStep.Initializing -> {

View file

@ -52,11 +52,11 @@ package im.vector.app.features.crypto.recover
* 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 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()
data class GetBackupSecretPassForMigration(val isPasswordVisible: Boolean, val useKey: Boolean) : GetBackupSecretForMigration()

View file

@ -17,10 +17,12 @@
package im.vector.app.features.crypto.recover
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
sealed class BootstrapViewEvents : VectorViewEvents {
data class Dismiss(val success: Boolean) : BootstrapViewEvents()
data class ModalError(val error: String) : BootstrapViewEvents()
object RecoveryKeySaved : 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.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.login.ReAuthHelper
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
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.extensions.tryOrNull
import org.matrix.android.sdk.api.pushrules.RuleIds
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.roomSummaryQueryParams
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.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.rx
import timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class HomeActivityViewModel @AssistedInject constructor(
@Assisted initialState: HomeActivityViewState,
@ -74,7 +82,6 @@ class HomeActivityViewModel @AssistedInject constructor(
init {
cleanupFiles()
observeInitialSync()
mayBeInitializeCrossSigning()
checkSessionPushIsOn()
observeCrossSigningReset()
}
@ -122,10 +129,10 @@ class HomeActivityViewModel @AssistedInject constructor(
// Schedule a check of the bootstrap when the init sync will be finished
checkBootstrap = true
}
is InitialSyncProgressService.Status.Idle -> {
is InitialSyncProgressService.Status.Idle -> {
if (checkBootstrap) {
checkBootstrap = false
maybeBootstrapCrossSigning()
maybeBootstrapCrossSigningAfterInitialSync()
}
}
}
@ -139,29 +146,6 @@ class HomeActivityViewModel @AssistedInject constructor(
.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
* push setting for the session was set to off
@ -197,56 +181,66 @@ class HomeActivityViewModel @AssistedInject constructor(
}
}
private fun maybeBootstrapCrossSigning() {
// In case of account creation, it is already done before
if (args.accountCreation) return
private fun maybeBootstrapCrossSigningAfterInitialSync() {
// We do not use the viewModel context because we do not want to tie this action to activity view model
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
session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback<MXUsersDevicesMap<CryptoDeviceInfo>> {
override fun onSuccess(data: MXUsersDevicesMap<CryptoDeviceInfo>) {
// Is there already cross signing keys here?
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
if (mxCrossSigningInfo != null) {
// Cross-signing is already set up for this user, is it trusted?
if (!mxCrossSigningInfo.isTrusted()) {
// New session
_viewEvents.post(
HomeActivityViewEvents.OnNewSession(
session.getUser(session.myUserId)?.toMatrixItem(),
// If it's an old unverified, we should send requests
// instead of waiting for an incoming one
reAuthHelper.data != null
)
)
}
} else {
// Initialize cross-signing
val password = reAuthHelper.data
if (password == null) {
// Check this is not an SSO account
if (session.getHomeServerCapabilities().canChangePassword) {
// Ask password to the user: Upgrade security
_viewEvents.post(HomeActivityViewEvents.AskPasswordToInitCrossSigning(session.getUser(session.myUserId)?.toMatrixItem()))
}
// Else (SSO) just ignore for the moment
} else {
// We do not use the viewModel context because we do not want to cancel this action
Timber.d("Initialize cross signing")
// From there we are up to date with server
// Is there already cross signing keys here?
val mxCrossSigningInfo = session.cryptoService().crossSigningService().getMyCrossSigningKeys()
if (mxCrossSigningInfo != null) {
// Cross-signing is already set up for this user, is it trusted?
if (!mxCrossSigningInfo.isTrusted()) {
// New session
_viewEvents.post(
HomeActivityViewEvents.OnNewSession(
session.getUser(session.myUserId)?.toMatrixItem(),
// If it's an old unverified, we should send requests
// instead of waiting for an incoming one
reAuthHelper.data != null
)
)
}
} else {
// Try to initialize cross signing in background if possible
Timber.d("Initialize cross signing...")
awaitCallback<Unit> {
try {
session.cryptoService().crossSigningService().initializeCrossSigning(
authParams = UserPasswordAuth(
session = null,
user = session.myUserId,
password = password
),
callback = NoOpMatrixCallback()
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
// We missed server grace period or it's not setup, see if we remember locally password
if (flowResponse.nextUncompletedStage() == LoginFlowTypes.PASSWORD
&& errCode == null
&& 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) {

View file

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

View file

@ -229,10 +229,13 @@ class DefaultNavigator @Inject constructor(
}
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 ->
if (session.cryptoService().crossSigningService().canCrossSign() && context is AppCompatActivity) {
BootstrapBottomSheet.show(context.supportFragmentManager, SetupMode.NORMAL)
if (session.cryptoService().crossSigningService().getMyCrossSigningKeys() == null
|| session.cryptoService().crossSigningService().canCrossSign()) {
(context as? AppCompatActivity)?.let {
BootstrapBottomSheet.show(it.supportFragmentManager, SetupMode.NORMAL)
}
} else {
context.startActivity(KeysBackupSetupActivity.intent(context, showManualExport))
}

View file

@ -311,10 +311,6 @@ class VectorSettingsSecurityPrivacyFragment @Inject constructor(
}
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 {

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
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
@ -23,16 +24,16 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
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.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.databinding.FragmentDeactivateAccountBinding
import im.vector.app.features.MainActivity
import im.vector.app.features.MainActivityArgs
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.settings.VectorSettingsActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject
@ -46,6 +47,25 @@ class DeactivateAccountFragment @Inject constructor(
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() {
super.onResume()
(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?) {
super.onViewCreated(view, savedInstanceState)
setupUi()
setupViewListeners()
observeViewEvents()
}
private fun setupUi() {
views.deactivateAccountPassword.textChanges()
.subscribe {
views.deactivateAccountPasswordTil.error = null
views.deactivateAccountSubmit.isEnabled = it.isNotEmpty()
}
.disposeOnDestroyView()
}
private fun setupViewListeners() {
views.deactivateAccountPasswordReveal.setOnClickListener {
viewModel.handle(DeactivateAccountAction.TogglePassword)
}
views.deactivateAccountSubmit.debouncedClicks {
viewModel.handle(DeactivateAccountAction.DeactivateAccount(
views.deactivateAccountPassword.text.toString(),
views.deactivateAccountEraseCheckbox.isChecked))
views.deactivateAccountEraseCheckbox.isChecked)
)
}
}
private fun observeViewEvents() {
viewModel.observeViewEvents {
when (it) {
is DeactivateAccountViewEvents.Loading -> {
is DeactivateAccountViewEvents.Loading -> {
settingsActivity?.ignoreInvalidTokenError = true
showLoadingDialog(it.message)
}
DeactivateAccountViewEvents.EmptyPassword -> {
DeactivateAccountViewEvents.InvalidAuth -> {
dismissLoadingDialog()
settingsActivity?.ignoreInvalidTokenError = false
views.deactivateAccountPasswordTil.error = getString(R.string.error_empty_field_your_password)
}
DeactivateAccountViewEvents.InvalidPassword -> {
settingsActivity?.ignoreInvalidTokenError = false
views.deactivateAccountPasswordTil.error = getString(R.string.settings_fail_to_update_password_invalid_current_password)
}
is DeactivateAccountViewEvents.OtherFailure -> {
is DeactivateAccountViewEvents.OtherFailure -> {
settingsActivity?.ignoreInvalidTokenError = false
dismissLoadingDialog()
displayErrorDialog(it.throwable)
}
DeactivateAccountViewEvents.Done ->
DeactivateAccountViewEvents.Done -> {
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
}
}
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
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
/**
* Transient events for deactivate account settings screen
*/
sealed class DeactivateAccountViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DeactivateAccountViewEvents()
object EmptyPassword : DeactivateAccountViewEvents()
object InvalidPassword : DeactivateAccountViewEvents()
object InvalidAuth : DeactivateAccountViewEvents()
data class OtherFailure(val throwable: Throwable) : 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.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
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 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 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(
val passwordShown: Boolean = false
) : 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,
private val session: Session)
: VectorViewModel<DeactivateAccountViewState, DeactivateAccountAction, DeactivateAccountViewEvents>(initialState) {
@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel
}
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
override fun handle(action: DeactivateAccountAction) {
when (action) {
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
DeactivateAccountAction.TogglePassword -> handleTogglePassword()
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
}
@ -63,20 +93,22 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v
}
private fun handleDeactivateAccount(action: DeactivateAccountAction.DeactivateAccount) {
if (action.password.isEmpty()) {
_viewEvents.post(DeactivateAccountViewEvents.EmptyPassword)
return
}
_viewEvents.post(DeactivateAccountViewEvents.Loading())
viewModelScope.launch {
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
} catch (failure: Exception) {
if (failure.isInvalidPassword()) {
DeactivateAccountViewEvents.InvalidPassword
if (failure.isInvalidUIAAuth()) {
DeactivateAccountViewEvents.InvalidAuth
} else {
DeactivateAccountViewEvents.OtherFailure(failure)
}

View file

@ -18,4 +18,9 @@ package im.vector.app.features.settings.crosssigning
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.core.resources.ColorProvider
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.genericItemWithValue
import im.vector.app.core.ui.list.genericPositiveButtonItem
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.core.utils.DimensionConverter
import me.gujun.android.span.span
import javax.inject.Inject
@ -31,7 +34,9 @@ class CrossSigningSettingsController @Inject constructor(
private val dimensionConverter: DimensionConverter
) : TypedEpoxyController<CrossSigningSettingsViewState>() {
interface InteractionListener
interface InteractionListener {
fun didTapInitializeCrossSigning()
}
var interactionListener: InteractionListener? = null
@ -44,6 +49,13 @@ class CrossSigningSettingsController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_trusted)
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 -> {
genericItem {
@ -51,6 +63,13 @@ class CrossSigningSettingsController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_custom)
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 -> {
genericItem {
@ -58,12 +77,27 @@ class CrossSigningSettingsController @Inject constructor(
titleIconResourceId(R.drawable.ic_shield_black)
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 -> {
genericItem {
id("not")
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
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
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.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import javax.inject.Inject
@ -47,19 +53,55 @@ class CrossSigningSettingsFragment @Inject constructor(
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?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
viewModel.observeViewEvents {
when (it) {
viewModel.observeViewEvents { event ->
when (event) {
is CrossSigningSettingsViewEvents.Failure -> {
AlertDialog.Builder(requireContext())
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(it.throwable))
.setMessage(errorFormatter.toHumanReadable(event.throwable))
.setPositiveButton(R.string.ok, null)
.show()
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
}
}
@ -83,4 +125,8 @@ class CrossSigningSettingsFragment @Inject constructor(
controller.interactionListener = null
super.onDestroyView()
}
override fun didTapInitializeCrossSigning() {
viewModel.handle(CrossSigningSettingsAction.InitializeCrossSigning)
}
}

View file

@ -17,10 +17,14 @@
package im.vector.app.features.settings.crosssigning
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
/**
* Transient events for cross signing settings screen
*/
sealed class CrossSigningSettingsViewEvents : VectorViewEvents {
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
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
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.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper
import io.reactivex.Observable
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.crypto.crosssigning.MXCrossSigningInfo
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.model.rest.DefaultBaseAuth
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 timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted private val initialState: CrossSigningSettingsViewState,
private val session: Session)
: VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
class CrossSigningSettingsViewModel @AssistedInject constructor(
@Assisted private val initialState: CrossSigningSettingsViewState,
private val session: Session,
private val reAuthHelper: ReAuthHelper,
private val stringProvider: StringProvider
) : VectorViewModel<CrossSigningSettingsViewState, CrossSigningSettingsAction, CrossSigningSettingsViewEvents>(initialState) {
init {
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
interface Factory {
fun create(initialState: CrossSigningSettingsViewState): CrossSigningSettingsViewModel
}
override fun handle(action: CrossSigningSettingsAction) {
// No op for the moment
// when (action) {
// }.exhaustive
when (action) {
CrossSigningSettingsAction.InitializeCrossSigning -> {
_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> {

View file

@ -22,7 +22,7 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
sealed class DevicesAction : VectorViewModelAction {
object Refresh : 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 PromptRename(val deviceId: String) : DevicesAction()
@ -30,4 +30,8 @@ sealed class DevicesAction : VectorViewModelAction {
data class VerifyMyDeviceManually(val deviceId: String) : DevicesAction()
object CompleteSecurity : 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 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.rest.DeviceInfo
@ -27,9 +28,12 @@ import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
*/
sealed class DevicesViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : DevicesViewEvents()
// object HideLoading : 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()

View file

@ -27,16 +27,21 @@ import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
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.resources.StringProvider
import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.login.ReAuthHelper
import io.reactivex.Observable
import io.reactivex.functions.BiFunction
import io.reactivex.subjects.PublishSubject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
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.failure.Failure
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.VerificationTransaction
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.fromBase64
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.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.rx
import timber.log.Timber
import java.util.concurrent.TimeUnit
import javax.net.ssl.HttpsURLConnection
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
data class DevicesViewState(
val myDeviceId: String = "",
@ -70,9 +84,14 @@ data class DeviceFullInfo(
class DevicesViewModel @AssistedInject constructor(
@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 {
var uiaContinuation: Continuation<UIABaseAuth>? = null
var pendingAuth: UIABaseAuth? = null
@AssistedFactory
interface Factory {
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()
init {
@ -189,13 +204,43 @@ class DevicesViewModel @AssistedInject constructor(
return when (action) {
is DevicesAction.Refresh -> queryRefreshDevicesList()
is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action)
is DevicesAction.PromptRename -> handlePromptRename(action)
is DevicesAction.VerifyMyDevice -> handleInteractiveVerification(action)
is DevicesAction.CompleteSecurity -> handleCompleteSecurity()
is DevicesAction.MarkAsManuallyVerified -> handleVerifyManually(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> {
override fun onFailure(failure: Throwable) {
var isPasswordRequestFound = false
if (failure is Failure.RegistrationFlowError) {
// 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) {
_currentDeviceId = deviceId
_currentSession = failure.registrationFlowResponse.session
setState {
copy(
request = Success(Unit)
)
viewModelScope.launch(Dispatchers.IO) {
try {
awaitCallback<Unit> {
session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
Timber.d("## UIA : deleteDevice 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 : deleteDevice UIA > start reauth activity")
_viewEvents.post(DevicesViewEvents.RequestReAuth(flowResponse, errCode))
pendingAuth = DefaultBaseAuth(session = flowResponse.session)
uiaContinuation = promise
}
}
_viewEvents.post(DevicesViewEvents.RequestPassword)
}
}, it)
}
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 {
copy(
request = Success(data)
request = Success(Unit)
)
}
// force settings update
queryRefreshDevicesList()
}
})
}
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
} catch (failure: Throwable) {
setState {
copy(
request = Fail(failure)
)
}
_viewEvents.post(DevicesViewEvents.Failure(failure))
if (failure is Failure.OtherServerError && failure.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED) {
_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
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -29,14 +30,16 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
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.configureWith
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.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity
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 javax.inject.Inject
@ -52,7 +55,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
// used to avoid requesting to enter the password for each deletion
// 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 {
return FragmentGenericRecyclerBinding.inflate(inflater, container, false)
@ -71,7 +74,7 @@ class VectorSettingsDevicesFragment @Inject constructor(
when (it) {
is DevicesViewEvents.Loading -> showLoading(it.message)
is DevicesViewEvents.Failure -> showFailure(it.throwable)
is DevicesViewEvents.RequestPassword -> maybeShowDeleteDeviceWithPasswordDialog()
is DevicesViewEvents.RequestReAuth -> askForReAuthentication(it)
is DevicesViewEvents.PromptRenameDevice -> displayDeviceRenameDialog(it.deviceInfo)
is DevicesViewEvents.ShowVerifyDevice -> {
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() {
devicesController.callback = null
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() {
viewModel.handle(DevicesAction.Refresh)
}
@ -154,17 +142,34 @@ class VectorSettingsDevicesFragment @Inject constructor(
.show()
}
/**
* Show a dialog to ask for user password, or use a previously entered password.
*/
private fun maybeShowDeleteDeviceWithPasswordDialog() {
if (mAccountPass.isNotEmpty()) {
viewModel.handle(DevicesAction.Password(mAccountPass))
} else {
PromptPasswordDialog().show(requireActivity()) { password ->
mAccountPass = password
viewModel.handle(DevicesAction.Password(mAccountPass))
private val reAuthActivityResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
when (activityResult.data?.extras?.getString(ReAuthActivity.RESULT_FLOW_TYPE)) {
LoginFlowTypes.SSO -> {
viewModel.handle(DevicesAction.SsoAuthDone)
}
LoginFlowTypes.PASSWORD -> {
val password = activityResult.data?.extras?.getString(ReAuthActivity.RESULT_VALUE) ?: ""
viewModel.handle(DevicesAction.PasswordAuthDone(password))
}
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 ContinueThreePid(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()
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
import android.app.Activity
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
@ -26,7 +27,6 @@ import androidx.appcompat.app.AppCompatActivity
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
import im.vector.app.core.dialogs.PromptPasswordDialog
import im.vector.app.core.dialogs.withColoredButton
import im.vector.app.core.extensions.cleanup
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.isEmail
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.VectorBaseFragment
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 javax.inject.Inject
@ -64,15 +66,42 @@ class ThreePidsSettingsFragment @Inject constructor(
viewModel.observeViewEvents {
when (it) {
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
ThreePidsSettingsViewEvents.RequestPassword -> askUserPassword()
is ThreePidsSettingsViewEvents.Failure -> displayErrorDialog(it.throwable)
is ThreePidsSettingsViewEvents.RequestReAuth -> askAuthentication(it)
}.exhaustive
}
}
private fun askUserPassword() {
PromptPasswordDialog().show(requireActivity()) { password ->
viewModel.handle(ThreePidsSettingsAction.AccountPassword(password))
// private fun askUserPassword() {
// PromptPasswordDialog().show(requireActivity()) { 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
import im.vector.app.core.platform.VectorViewEvents
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
sealed class ThreePidsSettingsViewEvents : VectorViewEvents {
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.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.error.SsoFlowNotSupportedYet
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ReadOnceTrue
import im.vector.app.features.auth.ReAuthActivity
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.LoginFlowTypes
import org.matrix.android.sdk.api.failure.Failure
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session
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 timber.log.Timber
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
class ThreePidsSettingsViewModel @AssistedInject constructor(
@Assisted initialState: ThreePidsSettingsViewState,
@ -48,36 +55,16 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
// UIA session
private var pendingThreePid: ThreePid? = null
private var pendingSession: String? = null
// private var pendingSession: String? = null
private val loadingCallback: MatrixCallback<Unit> = object : MatrixCallback<Unit> {
override fun onFailure(failure: Throwable) {
isLoading(false)
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))
}
_viewEvents.post(ThreePidsSettingsViewEvents.Failure(failure))
}
override fun onSuccess(data: Unit) {
pendingThreePid = null
pendingSession = null
isLoading(false)
}
}
@ -142,16 +129,50 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
override fun handle(action: ThreePidsSettingsAction) {
when (action) {
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
is ThreePidsSettingsAction.AddThreePid -> handleAddThreePid(action)
is ThreePidsSettingsAction.ContinueThreePid -> handleContinueThreePid(action)
is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action)
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
is ThreePidsSettingsAction.AccountPassword -> handleAccountPassword(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(action)
is ThreePidsSettingsAction.ChangeUiState -> handleChangeUiState(action)
is ThreePidsSettingsAction.SubmitCode -> handleSubmitCode(action)
is ThreePidsSettingsAction.CancelThreePid -> handleCancelThreePid(action)
is ThreePidsSettingsAction.DeleteThreePid -> handleDeleteThreePid(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
}
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) {
isLoading(true)
setState {
@ -168,7 +189,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
override fun onSuccess(data: Unit) {
// then finalize
pendingThreePid = action.threePid
session.finalizeAddingThreePid(action.threePid, null, null, loadingCallback)
session.finalizeAddingThreePid(action.threePid, uiaInterceptor, loadingCallback)
}
override fun onFailure(failure: Throwable) {
@ -232,7 +253,7 @@ class ThreePidsSettingsViewModel @AssistedInject constructor(
isLoading(true)
pendingThreePid = action.threePid
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) {
val safeSession = pendingSession ?: return Unit
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending session"))) }
val safeThreePid = pendingThreePid ?: return Unit
.also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
isLoading(true)
viewModelScope.launch {
session.finalizeAddingThreePid(safeThreePid, safeSession, action.password, loadingCallback)
}
}
// private fun handleAccountPassword(action: ThreePidsSettingsAction.AccountPassword) {
// val safeThreePid = pendingThreePid ?: return Unit
// .also { _viewEvents.post(ThreePidsSettingsViewEvents.Failure(IllegalStateException("No pending threePid"))) }
// isLoading(true)
// viewModelScope.launch {
// session.finalizeAddingThreePid(safeThreePid, uiaInterceptor, loadingCallback)
// }
// }
private fun handleDeleteThreePid(action: ThreePidsSettingsAction.DeleteThreePid) {
isLoading(true)

View file

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

View file

@ -0,0 +1,48 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M19.4782,26.0077l0,0l2.8609,-8.8002l-5.7177,0z"
android:strokeWidth="1"
android:fillColor="#E24329"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M12.6142,17.2076L11.743,19.8812C11.6642,20.124 11.7493,20.392 11.9574,20.5433L19.4782,26.0077L12.6142,17.2076Z"
android:strokeWidth="1"
android:fillColor="#FCA326"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M12.6142,17.2076L16.6214,17.2076L14.8968,11.9077C14.8086,11.6366 14.4239,11.6366 14.3325,11.9077L12.6142,17.2076Z"
android:strokeWidth="1"
android:fillColor="#E24329"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M26.3464,17.2076L27.2145,19.8812C27.2933,20.124 27.2082,20.392 27.0001,20.5433L19.4782,26.0077L26.3464,17.2076Z"
android:strokeWidth="1"
android:fillColor="#FCA326"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M26.3464,17.2076L22.3392,17.2076L24.0606,11.9077C24.1489,11.6366 24.5335,11.6366 24.625,11.9077L26.3464,17.2076Z"
android:strokeWidth="1"
android:fillColor="#E24329"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M19.4782,26.0077l2.8609,-8.8001l4.0073,0z"
android:strokeWidth="1"
android:fillColor="#FC6D26"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M19.4782,26.0077l-6.864,-8.8001l4.0072,0z"
android:strokeWidth="1"
android:fillColor="#FC6D26"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View file

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/bootstrapDescriptionText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?riotx_text_primary"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/waitingProgress"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/re_authentication_activity_title" />
<ProgressBar
android:id="@+id/waitingProgress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintBottom_toTopOf="@id/reAuthFailureText"
app:layout_constraintTop_toBottomOf="@+id/bootstrapDescriptionText" />
<TextView
android:id="@+id/reAuthFailureText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textColor="?colorError"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/buttonFlow"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintTop_toBottomOf="@id/waitingProgress"
tools:text="Authentication failed" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapCancelButton"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/cancel"
tools:ignore="MissingConstraints" />
<com.google.android.material.button.MaterialButton
android:id="@+id/bootstrapRetryButton"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/global_retry"
tools:ignore="MissingConstraints" />
<androidx.constraintlayout.helper.widget.Flow
android:id="@+id/buttonFlow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:constraint_referenced_ids="bootstrapCancelButton, bootstrapRetryButton"
app:layout_constraintTop_toBottomOf="@id/reAuthFailureText"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -31,75 +31,14 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" />
<TextView
android:id="@+id/deactivateAccountPromptPassword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/deactivate_account_prompt_password"
android:textColor="?riotx_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
<FrameLayout
android:id="@+id/deactivateAccountPasswordContainer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:hint="@string/auth_password_placeholder"
android:inputType="textPassword"
android:maxLines="1"
android:nextFocusDown="@+id/login_password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPromptPassword">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/deactivateAccountPasswordTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/deactivateAccountPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/deactivateAccountPasswordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye"
tools:contentDescription="@string/a11y_show_password"
app:tint="?attr/colorAccent"
tools:ignore="MissingPrefix" />
</FrameLayout>
<Button
android:id="@+id/deactivateAccountSubmit"
style="@style/VectorButtonStyleDestructive"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:text="@string/deactivate_account_submit"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountPasswordContainer" />
app:layout_constraintTop_toBottomOf="@+id/deactivateAccountEraseCheckbox" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -143,7 +143,6 @@
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp"
android:visibility="gone"
tools:visibility="visible">

View file

@ -88,7 +88,6 @@
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -0,0 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/reAuthConfirmText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:text="@string/re_authentication_default_confirm_text"
android:textColor="?riotx_text_primary"
android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@id/reAuthConfirmButton"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:layout_constraintVertical_chainStyle="packed" />
<FrameLayout
android:id="@+id/passwordContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintTop_toBottomOf="@id/reAuthConfirmText">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/passwordFieldTil"
style="@style/VectorTextInputLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/login_signup_password_hint"
app:errorEnabled="true"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/passwordField"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:imeOptions="actionDone"
android:inputType="textPassword"
android:maxLines="1"
android:paddingEnd="48dp"
tools:ignore="RtlSymmetry" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/passwordReveal"
android:layout_width="@dimen/layout_touch_size"
android:layout_height="@dimen/layout_touch_size"
android:layout_gravity="end"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackground"
android:scaleType="center"
android:src="@drawable/ic_eye"
app:tint="?attr/colorAccent"
tools:contentDescription="@string/a11y_show_password"
tools:ignore="MissingPrefix" />
</FrameLayout>
<!-- <TextView-->
<!-- android:id="@+id/loginPasswordNotice"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:gravity="start"-->
<!-- android:text="@string/login_signin_matrix_id_password_notice"-->
<!-- android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"-->
<!-- android:visibility="gone"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintStart_toStartOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@id/passwordContainer"-->
<!-- tools:visibility="visible" />-->
<TextView
android:id="@+id/genericErrorText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="start"
android:text="@string/authentication_error"
android:textAppearance="@style/TextAppearance.Vector.Login.Text.Small"
android:textColor="?colorError"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/passwordContainer"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/reAuthConfirmButton"
style="@style/VectorButtonStylePositive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="20dp"
android:text="@string/_continue"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/genericErrorText" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:padding="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemGenericItemButton"
style="@style/VectorButtonStylePositive"
android:textAllCaps="false"
app:iconGravity="textStart"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Action Name" />
</LinearLayout>

View file

@ -46,6 +46,7 @@
<attr name="vctr_social_login_button_facebook_style" format="reference" />
<attr name="vctr_social_login_button_twitter_style" format="reference" />
<attr name="vctr_social_login_button_apple_style" format="reference" />
<attr name="vctr_social_login_button_gitlab_style" format="reference" />
<attr name="vctr_chat_effect_snow_background" format="color" />
</declare-styleable>

View file

@ -333,6 +333,7 @@
<string name="login_error_ssl_handshake">Your device is using an outdated TLS security protocol, vulnerable to attack, for your security you will not be able to connect</string>
<string name="login_error_forbidden">Invalid username/password</string>
<string name="error_unauthorized">Unauthorized, missing valid authentication credentials</string>
<string name="login_error_unknown_token">The access token specified was not recognised</string>
<string name="login_error_bad_json">Malformed JSON</string>
<string name="login_error_not_json">Did not contain valid JSON</string>
@ -2623,6 +2624,7 @@
<string name="mark_as_verified">Mark as Trusted</string>
<string name="error_sso_flow_not_supported_yet">Sorry, this operation is not possible yet for accounts connected using Single Sign-On.</string>
<string name="failed_to_initialize_cross_signing">Failed to set up Cross Signing</string>
<string name="error_empty_field_choose_user_name">Please choose a username.</string>
<string name="error_empty_field_choose_password">Please choose a password.</string>
@ -2792,4 +2794,8 @@
<string name="warning_unsaved_change_discard">Discard changes</string>
<string name="matrix_to_card_title">Matrix Link</string>
<string name="re_authentication_activity_title">Re-Authentication Needed</string>
<string name="re_authentication_default_confirm_text">Element requires you to enter your credentials to perform this action.</string>
<string name="authentication_error">Failed to authenticate</string>
</resources>

View file

@ -1,17 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="WidgetButtonSocialLogin" parent="Widget.MaterialComponents.Button">
<style name="WidgetButtonSocialLogin" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="android:textAllCaps">false</item>
<item name="fontFamily">sans-serif-medium</item>
<item name="android:layout_width">wrap_content</item>
<item name="android:layout_height">wrap_content</item>
<item name="iconGravity">start</item>
<item name="android:textSize">14sp</item>
<item name="android:textAlignment">textStart</item>
<item name="android:textAlignment">center</item>
<item name="android:paddingStart">2dp</item>
<item name="android:paddingEnd">8dp</item>
<!-- Compensate icon size to center text correctly-->
<item name="android:paddingEnd">38dp</item>
<item name="android:clipToPadding">false</item>
<item name="iconSize">38dp</item>
<item name="strokeColor">@color/black_54</item>
</style>
<style name="WidgetButtonSocialLogin.Google">
@ -99,4 +102,21 @@
<item name="android:backgroundTint">@color/white</item>
</style>
<style name="WidgetButtonSocialLogin.Gitlab" parent="WidgetButtonSocialLogin">
<item name="icon">@drawable/ic_social_gitlab</item>
<item name="iconTint">@android:color/transparent</item>
<item name="iconTintMode">add</item>
</style>
<style name="WidgetButtonSocialLogin.Gitlab.Light">
<item name="android:textColor">@color/black</item>
<item name="android:backgroundTint">@color/white</item>
</style>
<style name="WidgetButtonSocialLogin.Gitlab.Dark">
<item name="android:textColor">@color/white</item>
<item name="android:backgroundTint">@color/black</item>
</style>
</resources>

View file

@ -200,6 +200,7 @@
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Dark</item>
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Dark</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Dark</item>
<item name="vctr_social_login_button_gitlab_style">@style/WidgetButtonSocialLogin.Gitlab.Dark</item>
<!-- chat effect -->
<item name="vctr_chat_effect_snow_background">@android:color/transparent</item>

View file

@ -202,6 +202,7 @@
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Light</item>
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Light</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Light</item>
<item name="vctr_social_login_button_gitlab_style">@style/WidgetButtonSocialLogin.Gitlab.Light</item>
<!-- chat effect -->
<item name="vctr_chat_effect_snow_background">@color/black_alpha</item>