mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
moved logic to separate controller
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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"), //
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
41
src/main/resources/fxml/hub_check_host_authenticity.fxml
Normal file
41
src/main/resources/fxml/hub_check_host_authenticity.fxml
Normal 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>
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user