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) }
+ }
+
}