diff --git a/CHANGES.md b/CHANGES.md index 505d4f1497..948c2aa8d4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Changes in Element 1.0.16 (2020-XX-XX) +Changes in Element 1.X.X (2020-XX-XX) =================================================== Features ✨: @@ -25,14 +25,22 @@ Test: Other changes: - -Changes in Element 1.0.15 (2020-XX-XX) +Changes in Element 1.0.16 (2020-02-04) +=================================================== + +Bugfix 🐛: + - Fix crash on API < 30 and light theme (#2774) + +Changes in Element 1.0.15 (2020-02-03) =================================================== Features ✨: - - + - 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. @@ -40,9 +48,9 @@ Bugfix 🐛: - UrlPreview should be updated when the url is edited and changed (#2678) - When receiving a new pepper from identity server, use it on the next hash lookup (#2708) - Crashes reported by PlayStore (new in 1.0.14) (#2707) - -Translations 🗣: - - + - Widgets: Support $matrix_widget_id parameter (#2748) + - Data for Worker overload (#2721) + - Fix multiple tasks SDK API changes ⚠️: - Increase targetSdkVersion to 30 (#2600) @@ -50,9 +58,6 @@ SDK API changes ⚠️: Build 🧱: - Compile with Android SDK 30 (Android 11) -Test: - - - Other changes: - Update Dagger to 2.31 version so we can use the embedded AssistedInject feature diff --git a/fastlane/metadata/android/en-US/changelogs/40100150.txt b/fastlane/metadata/android/en-US/changelogs/40100150.txt new file mode 100644 index 0000000000..c3e10cefbd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100150.txt @@ -0,0 +1,2 @@ +Main changes in this version: Social Login support. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.15 \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/40100160.txt b/fastlane/metadata/android/en-US/changelogs/40100160.txt new file mode 100644 index 0000000000..539ddbe63a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/40100160.txt @@ -0,0 +1,2 @@ +Main changes in this version: Social Login support. +Full changelog: https://github.com/vector-im/element-android/releases/tag/v1.0.15 and https://github.com/vector-im/element-android/releases/tag/v1.0.16 \ No newline at end of file diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt index 9996eef0a8..b0df6fcb44 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/account/DeactivateAccountTest.kt @@ -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) { + 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) diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index 3d5856fc64..eb7e4a9fbe 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -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 { session.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = session.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = session.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, it) } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt index 0e3b29118c..cf31294e2f 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/UnwedgingTest.kt @@ -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 { bobSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, it) } // Wait until we received back the key diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt index 38c57bd22a..44af87bcbe 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/crosssigning/XSigningTest.kt @@ -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 { aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } - mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + mTestHelper.doSync { + aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) + } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) } // Check that alice can see bob keys mTestHelper.doSync> { aliceSession.cryptoService().downloadKeys(listOf(bobSession.myUserId), true, it) } @@ -122,8 +144,16 @@ class XSigningTest : InstrumentedTest { password = TestConstants.PASSWORD ) - mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(aliceAuthParams, it) } - mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(bobAuthParams, it) } + mTestHelper.doSync { aliceSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(aliceAuthParams) + } + }, it) } + mTestHelper.doSync { bobSession.cryptoService().crossSigningService().initializeCrossSigning(object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume(bobAuthParams) + } + }, it) } // Check that alice can see bob keys val bobUserId = bobSession.myUserId diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt index 2c4d89b070..8c3917adc1 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/gossiping/KeyShareTests.kt @@ -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 { aliceSession1.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession1.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { aliceSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = aliceSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { bobSession.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = bobSession.myUserId, - password = TestConstants.PASSWORD - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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()?.body}") assert(dRes?.clearEvent == null) } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt index 1385dac1ec..397f7f9441 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/crypto/verification/qrcode/VerificationTest.kt @@ -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 { 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) { + promise.resume( + UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, callback) } mTestHelper.doSync { 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) { + promise.resume( + UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD, + session = flowResponse.session + ) + ) + } + }, callback) } val aliceVerificationService = aliceSession.cryptoService().verificationService() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/TokenBasedAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/TokenBasedAuth.kt new file mode 100644 index 0000000000..e522352c38 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/TokenBasedAuth.kt @@ -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 = mapOf( + "session" to session, + "token" to token, + "transactionId" to transactionId, + "type" to type + ) +} diff --git a/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt similarity index 55% rename from vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt index 7b22072c34..d5e323e457 100644 --- a/vector/src/main/java/im/vector/app/core/error/SsoFlowNotSupportedYet.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UIABaseAuth.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 New Vector Ltd + * Copyright 2020 The Matrix.org Foundation C.I.C. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,18 @@ * limitations under the License. */ -package im.vector.app.core.error +package org.matrix.android.sdk.api.auth -class SsoFlowNotSupportedYet : Throwable() +interface UIABaseAuth { + /** + * This is a session identifier that the client must pass back to the homeserver, + * if one is provided, in subsequent attempts to authenticate in the same API call. + */ + val session: String? + + fun hasAuthInfo(): Boolean + + fun copyWithSession(session: String): UIABaseAuth + + fun asMap() : Map +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt new file mode 100644 index 0000000000..16a5c8073d --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserInteractiveAuthInterceptor.kt @@ -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) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserPasswordAuth.kt similarity index 74% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserPasswordAuth.kt index ba8b34096c..e985c5f08a 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/UserPasswordAuth.kt @@ -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 = mapOf( + "session" to session, + "user" to user, + "password" to password, + "type" to type + ) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt index 6759c59237..cfaf74ce24 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/data/SsoIdentityProvider.kt @@ -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" } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt similarity index 93% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt index 3461a4d738..2b1c1c09b3 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/RegistrationFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/registration/RegistrationFlowResponse.kt @@ -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() } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt index 4711f7957d..c06cdd9e23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Extensions.kt @@ -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 } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt index de881b9e02..b241903364 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/Failure.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt index 895be0031a..3820a442aa 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/failure/MatrixError.kt @@ -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? = null, + @Json(name = "flows") + val flows: List? = null, + @Json(name = "params") + val params: JsonDict? = null ) { companion object { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt index dea8758140..039025e0df 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/Session.kt @@ -251,6 +251,8 @@ interface Session : val sharedSecretStorageService: SharedSecretStorageService + fun getUiaSsoFallbackUrl(authenticationSessionId: String): String + /** * Maintenance API, allows to print outs info on DB size to logcat */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt index 8915202f35..eb327dfd56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/account/AccountService.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt index 0eefca1b4c..fa5ea359e8 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/CryptoService.kt @@ -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) - fun deleteDevice(deviceId: String, callback: MatrixCallback) + fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) fun deleteDeviceWithUserPassword(deviceId: String, authSession: String?, password: String, callback: MatrixCallback) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt index 6a646cd4c7..359e33cc2c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/crypto/crosssigning/CrossSigningService.kt @@ -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) fun isCrossSigningInitialized(): Boolean = getMyCrossSigningKeys() != null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt index 1fd8360253..a4d5b665c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/profile/ProfileService.kt @@ -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): Cancelable /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt index 642279cc27..e0c52cf9ca 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt @@ -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" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt index 2b26115f30..d0d17e2cd5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/data/LoginFlowResponse.kt @@ -43,5 +43,6 @@ internal data class LoginFlow( * See MSC #2858 */ @Json(name = "org.matrix.msc2858.identity_providers") - val ssoIdentityProvider: List? + val ssoIdentityProvider: List? = null + ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt index 9c6b942a4f..163009d918 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/DefaultRegistrationWizard.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt new file mode 100644 index 0000000000..1a0383cb22 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/registration/UIAExt.kt @@ -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 { 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) + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt index ebd809f777..678bc9819f 100755 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/DefaultCryptoService.kt @@ -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) { + override fun deleteDevice(deviceId: String, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor, callback: MatrixCallback) { deleteDeviceTask - .configureWith(DeleteDeviceTask.Params(deviceId)) { + .configureWith(DeleteDeviceTask.Params(deviceId, userInteractiveAuthInterceptor, null)) { this.executionThread = TaskThread.CRYPTO this.callback = callback } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt index bcad448eb6..9b282f0a84 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -19,30 +19,30 @@ package org.matrix.android.sdk.internal.crypto.crosssigning import androidx.lifecycle.LiveData import androidx.work.BackoffPolicy 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 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 import org.matrix.android.sdk.internal.crypto.tasks.UploadSignaturesTask +import org.matrix.android.sdk.internal.di.SessionId import org.matrix.android.sdk.internal.di.UserId +import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.session.SessionScope import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.TaskThread import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.JsonCanonicalizer import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo -import org.matrix.android.sdk.internal.di.SessionId -import org.matrix.android.sdk.internal.di.WorkManagerProvider import org.matrix.android.sdk.internal.worker.WorkerParamsFactory import org.matrix.olm.OlmPkSigning import org.matrix.olm.OlmUtility @@ -61,7 +61,10 @@ internal class DefaultCrossSigningService @Inject constructor( private val taskExecutor: TaskExecutor, private val coroutineDispatchers: MatrixCoroutineDispatchers, private val cryptoCoroutineScope: CoroutineScope, - private val workManagerProvider: WorkManagerProvider) : CrossSigningService, DeviceListManager.UserDevicesUpdateListener { + private val workManagerProvider: WorkManagerProvider, + private val updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository +) : CrossSigningService, + DeviceListManager.UserDevicesUpdateListener { private var olmUtility: OlmUtility? = null @@ -147,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) { + override fun initializeCrossSigning(uiaInterceptor: UserInteractiveAuthInterceptor?, callback: MatrixCallback) { Timber.d("## CrossSigning initializeCrossSigning") val params = InitializeCrossSigningTask.Params( - authParams = authParams + interactiveAuthInterceptor = uiaInterceptor ) initializeCrossSigningTask.configureWith(params) { this.callbackThread = TaskThread.CRYPTO @@ -689,7 +692,7 @@ internal class DefaultCrossSigningService @Inject constructor( return DeviceTrustResult.Success(DeviceTrustLevel(crossSigningVerified = true, locallyVerified = locallyTrusted)) } - fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo) : DeviceTrustResult { + fun checkDeviceTrust(myKeys: MXCrossSigningInfo?, otherKeys: MXCrossSigningInfo?, otherDevice: CryptoDeviceInfo): DeviceTrustResult { val locallyTrusted = otherDevice.trustLevel?.isLocallyVerified() myKeys ?: return legacyFallbackTrust(locallyTrusted, DeviceTrustResult.CrossSigningNotConfigured(userId)) @@ -747,8 +750,11 @@ internal class DefaultCrossSigningService @Inject constructor( } override fun onUsersDeviceUpdate(userIds: List) { - Timber.d("## CrossSigning - onUsersDeviceUpdate for $userIds") - val workerParams = UpdateTrustWorker.Params(sessionId = sessionId, updatedUserIds = userIds) + Timber.d("## CrossSigning - onUsersDeviceUpdate for ${userIds.size} users: $userIds") + val workerParams = UpdateTrustWorker.Params( + sessionId = sessionId, + filename = updateTrustWorkerDataRepository.createParam(userIds) + ) val workerData = WorkerParamsFactory.toData(workerParams) val workRequest = workManagerProvider.matrixOneTimeWorkRequestBuilder() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt index 665d770e7f..1660bae0b7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorker.kt @@ -55,7 +55,11 @@ internal class UpdateTrustWorker(context: Context, internal data class Params( override val sessionId: String, override val lastFailureMessage: String? = null, - val updatedUserIds: List + // Kept for compatibility, but not used anymore (can be used for pending Worker) + val updatedUserIds: List? = null, + // Passing a long list of userId can break the Work Manager due to data size limitation. + // so now we use a temporary file to store the data + val filename: String? = null ) : SessionWorkerParams @Inject lateinit var crossSigningService: DefaultCrossSigningService @@ -64,6 +68,7 @@ internal class UpdateTrustWorker(context: Context, @CryptoDatabase @Inject lateinit var realmConfiguration: RealmConfiguration @UserId @Inject lateinit var myUserId: String @Inject lateinit var crossSigningKeysMapper: CrossSigningKeysMapper + @Inject lateinit var updateTrustWorkerDataRepository: UpdateTrustWorkerDataRepository @SessionDatabase @Inject lateinit var sessionRealmConfiguration: RealmConfiguration // @Inject lateinit var roomSummaryUpdater: RoomSummaryUpdater @@ -74,7 +79,17 @@ internal class UpdateTrustWorker(context: Context, } override suspend fun doSafeWork(params: Params): Result { - var userList = params.updatedUserIds + var userList = params.filename + ?.let { updateTrustWorkerDataRepository.getParam(it) } + ?.userIds + ?: params.updatedUserIds.orEmpty() + + if (userList.isEmpty()) { + // This should not happen, but let's avoid go further in case of empty list + cleanup(params) + return Result.success() + } + // Unfortunately we don't have much info on what did exactly changed (is it the cross signing keys of that user, // or a new device?) So we check all again :/ @@ -213,9 +228,15 @@ internal class UpdateTrustWorker(context: Context, } } + cleanup(params) return Result.success() } + private fun cleanup(params: Params) { + params.filename + ?.let { updateTrustWorkerDataRepository.delete(it) } + } + private fun updateCrossSigningKeysTrust(realm: Realm, userId: String, verified: Boolean) { val xInfoEntity = realm.where(CrossSigningInfoEntity::class.java) .equalTo(CrossSigningInfoEntityFields.USER_ID, userId) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt new file mode 100644 index 0000000000..0878a9f765 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/crosssigning/UpdateTrustWorkerDataRepository.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2021 The Matrix.org Foundation C.I.C. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.matrix.android.sdk.internal.crypto.crosssigning + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import org.matrix.android.sdk.api.extensions.tryOrNull +import org.matrix.android.sdk.internal.di.MoshiProvider +import org.matrix.android.sdk.internal.di.SessionFilesDirectory +import java.io.File +import java.util.UUID +import javax.inject.Inject + +@JsonClass(generateAdapter = true) +internal data class UpdateTrustWorkerData( + @Json(name = "userIds") + val userIds: List +) + +internal class UpdateTrustWorkerDataRepository @Inject constructor( + @SessionFilesDirectory parentDir: File +) { + private val workingDirectory = File(parentDir, "tw") + private val jsonAdapter = MoshiProvider.providesMoshi().adapter(UpdateTrustWorkerData::class.java) + + // Return the path of the created file + fun createParam(userIds: List): String { + val filename = "${UUID.randomUUID()}.json" + workingDirectory.mkdirs() + val file = File(workingDirectory, filename) + + UpdateTrustWorkerData(userIds = userIds) + .let { jsonAdapter.toJson(it) } + .let { file.writeText(it) } + + return filename + } + + fun getParam(filename: String): UpdateTrustWorkerData? { + return File(workingDirectory, filename) + .takeIf { it.exists() } + ?.readText() + ?.let { jsonAdapter.fromJson(it) } + } + + fun delete(filename: String) { + tryOrNull("Unable to delete $filename") { + File(workingDirectory, filename).delete() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt new file mode 100644 index 0000000000..bbb4a3a654 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DefaultBaseAuth.kt @@ -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 = mapOf("session" to session) +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt index 0ce6f1f41c..f636ab890d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/DeleteDeviceParams.kt @@ -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? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt index 3418bb327d..d24b7ae5f0 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/model/rest/UploadSigningKeysBody.kt @@ -30,5 +30,5 @@ internal data class UploadSigningKeysBody( val userSigningKey: RestKeyInfo? = null, @Json(name = "auth") - val auth: UserPasswordAuth? = null + val auth: Map? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt index 8f1569a037..ff25ac0f66 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceTask.kt @@ -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 { 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(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 + } } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt index b4c1e6d27c..dc0077425e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/DeleteDeviceWithUserPasswordTask.kt @@ -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() ) ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt index 6c0a76fa7d..ef31130f55 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/InitializeCrossSigningTask.kt @@ -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 { 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() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt index cceff355bb..14fad2ea38 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/UploadSigningKeysTask.kt @@ -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? = 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 ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt index 9fb1cbb7d7..d67b21567e 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DeactivateAccountTask.kt @@ -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 { 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(globalErrorReceiver) { - apiCall = accountAPI.deactivate(deactivateAccountParams) + try { + executeRequest(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") } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt index 1165d2116b..25b67159a9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/account/DefaultAccountService.kt @@ -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)) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt index 6cf65b867c..1d6cd61060 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/ImageCompressor.kt @@ -28,9 +28,8 @@ import java.io.File import java.util.UUID import javax.inject.Inject -internal class ImageCompressor @Inject constructor() { +internal class ImageCompressor @Inject constructor(private val context: Context) { suspend fun compress( - context: Context, imageFile: File, desiredWidth: Int, desiredHeight: Int, @@ -46,7 +45,7 @@ internal class ImageCompressor @Inject constructor() { } } ?: return@withContext imageFile - val destinationFile = createDestinationFile(context) + val destinationFile = createDestinationFile() runCatching { destinationFile.outputStream().use { @@ -118,7 +117,7 @@ internal class ImageCompressor @Inject constructor() { } } - private fun createDestinationFile(context: Context): File { + private fun createDestinationFile(): File { return File.createTempFile(UUID.randomUUID().toString(), null, context.cacheDir) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt index 672d407d25..3b727690bf 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/content/UploadContentWorker.kt @@ -156,7 +156,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter // Do not compress gif && attachment.mimeType != MimeTypes.Gif && params.compressBeforeSending) { - fileToUpload = imageCompressor.compress(context, workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) + fileToUpload = imageCompressor.compress(workingFile, MAX_IMAGE_SIZE, MAX_IMAGE_SIZE) .also { compressedFile -> // Get new Bitmap size compressedFile.inputStream().use { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt index 500d43408e..b3216d744d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/DefaultProfileService.kt @@ -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): 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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt index 4e46dd096d..6301929545 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddThreePidBody.kt @@ -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? = null ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt index 1e3a2cb501..916a602936 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/profile/FinalizeAddingThreePidTask.kt @@ -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 { 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 + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt index fb840b4eb3..5e823fc87f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomBodyBuilder.kt @@ -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) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt index 8c71604183..fff780fb0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt @@ -140,14 +140,13 @@ internal class RoomSummaryUpdater @Inject constructor( .queryActiveRoomMembersEvent() .notEqualTo(RoomMemberSummaryEntityFields.USER_ID, userId) .findAll() - .asSequence() .map { it.userId } roomSummaryEntity.otherMemberIds.clear() roomSummaryEntity.otherMemberIds.addAll(otherRoomMembers) if (roomSummaryEntity.isEncrypted) { // mmm maybe we could only refresh shield instead of checking trust also? - crossSigningService.onUsersDeviceUpdate(roomSummaryEntity.otherMemberIds.toList()) + crossSigningService.onUsersDeviceUpdate(otherRoomMembers) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt index c41f1df0de..000b9e38b9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/widgets/helper/WidgetFactory.kt @@ -53,7 +53,7 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use } } val isAddedByMe = widgetEvent.senderId == userId - val computedUrl = widgetContent.computeURL(widgetEvent.roomId) + val computedUrl = widgetContent.computeURL(widgetEvent.roomId, widgetId) return Widget( widgetContent = widgetContent, event = widgetEvent, @@ -65,13 +65,14 @@ internal class WidgetFactory @Inject constructor(private val userDataSource: Use ) } - private fun WidgetContent.computeURL(roomId: String?): String? { + private fun WidgetContent.computeURL(roomId: String?, widgetId: String): String? { var computedUrl = url ?: return null val myUser = userDataSource.getUser(userId) computedUrl = computedUrl .replace("\$matrix_user_id", userId) .replace("\$matrix_display_name", myUser?.displayName ?: userId) .replace("\$matrix_avatar_url", myUser?.avatarUrl ?: "") + .replace("\$matrix_widget_id", widgetId) if (roomId != null) { computedUrl = computedUrl.replace("\$matrix_room_id", roomId) diff --git a/vector/build.gradle b/vector/build.gradle index 1893151d2f..c06d858379 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -13,7 +13,7 @@ kapt { // Note: 2 digits max for each value ext.versionMajor = 1 ext.versionMinor = 0 -ext.versionPatch = 15 +ext.versionPatch = 17 static def getGitTimestamp() { def cmd = 'git show -s --format=%ct' diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt index d9005e4a63..571bcf474c 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionInteractiveTest.kt @@ -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 { existingSession!!.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = existingSession!!.myUserId, - password = "password" - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + promise.resume( + UserPasswordAuth( + user = existingSession!!.myUserId, + password = "password", + session = flowResponse.session + ) + ) + } + }, it) } } diff --git a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt index 8a21260ac7..c51ff29669 100644 --- a/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt +++ b/vector/src/androidTest/java/im/vector/app/VerifySessionPassphraseTest.kt @@ -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 { existingSession!!.cryptoService().crossSigningService() - .initializeCrossSigning(UserPasswordAuth( - user = existingSession!!.myUserId, - password = "password" - ), it) + .initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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) { + promise.resume( + UserPasswordAuth( + user = existingSession!!.myUserId, + password = password, + session = flowResponse.session + ) + ) + } + }, passphrase = passphrase, setupMode = SetupMode.NORMAL )) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index ece9bab30d..c14eb1045c 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -63,7 +63,6 @@ @@ -244,6 +243,27 @@ + + + + + + + + + + + + + + + + + // 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) is DialPadLookup.Failure -> stringProvider.getString(R.string.call_dial_pad_lookup_error) else -> throwable.localizedMessage diff --git a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt index 6f41a6a846..0b951fb5a2 100644 --- a/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt +++ b/vector/src/main/java/im/vector/app/core/platform/VectorBaseFragment.kt @@ -200,6 +200,7 @@ abstract class VectorBaseFragment : 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)) diff --git a/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt b/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt new file mode 100644 index 0000000000..d18adde4ba --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/ui/list/GenericPositiveButtonItem.kt @@ -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() { + + @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(R.id.itemGenericItemButton) + } +} diff --git a/vector/src/main/java/im/vector/app/features/MainActivity.kt b/vector/src/main/java/im/vector/app/features/MainActivity.kt index f8b8229e70..351163b026 100644 --- a/vector/src/main/java/im/vector/app/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/app/features/MainActivity.kt @@ -22,15 +22,13 @@ import android.os.Bundle import android.os.Parcelable import androidx.appcompat.app.AlertDialog import androidx.lifecycle.Lifecycle -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.viewModel +import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.extensions.startSyncing -import im.vector.app.core.platform.EmptyViewModel import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.deleteAllFiles import im.vector.app.databinding.FragmentLoadingBinding @@ -83,8 +81,6 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } - private val emptyViewModel: EmptyViewModel by viewModel() - override fun getBinding() = FragmentLoadingBinding.inflate(layoutInflater) private lateinit var args: MainActivityArgs @@ -150,7 +146,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } when { args.isAccountDeactivated -> { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { // Just do the local cleanup Timber.w("Account deactivated, start app") sessionHolder.clearActiveSession() @@ -159,7 +155,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } args.clearCredentials -> { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { try { session.signOut(!args.isUserLoggedOut) Timber.w("SIGN_OUT: success, start app") @@ -172,7 +168,7 @@ class MainActivity : VectorBaseActivity(), UnlockedActiv } } args.clearCache -> { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { try { session.clearCache() doLocalCleanup(clearPreferences = false) diff --git a/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt new file mode 100644 index 0000000000..917f60dacb --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/PromptFragment.kt @@ -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() { + + 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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt similarity index 59% rename from vector/src/main/java/im/vector/app/core/platform/EmptyState.kt rename to vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt index c58532c6e0..036afda405 100644 --- a/vector/src/main/java/im/vector/app/core/platform/EmptyState.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActions.kt @@ -14,10 +14,14 @@ * limitations under the License. */ -package im.vector.app.core.platform +package im.vector.app.features.auth -import com.airbnb.mvrx.MvRxState +import im.vector.app.core.platform.VectorViewModelAction -data class EmptyState( - val dummy: Int = 0 -) : MvRxState +sealed class ReAuthActions : VectorViewModelAction { + object StartSSOFallback : ReAuthActions() + object FallBackPageLoaded : ReAuthActions() + object FallBackPageClosed : ReAuthActions() + object TogglePassVisibility : ReAuthActions() + data class ReAuthWithPass(val password: String) : ReAuthActions() +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt new file mode 100644 index 0000000000..0385973386 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthActivity.kt @@ -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(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)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt similarity index 65% rename from vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt rename to vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt index 420c58d44a..8cf9be6fb1 100644 --- a/vector/src/main/java/im/vector/app/core/platform/EmptyViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthEvents.kt @@ -14,13 +14,12 @@ * limitations under the License. */ -package im.vector.app.core.platform +package im.vector.app.features.auth -/** - * Mainly used to get a viewModelScope - */ -class EmptyViewModel(initialState: EmptyState) : VectorViewModel(initialState) { - override fun handle(action: EmptyAction) { - // N/A - } +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() } diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt new file mode 100644 index 0000000000..540a08405c --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthState.kt @@ -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) +} diff --git a/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt new file mode 100644 index 0000000000..4204da0d24 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/auth/ReAuthViewModel.kt @@ -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(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: ReAuthState): ReAuthViewModel + } + + companion object : MvRxViewModelFactory { + + 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)) + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt deleted file mode 100644 index feea484f06..0000000000 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapAccountPasswordFragment.kt +++ /dev/null @@ -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() { - - 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) - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt index 0785290d2a..ce06fe726f 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapActions.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt index 149bd629e1..5cc86fdf15 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapBottomSheet.kt @@ -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 + 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 { 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 { + 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 diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt index 47e373ed0a..d1a1237463 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapCrossSigningTask.kt @@ -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 { - 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) } diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt new file mode 100644 index 0000000000..507050c2e8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapReAuthFragment.kt @@ -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() { + + 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 + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt index 3a6f57198e..42278cd948 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapSharedViewModel.kt @@ -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? = 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(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) { + 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 -> { diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt index 222a5d78c6..09f0e90d5d 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapStep.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt index 10a092ccbb..3f06623ad6 100644 --- a/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/crypto/recover/BootstrapViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt index 45fce13ab9..afaa290190 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivityViewModel.kt @@ -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> { + session.cryptoService().downloadKeys(listOf(session.myUserId), true, it) + } + } - // Ensure keys of the user are downloaded - session.cryptoService().downloadKeys(listOf(session.myUserId), true, object : MatrixCallback> { - override fun onSuccess(data: MXUsersDevicesMap) { - // 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 { + 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) { + // 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) { diff --git a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt index 7183f27980..6c0e142b38 100644 --- a/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt +++ b/vector/src/main/java/im/vector/app/features/link/LinkHandlerActivity.kt @@ -19,13 +19,11 @@ package im.vector.app.features.link import android.content.Intent import android.net.Uri import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.viewModelScope -import com.airbnb.mvrx.viewModel +import androidx.lifecycle.lifecycleScope import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ScreenComponent import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.platform.EmptyViewModel import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityProgressBinding @@ -48,8 +46,6 @@ class LinkHandlerActivity : VectorBaseActivity() { @Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var permalinkHandler: PermalinkHandler - private val emptyViewModel: EmptyViewModel by viewModel() - override fun injectWith(injector: ScreenComponent) { injector.inject(this) } @@ -155,7 +151,7 @@ class LinkHandlerActivity : VectorBaseActivity() { // Should not happen startLoginActivity(uri) } else { - emptyViewModel.viewModelScope.launch { + lifecycleScope.launch { try { session.signOut(true) Timber.d("## displayAlreadyLoginPopup(): logout succeeded") diff --git a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt index 9290479a7a..4dc688ad22 100644 --- a/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt +++ b/vector/src/main/java/im/vector/app/features/login/SocialLoginButtonsView.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 991f776a45..9728bbf130 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -230,10 +230,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)) } diff --git a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt index a29b81b3d0..6952d5f653 100644 --- a/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt +++ b/vector/src/main/java/im/vector/app/features/popup/PopupAlertManager.kt @@ -15,11 +15,11 @@ */ package im.vector.app.features.popup -import android.annotation.SuppressLint import android.app.Activity import android.os.Build import android.os.Handler import android.os.Looper +import android.view.View import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS import com.tapadoo.alerter.Alerter import im.vector.app.R @@ -171,28 +171,38 @@ class PopupAlertManager @Inject constructor() { } } - @SuppressLint("InlinedApi") private fun clearLightStatusBar() { - weakCurrentActivity?.get() - ?.takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } - // Do not change anything on Dark themes - ?.takeIf { ThemeUtils.isLightTheme(it) } - ?.let { it.window?.decorView } - ?.let { view -> - view.windowInsetsController?.setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + weakCurrentActivity?.get() + // Do not change anything on Dark themes + ?.takeIf { ThemeUtils.isLightTheme(it) } + ?.window?.decorView + ?.let { view -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + view.windowInsetsController?.setSystemBarsAppearance(0, APPEARANCE_LIGHT_STATUS_BARS) + } else { + @Suppress("DEPRECATION") + view.systemUiVisibility = view.systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() + } + } + } } - @SuppressLint("InlinedApi") private fun setLightStatusBar() { - weakCurrentActivity?.get() - ?.takeIf { Build.VERSION.SDK_INT >= Build.VERSION_CODES.M } - // Do not change anything on Dark themes - ?.takeIf { ThemeUtils.isLightTheme(it) } - ?.let { it.window?.decorView } - ?.let { view -> - view.windowInsetsController?.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS) - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + weakCurrentActivity?.get() + // Do not change anything on Dark themes + ?.takeIf { ThemeUtils.isLightTheme(it) } + ?.window?.decorView + ?.let { view -> + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + view.windowInsetsController?.setSystemBarsAppearance(APPEARANCE_LIGHT_STATUS_BARS, APPEARANCE_LIGHT_STATUS_BARS) + } else { + @Suppress("DEPRECATION") + view.systemUiVisibility = view.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + } + } } private fun showAlert(alert: VectorAlert, activity: Activity, animate: Boolean = true) { diff --git a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt index e29c197ab8..a692eebe40 100644 --- a/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt +++ b/vector/src/main/java/im/vector/app/features/roommemberprofile/RoomMemberProfileController.kt @@ -80,8 +80,6 @@ class RoomMemberProfileController @Inject constructor( action = { callback?.onIgnoreClicked() } ) if (!state.isMine) { - buildProfileSection(stringProvider.getString(R.string.room_profile_section_more)) - buildProfileAction( id = "direct", editable = false, diff --git a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt index 727a6f765e..c12df073ee 100644 --- a/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorSettingsSecurityPrivacyFragment.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt new file mode 100644 index 0000000000..c3fa844805 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt index 3d128eb755..2cc80bfa23 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountFragment.kt @@ -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) - } } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt index 46acb4aee4..1b0ec2de0c 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt index 6a7084fb81..49cb75c9d6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/account/deactivation/DeactivateAccountViewModel.kt @@ -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(initialState) { @@ -49,10 +52,37 @@ class DeactivateAccountViewModel @AssistedInject constructor(@Assisted private v fun create(initialState: DeactivateAccountViewState): DeactivateAccountViewModel } + var uiaContinuation: Continuation? = 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(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) { + _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) } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt index af6ca9f4b7..735c456ff9 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsAction.kt @@ -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() +} diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt index 82279a3906..6425256929 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsController.kt @@ -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() { - 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() + })) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt index 63611efae5..80e44174ff 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsFragment.kt @@ -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) + } } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt index b81a321f3f..1c11560d40 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt index fdf5d611fa..04100056aa 100644 --- a/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/crosssigning/CrossSigningSettingsViewModel.kt @@ -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(initialState) { +class CrossSigningSettingsViewModel @AssistedInject constructor( + @Assisted private val initialState: CrossSigningSettingsViewState, + private val session: Session, + private val reAuthHelper: ReAuthHelper, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { init { Observable.combineLatest, Optional, Pair, Optional>>( @@ -58,15 +81,82 @@ class CrossSigningSettingsViewModel @AssistedInject constructor(@Assisted privat } } + var uiaContinuation: Continuation? = 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 { + session.cryptoService().crossSigningService().initializeCrossSigning( + object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, + errCode: String?, + promise: Continuation) { + 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(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 { diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt index 2b0991ab4e..46a476c270 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt index 60d7491603..8535c698a7 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewEvents.kt @@ -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() diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt index eb034530ef..b91b5255b6 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/DevicesViewModel.kt @@ -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(initialState), VerificationService.Listener { + var uiaContinuation: Continuation? = 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 = 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(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 { - 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 { + session.cryptoService().deleteDevice(deviceId, object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + 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 { - 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") } - }) + } } } diff --git a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt index 1bf538d458..0040f73fec 100644 --- a/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/devices/VectorSettingsDevicesFragment.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt index 0be3c6a198..d223009e69 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsAction.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt index d6da04affc..0a7489e2cc 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsFragment.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt index 1ac2d10458..0346fd137e 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt index 65cfadb6a9..a1d4d6227b 100644 --- a/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/settings/threepids/ThreePidsSettingsViewModel.kt @@ -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 = object : MatrixCallback { 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(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? = null + var pendingAuth: UIABaseAuth? = null + + private val uiaInterceptor = object : UserInteractiveAuthInterceptor { + override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation) { + _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) diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt index a73facc009..1c3ad7563c 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/ServerBackupStatusViewModel.kt @@ -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()) diff --git a/vector/src/main/res/drawable/ic_social_gitlab.xml b/vector/src/main/res/drawable/ic_social_gitlab.xml new file mode 100644 index 0000000000..9399f6448a --- /dev/null +++ b/vector/src/main/res/drawable/ic_social_gitlab.xml @@ -0,0 +1,48 @@ + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_bootstrap_reauth.xml b/vector/src/main/res/layout/fragment_bootstrap_reauth.xml new file mode 100644 index 0000000000..1bc6725c64 --- /dev/null +++ b/vector/src/main/res/layout/fragment_bootstrap_reauth.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/fragment_deactivate_account.xml b/vector/src/main/res/layout/fragment_deactivate_account.xml index db85c607e1..4bbf0a496c 100644 --- a/vector/src/main/res/layout/fragment_deactivate_account.xml +++ b/vector/src/main/res/layout/fragment_deactivate_account.xml @@ -31,75 +31,14 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/deactivateAccountContent" /> - - - - - - - - - - - - - - -