Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -75,6 +78,8 @@ class UnlockVaultPresenter @Inject constructor(
exceptionMappings: ExceptionHandlers
) : Presenter<UnlockVaultView>(exceptionMappings) {

private val trustedCryptomatorCloudDomain = ".cryptomator.cloud"

private var startedUsingPrepareUnlock = false
private var retryUnlockHandler: Handler? = null
private var pendingUnlock: PendingUnlock? = null
Expand Down Expand Up @@ -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<String>, 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)
Expand Down Expand Up @@ -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)
}
})
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,7 +39,8 @@ class UnlockVaultActivity : BaseActivity<ActivityUnlockVaultBinding>(ActivityUnl
HubUserSetupRequiredDialog.Callback, //
HubVaultArchivedDialog.Callback, //
HubLicenseUpgradeRequiredDialog.Callback, //
HubVaultAccessForbiddenDialog.Callback {
HubVaultAccessForbiddenDialog.Callback, //
HubCheckHostAuthenticityDialog.Callback {

@Inject
lateinit var presenter: UnlockVaultPresenter
Expand Down Expand Up @@ -188,4 +190,12 @@ class UnlockVaultActivity : BaseActivity<ActivityUnlockVaultBinding>(ActivityUnl
finish()
}

override fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) {
presenter.onHubCheckHostsAllowed(unverifiedHubVaultConfig, vault)
}

override fun onHubCheckHostsDenied(unverifiedHubVaultConfig: UnverifiedHubVaultConfig) {
finish()
}

}
Original file line number Diff line number Diff line change
@@ -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<HubCheckHostAuthenticityDialog.Callback, DialogHubCheckHostAuthenticityBinding>(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<String>
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<String>, 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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingEnd="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin">

<TextView
android:id="@+id/tv_hostnames"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textSize="14sp" />

</LinearLayout>
8 changes: 8 additions & 0 deletions presentation/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
<string name="error_hub_invalid_version">Unsupported Hub version.</string>
<string name="error_hub_unlock_pre_31">Hub is only supported on Android 12 and above.</string>

<string name="error_hub_not_trustworthy">Hub is not trustworthy.</string>

<!-- # clouds -->

<!-- ## cloud names -->
Expand Down Expand Up @@ -249,6 +251,7 @@
<string name="screen_settings_live_search_summary">Update search results while entering the query</string>
<string name="screen_settings_glob_search">Search using glob pattern</string>
<string name="screen_settings_glob_search_summary">Use glob pattern matching like alice.*.jpg</string>
<string name="screen_hub_reset_unknown_hub_hosts_title">Reset Unknown Hub Hosts</string>

<string name="screen_settings_section_auto_lock">Automatic locking</string>
<string name="screen_settings_auto_lock_timeout">Lock after</string>
Expand Down Expand Up @@ -558,6 +561,9 @@
<string name="dialog_hub_user_setup_required_neutral_button">Go to Profile</string>
<string name="dialog_hub_user_setup_required_negative_button" translatable="false">@string/dialog_button_cancel</string>

<string name="dialog_hub_check_host_authenticity_title">Trust this hosts?</string>
<string name="dialog_hub_check_host_authenticity_neutral_button" translatable="false">@string/dialog_unable_to_share_positive_button</string>

<string name="permission_snackbar_auth_local_vault">Cryptomator needs storage access to use local vaults</string>
<string name="permission_snackbar_auth_auto_upload">Cryptomator needs storage access to use auto photo upload</string>
<string name="permission_snackbar_notifications">Cryptomator needs notification permissions to display vault status for example</string>
Expand Down Expand Up @@ -633,6 +639,8 @@

<string name="notification_authenticating">Authenticating&#8230;</string>

<string name="notification_cleared_trusted_hosts">Cleared trusted hosts</string>

<string name="screen_settings_lru_cache">Cache</string>
<string name="screen_settings_lru_cache_toggle" translatable="false">@string/screen_settings_section_auto_photo_upload_toggle</string>
<string name="screen_settings_lru_cache_toggle_summary">Cache recently accessed files encrypted locally on the device for later reuse when reopened</string>
Expand Down
4 changes: 4 additions & 0 deletions presentation/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@
android:summary="@string/screen_settings_cryptomator_variants_summary"
android:title="@string/screen_settings_cryptomator_variants_label" />

<androidx.preference.PreferenceScreen
android:key="clearTrustedHubHosts"
android:title="@string/screen_hub_reset_unknown_hub_hosts_title" />

</PreferenceCategory>

<PreferenceCategory android:title="@string/screen_settings_section_search">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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"
Expand Down Expand Up @@ -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) {
Expand Down
Loading