diff --git a/CHANGES.md b/CHANGES.md index e7f920d788..19c7b7e618 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ Changes in RiotX 0.19.0 (2020-XX-XX) =================================================== Features ✨: + - Change password (#528) - Cross-Signing | Support SSSS secret sharing (#944) - Cross-Signing | Verify new session from existing session (#1134) - Cross-Signing | Bootstraping cross signing with 4S from mobile (#985) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt index 140d1c259f..04e53e79f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/AuthenticationService.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig import im.vector.matrix.android.api.auth.data.LoginFlowResult import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.login.LoginWizard +import im.vector.matrix.android.api.auth.password.PasswordWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.util.Cancelable @@ -89,4 +90,9 @@ interface AuthenticationService { fun createSessionFromSso(homeServerConnectionConfig: HomeServerConnectionConfig, credentials: Credentials, callback: MatrixCallback): Cancelable + + /** + * Return a PasswordWizard, to update password. + */ + fun getPasswordWizard(): PasswordWizard } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt new file mode 100644 index 0000000000..cd41d79b0a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/auth/password/PasswordWizard.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.api.auth.password + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +interface PasswordWizard { + + /** + * Ask the homeserver to update the password with the provided new password. + */ + fun updatePassword(sessionId: String, userId: String, oldPassword: String, newPassword: String, callback: MatrixCallback): Cancelable +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt index 5dfb0eab9b..260b3bb8a4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/failure/Extensions.kt @@ -31,3 +31,8 @@ fun Throwable.shouldBeRetried(): Boolean { return this is Failure.NetworkConnection || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED) } + +fun Throwable.isInvalidPassword(): Boolean { + return this is Failure.ServerError + && error.message == "Invalid password" +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt index 2f03c99421..f4258acf54 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/AuthAPI.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.PasswordLoginParams import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.login.ResetPasswordMailConfirmed +import im.vector.matrix.android.internal.auth.data.UpdatePasswordParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationParams import im.vector.matrix.android.internal.auth.registration.AddThreePidRegistrationResponse import im.vector.matrix.android.internal.auth.registration.RegistrationParams @@ -102,4 +103,10 @@ internal interface AuthAPI { */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") fun resetPasswordMailConfirmed(@Body params: ResetPasswordMailConfirmed): Call + + /** + * Ask the homeserver to update the password with the provided new password. + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "account/password") + fun updatePassword(@Body params: UpdatePasswordParams): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt index 85c2cdbf3d..1e869dd1d8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/DefaultAuthenticationService.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.auth.data.Versions import im.vector.matrix.android.api.auth.data.isLoginAndRegistrationSupportedBySdk import im.vector.matrix.android.api.auth.data.isSupportedBySdk import im.vector.matrix.android.api.auth.login.LoginWizard +import im.vector.matrix.android.api.auth.password.PasswordWizard import im.vector.matrix.android.api.auth.registration.RegistrationWizard import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.Session @@ -37,6 +38,7 @@ import im.vector.matrix.android.internal.auth.data.LoginFlowResponse import im.vector.matrix.android.internal.auth.data.RiotConfig import im.vector.matrix.android.internal.auth.db.PendingSessionData import im.vector.matrix.android.internal.auth.login.DefaultLoginWizard +import im.vector.matrix.android.internal.auth.password.DefaultPasswordWizard import im.vector.matrix.android.internal.auth.registration.DefaultRegistrationWizard import im.vector.matrix.android.internal.di.Unauthenticated import im.vector.matrix.android.internal.network.RetrofitFactory @@ -66,6 +68,7 @@ internal class DefaultAuthenticationService @Inject constructor( private var currentLoginWizard: LoginWizard? = null private var currentRegistrationWizard: RegistrationWizard? = null + private var currentPasswordWizard: PasswordWizard? = null override fun hasAuthenticatedSessions(): Boolean { return sessionParamsStore.getLast() != null @@ -221,6 +224,22 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getPasswordWizard(): PasswordWizard { + return currentPasswordWizard + ?: let { + sessionParamsStore.getLast()?.homeServerConnectionConfig?.let { + DefaultPasswordWizard( + okHttpClient, + retrofitFactory, + coroutineDispatchers, + it + ).also { + currentPasswordWizard = it + } + } ?: error("HomeServerConnectionConfig is null") + } + } + override fun cancelPendingLoginOrRegistration() { currentLoginWizard = null currentRegistrationWizard = null diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt new file mode 100644 index 0000000000..250bd9ec2e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/data/UpdatePasswordParams.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.data + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth + +/** + * Class to pass request parameters to update the password. + */ +@JsonClass(generateAdapter = true) +internal data class UpdatePasswordParams( + @Json(name = "auth") + val auth: UserPasswordAuth? = null, + + @Json(name = "new_password") + val newPassword: String? = null +) { + companion object { + fun create(sessionId: String, userId: String, oldPassword: String, newPassword: String): UpdatePasswordParams { + return UpdatePasswordParams( + auth = UserPasswordAuth(session = sessionId, user = userId, password = oldPassword), + newPassword = newPassword + ) + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt new file mode 100644 index 0000000000..11c628e69a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/auth/password/DefaultPasswordWizard.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2020 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.matrix.android.internal.auth.password + +import dagger.Lazy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig +import im.vector.matrix.android.api.auth.password.PasswordWizard +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.auth.AuthAPI +import im.vector.matrix.android.internal.auth.data.UpdatePasswordParams +import im.vector.matrix.android.internal.auth.registration.RegistrationFlowResponse +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.network.RetrofitFactory +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.task.launchToCallback +import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers +import kotlinx.coroutines.GlobalScope +import okhttp3.OkHttpClient +import timber.log.Timber + +internal class DefaultPasswordWizard( + okHttpClient: Lazy, + retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: MatrixCoroutineDispatchers, + private val homeServerConnectionConfig: HomeServerConnectionConfig +) : PasswordWizard { + + private val authAPI = retrofitFactory.create(okHttpClient, homeServerConnectionConfig.homeServerUri.toString()) + .create(AuthAPI::class.java) + + override fun updatePassword(sessionId: String, userId: String, oldPassword: String, newPassword: String, callback: MatrixCallback): Cancelable { + return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) { + updatePasswordInternal(sessionId, userId, oldPassword, newPassword) + } + } + + private suspend fun updatePasswordInternal(sessionId: String, userId: String, oldPassword: String, newPassword: String) { + val params = UpdatePasswordParams.create(sessionId, userId, oldPassword, newPassword) + try { + executeRequest(null) { + apiCall = authAPI.updatePassword(params) + } + } catch (throwable: Throwable) { + if (throwable is Failure.OtherServerError + && throwable.httpCode == 401 + /* Avoid infinite loop */ + && params.auth?.session == null) { + try { + MoshiProvider.providesMoshi() + .adapter(RegistrationFlowResponse::class.java) + .fromJson(throwable.errorBody) + } catch (e: Exception) { + null + }?.let { + // Retry with authentication + try { + executeRequest(null) { + apiCall = authAPI.updatePassword( + params.copy(auth = params.auth?.copy(session = it.session)) + ) + } + return + } catch (failure: Throwable) { + throw failure + } + } + } + throw throwable + } + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt index 45ad43a0ef..5e672d4f59 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/rest/UserPasswordAuth.kt @@ -21,7 +21,7 @@ import com.squareup.moshi.JsonClass import im.vector.matrix.android.internal.auth.data.LoginFlowTypes /** - * This class provides the authentication data to delete a device + * This class provides the authentication data by using user and password */ @JsonClass(generateAdapter = true) data class UserPasswordAuth( diff --git a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt index c68972cdd4..cd2f59bc1f 100644 --- a/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/riotx/core/di/FragmentModule.kt @@ -74,6 +74,7 @@ import im.vector.riotx.features.roomprofile.RoomProfileFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment +import im.vector.riotx.features.settings.VectorSettingsGeneralFragment import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment import im.vector.riotx.features.settings.VectorSettingsLabsFragment import im.vector.riotx.features.settings.VectorSettingsNotificationPreferenceFragment @@ -280,6 +281,11 @@ interface FragmentModule { @FragmentKey(VectorSettingsDevicesFragment::class) fun bindVectorSettingsDevicesFragment(fragment: VectorSettingsDevicesFragment): Fragment + @Binds + @IntoMap + @FragmentKey(VectorSettingsGeneralFragment::class) + fun bindVectorSettingsGeneralFragment(fragment: VectorSettingsGeneralFragment): Fragment + @Binds @IntoMap @FragmentKey(PublicRoomsFragment::class) diff --git a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt index 0a670e2c5a..f7a2d4735a 100644 --- a/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/settings/VectorSettingsGeneralFragment.kt @@ -35,6 +35,9 @@ import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.cache.DiskCache import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.auth.AuthenticationService +import im.vector.matrix.android.api.failure.isInvalidPassword import im.vector.riotx.R import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.showPassword @@ -56,8 +59,11 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File +import javax.inject.Inject -class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { +class VectorSettingsGeneralFragment @Inject constructor( + private val authenticationService: AuthenticationService +) : VectorSettingsBaseFragment() { override var titleRes = R.string.settings_general_title override val preferenceXmlRes = R.xml.vector_settings_general @@ -109,8 +115,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { // Password mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { - notImplemented() - // onPasswordUpdateClick() + onPasswordUpdateClick() false } @@ -771,7 +776,26 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() { val oldPwd = oldPasswordText.text.toString().trim() val newPwd = newPasswordText.text.toString().trim() - notImplemented() + authenticationService.getLastAuthenticatedSession()?.let { + showPasswordLoadingView(true) + authenticationService.getPasswordWizard().updatePassword(it.sessionId, it.myUserId, oldPwd, newPwd, object : MatrixCallback { + override fun onSuccess(data: Unit) { + showPasswordLoadingView(false) + dialog.dismiss() + activity.toast(R.string.settings_password_updated) + } + + override fun onFailure(failure: Throwable) { + showPasswordLoadingView(false) + if (failure.isInvalidPassword()) { + activity.toast(R.string.settings_fail_to_update_password_invalid_current_password) + } else { + activity.toast(R.string.settings_fail_to_update_password) + } + } + }) + } + /* TODO showPasswordLoadingView(true)