moved logic to separate controller

This commit is contained in:
Sebastian Stenzel
2026-03-11 18:52:36 +01:00
parent bc41429982
commit b981f0dc19
9 changed files with 205 additions and 65 deletions

View File

@@ -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<Instant> lastSuccessfulUpdateCheck;
public final ObjectProperty<Path> previouslyUsedVaultDirectory;
public final StringProperty lastUpdateAttemptedByVersion;
public final ObservableSet<String> 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;
}

View File

@@ -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<String> trustedHosts = Set.of();
}

View File

@@ -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"), //

View File

@@ -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;

View File

@@ -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<Scene> authFlowScene;
private final CompletableFuture<ReceivedKey> result;
private final Settings settings;
private final Set<String> hostnames;
@FXML
private ListView<String> hostnamesList;
@Inject
public CheckHostAuthenticityController(@KeyLoading Stage window, HubConfig hubConfig, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, CompletableFuture<ReceivedKey> 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;
}
}
}

View File

@@ -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)

View File

@@ -38,21 +38,19 @@ public class HubKeyLoadingStrategy implements KeyLoadingStrategy, FilesystemOwne
private final Stage window;
private final KeychainManager keychainManager;
private final AtomicReference<String> fsOwnerId;
private final HubConfig hubConfig;
private final Lazy<Scene> authFlowScene;
private final Lazy<Scene> checkHostAuthenticityScene;
private final Lazy<Scene> noKeychainScene;
private final CompletableFuture<ReceivedKey> result;
private final DeviceKey deviceKey;
@Inject
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_AUTH_FLOW) Lazy<Scene> authFlowScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<ReceivedKey> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference<String> fsOwnerId, HubConfig hubConfig) {
public HubKeyLoadingStrategy(@KeyLoading Stage window, @FxmlScene(FxmlFile.HUB_CHECK_HOST_AUTHENTICITY) Lazy<Scene> checkHostAuthenticityScene, @FxmlScene(FxmlFile.HUB_NO_KEYCHAIN) Lazy<Scene> noKeychainScene, CompletableFuture<ReceivedKey> result, DeviceKey deviceKey, KeychainManager keychainManager, @Named("windowTitle") String windowTitle, @Named("filesystemOwnerId") AtomicReference<String> 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> scene) {
Platform.runLater(() -> {
window.setScene(scene.get());

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>
<HBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.keyloading.hub.CheckHostAuthenticityController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12"
alignment="TOP_LEFT"
accessibleRole="DIALOG">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<VBox HBox.hgrow="ALWAYS">
<Label styleClass="label-large" text="%hub.checkHostAuthenticity.message" wrapText="true" textAlignment="LEFT">
<padding>
<Insets bottom="6" top="6"/>
</padding>
</Label>
<Label text="%hub.checkHostAuthenticity.description" wrapText="true"/>
<ListView fx:id="hostnamesList" VBox.vgrow="ALWAYS" minHeight="60"/>
<Region VBox.vgrow="ALWAYS" minHeight="18"/>
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%hub.checkHostAuthenticity.denyBtn" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#deny"/>
<Button text="%hub.checkHostAuthenticity.trustBtn" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#trust"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</HBox>

View File

@@ -162,6 +162,11 @@ unlock.error.title=Unlock "%s" failed
hub.noKeychain.message=Unable to access device key
hub.noKeychain.description=In order to unlock Hub vaults, a device key is required, which is secured using a keychain. To proceed, enable “%s” and select a keychain in the preferences.
hub.noKeychain.openBtn=Open Preferences
### Check Host Authenticity
hub.checkHostAuthenticity.message=Trust this host?
hub.checkHostAuthenticity.description=Do you want to trust this hostname?
hub.checkHostAuthenticity.trustBtn=Trust
hub.checkHostAuthenticity.denyBtn=Deny
### Waiting
hub.auth.message=Waiting for authentication…
hub.auth.description=You should automatically be redirected to the login page.
@@ -717,4 +722,4 @@ eventView.entry.inUse.ignoreLock=Ignore use status
## FileIsInUse Notification
notification.inUse.message=File is in use on another device
notification.inUse.description=The file is open by %s on %s. Ask them to close the file and let synchronization finish. You can ignore the status to open it now, but this may cause conflicts or overwrite newer changes.
notification.inUse.action=Ignore Use Status
notification.inUse.action=Ignore Use Status