diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java index c30f606a3..c8110be58 100644 --- a/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/src/main/java/org/cryptomator/common/settings/Settings.java @@ -24,9 +24,12 @@ import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import javafx.collections.ObservableSet; import javafx.geometry.NodeOrientation; import java.nio.file.Path; import java.time.Instant; +import java.util.HashSet; +import java.util.Set; public class Settings { @@ -78,6 +81,7 @@ public class Settings { public final ObjectProperty lastSuccessfulUpdateCheck; public final ObjectProperty previouslyUsedVaultDirectory; public final StringProperty lastUpdateAttemptedByVersion; + public final ObservableSet trustedHosts; public static Settings create(SettingsProvider provider, Environment env) { var defaults = new SettingsJson(); @@ -118,6 +122,7 @@ public class Settings { this.lastSuccessfulUpdateCheck = new SimpleObjectProperty<>(this, "lastSuccessfulUpdateCheck", json.lastSuccessfulUpdateCheck); this.previouslyUsedVaultDirectory = new SimpleObjectProperty<>(this, "previouslyUsedVaultDirectory", json.previouslyUsedVaultDirectory); this.lastUpdateAttemptedByVersion = new SimpleStringProperty(this, "lastUpdateAttemptedByVersion", json.lastUpdateAttemptedByVersion); + this.trustedHosts = FXCollections.observableSet(json.trustedHosts); this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList()); @@ -149,6 +154,7 @@ public class Settings { lastSuccessfulUpdateCheck.addListener(this::somethingChanged); previouslyUsedVaultDirectory.addListener(this::somethingChanged); lastUpdateAttemptedByVersion.addListener(this::somethingChanged); + trustedHosts.addListener(this::somethingChanged); } @SuppressWarnings("deprecation") @@ -207,6 +213,7 @@ public class Settings { json.lastSuccessfulUpdateCheck = lastSuccessfulUpdateCheck.get(); json.previouslyUsedVaultDirectory = previouslyUsedVaultDirectory.get(); json.lastUpdateAttemptedByVersion = lastUpdateAttemptedByVersion.get(); + json.trustedHosts = Set.copyOf(trustedHosts); return json; } diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJson.java b/src/main/java/org/cryptomator/common/settings/SettingsJson.java index e0cdb7b5e..c2126b2a3 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsJson.java +++ b/src/main/java/org/cryptomator/common/settings/SettingsJson.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.nio.file.Path; import java.time.Instant; import java.util.List; +import java.util.Set; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @@ -99,4 +100,7 @@ class SettingsJson { @JsonProperty("lastUpdateAttemptedByVersion") String lastUpdateAttemptedByVersion; + + @JsonProperty("trustedHosts") + Set trustedHosts = Set.of(); } diff --git a/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 85a64f293..68bf2adff 100644 --- a/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -19,6 +19,7 @@ public enum FxmlFile { HEALTH_START("/fxml/health_start.fxml"), // HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // HUB_NO_KEYCHAIN("/fxml/hub_no_keychain.fxml"), // + HUB_CHECK_HOST_AUTHENTICITY("/fxml/hub_check_host_authenticity.fxml"), // HUB_AUTH_FLOW("/fxml/hub_auth_flow.fxml"), // HUB_INVALID_LICENSE("/fxml/hub_invalid_license.fxml"), // HUB_RECEIVE_KEY("/fxml/hub_receive_key.fxml"), // diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java index 06e488581..f56ca3deb 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/AuthFlowController.java @@ -1,6 +1,5 @@ package org.cryptomator.ui.keyloading.hub; -import com.nimbusds.jose.JWEObject; import dagger.Lazy; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; @@ -12,8 +11,6 @@ import javax.inject.Inject; import javax.inject.Named; import javafx.application.Application; import javafx.application.Platform; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.StringBinding; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.concurrent.WorkerStateEvent; diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/CheckHostAuthenticityController.java b/src/main/java/org/cryptomator/ui/keyloading/hub/CheckHostAuthenticityController.java new file mode 100644 index 000000000..31f94ce78 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/CheckHostAuthenticityController.java @@ -0,0 +1,128 @@ +package org.cryptomator.ui.keyloading.hub; + +import dagger.Lazy; +import org.cryptomator.common.settings.Settings; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.keyloading.KeyLoading; +import org.cryptomator.ui.keyloading.KeyLoadingScoped; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.ListView; +import javafx.stage.Stage; +import java.net.URI; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +@KeyLoadingScoped +public class CheckHostAuthenticityController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(CheckHostAuthenticityController.class); + + private final Stage window; + private final HubConfig hubConfig; + private final Lazy authFlowScene; + private final CompletableFuture result; + private final Settings settings; + private final Set hostnames; + + @FXML + private ListView hostnamesList; + + @Inject + public CheckHostAuthenticityController(@KeyLoading Stage window, HubConfig hubConfig, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, CompletableFuture result, Settings settings) { + this.window = window; + this.hubConfig = hubConfig; + this.authFlowScene = authFlowScene; + this.result = result; + this.settings = settings; + this.hostnames = new HashSet<>(); + } + + @FXML + public void initialize() { + var authUri = URI.create(hubConfig.authEndpoint); + var tokenUri = URI.create(hubConfig.tokenEndpoint); + var apiBaseUri = hubConfig.getApiBaseUrl(); + var webappBaseUri = hubConfig.getWebappBaseUrl(); + + if (!isConsistentHubConfig()) { + LOG.warn("Inconsistent hub config detected. Denying access to protect the user."); + Platform.runLater(this::deny); + } else if (configContainsAllowedHosts()) { + trust(); + } else if (Boolean.getBoolean("cryptomator.allowUnknownHubHosts")) { + hostnames.addAll(List.of(authUri.getAuthority(), tokenUri.getAuthority(), apiBaseUri.getAuthority(), webappBaseUri.getAuthority())); + hostnamesList.getItems().addAll(hostnames); + } else { + LOG.warn("Cryptomator is not allowed to connect to {}. Check your cryptomator.allowedHubHosts config.", webappBaseUri); + Platform.runLater(this::deny); + } + } + + @FXML + public void trust() { + settings.trustedHosts.addAll(hostnames); + window.setScene(authFlowScene.get()); + } + + @FXML + public void deny() { + result.cancel(true); + window.close(); // TODO: show "denied" scene with explanation and "learn more" link to documentation + } + + private boolean isConsistentHubConfig() { + //hub endpoints are consistent + //apiBaseURL.host == deviceUrl.host == authSuccessUrl.host == authErrorUrl.host + var expectedHubHubHost = URI.create(hubConfig.authSuccessUrl).getHost(); //apiBaseURL could be null! hence, the authSuccessUrl + if (hubConfig.apiBaseUrl != null && hasDifferentHost(hubConfig.apiBaseUrl, expectedHubHubHost)) { + return false; + } + if (hasDifferentHost(hubConfig.devicesResourceUrl, expectedHubHubHost)) { + return false; + } + if (hasDifferentHost(hubConfig.authErrorUrl, expectedHubHubHost)) { + return false; + } + + //auth endpoints are consistent + //authUrl.host == tokenUrl.host + var expectedHubAuthHost = URI.create(hubConfig.authEndpoint).getHost(); + if (hasDifferentHost(hubConfig.tokenEndpoint, expectedHubAuthHost)) { + return false; + } + return true; + } + + private boolean configContainsAllowedHosts() { + var allowedHubHostsString = System.getProperty("cryptomator.allowedHubHosts", ""); + //https://example.com,http://foo.bar:3333 + var allowedHubHosts = Arrays.stream(allowedHubHostsString.split(",")).map(String::trim).toList(); //foo.bar + + var expectedHubHubAuthorities = URI.create(hubConfig.authSuccessUrl).getAuthority(); //apiBaseURL could be null! hence, the authSuccessUrl + var expectedHubAuthAuthorities = URI.create(hubConfig.authEndpoint).getAuthority(); + //are the hosts also allowed? + var isHubHubHostAllowed = allowedHubHosts.stream().anyMatch(expectedHubHubAuthorities::equals); + var isHubAuthHostAllowed = allowedHubHosts.stream().anyMatch(expectedHubAuthAuthorities::equals); + return isHubAuthHostAllowed && isHubHubHostAllowed; + } + + private boolean hasDifferentHost(String uri, String host) { + try { + return !URI.create(uri).getHost().equals(host); + } catch (IllegalArgumentException e) { + return true; + } + } + +} diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java index 10a032a35..f5d5cf55f 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingModule.java @@ -98,6 +98,13 @@ public abstract class HubKeyLoadingModule { return fxmlLoaders.createScene(FxmlFile.HUB_NO_KEYCHAIN); } + @Provides + @FxmlScene(FxmlFile.HUB_CHECK_HOST_AUTHENTICITY) + @KeyLoadingScoped + static Scene provideHubCheckHostAuthenticityScene(@KeyLoading FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HUB_CHECK_HOST_AUTHENTICITY); + } + @Provides @FxmlScene(FxmlFile.HUB_AUTH_FLOW) @KeyLoadingScoped @@ -180,6 +187,11 @@ public abstract class HubKeyLoadingModule { @FxControllerKey(NoKeychainController.class) abstract FxController bindNoKeychainController(NoKeychainController controller); + @Binds + @IntoMap + @FxControllerKey(CheckHostAuthenticityController.class) + abstract FxController bindCheckHostAuthenticityController(CheckHostAuthenticityController controller); + @Binds @IntoMap @FxControllerKey(AuthFlowController.class) diff --git a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java index 6fe01e0ae..614db9eb1 100644 --- a/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java +++ b/src/main/java/org/cryptomator/ui/keyloading/hub/HubKeyLoadingStrategy.java @@ -38,21 +38,19 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwne private final Stage window; private final KeychainManager keychainManager; private final AtomicReference fsOwnerId; - private final HubConfig hubConfig; - private final Lazy authFlowScene; + private final Lazy checkHostAuthenticityScene; private final Lazy noKeychainScene; private final CompletableFuture result; private final DeviceKey deviceKey; @Inject - public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference fsOwnerId, HubConfig hubConfig) { + public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_CHECK_HOST_AUTHENTICITY) Lazy checkHostAuthenticityScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy noKeychainScene, CompletableFuture result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference fsOwnerId) { this.window = window; this.keychainManager = keychainManager; this.fsOwnerId = fsOwnerId; - this.hubConfig = hubConfig; window.setTitle(windowTitle); window.setOnCloseRequest(_ -> result.cancel(true)); - this.authFlowScene = authFlowScene; + this.checkHostAuthenticityScene = checkHostAuthenticityScene; this.noKeychainScene = noKeychainScene; this.result = result; this.deviceKey = deviceKey; @@ -66,19 +64,9 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwne throw new NoKeychainAccessProviderException(); } var keypair = deviceKey.get(); - - //check hub config - isConsistentHubConfig(); - if (configContainsAllowedHosts()) { - showWindow(authFlowScene); - var jwe = result.get(); - return jwe.decryptMasterkey(keypair.getPrivate()); - } else { - var showUnknownHubHostDialog = Environment.getInstance().allowUnknownHubHosts(); - //TODO show window - throw new MasterkeyLoadingFailedException("Unknown hub host in vault config"); - } - + showWindow(checkHostAuthenticityScene); + var jwe = result.get(); + return jwe.decryptMasterkey(keypair.getPrivate()); } catch (NoKeychainAccessProviderException e) { showWindow(noKeychainScene); throw new UnlockCancelledException("Unlock canceled due to missing prerequisites", e); @@ -94,49 +82,6 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwne } } - private void isConsistentHubConfig() { - //hub endpoints are consistent - //apiBaseURL.host == deviceUrl.host == authSuccessUrl.host == authErrorUrl.host - var expectedHubHubHost = URI.create(hubConfig.authSuccessUrl).getHost(); //apiBaseURL could be null! hence, the authSuccessUrl - if (hubConfig.apiBaseUrl != null && hasDifferentHost(hubConfig.apiBaseUrl, expectedHubHubHost)) { - //throw - } - if (hasDifferentHost(hubConfig.devicesResourceUrl, expectedHubHubHost)) { - //throw - } - if (hasDifferentHost(hubConfig.authErrorUrl, expectedHubHubHost)) { - //throw - } - - //auth endpoints are consistent - //authUrl.host == tokenUrl.host - var expectedHubAuthHost = URI.create(hubConfig.authEndpoint).getHost(); - if (hasDifferentHost(hubConfig.tokenEndpoint, expectedHubAuthHost)) { - //throw - } - } - - private boolean configContainsAllowedHosts() { - var allowedHubHostsString = System.getProperty("cryptomator.allowedHubHosts", ""); - //https://example.com,http://foo.bar:3333 - var allowedHubHosts = Arrays.stream(allowedHubHostsString.split(",")).map(String::trim).toList(); //foo.bar - - var expectedHubHubAuthorities = URI.create(hubConfig.authSuccessUrl).getAuthority(); //apiBaseURL could be null! hence, the authSuccessUrl - var expectedHubAuthAuthorities = URI.create(hubConfig.authEndpoint).getAuthority(); - //are the hosts also allowed? - var isHubHubHostAllowed = allowedHubHosts.stream().anyMatch(expectedHubHubAuthorities::equals); - var isHubAuthHostAllowed = allowedHubHosts.stream().anyMatch(expectedHubAuthAuthorities::equals); - return isHubAuthHostAllowed && isHubHubHostAllowed; - } - - private boolean hasDifferentHost(String uri, String host) { - try { - return !URI.create(uri).getHost().equals(host); - } catch (IllegalArgumentException e) { - return true; - } - } - private void showWindow(Lazy scene) { Platform.runLater(() -> { window.setScene(scene.get()); diff --git a/src/main/resources/fxml/hub_check_host_authenticity.fxml b/src/main/resources/fxml/hub_check_host_authenticity.fxml new file mode 100644 index 000000000..1cbf039d4 --- /dev/null +++ b/src/main/resources/fxml/hub_check_host_authenticity.fxml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + +