diff --git a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt index 6b729c46c..7d3a19210 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt @@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter import android.content.Intent import android.net.Uri import android.os.Handler +import android.widget.Toast import androidx.biometric.BiometricManager import com.google.common.base.Optional import net.openid.appauth.AuthorizationException @@ -50,11 +51,13 @@ import org.cryptomator.presentation.model.ProgressStateModel import org.cryptomator.presentation.model.VaultModel import org.cryptomator.presentation.ui.activity.view.UnlockVaultView import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog +import org.cryptomator.presentation.ui.dialog.HubCheckHostAuthenticityDialog import org.cryptomator.presentation.workflow.ActivityResult import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler import org.cryptomator.util.SharedPreferencesHandler import org.cryptomator.util.crypto.CryptoMode import java.io.Serializable +import java.net.URI import javax.inject.Inject import timber.log.Timber @@ -75,6 +78,8 @@ class UnlockVaultPresenter @Inject constructor( exceptionMappings: ExceptionHandlers ) : Presenter(exceptionMappings) { + private val trustedCryptomatorCloudDomain = ".cryptomator.cloud" + private var startedUsingPrepareUnlock = false private var retryUnlockHandler: Handler? = null private var pendingUnlock: PendingUnlock? = null @@ -154,22 +159,85 @@ class UnlockVaultPresenter @Inject constructor( else -> {} } } else if (unverifiedVaultConfig.isPresent && unverifiedVaultConfig.get().keyLoadingStrategy() == KeyLoadingStrategy.HUB) { - when (intent.vaultAction()) { - UnlockVaultIntent.VaultAction.UNLOCK -> { - val unverifiedHubVaultConfig = unverifiedVaultConfig.get() as UnverifiedHubVaultConfig - if (hubAuthService == null) { - hubAuthService = AuthorizationService(context()) - } - view?.showProgress(ProgressModel.GENERIC) - unlockHubVault(unverifiedHubVaultConfig, vault) + val unverifiedHubVaultConfig = unverifiedVaultConfig.get() as UnverifiedHubVaultConfig + if (!isConsistentHubConfig(unverifiedHubVaultConfig)) { + Timber.tag("UnlockVaultPresenter").e("Inconsistent hub config detected. Denying access to protect the user.") + Toast.makeText(context(), R.string.error_hub_not_trustworthy, Toast.LENGTH_LONG).show() + finish() + } else if (configContainsAllowedHosts(unverifiedHubVaultConfig) && !isHttpHost(unverifiedHubVaultConfig)) { + allowedHubHosts(unverifiedHubVaultConfig, vault) + } else if (isCryptomatorCloud(unverifiedHubVaultConfig) && !isHttpHost(unverifiedHubVaultConfig)) { + allowedHubHosts(unverifiedHubVaultConfig, vault) + } else if (isCryptomatorCloud(unverifiedHubVaultConfig) && isHttpHost(unverifiedHubVaultConfig)) { + Timber.tag("UnlockVaultPresenter").e("Cryptomator Cloud with http is not supported.") + Toast.makeText(context(), R.string.error_hub_not_trustworthy, Toast.LENGTH_LONG).show() + finish() + } else if (!isHttpHost(unverifiedHubVaultConfig)) { + val hostnames = setOf(unverifiedHubVaultConfig.apiBaseUrl.authority, unverifiedHubVaultConfig.authEndpoint.authority).toTypedArray() + view?.showDialog(HubCheckHostAuthenticityDialog.newInstance(hostnames, unverifiedHubVaultConfig, vault)) + } else { + Timber.tag("UnlockVaultPresenter").e("Cryptomator is not allowed to connect to " + unverifiedHubVaultConfig.apiBaseUrl.authority) + Toast.makeText(context(), R.string.error_hub_not_trustworthy, Toast.LENGTH_LONG).show() + finish() + } + } + } + + fun allowedHubHosts(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) { + when (intent.vaultAction()) { + UnlockVaultIntent.VaultAction.UNLOCK -> { + if (hubAuthService == null) { + hubAuthService = AuthorizationService(context()) } - UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> showErrorAndFinish(HubVaultOperationNotSupportedException()) - UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException()) - UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException()) + view?.showProgress(ProgressModel.GENERIC) + unlockHubVault(unverifiedHubVaultConfig, vault) } + UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> showErrorAndFinish(HubVaultOperationNotSupportedException()) + UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException()) + UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException()) } } + private fun isConsistentHubConfig(unverifiedVaultConfig: UnverifiedHubVaultConfig): Boolean { + return getAuthority(unverifiedVaultConfig.tokenEndpoint) == getAuthority(unverifiedVaultConfig.authEndpoint) + } + + private fun isCryptomatorCloud(unverifiedHubVaultConfig: UnverifiedHubVaultConfig): Boolean { + return unverifiedHubVaultConfig.apiBaseUrl.host.endsWith(trustedCryptomatorCloudDomain) + && unverifiedHubVaultConfig.authEndpoint.host.endsWith(trustedCryptomatorCloudDomain) + } + + private fun configContainsAllowedHosts(unverifiedVaultConfig: UnverifiedHubVaultConfig): Boolean { + val allowedHubHosts = sharedPreferencesHandler.getTrustedHubHosts() + return containsAllowedHosts(allowedHubHosts, unverifiedVaultConfig) + } + + private fun containsAllowedHosts(allowedHubHosts: Set, unverifiedVaultConfig: UnverifiedHubVaultConfig): Boolean { + val canonicalHubHost = getAuthority(unverifiedVaultConfig.apiBaseUrl) + val canonicalAuthHost = getAuthority(unverifiedVaultConfig.authEndpoint) + return allowedHubHosts.contains(canonicalHubHost) && allowedHubHosts.contains(canonicalAuthHost); + } + + private fun isHttpHost(unverifiedHubVaultConfig: UnverifiedHubVaultConfig): Boolean { + return "http".equals(unverifiedHubVaultConfig.apiBaseUrl.scheme, ignoreCase = true) + || "http".equals(unverifiedHubVaultConfig.authEndpoint.scheme, ignoreCase = true) + } + + private fun getAuthority(uri: URI): String { + return when (uri.port) { + -1 -> "%s://%s".format(uri.scheme, uri.host) + 80 -> "http://%s".format(uri.host) + 443 -> "https://%s".format(uri.host) + else -> "%s://%s:%s".format(uri.scheme, uri.host, uri.port) + } + } + + fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) { + sharedPreferencesHandler.addTrustedHubHosts(getAuthority(unverifiedHubVaultConfig.apiBaseUrl)) + sharedPreferencesHandler.addTrustedHubHosts(getAuthority(unverifiedHubVaultConfig.authEndpoint)) + onUnverifiedVaultConfigRetrieved(Optional.of(unverifiedHubVaultConfig), vault) + } + private fun showErrorAndFinish(e: Throwable) { showError(e) finishWithResult(null) @@ -449,7 +517,7 @@ class UnlockVaultPresenter @Inject constructor( } override fun onError(e: Throwable) { - Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords") + Timber.tag("UnlockVaultPresenter").e(e, "Error while removing vault passwords") finishWithResult(null) } }) @@ -515,7 +583,7 @@ class UnlockVaultPresenter @Inject constructor( @Callback(dispatchResultOkOnly = false) fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, oldPassword: String, newPassword: String) { - if(result.isResultOk) { + if (result.isResultOk) { val cloud = result.getSingleResult(CloudModel::class.java).toCloud() val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build() onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), unverifiedVaultConfig, oldPassword, newPassword) diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt index ed72ddc53..9d18010b8 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt @@ -17,6 +17,7 @@ import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog import org.cryptomator.presentation.ui.dialog.CreateHubDeviceDialog import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog +import org.cryptomator.presentation.ui.dialog.HubCheckHostAuthenticityDialog import org.cryptomator.presentation.ui.dialog.HubLicenseUpgradeRequiredDialog import org.cryptomator.presentation.ui.dialog.HubUserSetupRequiredDialog import org.cryptomator.presentation.ui.dialog.HubVaultAccessForbiddenDialog @@ -38,7 +39,8 @@ class UnlockVaultActivity : BaseActivity(ActivityUnl HubUserSetupRequiredDialog.Callback, // HubVaultArchivedDialog.Callback, // HubLicenseUpgradeRequiredDialog.Callback, // - HubVaultAccessForbiddenDialog.Callback { + HubVaultAccessForbiddenDialog.Callback, // + HubCheckHostAuthenticityDialog.Callback { @Inject lateinit var presenter: UnlockVaultPresenter @@ -188,4 +190,12 @@ class UnlockVaultActivity : BaseActivity(ActivityUnl finish() } + override fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) { + presenter.onHubCheckHostsAllowed(unverifiedHubVaultConfig, vault) + } + + override fun onHubCheckHostsDenied(unverifiedHubVaultConfig: UnverifiedHubVaultConfig) { + finish() + } + } diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubCheckHostAuthenticityDialog.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubCheckHostAuthenticityDialog.kt new file mode 100644 index 000000000..62c2615d2 --- /dev/null +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/dialog/HubCheckHostAuthenticityDialog.kt @@ -0,0 +1,58 @@ +package org.cryptomator.presentation.ui.dialog + +import android.content.DialogInterface +import android.os.Bundle +import androidx.appcompat.app.AlertDialog +import org.cryptomator.domain.UnverifiedHubVaultConfig +import org.cryptomator.domain.UnverifiedVaultConfig +import org.cryptomator.domain.Vault +import org.cryptomator.generator.Dialog +import org.cryptomator.presentation.R +import org.cryptomator.presentation.databinding.DialogHubCheckHostAuthenticityBinding + +@Dialog +class HubCheckHostAuthenticityDialog : BaseDialog(DialogHubCheckHostAuthenticityBinding::inflate) { + + interface Callback { + + fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) + fun onHubCheckHostsDenied(unverifiedHubVaultConfig: UnverifiedHubVaultConfig) + + } + + public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog { + val unverifiedHubVaultConfig = requireArguments().getSerializable(UNVERIFIED_VAULT_CONFIG_ARG) as UnverifiedHubVaultConfig + val vault = requireArguments().getSerializable(VAULT_ARG) as Vault + return builder // + .setTitle(R.string.dialog_hub_check_host_authenticity_title) // + .setPositiveButton(requireActivity().getString(R.string.dialog_hub_check_host_authenticity_neutral_button)) { _: DialogInterface, _: Int -> callback?.onHubCheckHostsAllowed(unverifiedHubVaultConfig, vault) } + .setNegativeButton(requireActivity().getString(R.string.dialog_button_cancel)) { _: DialogInterface, _: Int -> callback?.onHubCheckHostsDenied(unverifiedHubVaultConfig) } + .create() + } + + override fun onStart() { + super.onStart() + val dialog = dialog as AlertDialog? + dialog?.setCanceledOnTouchOutside(false) + } + + public override fun setupView() { + val hostnames = requireArguments().getSerializable(HOSTNAMES_ARG) as Array + binding.tvHostnames.text = hostnames.sorted().joinToString(separator = "\n") { "• $it" } + } + + companion object { + private const val HOSTNAMES_ARG = "hostnames" + private const val UNVERIFIED_VAULT_CONFIG_ARG = "unverifiedVaultConfig" + private const val VAULT_ARG = "vault" + fun newInstance(hostnames: Array, unverifiedVaultConfig: UnverifiedVaultConfig, vault: Vault): HubCheckHostAuthenticityDialog { + val dialog = HubCheckHostAuthenticityDialog() + val args = Bundle() + args.putSerializable(HOSTNAMES_ARG, hostnames) + args.putSerializable(UNVERIFIED_VAULT_CONFIG_ARG, unverifiedVaultConfig) + args.putSerializable(VAULT_ARG, vault) + dialog.arguments = args + return dialog + } + } +} diff --git a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt index 02a4a9ab4..d049d3277 100644 --- a/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt +++ b/presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt @@ -71,6 +71,12 @@ class SettingsFragment : PreferenceFragmentCompatLayout() { true } + private val clearTrustedHubHostsClickListener = Preference.OnPreferenceClickListener { + sharedPreferencesHandler.clearTrustedHubHosts() + Toast.makeText(requireContext(), R.string.notification_cleared_trusted_hosts, Toast.LENGTH_LONG).show() + true + } + private val useAutoPhotoUploadChangedListener = Preference.OnPreferenceChangeListener { _, newValue -> onUseAutoPhotoUploadChanged(TRUE == newValue) true @@ -226,6 +232,7 @@ class SettingsFragment : PreferenceFragmentCompatLayout() { super.onResume() (findPreference(SEND_ERROR_REPORT_ITEM_KEY) as Preference?)?.onPreferenceClickListener = sendErrorReportClickListener (findPreference(LRU_CACHE_CLEAR_ITEM_KEY) as Preference?)?.onPreferenceClickListener = clearCacheClickListener + (findPreference(CLEAR_TRUSTED_HUB_HOSTS) as Preference?)?.onPreferenceClickListener = clearTrustedHubHostsClickListener (findPreference(SharedPreferencesHandler.DEBUG_MODE) as Preference?)?.onPreferenceChangeListener = debugModeChangedListener (findPreference(SharedPreferencesHandler.DISABLE_APP_WHEN_OBSCURED) as Preference?)?.onPreferenceChangeListener = disableAppWhenObscuredChangedListener (findPreference(SharedPreferencesHandler.SECURE_SCREEN) as Preference?)?.onPreferenceChangeListener = disableSecureScreenChangedListener @@ -327,6 +334,7 @@ class SettingsFragment : PreferenceFragmentCompatLayout() { private const val UPDATE_INTERVAL_ITEM_KEY = "updateInterval" private const val DISPLAY_LRU_CACHE_SIZE_ITEM_KEY = "displayLruCacheSize" private const val LRU_CACHE_CLEAR_ITEM_KEY = "lruCacheClear" + private const val CLEAR_TRUSTED_HUB_HOSTS = "clearTrustedHubHosts" } } diff --git a/presentation/src/main/res/layout/dialog_hub_check_host_authenticity.xml b/presentation/src/main/res/layout/dialog_hub_check_host_authenticity.xml new file mode 100644 index 000000000..b3a07349e --- /dev/null +++ b/presentation/src/main/res/layout/dialog_hub_check_host_authenticity.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index b26d4ca70..7fdc301d9 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -51,6 +51,8 @@ Unsupported Hub version. Hub is only supported on Android 12 and above. + Hub is not trustworthy. + @@ -249,6 +251,7 @@ Update search results while entering the query Search using glob pattern Use glob pattern matching like alice.*.jpg + Reset Unknown Hub Hosts Automatic locking Lock after @@ -558,6 +561,9 @@ Go to Profile @string/dialog_button_cancel + Trust this hosts? + @string/dialog_unable_to_share_positive_button + Cryptomator needs storage access to use local vaults Cryptomator needs storage access to use auto photo upload Cryptomator needs notification permissions to display vault status for example @@ -633,6 +639,8 @@ Authenticating… + Cleared trusted hosts + Cache @string/screen_settings_section_auto_photo_upload_toggle Cache recently accessed files encrypted locally on the device for later reuse when reopened diff --git a/presentation/src/main/res/xml/preferences.xml b/presentation/src/main/res/xml/preferences.xml index ac100058b..0ac6ba89f 100644 --- a/presentation/src/main/res/xml/preferences.xml +++ b/presentation/src/main/res/xml/preferences.xml @@ -50,6 +50,10 @@ android:summary="@string/screen_settings_cryptomator_variants_summary" android:title="@string/screen_settings_cryptomator_variants_label" /> + + diff --git a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt index 0847e17c8..6b9957666 100644 --- a/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt +++ b/util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt @@ -283,6 +283,28 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen return defaultSharedPreferences.getBoolean(MICROSOFT_WORKAROUND, false) } + fun addTrustedHubHosts(host: String) { + val hosts = defaultSharedPreferences + .getStringSet(TRUSTED_HUB_HOSTS, emptySet()) + ?.toMutableSet() ?: mutableSetOf() + + hosts.add(host) + + defaultSharedPreferences.edit() + .putStringSet(TRUSTED_HUB_HOSTS, hosts) + .apply() + } + + fun getTrustedHubHosts(): Set { + return defaultSharedPreferences + .getStringSet(TRUSTED_HUB_HOSTS, emptySet()) + ?.toSet() ?: emptySet() + } + + fun clearTrustedHubHosts() { + defaultSharedPreferences.edit().putStringSet(TRUSTED_HUB_HOSTS, mutableSetOf()).apply() + } + companion object { private const val SCREEN_LOCK_DIALOG_SHOWN = "askForScreenLockDialogShown" @@ -318,6 +340,7 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen const val BIOMETRIC_AUTHENTICATION = "biometricAuthentication" const val CRYPTOMATOR_VARIANTS = "cryptomatorVariants" const val LICENSES_ACTIVITY = "licensesActivity" + const val TRUSTED_HUB_HOSTS = "trustedHubHosts" } private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {