diff --git a/CHANGELOG.md b/CHANGELOG.md index c58b91e05a3..60675f6a75b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Summary * Enhancement - Improve grid mode: [#4027](https://github.com/owncloud/android/issues/4027) * Enhancement - Improve UX of creation dialog: [#4031](https://github.com/owncloud/android/issues/4031) * Enhancement - File name conflict starting by (1): [#4040](https://github.com/owncloud/android/pull/4040) +* Enhancement - Prevent http traffic with branding options: [#4066](https://github.com/owncloud/android/issues/4066) * Enhancement - Support "per app" language change on Android 13+: [#4082](https://github.com/owncloud/android/issues/4082) Details @@ -138,6 +139,13 @@ Details https://github.com/owncloud/android/issues/3946 https://github.com/owncloud/android/pull/4040 +* Enhancement - Prevent http traffic with branding options: [#4066](https://github.com/owncloud/android/issues/4066) + + Adding branding option for prevent http traffic. + + https://github.com/owncloud/android/issues/4066 + https://github.com/owncloud/android/pull/4110 + * Enhancement - Support "per app" language change on Android 13+: [#4082](https://github.com/owncloud/android/issues/4082) The locales_config.xml file has been created for the application to detect the language that diff --git a/changelog/unreleased/4110 b/changelog/unreleased/4110 new file mode 100644 index 00000000000..6fa55b21cb1 --- /dev/null +++ b/changelog/unreleased/4110 @@ -0,0 +1,6 @@ +Enhancement: Prevent http traffic with branding options + +Adding branding option for prevent http traffic. + +https://github.com/owncloud/android/issues/4066 +https://github.com/owncloud/android/pull/4110 \ No newline at end of file diff --git a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt index c161d009285..d56393871b9 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/dependecyinjection/ViewModelModule.kt @@ -78,7 +78,7 @@ val viewModelModule = module { PassCodeViewModel(get(), get(), action) } - viewModel { AuthenticationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } + viewModel { AuthenticationViewModel(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } viewModel { OAuthViewModel(get(), get(), get(), get()) } viewModel { SettingsViewModel(get()) } viewModel { SettingsSecurityViewModel(get(), get()) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt index c8114fc8ea5..770fa31c70a 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/AuthenticationViewModel.kt @@ -25,6 +25,7 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.owncloud.android.MainApp +import com.owncloud.android.R import com.owncloud.android.domain.authentication.oauth.RegisterClientUseCase import com.owncloud.android.domain.authentication.oauth.RequestTokenUseCase import com.owncloud.android.domain.authentication.oauth.model.ClientRegistrationInfo @@ -45,6 +46,7 @@ import com.owncloud.android.domain.webfinger.usecases.GetOwnCloudInstancesFromAu import com.owncloud.android.extensions.ViewModelExt.runUseCaseWithResult import com.owncloud.android.presentation.authentication.oauth.OAuthUtils import com.owncloud.android.presentation.common.UIResult +import com.owncloud.android.providers.ContextProvider import com.owncloud.android.providers.CoroutinesDispatcherProvider import com.owncloud.android.providers.WorkManagerProvider import kotlinx.coroutines.launch @@ -65,6 +67,7 @@ class AuthenticationViewModel( private val requestTokenUseCase: RequestTokenUseCase, private val registerClientUseCase: RegisterClientUseCase, private val coroutinesDispatcherProvider: CoroutinesDispatcherProvider, + private val contextProvider: ContextProvider, ) : ViewModel() { val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier() @@ -99,7 +102,11 @@ class AuthenticationViewModel( showLoading = true, liveData = _serverInfo, useCase = getServerInfoAsyncUseCase, - useCaseParams = GetServerInfoAsyncUseCase.Params(serverPath = serverUrl, creatingAccount = creatingAccount) + useCaseParams = GetServerInfoAsyncUseCase.Params( + serverPath = serverUrl, + creatingAccount = creatingAccount, + secureConnectionEnforced = contextProvider.getBoolean(R.bool.enforce_secure_connection), + ) ) } diff --git a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt index c7370bd6c0c..2d840415aa1 100644 --- a/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt +++ b/owncloudApp/src/main/java/com/owncloud/android/presentation/authentication/LoginActivity.kt @@ -50,6 +50,7 @@ import com.owncloud.android.domain.authentication.oauth.model.ResponseType import com.owncloud.android.domain.authentication.oauth.model.TokenRequest import com.owncloud.android.domain.exceptions.NoNetworkConnectionException import com.owncloud.android.domain.exceptions.OwncloudVersionNotSupportedException +import com.owncloud.android.domain.exceptions.SSLErrorException import com.owncloud.android.domain.exceptions.ServerNotReachableException import com.owncloud.android.domain.exceptions.StateMismatchException import com.owncloud.android.domain.exceptions.UnauthorizedException @@ -272,6 +273,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted text = getString(R.string.error_no_network_connection) setCompoundDrawablesWithIntrinsicBounds(R.drawable.no_network, 0, 0, 0) } + else -> binding.webfingerStatusText.run { text = uiResult.getThrowableOrNull()?.parseError("", resources, true) setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) @@ -355,6 +357,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted performGetAuthorizationCodeRequest(oidcServerConfiguration.authorizationEndpoint.toUri()) } } + else -> { binding.serverStatusText.run { text = getString(R.string.auth_unsupported_auth_method) @@ -379,14 +382,22 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted when (uiResult.error) { is CertificateCombinedException -> showUntrustedCertDialog(uiResult.error) + is OwncloudVersionNotSupportedException -> binding.serverStatusText.run { text = getString(R.string.server_not_supported) setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) } + is NoNetworkConnectionException -> binding.serverStatusText.run { text = getString(R.string.error_no_network_connection) setCompoundDrawablesWithIntrinsicBounds(R.drawable.no_network, 0, 0, 0) } + + is SSLErrorException -> binding.serverStatusText.run { + text = getString(R.string.ssl_connection_not_secure) + setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) + } + else -> binding.serverStatusText.run { text = uiResult.error?.parseError("", resources, true) setCompoundDrawablesWithIntrinsicBounds(R.drawable.common_error, 0, 0, 0) @@ -430,6 +441,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted } showOrHideBasicAuthFields(shouldBeVisible = false) } + else -> { binding.serverStatusText.isVisible = false binding.authStatusText.run { @@ -461,6 +473,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted ) } } + is UIResult.Error -> { Timber.e(uiResult.error, "Client registration failed.") performGetAuthorizationCodeRequest(authorizationEndpoint) @@ -585,6 +598,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted clientRegistrationInfo = clientRegistrationInfo ) } + is UIResult.Error -> { Timber.e(uiResult.error, "OAuth request to exchange authorization code for tokens failed") updateOAuthStatusIconAndText(uiResult.error) diff --git a/owncloudApp/src/main/res/values/setup.xml b/owncloudApp/src/main/res/values/setup.xml index 4f6d7cd33df..9930ea6728e 100644 --- a/owncloudApp/src/main/res/values/setup.xml +++ b/owncloudApp/src/main/res/values/setup.xml @@ -129,4 +129,8 @@ + + + false + diff --git a/owncloudApp/src/main/res/values/strings.xml b/owncloudApp/src/main/res/values/strings.xml index 7f47a1afe78..a6ca76ab99c 100644 --- a/owncloudApp/src/main/res/values/strings.xml +++ b/owncloudApp/src/main/res/values/strings.xml @@ -405,6 +405,7 @@ Login with oAuth2 Connecting to OAuth2 server… + Connection is not secure, http traffic is not allowed. The identity of the server could not be verified - The server certificate is not trusted - The server certificate expired diff --git a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt index 60813efb16c..2ba4770cbe8 100644 --- a/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt +++ b/owncloudApp/src/test/java/com/owncloud/android/presentation/viewmodels/authentication/AuthenticationViewModelTest.kt @@ -21,6 +21,7 @@ package com.owncloud.android.presentation.viewmodels.authentication +import com.owncloud.android.R import com.owncloud.android.domain.UseCaseResult import com.owncloud.android.domain.authentication.oauth.RegisterClientUseCase import com.owncloud.android.domain.authentication.oauth.RequestTokenUseCase @@ -125,6 +126,7 @@ class AuthenticationViewModelTest : ViewModelTest() { every { anyConstructed().generateRandomCodeVerifier() } returns "CODE VERIFIER" every { anyConstructed().generateCodeChallenge(any()) } returns "CODE CHALLENGE" every { anyConstructed().generateRandomState() } returns "STATE" + every { contextProvider.getBoolean(R.bool.enforce_secure_connection) } returns false testCoroutineDispatcher.pauseDispatcher() @@ -142,7 +144,8 @@ class AuthenticationViewModelTest : ViewModelTest() { requestTokenUseCase = requestTokenUseCase, registerClientUseCase = registerClientUseCase, workManagerProvider = workManagerProvider, - coroutinesDispatcherProvider = coroutineDispatcherProvider + coroutinesDispatcherProvider = coroutineDispatcherProvider, + contextProvider = contextProvider, ) } diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt index b2d94d34bed..3981feaf3c2 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/exceptions/SSLErrorException.kt @@ -21,4 +21,4 @@ package com.owncloud.android.domain.exceptions import java.lang.Exception -class SSLErrorException : Exception() +class SSLErrorException(override val message: String? = null) : Exception(message) diff --git a/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt b/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt index 6e1f7f9dca0..8326cb4dca6 100644 --- a/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt +++ b/owncloudDomain/src/main/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCase.kt @@ -20,6 +20,7 @@ package com.owncloud.android.domain.server.usecases import com.owncloud.android.domain.BaseUseCaseWithResult +import com.owncloud.android.domain.exceptions.SSLErrorException import com.owncloud.android.domain.server.ServerInfoRepository import com.owncloud.android.domain.server.model.ServerInfo import com.owncloud.android.domain.server.model.ServerInfo.Companion.HTTPS_PREFIX @@ -27,16 +28,21 @@ import com.owncloud.android.domain.server.model.ServerInfo.Companion.HTTP_PREFIX import java.util.Locale class GetServerInfoAsyncUseCase( - private val serverInfoRepository: ServerInfoRepository + private val serverInfoRepository: ServerInfoRepository, ) : BaseUseCaseWithResult() { override fun run(params: Params): ServerInfo { val normalizedServerUrl = normalizeProtocolPrefix(params.serverPath).trimEnd(TRAILING_SLASH) - return serverInfoRepository.getServerInfo(normalizedServerUrl, params.creatingAccount) + val serverInfo = serverInfoRepository.getServerInfo(normalizedServerUrl, params.creatingAccount) + if (!serverInfo.isSecureConnection && params.secureConnectionEnforced) { + throw SSLErrorException("Connection is not secure, http traffic is not allowed.") + } + return serverInfo } data class Params( val serverPath: String, val creatingAccount: Boolean, + val secureConnectionEnforced: Boolean, ) /** diff --git a/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt b/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt index 52fdc5b9a76..c5727975bfd 100644 --- a/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt +++ b/owncloudDomain/src/test/java/com/owncloud/android/domain/server/usecases/GetServerInfoAsyncUseCaseTest.kt @@ -18,8 +18,10 @@ */ package com.owncloud.android.domain.server.usecases +import com.owncloud.android.domain.exceptions.SSLErrorException import com.owncloud.android.domain.server.ServerInfoRepository import com.owncloud.android.domain.server.usecases.GetServerInfoAsyncUseCase.Companion.TRAILING_SLASH +import com.owncloud.android.testutil.OC_INSECURE_SERVER_INFO_BASIC_AUTH import com.owncloud.android.testutil.OC_SECURE_SERVER_INFO_BASIC_AUTH import io.mockk.every import io.mockk.spyk @@ -32,7 +34,11 @@ class GetServerInfoAsyncUseCaseTest { private val repository: ServerInfoRepository = spyk() private val useCase = GetServerInfoAsyncUseCase((repository)) - private val useCaseParams = GetServerInfoAsyncUseCase.Params(serverPath = "http://demo.owncloud.com", false) + private val useCaseParams = GetServerInfoAsyncUseCase.Params( + serverPath = "http://demo.owncloud.com", + creatingAccount = false, + secureConnectionEnforced = false, + ) private val useCaseParamsWithSlash = useCaseParams.copy(serverPath = useCaseParams.serverPath.plus(TRAILING_SLASH)) @Test @@ -70,4 +76,29 @@ class GetServerInfoAsyncUseCaseTest { verify(exactly = 1) { repository.getServerInfo(useCaseParams.serverPath, false) } } + + @Test + fun `Should throw SSLErrorException when secureConnectionEnforced is true and ServerInfoRepository returns ServerInfo with isSecureConnection returning false`() { + every { repository.getServerInfo(useCaseParams.serverPath, false) } returns OC_INSECURE_SERVER_INFO_BASIC_AUTH + + val useCaseResult = useCase.execute(useCaseParams.copy(secureConnectionEnforced = true)) + + assertTrue(useCaseResult.isError) + assertTrue(useCaseResult.getThrowableOrNull() is SSLErrorException) + + verify(exactly = 1) { repository.getServerInfo(useCaseParams.serverPath, false) } + } + + @Test + fun `Should work correctly when secureConnectionEnforced is true and ServerInfoRepository returns ServerInfo with isSecureConnection returning true`() { + every { repository.getServerInfo(useCaseParams.serverPath, false) } returns OC_SECURE_SERVER_INFO_BASIC_AUTH + + val useCaseResult = useCase.execute(useCaseParams.copy(secureConnectionEnforced = true)) + + assertTrue(useCaseResult.isSuccess) + assertEquals(OC_SECURE_SERVER_INFO_BASIC_AUTH, useCaseResult.getDataOrNull()) + + verify(exactly = 1) { repository.getServerInfo(useCaseParams.serverPath, false) } + } + }