Skip to content

Commit a9faa2b

Browse files
authored
Merge pull request #626 from cryptomator/feature/hub-host-trust-validation
Hub host trust validation
2 parents add3c80 + b8f841f commit a9faa2b

File tree

8 files changed

+211
-14
lines changed

8 files changed

+211
-14
lines changed

presentation/src/main/java/org/cryptomator/presentation/presenter/UnlockVaultPresenter.kt

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.cryptomator.presentation.presenter
33
import android.content.Intent
44
import android.net.Uri
55
import android.os.Handler
6+
import android.widget.Toast
67
import androidx.biometric.BiometricManager
78
import com.google.common.base.Optional
89
import net.openid.appauth.AuthorizationException
@@ -50,11 +51,13 @@ import org.cryptomator.presentation.model.ProgressStateModel
5051
import org.cryptomator.presentation.model.VaultModel
5152
import org.cryptomator.presentation.ui.activity.view.UnlockVaultView
5253
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
54+
import org.cryptomator.presentation.ui.dialog.HubCheckHostAuthenticityDialog
5355
import org.cryptomator.presentation.workflow.ActivityResult
5456
import org.cryptomator.presentation.workflow.AuthenticationExceptionHandler
5557
import org.cryptomator.util.SharedPreferencesHandler
5658
import org.cryptomator.util.crypto.CryptoMode
5759
import java.io.Serializable
60+
import java.net.URI
5861
import javax.inject.Inject
5962
import timber.log.Timber
6063

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

81+
private val trustedCryptomatorCloudDomain = ".cryptomator.cloud"
82+
7883
private var startedUsingPrepareUnlock = false
7984
private var retryUnlockHandler: Handler? = null
8085
private var pendingUnlock: PendingUnlock? = null
@@ -154,22 +159,85 @@ class UnlockVaultPresenter @Inject constructor(
154159
else -> {}
155160
}
156161
} else if (unverifiedVaultConfig.isPresent && unverifiedVaultConfig.get().keyLoadingStrategy() == KeyLoadingStrategy.HUB) {
157-
when (intent.vaultAction()) {
158-
UnlockVaultIntent.VaultAction.UNLOCK -> {
159-
val unverifiedHubVaultConfig = unverifiedVaultConfig.get() as UnverifiedHubVaultConfig
160-
if (hubAuthService == null) {
161-
hubAuthService = AuthorizationService(context())
162-
}
163-
view?.showProgress(ProgressModel.GENERIC)
164-
unlockHubVault(unverifiedHubVaultConfig, vault)
162+
val unverifiedHubVaultConfig = unverifiedVaultConfig.get() as UnverifiedHubVaultConfig
163+
if (!isConsistentHubConfig(unverifiedHubVaultConfig)) {
164+
Timber.tag("UnlockVaultPresenter").e("Inconsistent hub config detected. Denying access to protect the user.")
165+
Toast.makeText(context(), R.string.error_hub_not_trustworthy, Toast.LENGTH_LONG).show()
166+
finish()
167+
} else if (configContainsAllowedHosts(unverifiedHubVaultConfig) && !isHttpHost(unverifiedHubVaultConfig)) {
168+
allowedHubHosts(unverifiedHubVaultConfig, vault)
169+
} else if (isCryptomatorCloud(unverifiedHubVaultConfig) && !isHttpHost(unverifiedHubVaultConfig)) {
170+
allowedHubHosts(unverifiedHubVaultConfig, vault)
171+
} else if (isCryptomatorCloud(unverifiedHubVaultConfig) && isHttpHost(unverifiedHubVaultConfig)) {
172+
Timber.tag("UnlockVaultPresenter").e("Cryptomator Cloud with http is not supported.")
173+
Toast.makeText(context(), R.string.error_hub_not_trustworthy, Toast.LENGTH_LONG).show()
174+
finish()
175+
} else if (!isHttpHost(unverifiedHubVaultConfig)) {
176+
val hostnames = setOf(unverifiedHubVaultConfig.apiBaseUrl.authority, unverifiedHubVaultConfig.authEndpoint.authority).toTypedArray()
177+
view?.showDialog(HubCheckHostAuthenticityDialog.newInstance(hostnames, unverifiedHubVaultConfig, vault))
178+
} else {
179+
Timber.tag("UnlockVaultPresenter").e("Cryptomator is not allowed to connect to " + unverifiedHubVaultConfig.apiBaseUrl.authority)
180+
Toast.makeText(context(), R.string.error_hub_not_trustworthy, Toast.LENGTH_LONG).show()
181+
finish()
182+
}
183+
}
184+
}
185+
186+
fun allowedHubHosts(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) {
187+
when (intent.vaultAction()) {
188+
UnlockVaultIntent.VaultAction.UNLOCK -> {
189+
if (hubAuthService == null) {
190+
hubAuthService = AuthorizationService(context())
165191
}
166-
UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> showErrorAndFinish(HubVaultOperationNotSupportedException())
167-
UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException())
168-
UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException())
192+
view?.showProgress(ProgressModel.GENERIC)
193+
unlockHubVault(unverifiedHubVaultConfig, vault)
169194
}
195+
UnlockVaultIntent.VaultAction.UNLOCK_FOR_BIOMETRIC_AUTH -> showErrorAndFinish(HubVaultOperationNotSupportedException())
196+
UnlockVaultIntent.VaultAction.CHANGE_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException())
197+
UnlockVaultIntent.VaultAction.ENCRYPT_PASSWORD -> showErrorAndFinish(HubVaultOperationNotSupportedException())
170198
}
171199
}
172200

201+
private fun isConsistentHubConfig(unverifiedVaultConfig: UnverifiedHubVaultConfig): Boolean {
202+
return getAuthority(unverifiedVaultConfig.tokenEndpoint) == getAuthority(unverifiedVaultConfig.authEndpoint)
203+
}
204+
205+
private fun isCryptomatorCloud(unverifiedHubVaultConfig: UnverifiedHubVaultConfig): Boolean {
206+
return unverifiedHubVaultConfig.apiBaseUrl.host.endsWith(trustedCryptomatorCloudDomain)
207+
&& unverifiedHubVaultConfig.authEndpoint.host.endsWith(trustedCryptomatorCloudDomain)
208+
}
209+
210+
private fun configContainsAllowedHosts(unverifiedVaultConfig: UnverifiedHubVaultConfig): Boolean {
211+
val allowedHubHosts = sharedPreferencesHandler.getTrustedHubHosts()
212+
return containsAllowedHosts(allowedHubHosts, unverifiedVaultConfig)
213+
}
214+
215+
private fun containsAllowedHosts(allowedHubHosts: Set<String>, unverifiedVaultConfig: UnverifiedHubVaultConfig): Boolean {
216+
val canonicalHubHost = getAuthority(unverifiedVaultConfig.apiBaseUrl)
217+
val canonicalAuthHost = getAuthority(unverifiedVaultConfig.authEndpoint)
218+
return allowedHubHosts.contains(canonicalHubHost) && allowedHubHosts.contains(canonicalAuthHost);
219+
}
220+
221+
private fun isHttpHost(unverifiedHubVaultConfig: UnverifiedHubVaultConfig): Boolean {
222+
return "http".equals(unverifiedHubVaultConfig.apiBaseUrl.scheme, ignoreCase = true)
223+
|| "http".equals(unverifiedHubVaultConfig.authEndpoint.scheme, ignoreCase = true)
224+
}
225+
226+
private fun getAuthority(uri: URI): String {
227+
return when (uri.port) {
228+
-1 -> "%s://%s".format(uri.scheme, uri.host)
229+
80 -> "http://%s".format(uri.host)
230+
443 -> "https://%s".format(uri.host)
231+
else -> "%s://%s:%s".format(uri.scheme, uri.host, uri.port)
232+
}
233+
}
234+
235+
fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) {
236+
sharedPreferencesHandler.addTrustedHubHosts(getAuthority(unverifiedHubVaultConfig.apiBaseUrl))
237+
sharedPreferencesHandler.addTrustedHubHosts(getAuthority(unverifiedHubVaultConfig.authEndpoint))
238+
onUnverifiedVaultConfigRetrieved(Optional.of(unverifiedHubVaultConfig), vault)
239+
}
240+
173241
private fun showErrorAndFinish(e: Throwable) {
174242
showError(e)
175243
finishWithResult(null)
@@ -449,7 +517,7 @@ class UnlockVaultPresenter @Inject constructor(
449517
}
450518

451519
override fun onError(e: Throwable) {
452-
Timber.tag("VaultListPresenter").e(e, "Error while removing vault passwords")
520+
Timber.tag("UnlockVaultPresenter").e(e, "Error while removing vault passwords")
453521
finishWithResult(null)
454522
}
455523
})
@@ -515,7 +583,7 @@ class UnlockVaultPresenter @Inject constructor(
515583

516584
@Callback(dispatchResultOkOnly = false)
517585
fun changePasswordAfterAuthentication(result: ActivityResult, vault: Vault, unverifiedVaultConfig: UnverifiedVaultConfig, oldPassword: String, newPassword: String) {
518-
if(result.isResultOk) {
586+
if (result.isResultOk) {
519587
val cloud = result.getSingleResult(CloudModel::class.java).toCloud()
520588
val vaultWithUpdatedCloud = Vault.aCopyOf(vault).withCloud(cloud).build()
521589
onChangePasswordClick(VaultModel(vaultWithUpdatedCloud), unverifiedVaultConfig, oldPassword, newPassword)

presentation/src/main/java/org/cryptomator/presentation/ui/activity/UnlockVaultActivity.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import org.cryptomator.presentation.ui.dialog.BiometricAuthKeyInvalidatedDialog
1717
import org.cryptomator.presentation.ui.dialog.ChangePasswordDialog
1818
import org.cryptomator.presentation.ui.dialog.CreateHubDeviceDialog
1919
import org.cryptomator.presentation.ui.dialog.EnterPasswordDialog
20+
import org.cryptomator.presentation.ui.dialog.HubCheckHostAuthenticityDialog
2021
import org.cryptomator.presentation.ui.dialog.HubLicenseUpgradeRequiredDialog
2122
import org.cryptomator.presentation.ui.dialog.HubUserSetupRequiredDialog
2223
import org.cryptomator.presentation.ui.dialog.HubVaultAccessForbiddenDialog
@@ -38,7 +39,8 @@ class UnlockVaultActivity : BaseActivity<ActivityUnlockVaultBinding>(ActivityUnl
3839
HubUserSetupRequiredDialog.Callback, //
3940
HubVaultArchivedDialog.Callback, //
4041
HubLicenseUpgradeRequiredDialog.Callback, //
41-
HubVaultAccessForbiddenDialog.Callback {
42+
HubVaultAccessForbiddenDialog.Callback, //
43+
HubCheckHostAuthenticityDialog.Callback {
4244

4345
@Inject
4446
lateinit var presenter: UnlockVaultPresenter
@@ -188,4 +190,12 @@ class UnlockVaultActivity : BaseActivity<ActivityUnlockVaultBinding>(ActivityUnl
188190
finish()
189191
}
190192

193+
override fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault) {
194+
presenter.onHubCheckHostsAllowed(unverifiedHubVaultConfig, vault)
195+
}
196+
197+
override fun onHubCheckHostsDenied(unverifiedHubVaultConfig: UnverifiedHubVaultConfig) {
198+
finish()
199+
}
200+
191201
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package org.cryptomator.presentation.ui.dialog
2+
3+
import android.content.DialogInterface
4+
import android.os.Bundle
5+
import androidx.appcompat.app.AlertDialog
6+
import org.cryptomator.domain.UnverifiedHubVaultConfig
7+
import org.cryptomator.domain.UnverifiedVaultConfig
8+
import org.cryptomator.domain.Vault
9+
import org.cryptomator.generator.Dialog
10+
import org.cryptomator.presentation.R
11+
import org.cryptomator.presentation.databinding.DialogHubCheckHostAuthenticityBinding
12+
13+
@Dialog
14+
class HubCheckHostAuthenticityDialog : BaseDialog<HubCheckHostAuthenticityDialog.Callback, DialogHubCheckHostAuthenticityBinding>(DialogHubCheckHostAuthenticityBinding::inflate) {
15+
16+
interface Callback {
17+
18+
fun onHubCheckHostsAllowed(unverifiedHubVaultConfig: UnverifiedHubVaultConfig, vault: Vault)
19+
fun onHubCheckHostsDenied(unverifiedHubVaultConfig: UnverifiedHubVaultConfig)
20+
21+
}
22+
23+
public override fun setupDialog(builder: AlertDialog.Builder): android.app.Dialog {
24+
val unverifiedHubVaultConfig = requireArguments().getSerializable(UNVERIFIED_VAULT_CONFIG_ARG) as UnverifiedHubVaultConfig
25+
val vault = requireArguments().getSerializable(VAULT_ARG) as Vault
26+
return builder //
27+
.setTitle(R.string.dialog_hub_check_host_authenticity_title) //
28+
.setPositiveButton(requireActivity().getString(R.string.dialog_hub_check_host_authenticity_neutral_button)) { _: DialogInterface, _: Int -> callback?.onHubCheckHostsAllowed(unverifiedHubVaultConfig, vault) }
29+
.setNegativeButton(requireActivity().getString(R.string.dialog_button_cancel)) { _: DialogInterface, _: Int -> callback?.onHubCheckHostsDenied(unverifiedHubVaultConfig) }
30+
.create()
31+
}
32+
33+
override fun onStart() {
34+
super.onStart()
35+
val dialog = dialog as AlertDialog?
36+
dialog?.setCanceledOnTouchOutside(false)
37+
}
38+
39+
public override fun setupView() {
40+
val hostnames = requireArguments().getSerializable(HOSTNAMES_ARG) as Array<String>
41+
binding.tvHostnames.text = hostnames.sorted().joinToString(separator = "\n") { "$it" }
42+
}
43+
44+
companion object {
45+
private const val HOSTNAMES_ARG = "hostnames"
46+
private const val UNVERIFIED_VAULT_CONFIG_ARG = "unverifiedVaultConfig"
47+
private const val VAULT_ARG = "vault"
48+
fun newInstance(hostnames: Array<String>, unverifiedVaultConfig: UnverifiedVaultConfig, vault: Vault): HubCheckHostAuthenticityDialog {
49+
val dialog = HubCheckHostAuthenticityDialog()
50+
val args = Bundle()
51+
args.putSerializable(HOSTNAMES_ARG, hostnames)
52+
args.putSerializable(UNVERIFIED_VAULT_CONFIG_ARG, unverifiedVaultConfig)
53+
args.putSerializable(VAULT_ARG, vault)
54+
dialog.arguments = args
55+
return dialog
56+
}
57+
}
58+
}

presentation/src/main/java/org/cryptomator/presentation/ui/fragment/SettingsFragment.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ class SettingsFragment : PreferenceFragmentCompatLayout() {
7171
true
7272
}
7373

74+
private val clearTrustedHubHostsClickListener = Preference.OnPreferenceClickListener {
75+
sharedPreferencesHandler.clearTrustedHubHosts()
76+
Toast.makeText(requireContext(), R.string.notification_cleared_trusted_hosts, Toast.LENGTH_LONG).show()
77+
true
78+
}
79+
7480
private val useAutoPhotoUploadChangedListener = Preference.OnPreferenceChangeListener { _, newValue ->
7581
onUseAutoPhotoUploadChanged(TRUE == newValue)
7682
true
@@ -226,6 +232,7 @@ class SettingsFragment : PreferenceFragmentCompatLayout() {
226232
super.onResume()
227233
(findPreference(SEND_ERROR_REPORT_ITEM_KEY) as Preference?)?.onPreferenceClickListener = sendErrorReportClickListener
228234
(findPreference(LRU_CACHE_CLEAR_ITEM_KEY) as Preference?)?.onPreferenceClickListener = clearCacheClickListener
235+
(findPreference(CLEAR_TRUSTED_HUB_HOSTS) as Preference?)?.onPreferenceClickListener = clearTrustedHubHostsClickListener
229236
(findPreference(SharedPreferencesHandler.DEBUG_MODE) as Preference?)?.onPreferenceChangeListener = debugModeChangedListener
230237
(findPreference(SharedPreferencesHandler.DISABLE_APP_WHEN_OBSCURED) as Preference?)?.onPreferenceChangeListener = disableAppWhenObscuredChangedListener
231238
(findPreference(SharedPreferencesHandler.SECURE_SCREEN) as Preference?)?.onPreferenceChangeListener = disableSecureScreenChangedListener
@@ -327,6 +334,7 @@ class SettingsFragment : PreferenceFragmentCompatLayout() {
327334
private const val UPDATE_INTERVAL_ITEM_KEY = "updateInterval"
328335
private const val DISPLAY_LRU_CACHE_SIZE_ITEM_KEY = "displayLruCacheSize"
329336
private const val LRU_CACHE_CLEAR_ITEM_KEY = "lruCacheClear"
337+
private const val CLEAR_TRUSTED_HUB_HOSTS = "clearTrustedHubHosts"
330338
}
331339

332340
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
3+
android:layout_width="match_parent"
4+
android:layout_height="match_parent"
5+
android:orientation="vertical"
6+
android:paddingStart="@dimen/activity_horizontal_margin"
7+
android:paddingTop="@dimen/activity_vertical_margin"
8+
android:paddingEnd="@dimen/activity_horizontal_margin"
9+
android:paddingBottom="@dimen/activity_vertical_margin">
10+
11+
<TextView
12+
android:id="@+id/tv_hostnames"
13+
android:layout_width="match_parent"
14+
android:layout_height="wrap_content"
15+
android:layout_marginTop="16dp"
16+
android:textSize="14sp" />
17+
18+
</LinearLayout>

presentation/src/main/res/values/strings.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
<string name="error_hub_invalid_version">Unsupported Hub version.</string>
5252
<string name="error_hub_unlock_pre_31">Hub is only supported on Android 12 and above.</string>
5353

54+
<string name="error_hub_not_trustworthy">Hub is not trustworthy.</string>
55+
5456
<!-- # clouds -->
5557

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

253256
<string name="screen_settings_section_auto_lock">Automatic locking</string>
254257
<string name="screen_settings_auto_lock_timeout">Lock after</string>
@@ -558,6 +561,9 @@
558561
<string name="dialog_hub_user_setup_required_neutral_button">Go to Profile</string>
559562
<string name="dialog_hub_user_setup_required_negative_button" translatable="false">@string/dialog_button_cancel</string>
560563

564+
<string name="dialog_hub_check_host_authenticity_title">Trust this hosts?</string>
565+
<string name="dialog_hub_check_host_authenticity_neutral_button" translatable="false">@string/dialog_unable_to_share_positive_button</string>
566+
561567
<string name="permission_snackbar_auth_local_vault">Cryptomator needs storage access to use local vaults</string>
562568
<string name="permission_snackbar_auth_auto_upload">Cryptomator needs storage access to use auto photo upload</string>
563569
<string name="permission_snackbar_notifications">Cryptomator needs notification permissions to display vault status for example</string>
@@ -633,6 +639,8 @@
633639

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

642+
<string name="notification_cleared_trusted_hosts">Cleared trusted hosts</string>
643+
636644
<string name="screen_settings_lru_cache">Cache</string>
637645
<string name="screen_settings_lru_cache_toggle" translatable="false">@string/screen_settings_section_auto_photo_upload_toggle</string>
638646
<string name="screen_settings_lru_cache_toggle_summary">Cache recently accessed files encrypted locally on the device for later reuse when reopened</string>

presentation/src/main/res/xml/preferences.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@
5050
android:summary="@string/screen_settings_cryptomator_variants_summary"
5151
android:title="@string/screen_settings_cryptomator_variants_label" />
5252

53+
<androidx.preference.PreferenceScreen
54+
android:key="clearTrustedHubHosts"
55+
android:title="@string/screen_hub_reset_unknown_hub_hosts_title" />
56+
5357
</PreferenceCategory>
5458

5559
<PreferenceCategory android:title="@string/screen_settings_section_search">

util/src/main/java/org/cryptomator/util/SharedPreferencesHandler.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,28 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
283283
return defaultSharedPreferences.getBoolean(MICROSOFT_WORKAROUND, false)
284284
}
285285

286+
fun addTrustedHubHosts(host: String) {
287+
val hosts = defaultSharedPreferences
288+
.getStringSet(TRUSTED_HUB_HOSTS, emptySet())
289+
?.toMutableSet() ?: mutableSetOf()
290+
291+
hosts.add(host)
292+
293+
defaultSharedPreferences.edit()
294+
.putStringSet(TRUSTED_HUB_HOSTS, hosts)
295+
.apply()
296+
}
297+
298+
fun getTrustedHubHosts(): Set<String> {
299+
return defaultSharedPreferences
300+
.getStringSet(TRUSTED_HUB_HOSTS, emptySet())
301+
?.toSet() ?: emptySet()
302+
}
303+
304+
fun clearTrustedHubHosts() {
305+
defaultSharedPreferences.edit().putStringSet(TRUSTED_HUB_HOSTS, mutableSetOf()).apply()
306+
}
307+
286308
companion object {
287309

288310
private const val SCREEN_LOCK_DIALOG_SHOWN = "askForScreenLockDialogShown"
@@ -318,6 +340,7 @@ constructor(context: Context) : SharedPreferences.OnSharedPreferenceChangeListen
318340
const val BIOMETRIC_AUTHENTICATION = "biometricAuthentication"
319341
const val CRYPTOMATOR_VARIANTS = "cryptomatorVariants"
320342
const val LICENSES_ACTIVITY = "licensesActivity"
343+
const val TRUSTED_HUB_HOSTS = "trustedHubHosts"
321344
}
322345

323346
private inline fun SharedPreferences.edit(operation: (SharedPreferences.Editor) -> Unit) {

0 commit comments

Comments
 (0)