Pārlūkot izejas kodu

Merge branch 'develop' into feature/integrations-api-1.1.0

# Conflicts:
#	src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
#	src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
Sebastian Stenzel 3 gadi atpakaļ
vecāks
revīzija
2b255ed101
24 mainītis faili ar 387 papildinājumiem un 196 dzēšanām
  1. 1 1
      pom.xml
  2. 3 3
      src/main/java/org/cryptomator/common/settings/UiTheme.java
  3. 20 2
      src/main/java/org/cryptomator/launcher/Cryptomator.java
  4. 7 0
      src/main/java/org/cryptomator/launcher/CryptomatorComponent.java
  5. 8 1
      src/main/java/org/cryptomator/ui/fxapp/FxApplication.java
  6. 2 125
      src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
  7. 156 0
      src/main/java/org/cryptomator/ui/preferences/InterfacePreferencesController.java
  8. 4 2
      src/main/java/org/cryptomator/ui/preferences/PreferencesController.java
  9. 5 0
      src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java
  10. 5 0
      src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java
  11. 22 3
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java
  12. 15 0
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
  13. 30 3
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
  14. 9 1
      src/main/resources/fxml/preferences.fxml
  15. 2 2
      src/main/resources/fxml/preferences_about.fxml
  16. 2 2
      src/main/resources/fxml/preferences_contribute.fxml
  17. 10 29
      src/main/resources/fxml/preferences_general.fxml
  18. 45 0
      src/main/resources/fxml/preferences_interface.fxml
  19. 3 3
      src/main/resources/fxml/preferences_updates.fxml
  20. 5 5
      src/main/resources/fxml/preferences_volume.fxml
  21. 14 12
      src/main/resources/i18n/strings.properties
  22. 1 1
      src/main/resources/license/THIRD-PARTY.txt
  23. 0 1
      src/test/java/org/cryptomator/ui/common/PasswordStrengthUtilTest.java
  24. 18 0
      src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java

+ 1 - 1
pom.xml

@@ -44,7 +44,7 @@
 		<guava.version>31.1-jre</guava.version>
 		<dagger.version>2.41</dagger.version>
 		<gson.version>2.9.0</gson.version>
-		<zxcvbn.version>1.5.2</zxcvbn.version>
+		<zxcvbn.version>1.6.0</zxcvbn.version>
 		<slf4j.version>1.7.36</slf4j.version>
 		<logback.version>1.2.11</logback.version>
 

+ 3 - 3
src/main/java/org/cryptomator/common/settings/UiTheme.java

@@ -3,9 +3,9 @@ package org.cryptomator.common.settings;
 import org.apache.commons.lang3.SystemUtils;
 
 public enum UiTheme {
-	LIGHT("preferences.general.theme.light"), //
-	DARK("preferences.general.theme.dark"), //
-	AUTOMATIC("preferences.general.theme.automatic");
+	LIGHT("preferences.interface.theme.light"), //
+	DARK("preferences.interface.theme.dark"), //
+	AUTOMATIC("preferences.interface.theme.automatic");
 
 	public static UiTheme[] applicableValues() {
 		if (SystemUtils.IS_OS_MAC || SystemUtils.IS_OS_WINDOWS) {

+ 20 - 2
src/main/java/org/cryptomator/launcher/Cryptomator.java

@@ -21,15 +21,18 @@ import javax.inject.Inject;
 import javax.inject.Singleton;
 import javafx.application.Application;
 import javafx.stage.Stage;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Executors;
 
 @Singleton
 public class Cryptomator {
 
+	private static final long STARTUP_TIME = System.currentTimeMillis();
 	// DaggerCryptomatorComponent gets generated by Dagger.
 	// Run Maven and include target/generated-sources/annotations in your IDE.
-	private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.create();
+	private static final CryptomatorComponent CRYPTOMATOR_COMPONENT = DaggerCryptomatorComponent.factory().create(STARTUP_TIME);
 	private static final Logger LOG = LoggerFactory.getLogger(Cryptomator.class);
 
 	private final LoggerConfiguration logConfig;
@@ -50,6 +53,20 @@ public class Cryptomator {
 	}
 
 	public static void main(String[] args) {
+		var printVersion = Optional.ofNullable(args) //
+				.stream() //Streams either one element (the args-array) or zero elements
+				.flatMap(Arrays::stream) //
+				.anyMatch(arg -> "-v".equals(arg) || "--version".equals(arg));
+
+		if (printVersion) {
+			var appVer = System.getProperty("cryptomator.appVersion", "SNAPSHOT");
+			var buildNumber = System.getProperty("cryptomator.buildNumber", "SNAPSHOT");
+
+			//Reduce noise for parsers by using System.out directly
+			System.out.printf("Cryptomator version %s (build %s)%n", appVer, buildNumber);
+			return;
+		}
+
 		int exitCode = CRYPTOMATOR_COMPONENT.application().run(args);
 		LOG.info("Exit {}", exitCode);
 		System.exit(exitCode); // end remaining non-daemon threads.
@@ -63,6 +80,7 @@ public class Cryptomator {
 	 */
 	private int run(String[] args) {
 		logConfig.init();
+		LOG.debug("Dagger graph initialized after {}ms", System.currentTimeMillis() - STARTUP_TIME);
 		LOG.info("Starting Cryptomator {} on {} {} ({})", env.getAppVersion().orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH);
 		debugMode.initialize();
 		supportedLanguages.applyPreferred();
@@ -112,7 +130,7 @@ public class Cryptomator {
 
 		@Override
 		public void start(Stage primaryStage) {
-			LOG.info("JavaFX application started.");
+			LOG.info("JavaFX runtime started after {}ms", System.currentTimeMillis() - STARTUP_TIME);
 			FxApplicationComponent component = CRYPTOMATOR_COMPONENT.fxAppComponentBuilder() //
 					.fxApplication(this) //
 					.primaryStage(primaryStage) //

+ 7 - 0
src/main/java/org/cryptomator/launcher/CryptomatorComponent.java

@@ -1,10 +1,12 @@
 package org.cryptomator.launcher;
 
+import dagger.BindsInstance;
 import dagger.Component;
 import org.cryptomator.common.CommonsModule;
 import org.cryptomator.logging.LoggerModule;
 import org.cryptomator.ui.fxapp.FxApplicationComponent;
 
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 @Singleton
@@ -15,4 +17,9 @@ public interface CryptomatorComponent {
 
 	FxApplicationComponent.Builder fxAppComponentBuilder();
 
+	@Component.Factory
+	interface Factory {
+		CryptomatorComponent create(@BindsInstance @Named("startupTime") long startupTime);
+	}
+
 }

+ 8 - 1
src/main/java/org/cryptomator/ui/fxapp/FxApplication.java

@@ -7,6 +7,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javafx.application.Platform;
 
 @FxApplicationScoped
@@ -14,6 +15,7 @@ public class FxApplication {
 
 	private static final Logger LOG = LoggerFactory.getLogger(FxApplication.class);
 
+	private final long startupTime;
 	private final Settings settings;
 	private final AppLaunchEventHandler launchEventHandler;
 	private final Lazy<TrayMenuComponent> trayMenu;
@@ -23,7 +25,8 @@ public class FxApplication {
 	private final AutoUnlocker autoUnlocker;
 
 	@Inject
-	FxApplication(Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
+	FxApplication(@Named("startupTime") long startupTime, Settings settings, AppLaunchEventHandler launchEventHandler, Lazy<TrayMenuComponent> trayMenu, FxApplicationWindows appWindows, FxApplicationStyle applicationStyle, FxApplicationTerminator applicationTerminator, AutoUnlocker autoUnlocker) {
+		this.startupTime = startupTime;
 		this.settings = settings;
 		this.launchEventHandler = launchEventHandler;
 		this.trayMenu = trayMenu;
@@ -58,6 +61,10 @@ public class FxApplication {
 					stage.setIconified(true);
 				}
 			}
+			LOG.debug("Main window initialized after {}ms", System.currentTimeMillis() - startupTime);
+		}).exceptionally(error -> {
+			LOG.error("Failed to show main window", error);
+			return null;
 		});
 
 		launchEventHandler.startHandlingLaunchEvents();

+ 2 - 125
src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java

@@ -1,38 +1,26 @@
 package org.cryptomator.ui.preferences;
 
-import com.google.common.base.Strings;
 import org.cryptomator.common.Environment;
-import org.cryptomator.common.LicenseHolder;
 import org.cryptomator.common.settings.Settings;
-import org.cryptomator.common.settings.UiTheme;
 import org.cryptomator.integrations.autostart.AutoStartProvider;
 import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
 import org.cryptomator.integrations.keychain.KeychainAccessProvider;
-import org.cryptomator.launcher.SupportedLanguages;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
-import org.cryptomator.ui.traymenu.TrayMenuComponent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javafx.application.Application;
 import javafx.beans.binding.Bindings;
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.value.ObservableValue;
 import javafx.fxml.FXML;
-import javafx.geometry.NodeOrientation;
 import javafx.scene.control.CheckBox;
 import javafx.scene.control.ChoiceBox;
-import javafx.scene.control.RadioButton;
-import javafx.scene.control.Toggle;
 import javafx.scene.control.ToggleGroup;
 import javafx.stage.Stage;
 import javafx.util.StringConverter;
 import java.util.List;
-import java.util.Locale;
 import java.util.Optional;
-import java.util.ResourceBundle;
 
 @PreferencesScoped
 public class GeneralPreferencesController implements FxController {
@@ -41,39 +29,23 @@ public class GeneralPreferencesController implements FxController {
 
 	private final Stage window;
 	private final Settings settings;
-	private final boolean trayMenuInitialized;
-	private final boolean trayMenuSupported;
 	private final Optional<AutoStartProvider> autoStartProvider;
-	private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
-	private final LicenseHolder licenseHolder;
-	private final ResourceBundle resourceBundle;
 	private final Application application;
 	private final Environment environment;
 	private final List<KeychainAccessProvider> keychainAccessProviders;
 	private final FxApplicationWindows appWindows;
-	public ChoiceBox<UiTheme> themeChoiceBox;
 	public ChoiceBox<KeychainAccessProvider> keychainBackendChoiceBox;
-	public CheckBox showMinimizeButtonCheckbox;
-	public CheckBox showTrayIconCheckbox;
 	public CheckBox startHiddenCheckbox;
-	public ChoiceBox<String> preferredLanguageChoiceBox;
 	public CheckBox debugModeCheckbox;
 	public CheckBox autoStartCheckbox;
 	public ToggleGroup nodeOrientation;
-	public RadioButton nodeOrientationLtr;
-	public RadioButton nodeOrientationRtl;
 
 	@Inject
-	GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, TrayMenuComponent trayMenu, Optional<AutoStartProvider> autoStartProvider, List<KeychainAccessProvider> keychainAccessProviders, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle, Application application, Environment environment, FxApplicationWindows appWindows) {
+	GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional<AutoStartProvider> autoStartProvider, List<KeychainAccessProvider> keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) {
 		this.window = window;
 		this.settings = settings;
-		this.trayMenuInitialized = trayMenu.isInitialized();
-		this.trayMenuSupported = trayMenu.isSupported();
 		this.autoStartProvider = autoStartProvider;
 		this.keychainAccessProviders = keychainAccessProviders;
-		this.selectedTabProperty = selectedTabProperty;
-		this.licenseHolder = licenseHolder;
-		this.resourceBundle = resourceBundle;
 		this.application = application;
 		this.environment = environment;
 		this.appWindows = appWindows;
@@ -81,32 +53,12 @@ public class GeneralPreferencesController implements FxController {
 
 	@FXML
 	public void initialize() {
-		themeChoiceBox.getItems().addAll(UiTheme.applicableValues());
-		if (!themeChoiceBox.getItems().contains(settings.theme().get())) {
-			settings.theme().set(UiTheme.LIGHT);
-		}
-		themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
-		themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
-
-		showMinimizeButtonCheckbox.selectedProperty().bindBidirectional(settings.showMinimizeButton());
-
-		showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
-
 		startHiddenCheckbox.selectedProperty().bindBidirectional(settings.startHidden());
 
-		preferredLanguageChoiceBox.getItems().add(null);
-		preferredLanguageChoiceBox.getItems().addAll(SupportedLanguages.LANGUAGAE_TAGS);
-		preferredLanguageChoiceBox.valueProperty().bindBidirectional(settings.languageProperty());
-		preferredLanguageChoiceBox.setConverter(new LanguageTagConverter(resourceBundle));
-
 		debugModeCheckbox.selectedProperty().bindBidirectional(settings.debugMode());
 
 		autoStartProvider.ifPresent(autoStart -> autoStartCheckbox.setSelected(autoStart.isEnabled()));
 
-		nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
-		nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);
-		nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);
-
 		var keychainSettingsConverter = new KeychainProviderClassNameConverter(keychainAccessProviders);
 		keychainBackendChoiceBox.getItems().addAll(keychainAccessProviders);
 		keychainBackendChoiceBox.setValue(keychainSettingsConverter.fromString(settings.keychainProvider().get()));
@@ -114,29 +66,10 @@ public class GeneralPreferencesController implements FxController {
 		Bindings.bindBidirectional(settings.keychainProvider(), keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter);
 	}
 
-
-	public boolean isTrayMenuInitialized() {
-		return trayMenuInitialized;
-	}
-
-	public boolean isTrayMenuSupported() {
-		return trayMenuSupported;
-	}
-
 	public boolean isAutoStartSupported() {
 		return autoStartProvider.isPresent();
 	}
 
-	private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
-		if (nodeOrientationLtr.equals(newValue)) {
-			settings.userInterfaceOrientation().set(NodeOrientation.LEFT_TO_RIGHT);
-		} else if (nodeOrientationRtl.equals(newValue)) {
-			settings.userInterfaceOrientation().set(NodeOrientation.RIGHT_TO_LEFT);
-		} else {
-			LOG.warn("Unexpected toggle option {}", newValue);
-		}
-	}
-
 	@FXML
 	public void toggleAutoStart() {
 		autoStartProvider.ifPresent(autoStart -> {
@@ -155,16 +88,6 @@ public class GeneralPreferencesController implements FxController {
 		});
 	}
 
-	public LicenseHolder getLicenseHolder() {
-		return licenseHolder;
-	}
-
-
-	@FXML
-	public void showContributeTab() {
-		selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
-	}
-
 	@FXML
 	public void showLogfileDirectory() {
 		environment.getLogDir().ifPresent(logDirPath -> application.getHostServices().showDocument(logDirPath.toUri().toString()));
@@ -172,53 +95,7 @@ public class GeneralPreferencesController implements FxController {
 
 	/* Helper classes */
 
-	private static class UiThemeConverter extends StringConverter<UiTheme> {
-
-		private final ResourceBundle resourceBundle;
-
-		UiThemeConverter(ResourceBundle resourceBundle) {
-			this.resourceBundle = resourceBundle;
-		}
-
-		@Override
-		public String toString(UiTheme impl) {
-			return resourceBundle.getString(impl.getDisplayName());
-		}
-
-		@Override
-		public UiTheme fromString(String string) {
-			throw new UnsupportedOperationException();
-		}
-
-	}
-
-	private static class LanguageTagConverter extends StringConverter<String> {
-
-		private final ResourceBundle resourceBundle;
-
-		LanguageTagConverter(ResourceBundle resourceBundle) {
-			this.resourceBundle = resourceBundle;
-		}
-
-		@Override
-		public String toString(String tag) {
-			if (tag == null) {
-				return resourceBundle.getString("preferences.general.language.auto");
-			} else {
-				var locale = Locale.forLanguageTag(tag);
-				var lang = locale.getDisplayLanguage(locale);
-				var region = locale.getDisplayCountry(locale);
-				return lang + (Strings.isNullOrEmpty(region) ? "" : " (" + region + ")");
-			}
-		}
-
-		@Override
-		public String fromString(String displayLanguage) {
-			throw new UnsupportedOperationException();
-		}
-	}
-
-	private class KeychainProviderDisplayNameConverter extends StringConverter<KeychainAccessProvider> {
+	private static class KeychainProviderDisplayNameConverter extends StringConverter<KeychainAccessProvider> {
 
 		@Override
 		public String toString(KeychainAccessProvider provider) {

+ 156 - 0
src/main/java/org/cryptomator/ui/preferences/InterfacePreferencesController.java

@@ -0,0 +1,156 @@
+package org.cryptomator.ui.preferences;
+
+import com.google.common.base.Strings;
+import org.cryptomator.common.LicenseHolder;
+import org.cryptomator.common.settings.Settings;
+import org.cryptomator.common.settings.UiTheme;
+import org.cryptomator.launcher.SupportedLanguages;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.traymenu.TrayMenuComponent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.fxml.FXML;
+import javafx.geometry.NodeOrientation;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.Toggle;
+import javafx.scene.control.ToggleGroup;
+import javafx.util.StringConverter;
+import java.util.Locale;
+import java.util.ResourceBundle;
+
+@PreferencesScoped
+public class InterfacePreferencesController implements FxController {
+
+	private static final Logger LOG = LoggerFactory.getLogger(InterfacePreferencesController.class);
+
+	private final Settings settings;
+	private final boolean trayMenuInitialized;
+	private final boolean trayMenuSupported;
+	private final ObjectProperty<SelectedPreferencesTab> selectedTabProperty;
+	private final LicenseHolder licenseHolder;
+	private final ResourceBundle resourceBundle;
+	public ChoiceBox<UiTheme> themeChoiceBox;
+	public CheckBox showMinimizeButtonCheckbox;
+	public CheckBox showTrayIconCheckbox;
+	public ChoiceBox<String> preferredLanguageChoiceBox;
+	public ToggleGroup nodeOrientation;
+	public RadioButton nodeOrientationLtr;
+	public RadioButton nodeOrientationRtl;
+
+	@Inject
+	InterfacePreferencesController(Settings settings, TrayMenuComponent trayMenu, ObjectProperty<SelectedPreferencesTab> selectedTabProperty, LicenseHolder licenseHolder, ResourceBundle resourceBundle) {
+		this.settings = settings;
+		this.trayMenuInitialized = trayMenu.isInitialized();
+		this.trayMenuSupported = trayMenu.isSupported();
+		this.selectedTabProperty = selectedTabProperty;
+		this.licenseHolder = licenseHolder;
+		this.resourceBundle = resourceBundle;
+	}
+
+	@FXML
+	public void initialize() {
+		themeChoiceBox.getItems().addAll(UiTheme.applicableValues());
+		if (!themeChoiceBox.getItems().contains(settings.theme().get())) {
+			settings.theme().set(UiTheme.LIGHT);
+		}
+		themeChoiceBox.valueProperty().bindBidirectional(settings.theme());
+		themeChoiceBox.setConverter(new UiThemeConverter(resourceBundle));
+
+		showMinimizeButtonCheckbox.selectedProperty().bindBidirectional(settings.showMinimizeButton());
+
+		showTrayIconCheckbox.selectedProperty().bindBidirectional(settings.showTrayIcon());
+
+		preferredLanguageChoiceBox.getItems().add(null);
+		preferredLanguageChoiceBox.getItems().addAll(SupportedLanguages.LANGUAGAE_TAGS);
+		preferredLanguageChoiceBox.valueProperty().bindBidirectional(settings.languageProperty());
+		preferredLanguageChoiceBox.setConverter(new LanguageTagConverter(resourceBundle));
+
+		nodeOrientationLtr.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.LEFT_TO_RIGHT);
+		nodeOrientationRtl.setSelected(settings.userInterfaceOrientation().get() == NodeOrientation.RIGHT_TO_LEFT);
+		nodeOrientation.selectedToggleProperty().addListener(this::toggleNodeOrientation);
+	}
+
+
+	public boolean isTrayMenuInitialized() {
+		return trayMenuInitialized;
+	}
+
+	public boolean isTrayMenuSupported() {
+		return trayMenuSupported;
+	}
+
+	private void toggleNodeOrientation(@SuppressWarnings("unused") ObservableValue<? extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
+		if (nodeOrientationLtr.equals(newValue)) {
+			settings.userInterfaceOrientation().set(NodeOrientation.LEFT_TO_RIGHT);
+		} else if (nodeOrientationRtl.equals(newValue)) {
+			settings.userInterfaceOrientation().set(NodeOrientation.RIGHT_TO_LEFT);
+		} else {
+			LOG.warn("Unexpected toggle option {}", newValue);
+		}
+	}
+
+	public LicenseHolder getLicenseHolder() {
+		return licenseHolder;
+	}
+
+
+	@FXML
+	public void showContributeTab() {
+		selectedTabProperty.set(SelectedPreferencesTab.CONTRIBUTE);
+	}
+
+	/* Helper classes */
+
+	private static class UiThemeConverter extends StringConverter<UiTheme> {
+
+		private final ResourceBundle resourceBundle;
+
+		UiThemeConverter(ResourceBundle resourceBundle) {
+			this.resourceBundle = resourceBundle;
+		}
+
+		@Override
+		public String toString(UiTheme impl) {
+			return resourceBundle.getString(impl.getDisplayName());
+		}
+
+		@Override
+		public UiTheme fromString(String string) {
+			throw new UnsupportedOperationException();
+		}
+
+	}
+
+	private static class LanguageTagConverter extends StringConverter<String> {
+
+		private final ResourceBundle resourceBundle;
+
+		LanguageTagConverter(ResourceBundle resourceBundle) {
+			this.resourceBundle = resourceBundle;
+		}
+
+		@Override
+		public String toString(String tag) {
+			if (tag == null) {
+				return resourceBundle.getString("preferences.interface.language.auto");
+			} else {
+				var locale = Locale.forLanguageTag(tag);
+				var lang = locale.getDisplayLanguage(locale);
+				var region = locale.getDisplayCountry(locale);
+				return lang + (Strings.isNullOrEmpty(region) ? "" : " (" + region + ")");
+			}
+		}
+
+		@Override
+		public String fromString(String displayLanguage) {
+			throw new UnsupportedOperationException();
+		}
+	}
+
+}

+ 4 - 2
src/main/java/org/cryptomator/ui/preferences/PreferencesController.java

@@ -24,6 +24,7 @@ public class PreferencesController implements FxController {
 	private final BooleanBinding updateAvailable;
 	public TabPane tabPane;
 	public Tab generalTab;
+	public Tab interfaceTab;
 	public Tab volumeTab;
 	public Tab updatesTab;
 	public Tab contributeTab;
@@ -50,10 +51,11 @@ public class PreferencesController implements FxController {
 
 	private Tab getTabToSelect(SelectedPreferencesTab selectedTab) {
 		return switch (selectedTab) {
-			case UPDATES -> updatesTab;
+			case GENERAL -> generalTab;
+			case INTERFACE -> interfaceTab;
 			case VOLUME -> volumeTab;
+			case UPDATES -> updatesTab;
 			case CONTRIBUTE -> contributeTab;
-			case GENERAL -> generalTab;
 			case ABOUT -> aboutTab;
 			case ANY -> updateAvailable.get() ? updatesTab : generalTab;
 		};

+ 5 - 0
src/main/java/org/cryptomator/ui/preferences/PreferencesModule.java

@@ -64,6 +64,11 @@ abstract class PreferencesModule {
 	@FxControllerKey(GeneralPreferencesController.class)
 	abstract FxController bindGeneralPreferencesController(GeneralPreferencesController controller);
 
+	@Binds
+	@IntoMap
+	@FxControllerKey(InterfacePreferencesController.class)
+	abstract FxController bindInterfacePreferencesController(InterfacePreferencesController controller);
+
 	@Binds
 	@IntoMap
 	@FxControllerKey(UpdatesPreferencesController.class)

+ 5 - 0
src/main/java/org/cryptomator/ui/preferences/SelectedPreferencesTab.java

@@ -11,6 +11,11 @@ public enum SelectedPreferencesTab {
 	 */
 	GENERAL,
 
+	/**
+	 * Show interface tab
+	 */
+	INTERFACE,
+
 	/**
 	 * Show volume tab
 	 */

+ 22 - 3
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactory.java

@@ -7,6 +7,7 @@ import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.jetbrains.annotations.Nullable;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
@@ -16,6 +17,7 @@ import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.function.Predicate;
 
 import static org.cryptomator.common.Constants.MASTERKEY_BACKUP_SUFFIX;
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
@@ -102,12 +104,29 @@ public class RecoveryKeyFactory {
 	 * @return <code>true</code> if this seems to be a legitimate recovery key
 	 */
 	public boolean validateRecoveryKey(String recoveryKey) {
+		return validateRecoveryKey(recoveryKey, null);
+	}
+
+	/**
+	 * Checks whether a String is a syntactically correct recovery key with a valid checksum and passes the extended validation.
+	 *
+	 * @param recoveryKey A word sequence which might be a recovery key
+	 * @param extendedValidation Additional verification of the decoded key (optional)
+	 * @return <code>true</code> if this seems to be a legitimate recovery key and passes the extended validation
+	 */
+	public boolean validateRecoveryKey(String recoveryKey, @Nullable Predicate<byte[]> extendedValidation) {
+		byte[] key = new byte[0];
 		try {
-			byte[] key = decodeRecoveryKey(recoveryKey);
-			Arrays.fill(key, (byte) 0x00);
-			return true;
+			key = decodeRecoveryKey(recoveryKey);
+			if (extendedValidation != null) {
+				return extendedValidation.test(key);
+			} else {
+				return true;
+			}
 		} catch (IllegalArgumentException e) {
 			return false;
+		} finally {
+			Arrays.fill(key, (byte) 0x00);
 		}
 	}
 

+ 15 - 0
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java

@@ -4,7 +4,9 @@ import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.IntoMap;
+import org.cryptomator.common.Nullable;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.ui.common.DefaultSceneFactory;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxControllerKey;
@@ -22,12 +24,25 @@ import javafx.beans.property.StringProperty;
 import javafx.scene.Scene;
 import javafx.stage.Modality;
 import javafx.stage.Stage;
+import java.io.IOException;
 import java.util.Map;
 import java.util.ResourceBundle;
 
 @Module
 abstract class RecoveryKeyModule {
 
+	@Provides
+	@Nullable
+	@RecoveryKeyWindow
+	@RecoveryKeyScoped
+	static VaultConfig.UnverifiedVaultConfig vaultConfig(@RecoveryKeyWindow Vault vault) {
+		try {
+			return vault.getVaultConfigCache().get();
+		} catch (IOException e) {
+			return null;
+		}
+	}
+
 	@Provides
 	@RecoveryKeyWindow
 	@RecoveryKeyScoped

+ 30 - 3
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java

@@ -3,10 +3,16 @@ package org.cryptomator.ui.recoverykey;
 import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import dagger.Lazy;
+import org.cryptomator.common.Nullable;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.cryptofs.VaultConfigLoadException;
+import org.cryptomator.cryptofs.VaultKeyInvalidException;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javafx.beans.binding.Bindings;
@@ -24,10 +30,12 @@ import java.util.Optional;
 @RecoveryKeyScoped
 public class RecoveryKeyRecoverController implements FxController {
 
-	private final static CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
+	private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
+	private static final CharMatcher ALLOWED_CHARS = CharMatcher.inRange('a', 'z').or(CharMatcher.is(' '));
 
 	private final Stage window;
 	private final Vault vault;
+	private final VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig;
 	private final StringProperty recoveryKey;
 	private final RecoveryKeyFactory recoveryKeyFactory;
 	private final BooleanBinding validRecoveryKey;
@@ -37,9 +45,10 @@ public class RecoveryKeyRecoverController implements FxController {
 	public TextArea textarea;
 
 	@Inject
-	public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
+	public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene) {
 		this.window = window;
 		this.vault = vault;
+		this.unverifiedVaultConfig = unverifiedVaultConfig;
 		this.recoveryKey = recoveryKey;
 		this.recoveryKeyFactory = recoveryKeyFactory;
 		this.validRecoveryKey = Bindings.createBooleanBinding(this::isValidRecoveryKey, recoveryKey);
@@ -96,6 +105,20 @@ public class RecoveryKeyRecoverController implements FxController {
 		window.setScene(resetPasswordScene.get());
 	}
 
+	private boolean checkKeyAgainstVaultConfig(byte[] key) {
+		try {
+			var config = unverifiedVaultConfig.verify(key, unverifiedVaultConfig.allegedVaultVersion());
+			LOG.info("Provided recovery key matches vault config signature for vault {}", config.getId());
+			return true;
+		} catch (VaultKeyInvalidException e) {
+			LOG.debug("Provided recovery key does not match vault config signature.");
+			return false;
+		} catch (VaultConfigLoadException e) {
+			LOG.error("Failed to parse vault config", e);
+			return false;
+		}
+	}
+
 	/* Getter/Setter */
 
 	public Vault getVault() {
@@ -107,7 +130,11 @@ public class RecoveryKeyRecoverController implements FxController {
 	}
 
 	public boolean isValidRecoveryKey() {
-		return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
+		if (unverifiedVaultConfig != null) {
+			return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get(), this::checkKeyAgainstVaultConfig);
+		} else {
+			return recoveryKeyFactory.validateRecoveryKey(recoveryKey.get());
+		}
 	}
 
 	public TextFormatter getRecoveryKeyTextFormatter() {

+ 9 - 1
src/main/resources/fxml/preferences.fxml

@@ -9,7 +9,7 @@
 		 fx:controller="org.cryptomator.ui.preferences.PreferencesController"
 		 minWidth="-Infinity"
 		 maxWidth="-Infinity"
-		 prefWidth="500"
+		 prefWidth="650"
 		 tabMinWidth="60"
 		 tabClosingPolicy="UNAVAILABLE"
 		 tabDragPolicy="FIXED">
@@ -22,6 +22,14 @@
 				<fx:include source="preferences_general.fxml"/>
 			</content>
 		</Tab>
+		<Tab fx:id="interfaceTab" id="INTERFACE" text="%preferences.interface">
+			<graphic>
+				<FontAwesome5IconView glyph="EYE"/>
+			</graphic>
+			<content>
+				<fx:include source="preferences_interface.fxml"/>
+			</content>
+		</Tab>
 		<Tab fx:id="volumeTab" id="VOLUME" text="%preferences.volume">
 			<graphic>
 				<FontAwesome5IconView glyph="HDD"/>

+ 2 - 2
src/main/resources/fxml/preferences_about.fxml

@@ -11,9 +11,9 @@
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.preferences.AboutController"
-	  spacing="18">
+	  spacing="24">
 	<padding>
-		<Insets topRightBottomLeft="12"/>
+		<Insets topRightBottomLeft="24"/>
 	</padding>
 	<children>
 		<HBox spacing="12" VBox.vgrow="NEVER">

+ 2 - 2
src/main/resources/fxml/preferences_contribute.fxml

@@ -13,9 +13,9 @@
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.preferences.SupporterCertificateController"
-	  spacing="18">
+	  spacing="24">
 	<padding>
-		<Insets topRightBottomLeft="12"/>
+		<Insets topRightBottomLeft="24"/>
 	</padding>
 	<children>
 		<StackPane VBox.vgrow="NEVER" prefHeight="60">

+ 10 - 29
src/main/resources/fxml/preferences_general.fxml

@@ -5,54 +5,35 @@
 <?import javafx.scene.control.ChoiceBox?>
 <?import javafx.scene.control.Hyperlink?>
 <?import javafx.scene.control.Label?>
-<?import javafx.scene.control.RadioButton?>
 <?import javafx.scene.control.ToggleGroup?>
 <?import javafx.scene.layout.HBox?>
 <?import javafx.scene.layout.VBox?>
+<?import javafx.scene.layout.Region?>
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.preferences.GeneralPreferencesController"
-	  spacing="6">
+	  spacing="12">
 	<fx:define>
 		<ToggleGroup fx:id="nodeOrientation"/>
 	</fx:define>
 	<padding>
-		<Insets topRightBottomLeft="12"/>
+		<Insets topRightBottomLeft="24"/>
 	</padding>
 	<children>
-		<HBox spacing="6" alignment="CENTER_LEFT">
-			<Label text="%preferences.general.theme"/>
-			<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
-			<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.general.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
-		</HBox>
-
-		<HBox spacing="6" alignment="CENTER_LEFT">
-			<Label text="%preferences.general.interfaceOrientation" HBox.hgrow="NEVER"/>
-			<RadioButton fx:id="nodeOrientationLtr" text="%preferences.general.interfaceOrientation.ltr" alignment="CENTER_LEFT" toggleGroup="${nodeOrientation}"/>
-			<RadioButton fx:id="nodeOrientationRtl" text="%preferences.general.interfaceOrientation.rtl" alignment="CENTER_RIGHT" toggleGroup="${nodeOrientation}"/>
-		</HBox>
-
-		<CheckBox fx:id="showMinimizeButtonCheckbox" text="%preferences.general.showMinimizeButton" visible="${controller.trayMenuInitialized}" managed="${controller.trayMenuInitialized}"/>
-
-		<CheckBox fx:id="showTrayIconCheckbox" text="%preferences.general.showTrayIcon" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
+		<CheckBox fx:id="autoStartCheckbox" text="%preferences.general.autoStart" visible="${controller.autoStartSupported}" managed="${controller.autoStartSupported}" onAction="#toggleAutoStart"/>
 
 		<CheckBox fx:id="startHiddenCheckbox" text="%preferences.general.startHidden" />
 
-		<HBox spacing="6" alignment="CENTER_LEFT">
-			<Label text="%preferences.general.language"/>
-			<ChoiceBox fx:id="preferredLanguageChoiceBox"/>
+		<HBox spacing="12" alignment="CENTER_LEFT">
+			<Label text="%preferences.general.keychainBackend"/>
+			<ChoiceBox fx:id="keychainBackendChoiceBox"/>
 		</HBox>
 
-		<HBox spacing="6" alignment="CENTER_LEFT">
+		<Region VBox.vgrow="ALWAYS"/>
+
+		<HBox spacing="12" alignment="CENTER_LEFT">
 			<CheckBox fx:id="debugModeCheckbox" text="%preferences.general.debugLogging"/>
 			<Hyperlink styleClass="hyperlink-underline" text="%preferences.general.debugDirectory" onAction="#showLogfileDirectory"/>
 		</HBox>
-
-		<HBox spacing="6" alignment="CENTER_LEFT">
-			<Label text="%preferences.general.keychainBackend"/>
-			<ChoiceBox fx:id="keychainBackendChoiceBox"/>
-		</HBox>
-
-		<CheckBox fx:id="autoStartCheckbox" text="%preferences.general.autoStart" visible="${controller.autoStartSupported}" managed="${controller.autoStartSupported}" onAction="#toggleAutoStart"/>
 	</children>
 </VBox>

+ 45 - 0
src/main/resources/fxml/preferences_interface.fxml

@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.CheckBox?>
+<?import javafx.scene.control.ChoiceBox?>
+<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.RadioButton?>
+<?import javafx.scene.control.ToggleGroup?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.VBox?>
+<VBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.preferences.InterfacePreferencesController"
+	  spacing="12">
+	<fx:define>
+		<ToggleGroup fx:id="nodeOrientation"/>
+	</fx:define>
+	<padding>
+		<Insets topRightBottomLeft="24"/>
+	</padding>
+	<children>
+		<HBox spacing="12" alignment="CENTER_LEFT">
+			<Label text="%preferences.interface.theme"/>
+			<ChoiceBox fx:id="themeChoiceBox" disable="${!controller.licenseHolder.validLicense}"/>
+			<Hyperlink styleClass="hyperlink-underline,hyperlink-muted" text="%preferences.interface.unlockThemes" onAction="#showContributeTab" visible="${!controller.licenseHolder.validLicense}" managed="${!controller.licenseHolder.validLicense}"/>
+		</HBox>
+
+		<HBox spacing="12" alignment="CENTER_LEFT">
+			<Label text="%preferences.interface.language"/>
+			<ChoiceBox fx:id="preferredLanguageChoiceBox"/>
+		</HBox>
+
+		<HBox spacing="12" alignment="CENTER_LEFT">
+			<Label text="%preferences.interface.interfaceOrientation" HBox.hgrow="NEVER"/>
+			<RadioButton fx:id="nodeOrientationLtr" text="%preferences.interface.interfaceOrientation.ltr" alignment="CENTER_LEFT" toggleGroup="${nodeOrientation}"/>
+			<RadioButton fx:id="nodeOrientationRtl" text="%preferences.interface.interfaceOrientation.rtl" alignment="CENTER_RIGHT" toggleGroup="${nodeOrientation}"/>
+		</HBox>
+
+
+		<CheckBox fx:id="showMinimizeButtonCheckbox" text="%preferences.interface.showMinimizeButton" visible="${controller.trayMenuInitialized}" managed="${controller.trayMenuInitialized}"/>
+
+		<CheckBox fx:id="showTrayIconCheckbox" text="%preferences.interface.showTrayIcon" visible="${controller.trayMenuSupported}" managed="${controller.trayMenuSupported}"/>
+	</children>
+</VBox>

+ 3 - 3
src/main/resources/fxml/preferences_updates.fxml

@@ -11,19 +11,19 @@
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.preferences.UpdatesPreferencesController"
-	  spacing="6">
+	  spacing="12">
 	<fx:define>
 		<FormattedString fx:id="linkLabel" format="%preferences.updates.updateAvailable" arg1="${controller.latestVersion}"/>
 	</fx:define>
 	<padding>
-		<Insets topRightBottomLeft="12"/>
+		<Insets topRightBottomLeft="24"/>
 	</padding>
 	<children>
 		<FormattedLabel format="%preferences.updates.currentVersion" arg1="${controller.currentVersion}" textAlignment="CENTER" wrapText="true"/>
 
 		<CheckBox fx:id="checkForUpdatesCheckbox" text="%preferences.updates.autoUpdateCheck"/>
 
-		<VBox alignment="CENTER" spacing="6">
+		<VBox alignment="CENTER" spacing="12">
 			<Button text="%preferences.updates.checkNowBtn" defaultButton="true" onAction="#checkNow" contentDisplay="${controller.checkForUpdatesButtonState}">
 				<graphic>
 					<FontAwesome5Spinner fx:id="spinner" glyphSize="12"/>

+ 5 - 5
src/main/resources/fxml/preferences_volume.fxml

@@ -10,23 +10,23 @@
 <VBox xmlns:fx="http://javafx.com/fxml"
 	  xmlns="http://javafx.com/javafx"
 	  fx:controller="org.cryptomator.ui.preferences.VolumePreferencesController"
-	  spacing="6">
+	  spacing="12">
 	<padding>
-		<Insets topRightBottomLeft="12"/>
+		<Insets topRightBottomLeft="24"/>
 	</padding>
 	<children>
-		<HBox spacing="6" alignment="CENTER_LEFT">
+		<HBox spacing="12" alignment="CENTER_LEFT">
 			<Label text="%preferences.volume.type"/>
 			<ChoiceBox fx:id="volumeTypeChoiceBox"/>
 		</HBox>
 
-		<HBox spacing="6" alignment="CENTER_LEFT" visible="${controller.showWebDavSettings}">
+		<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.showWebDavSettings}">
 			<Label text="%preferences.volume.webdav.port"/>
 			<NumericTextField fx:id="webDavPortField"/>
 			<Button text="%generic.button.apply" fx:id="changeWebDavPortButton" onAction="#doChangeWebDavPort"/>
 		</HBox>
 
-		<HBox spacing="6" alignment="CENTER_LEFT" visible="${controller.showWebDavScheme}">
+		<HBox spacing="12" alignment="CENTER_LEFT" visible="${controller.showWebDavScheme}">
 			<Label text="%preferences.volume.webdav.scheme"/>
 			<ChoiceBox fx:id="webDavUrlSchemeChoiceBox" maxWidth="Infinity"/>
 		</HBox>

+ 14 - 12
src/main/resources/i18n/strings.properties

@@ -191,23 +191,25 @@ health.fix.failTip=Fix failed, see log for details
 preferences.title=Preferences
 ## General
 preferences.general=General
-preferences.general.theme=Look & Feel
-preferences.general.theme.automatic=Automatic
-preferences.general.theme.light=Light
-preferences.general.theme.dark=Dark
-preferences.general.unlockThemes=Unlock dark mode
-preferences.general.showMinimizeButton=Show minimize button
-preferences.general.showTrayIcon=Show tray icon (requires restart)
 preferences.general.startHidden=Hide window when starting Cryptomator
-preferences.general.language=Language (requires restart)
-preferences.general.language.auto=System Default
 preferences.general.debugLogging=Enable debug logging
 preferences.general.debugDirectory=Reveal log files
 preferences.general.autoStart=Launch Cryptomator on system start
 preferences.general.keychainBackend=Store passwords with
-preferences.general.interfaceOrientation=Interface Orientation
-preferences.general.interfaceOrientation.ltr=Left to Right
-preferences.general.interfaceOrientation.rtl=Right to Left
+## Interface
+preferences.interface=Interface
+preferences.interface.theme=Look & Feel
+preferences.interface.theme.automatic=Automatic
+preferences.interface.theme.dark=Dark
+preferences.interface.theme.light=Light
+preferences.interface.unlockThemes=Unlock dark mode
+preferences.interface.language=Language (requires restart)
+preferences.interface.language.auto=System Default
+preferences.interface.interfaceOrientation=Interface Orientation
+preferences.interface.interfaceOrientation.ltr=Left to Right
+preferences.interface.interfaceOrientation.rtl=Right to Left
+preferences.interface.showMinimizeButton=Show minimize button
+preferences.interface.showTrayIcon=Show tray icon (requires restart)
 ## Volume
 preferences.volume=Virtual Drive
 preferences.volume.type=Volume Type

+ 1 - 1
src/main/resources/license/THIRD-PARTY.txt

@@ -75,7 +75,7 @@ Cryptomator uses 40 third-party dependencies under the following licenses:
 			- java jwt (com.auth0:java-jwt:3.19.1 - https://github.com/auth0/java-jwt)
 			- jnr-x86asm (com.github.jnr:jnr-x86asm:1.0.2 - http://github.com/jnr/jnr-x86asm)
 			- jnr-fuse (com.github.serceman:jnr-fuse:0.5.7 - https://github.com/SerCeMan/jnr-fuse)
-			- zxcvbn4j (com.nulab-inc:zxcvbn:1.5.2 - https://github.com/nulab/zxcvbn4j)
+			- zxcvbn4j (com.nulab-inc:zxcvbn:1.6.0 - https://github.com/nulab/zxcvbn4j)
 			- SLF4J API Module (org.slf4j:slf4j-api:1.7.36 - http://www.slf4j.org)
         The BSD 2-Clause License:
 			- EasyBind (com.tobiasdiez:easybind:2.2 - https://github.com/tobiasdiez/EasyBind)

+ 0 - 1
src/test/java/org/cryptomator/ui/common/PasswordStrengthUtilTest.java

@@ -22,7 +22,6 @@ public class PasswordStrengthUtilTest {
 	}
 
 	@Test
-	@Disabled("waiting on upstream fix") // https://github.com/nulab/zxcvbn4j/issues/54
 	public void testIssue979() {
 		PasswordStrengthUtil util = new PasswordStrengthUtil(Mockito.mock(ResourceBundle.class), Mockito.mock(Environment.class));
 		int result1 = util.computeRate("backed derrick buckling mountains glove client procedures desire destination sword hidden ram");

+ 18 - 0
src/test/java/org/cryptomator/ui/recoverykey/RecoveryKeyFactoryTest.java

@@ -7,10 +7,13 @@ import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.function.Predicate;
 
 public class RecoveryKeyFactoryTest {
 
@@ -75,4 +78,19 @@ public class RecoveryKeyFactoryTest {
 		Assertions.assertTrue(result);
 	}
 
+	@ParameterizedTest(name = "passing validation = {0}")
+	@DisplayName("validateRecoveryKey() with extended validation")
+	@ValueSource(booleans = {true, false})
+	public void testValidateValidateRecoveryKeyWithValidKey(boolean extendedValidationResult) {
+		Predicate<byte[]> validator = Mockito.mock(Predicate.class);
+		Mockito.doReturn(extendedValidationResult).when(validator).test(Mockito.any());
+		boolean result = inTest.validateRecoveryKey("""
+				pathway lift abuse plenty export texture gentleman landscape beyond ceiling around leaf cafe charity \
+				border breakdown victory surely computer cat linger restrict infer crowd live computer true written amazed \
+				investor boot depth left theory snow whereby terminal weekly reject happiness circuit partial cup ad \
+				""", validator);
+		Mockito.verify(validator).test(Mockito.any());
+		Assertions.assertEquals(extendedValidationResult, result);
+	}
+
 }