diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index a82e89ba7..abb1b4a92 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -1,7 +1,14 @@
name: Bug Report
description: Create a report to help us improve
-labels: ["type:bug"]
+type: "Bug"
body:
+ - type: input
+ id: summary
+ attributes:
+ label: Summary
+ placeholder: Please summarize your problem.
+ validations:
+ required: true
- type: checkboxes
id: terms
attributes:
@@ -11,13 +18,6 @@ body:
required: true
- label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md)
required: true
- - type: input
- id: summary
- attributes:
- label: Summary
- placeholder: Please summarize your problem.
- validations:
- required: true
- type: textarea
id: software-versions
attributes:
@@ -97,4 +97,4 @@ body:
id: further-info
attributes:
label: Anything else?
- description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?
+ description: Links? References? Screenshots? Configurations? Any data that might be necessary to reproduce the issue?
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml
index 652f27234..826f3410a 100644
--- a/.github/ISSUE_TEMPLATE/feature.yml
+++ b/.github/ISSUE_TEMPLATE/feature.yml
@@ -1,7 +1,14 @@
name: Feature Request
description: Suggest an idea for this project
-labels: ["type:feature-request"]
+type: "Feature"
body:
+ - type: input
+ id: summary
+ attributes:
+ label: Summary
+ placeholder: Please summarize your feature request.
+ validations:
+ required: true
- type: checkboxes
id: terms
attributes:
@@ -11,13 +18,6 @@ body:
required: true
- label: I agree to follow this project's [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/.github/CODE_OF_CONDUCT.md)
required: true
- - type: input
- id: summary
- attributes:
- label: Summary
- placeholder: Please summarize your feature request.
- validations:
- required: true
- type: textarea
id: motivation
attributes:
diff --git a/.github/workflows/debian.yml b/.github/workflows/debian.yml
index 16c5bc530..fa021d441 100644
--- a/.github/workflows/debian.yml
+++ b/.github/workflows/debian.yml
@@ -28,7 +28,7 @@ env:
jobs:
build:
name: Build Debian Package
- runs-on: ubuntu-20.04
+ runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- id: versions
diff --git a/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg b/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg
index f8e79de1b..be0642364 100644
--- a/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg
+++ b/dist/linux/common/org.cryptomator.Cryptomator.tray-unlocked.svg
@@ -1,12 +1,16 @@
\ No newline at end of file
diff --git a/dist/linux/common/org.cryptomator.Cryptomator.tray.svg b/dist/linux/common/org.cryptomator.Cryptomator.tray.svg
index 205998020..62cccb911 100644
--- a/dist/linux/common/org.cryptomator.Cryptomator.tray.svg
+++ b/dist/linux/common/org.cryptomator.Cryptomator.tray.svg
@@ -1,8 +1,10 @@
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 5aef0b15b..0e742157c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -33,10 +33,10 @@
org.ow2.asm,org.apache.jackrabbit,org.apache.httpcomponents
- 2.8.0
+ 2.9.0-beta2
1.5.0
1.3.0
- 1.2.4
+ 1.3.0
1.5.2
5.0.2
2.0.7
@@ -46,7 +46,7 @@
2.55
2.2
2.18.2
- 23.0.1
+ 23.0.2
4.4.0
9.37.3
1.5.16
diff --git a/src/main/java/org/cryptomator/JavaFXUtil.java b/src/main/java/org/cryptomator/JavaFXUtil.java
new file mode 100644
index 000000000..e1ec90587
--- /dev/null
+++ b/src/main/java/org/cryptomator/JavaFXUtil.java
@@ -0,0 +1,22 @@
+package org.cryptomator;
+
+import javafx.application.Platform;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+public class JavaFXUtil {
+
+ private JavaFXUtil() {}
+
+ public static boolean startPlatform() throws InterruptedException {
+ CountDownLatch latch = new CountDownLatch(1);
+ try {
+ Platform.startup(latch::countDown);
+ } catch (IllegalStateException e) {
+ //already initialized
+ latch.countDown();
+ }
+ return latch.await(5, TimeUnit.SECONDS);
+ }
+
+}
diff --git a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
index ac03e5ed6..04a46e742 100644
--- a/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
+++ b/src/main/java/org/cryptomator/common/keychain/KeychainManager.java
@@ -2,6 +2,7 @@ package org.cryptomator.common.keychain;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
+import org.cryptomator.common.Passphrase;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
@@ -13,20 +14,24 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
@Singleton
public class KeychainManager implements KeychainAccessProvider {
private final ObjectExpression keychain;
private final LoadingCache passphraseStoredProperties;
+ private final ReentrantReadWriteLock lock;
@Inject
KeychainManager(ObjectExpression selectedKeychain) {
this.keychain = selectedKeychain;
this.passphraseStoredProperties = Caffeine.newBuilder() //
- .weakValues() //
+ .softValues() //
.build(this::createStoredPassphraseProperty);
keychain.addListener(ignored -> passphraseStoredProperties.invalidateAll());
+ this.lock = new ReentrantReadWriteLock(false);
}
private KeychainAccessProvider getKeychainOrFail() throws KeychainAccessException {
@@ -42,29 +47,59 @@ public class KeychainManager implements KeychainAccessProvider {
return getClass().getName();
}
+ @Override
+ public void storePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
+ storePassphrase(key, displayName, passphrase, true);
+ }
+
+ //TODO: remove ignored parameter once the API is fixed
@Override
public void storePassphrase(String key, String displayName, CharSequence passphrase, boolean ignored) throws KeychainAccessException {
- getKeychainOrFail().storePassphrase(key, displayName, passphrase);
+ try {
+ lock.writeLock().lock();
+ var kc = getKeychainOrFail();
+ //this is the only keychain actually using the parameter
+ var usesOSAuth = (kc.getClass().getName().equals("org.cryptomator.macos.keychain.TouchIdKeychainAccess"));
+ kc.storePassphrase(key, displayName, passphrase, usesOSAuth);
+ } finally {
+ lock.writeLock().unlock();
+ }
setPassphraseStored(key, true);
}
@Override
public char[] loadPassphrase(String key) throws KeychainAccessException {
- char[] passphrase = getKeychainOrFail().loadPassphrase(key);
+ char[] passphrase = null;
+ try {
+ lock.readLock().lock();
+ passphrase = getKeychainOrFail().loadPassphrase(key);
+ } finally {
+ lock.readLock().unlock();
+ }
setPassphraseStored(key, passphrase != null);
return passphrase;
}
@Override
public void deletePassphrase(String key) throws KeychainAccessException {
- getKeychainOrFail().deletePassphrase(key);
+ try {
+ lock.writeLock().lock();
+ getKeychainOrFail().deletePassphrase(key);
+ } finally {
+ lock.writeLock().unlock();
+ }
setPassphraseStored(key, false);
}
@Override
public void changePassphrase(String key, String displayName, CharSequence passphrase) throws KeychainAccessException {
if (isPassphraseStored(key)) {
- getKeychainOrFail().changePassphrase(key, displayName, passphrase);
+ try {
+ lock.writeLock().lock();
+ getKeychainOrFail().changePassphrase(key, displayName, passphrase);
+ } finally {
+ lock.writeLock().unlock();
+ }
setPassphraseStored(key, true);
}
}
@@ -101,13 +136,11 @@ public class KeychainManager implements KeychainAccessProvider {
}
private void setPassphraseStored(String key, boolean value) {
- BooleanProperty property = passphraseStoredProperties.getIfPresent(key);
- if (property != null) {
- if (Platform.isFxApplicationThread()) {
- property.set(value);
- } else {
- Platform.runLater(() -> property.set(value));
- }
+ BooleanProperty property = passphraseStoredProperties.get(key, _ -> new SimpleBooleanProperty(value));
+ if (Platform.isFxApplicationThread()) {
+ property.set(value);
+ } else {
+ Platform.runLater(() -> property.set(value));
}
}
@@ -134,4 +167,22 @@ public class KeychainManager implements KeychainAccessProvider {
}
}
+ public ObjectExpression getKeychainImplementation() {
+ return this.keychain;
+ }
+
+ public static void migrate(KeychainAccessProvider oldProvider, KeychainAccessProvider newProvider, Map idsAndNames) throws KeychainAccessException {
+ if (oldProvider instanceof KeychainManager || newProvider instanceof KeychainManager) {
+ throw new IllegalArgumentException("KeychainManger must not be the source or target of migration");
+ }
+ for (var entry : idsAndNames.entrySet()) {
+ var passphrase = oldProvider.loadPassphrase(entry.getKey());
+ if (passphrase != null) {
+ var wrapper = new Passphrase(passphrase);
+ oldProvider.deletePassphrase(entry.getKey()); //we cannot apply "first-write-then-delete" pattern here, since we can potentially write to the same passphrase store (e.g., touchID and regular keychain)
+ newProvider.storePassphrase(entry.getKey(), entry.getValue(), wrapper);
+ wrapper.destroy();
+ }
+ }
+ }
}
diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
index 2ffda4d73..a25384c78 100644
--- a/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
+++ b/src/main/java/org/cryptomator/ui/addvaultwizard/ReadmeGenerator.java
@@ -76,8 +76,10 @@ public class ReadmeGenerator {
input.chars().forEachOrdered(c -> {
if (c < 128) {
sb.append((char) c);
+ } else if (c <= 0xFF) {
+ sb.append("\\'").append(String.format("%02X", c));
} else if (c < 0xFFFF) {
- sb.append("\\u").append(c);
+ sb.append("\\uc1\\u").append(c);
}
});
}
diff --git a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java
index 5e625deb2..807c3a9f1 100644
--- a/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java
+++ b/src/main/java/org/cryptomator/ui/dialogs/Dialogs.java
@@ -38,7 +38,7 @@ public class Dialogs {
.setMessageKey("removeVault.message") //
.setDescriptionKey("removeVault.description") //
.setIcon(FontAwesome5Icon.QUESTION) //
- .setOkButtonKey("removeVault.confirmBtn") //
+ .setOkButtonKey("generic.button.remove") //
.setCancelButtonKey("generic.button.cancel") //
.setOkAction(stage -> {
LOG.debug("Removing vault {}.", vault.getDisplayName());
@@ -64,7 +64,7 @@ public class Dialogs {
.setMessageKey("removeCert.message") //
.setDescriptionKey("removeCert.description") //
.setIcon(FontAwesome5Icon.QUESTION) //
- .setOkButtonKey("removeCert.confirmBtn") //
+ .setOkButtonKey("generic.button.remove") //
.setCancelButtonKey("generic.button.cancel") //
.setOkAction(stage -> {
settings.licenseKey.set(null);
diff --git a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java
index 84d9e4f75..08f77849e 100644
--- a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java
+++ b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialog.java
@@ -31,8 +31,9 @@ public class SimpleDialog {
FxmlLoaderFactory loaderFactory = FxmlLoaderFactory.forController( //
new SimpleDialogController(resolveText(builder.messageKey, null), //
resolveText(builder.descriptionKey, null), //
- builder.icon, resolveText(builder.okButtonKey, null), //
- resolveText(builder.cancelButtonKey, null), //
+ builder.icon, //
+ resolveText(builder.okButtonKey, null), //
+ builder.cancelButtonKey != null ? resolveText(builder.cancelButtonKey, null) : null, //
() -> builder.okAction.accept(dialogStage), //
() -> builder.cancelAction.accept(dialogStage)), //
Scene::new, builder.resourceBundle);
@@ -67,7 +68,6 @@ public class SimpleDialog {
private String descriptionKey;
private String okButtonKey;
private String cancelButtonKey;
-
private FontAwesome5Icon icon;
private Consumer okAction = Stage::close;
private Consumer cancelAction = Stage::close;
@@ -128,7 +128,6 @@ public class SimpleDialog {
Objects.requireNonNull(messageKey, "SimpleDialog messageKey must be set.");
Objects.requireNonNull(descriptionKey, "SimpleDialog descriptionKey must be set.");
Objects.requireNonNull(okButtonKey, "SimpleDialog okButtonKey must be set.");
- Objects.requireNonNull(cancelButtonKey, "SimpleDialog cancelButtonKey must be set.");
try {
return new SimpleDialog(this);
diff --git a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java
index 0eee1b308..bbf590145 100644
--- a/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java
+++ b/src/main/java/org/cryptomator/ui/dialogs/SimpleDialogController.java
@@ -14,6 +14,7 @@ public class SimpleDialogController implements FxController {
private final String cancelButtonText;
private final Runnable okAction;
private final Runnable cancelAction;
+ private final boolean cancelButtonVisible;
public SimpleDialogController(String message, String description, FontAwesome5Icon icon, String okButtonText, String cancelButtonText, Runnable okAction, Runnable cancelAction) {
this.message = message;
@@ -23,6 +24,11 @@ public class SimpleDialogController implements FxController {
this.cancelButtonText = cancelButtonText;
this.okAction = okAction;
this.cancelAction = cancelAction;
+ this.cancelButtonVisible = cancelButtonText != null && !cancelButtonText.isEmpty();
+ }
+
+ public boolean isCancelButtonVisible() {
+ return cancelButtonVisible;
}
public String getMessage() {
diff --git a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
index 68877430a..a13f3e223 100644
--- a/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
+++ b/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java
@@ -112,12 +112,12 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy {
}
private void savePasswordToSystemkeychain(Passphrase passphrase) {
- if (keychain.isSupported()) {
- try {
+ try {
+ if (keychain.isSupported() && !keychain.getPassphraseStoredProperty(vault.getId()).get()) {
keychain.storePassphrase(vault.getId(), vault.getDisplayName(), passphrase);
- } catch (KeychainAccessException e) {
- LOG.error("Failed to store passphrase in system keychain.", e);
}
+ } catch (KeychainAccessException e) {
+ LOG.error("Failed to store passphrase in system keychain.", e);
}
}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
index 13412dd27..c6e084518 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/MainWindowController.java
@@ -19,9 +19,11 @@ import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.fxml.FXML;
+import javafx.geometry.Rectangle2D;
import javafx.scene.layout.StackPane;
import javafx.stage.Screen;
import javafx.stage.Stage;
+import javafx.stage.WindowEvent;
@MainWindowScoped
public class MainWindowController implements FxController {
@@ -68,18 +70,15 @@ public class MainWindowController implements FxController {
int y = settings.windowYPosition.get();
int width = settings.windowWidth.get();
int height = settings.windowHeight.get();
- if (windowPositionSaved(x, y, width, height) ) {
- if(isWithinDisplayBounds(x, y, width, height)) { //use stored window position
- window.setX(x);
- window.setY(y);
- window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth()));
- window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight()));
- } else if(isWithinDisplayBounds((int) window.getX(), (int) window.getY(), width, height)) { //just reset position of upper left corner, keep window size
- window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth()));
- window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight()));
- } //else reset window completely
+ if (windowPositionSaved(x, y, width, height)) {
+ window.setX(x);
+ window.setY(y);
+ window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth()));
+ window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight()));
}
+ window.setOnShowing(this::checkDisplayBounds);
+
settings.windowXPosition.bind(window.xProperty());
settings.windowYPosition.bind(window.yProperty());
settings.windowWidth.bind(window.widthProperty());
@@ -90,6 +89,29 @@ public class MainWindowController implements FxController {
return x != 0 || y != 0 || width != 0 || height != 0;
}
+ private void checkDisplayBounds(WindowEvent windowEvent) {
+ int x = settings.windowXPosition.get();
+ int y = settings.windowYPosition.get();
+ int width = settings.windowWidth.get();
+ int height = settings.windowHeight.get();
+
+ Rectangle2D primaryScreenBounds = Screen.getPrimary().getBounds();
+ if (!isWithinDisplayBounds(x, y, width, height)) { //use stored window position
+ LOG.debug("Resetting window position due to insufficient screen overlap");
+ var centeredX = (primaryScreenBounds.getWidth() - window.getMinWidth()) / 2;
+ var centeredY = (primaryScreenBounds.getHeight() - window.getMinHeight()) / 2;
+ //check if we can keep width and height
+ if (isWithinDisplayBounds((int) centeredX, (int) centeredY, width, height)) {
+ //if so, keep window size
+ window.setWidth(Math.clamp(width, window.getMinWidth(), window.getMaxWidth()));
+ window.setHeight(Math.clamp(height, window.getMinHeight(), window.getMaxHeight()));
+ }
+ //reset position of upper left corner
+ window.setX(centeredX);
+ window.setY(centeredY);
+ }
+ }
+
private boolean isWithinDisplayBounds(int x, int y, int width, int height) {
// define a rect which is inset on all sides from the window's rect:
final int shrinkedX = x + 20; // 20px left
@@ -144,7 +166,7 @@ public class MainWindowController implements FxController {
return updateAvailable.get();
}
- public BooleanBinding licenseValidProperty(){
+ public BooleanBinding licenseValidProperty() {
return licenseHolder.validLicenseProperty();
}
diff --git a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
index 8212f598f..6a7c046f2 100644
--- a/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
+++ b/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java
@@ -8,9 +8,9 @@ import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab;
import org.cryptomator.ui.vaultoptions.VaultOptionsComponent;
import javax.inject.Inject;
+import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.stage.Stage;
@@ -21,7 +21,6 @@ public class VaultDetailLockedController implements FxController {
private final ReadOnlyObjectProperty vault;
private final FxApplicationWindows appWindows;
private final VaultOptionsComponent.Factory vaultOptionsWindow;
- private final KeychainManager keychain;
private final Stage mainWindow;
private final ObservableValue passwordSaved;
@@ -30,13 +29,11 @@ public class VaultDetailLockedController implements FxController {
this.vault = vault;
this.appWindows = appWindows;
this.vaultOptionsWindow = vaultOptionsWindow;
- this.keychain = keychain;
this.mainWindow = mainWindow;
- if (keychain.isSupported() && !keychain.isLocked()) {
- this.passwordSaved = vault.flatMap(v -> keychain.getPassphraseStoredProperty(v.getId())).orElse(false);
- } else {
- this.passwordSaved = new SimpleBooleanProperty(false);
- }
+ this.passwordSaved = Bindings.createBooleanBinding(() -> {
+ var v = vault.get();
+ return v != null && keychain.getPassphraseStoredProperty(v.getId()).getValue();
+ }, vault, keychain.getKeychainImplementation());
}
@FXML
diff --git a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
index 800a292a9..4505f412d 100644
--- a/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
+++ b/src/main/java/org/cryptomator/ui/preferences/GeneralPreferencesController.java
@@ -1,10 +1,13 @@
package org.cryptomator.ui.preferences;
+import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.common.Environment;
+import org.cryptomator.common.keychain.KeychainManager;
import org.cryptomator.common.settings.Settings;
import org.cryptomator.integrations.autostart.AutoStartProvider;
import org.cryptomator.integrations.autostart.ToggleAutoStartFailedException;
import org.cryptomator.integrations.common.NamedServiceProvider;
+import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.cryptomator.integrations.keychain.KeychainAccessProvider;
import org.cryptomator.integrations.quickaccess.QuickAccessService;
import org.cryptomator.ui.common.FxController;
@@ -14,6 +17,7 @@ import org.slf4j.LoggerFactory;
import javax.inject.Inject;
import javafx.application.Application;
+import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
@@ -23,6 +27,10 @@ import javafx.stage.Stage;
import javafx.util.StringConverter;
import java.util.List;
import java.util.Optional;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionStage;
+import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
@PreferencesScoped
public class GeneralPreferencesController implements FxController {
@@ -36,6 +44,8 @@ public class GeneralPreferencesController implements FxController {
private final Application application;
private final Environment environment;
private final List keychainAccessProviders;
+ private final KeychainManager keychain;
+ private final ExecutorService backgroundExecutor;
private final FxApplicationWindows appWindows;
public CheckBox useKeychainCheckbox;
public ChoiceBox keychainBackendChoiceBox;
@@ -47,12 +57,18 @@ public class GeneralPreferencesController implements FxController {
public CheckBox autoStartCheckbox;
public ToggleGroup nodeOrientation;
+ private CompletionStage keychainMigrations = CompletableFuture.completedFuture(null);
+
@Inject
- GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, List keychainAccessProviders, Application application, Environment environment, FxApplicationWindows appWindows) {
+ GeneralPreferencesController(@PreferencesWindow Stage window, Settings settings, Optional autoStartProvider, //
+ List keychainAccessProviders, KeychainManager keychain, Application application, //
+ Environment environment, FxApplicationWindows appWindows, ExecutorService backgroundExecutor) {
this.window = window;
this.settings = settings;
this.autoStartProvider = autoStartProvider;
this.keychainAccessProviders = keychainAccessProviders;
+ this.keychain = keychain;
+ this.backgroundExecutor = backgroundExecutor;
this.quickAccessServices = QuickAccessService.get().toList();
this.application = application;
this.environment = environment;
@@ -73,6 +89,7 @@ public class GeneralPreferencesController implements FxController {
Bindings.bindBidirectional(settings.keychainProvider, keychainBackendChoiceBox.valueProperty(), keychainSettingsConverter);
useKeychainCheckbox.selectedProperty().bindBidirectional(settings.useKeychain);
keychainBackendChoiceBox.disableProperty().bind(useKeychainCheckbox.selectedProperty().not());
+ keychainBackendChoiceBox.valueProperty().addListener(this::migrateKeychainEntries);
useQuickAccessCheckbox.selectedProperty().bindBidirectional(settings.useQuickAccess);
var quickAccessSettingsConverter = new ServiceToSettingsConverter<>(quickAccessServices);
@@ -83,6 +100,25 @@ public class GeneralPreferencesController implements FxController {
quickAccessServiceChoiceBox.disableProperty().bind(useQuickAccessCheckbox.selectedProperty().not());
}
+ private void migrateKeychainEntries(Observable observable, KeychainAccessProvider oldProvider, KeychainAccessProvider newProvider) {
+ //currently, we only migrate on macOS (touchID vs regular keychain)
+ if (SystemUtils.IS_OS_MAC) {
+ var idsAndNames = settings.directories.stream().collect(Collectors.toMap(vs -> vs.id, vs -> vs.displayName.getValue()));
+ if (!idsAndNames.isEmpty()) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Migrating keychain entries {} from {} to {}", idsAndNames.keySet(), oldProvider.displayName(), newProvider.displayName());
+ }
+ keychainMigrations = keychainMigrations.thenRunAsync(() -> {
+ try {
+ KeychainManager.migrate(oldProvider, newProvider, idsAndNames);
+ } catch (KeychainAccessException e) {
+ LOG.warn("Failed to migrate all entries from {} to {}", oldProvider.displayName(), newProvider.displayName(), e);
+ }
+ }, backgroundExecutor);
+ }
+ }
+ }
+
public boolean isAutoStartSupported() {
return autoStartProvider.isPresent();
}
diff --git a/src/main/resources/css/dark_theme.css b/src/main/resources/css/dark_theme.css
index abb6efe57..4d3db3968 100644
--- a/src/main/resources/css/dark_theme.css
+++ b/src/main/resources/css/dark_theme.css
@@ -359,6 +359,12 @@
-fx-background-color: PRIMARY;
}
+.notification-debug:hover .notification-label,
+.notification-update:hover .notification-label,
+.notification-support:hover .notification-label {
+ -fx-underline:true;
+}
+
/*******************************************************************************
* *
* ScrollBar *
diff --git a/src/main/resources/css/light_theme.css b/src/main/resources/css/light_theme.css
index 516dd0b26..11cb1a9df 100644
--- a/src/main/resources/css/light_theme.css
+++ b/src/main/resources/css/light_theme.css
@@ -358,6 +358,12 @@
-fx-background-color: PRIMARY;
}
+.notification-debug:hover .notification-label,
+.notification-update:hover .notification-label,
+.notification-support:hover .notification-label {
+ -fx-underline:true;
+}
+
/*******************************************************************************
* *
* ScrollBar *
diff --git a/src/main/resources/fxml/simple_dialog.fxml b/src/main/resources/fxml/simple_dialog.fxml
index 32ad63abf..0e9b01776 100644
--- a/src/main/resources/fxml/simple_dialog.fxml
+++ b/src/main/resources/fxml/simple_dialog.fxml
@@ -1,16 +1,16 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
diff --git a/src/main/resources/i18n/strings.properties b/src/main/resources/i18n/strings.properties
index 20c6c7fad..b3ecfba79 100644
--- a/src/main/resources/i18n/strings.properties
+++ b/src/main/resources/i18n/strings.properties
@@ -109,7 +109,6 @@ addvaultwizard.success.unlockNow=Unlock Now
removeVault.title=Remove "%s"
removeVault.message=Remove vault?
removeVault.description=This will only make Cryptomator forget about this vault. You can add it again. No encrypted files will be deleted from your hard drive.
-removeVault.confirmBtn=Remove Vault
# Contact Hub Admin
contactHubAdmin.title=Contact Admin
@@ -291,7 +290,7 @@ preferences.title=Preferences
## General
preferences.general=General
preferences.general.startHidden=Hide window when starting Cryptomator
-preferences.general.autoCloseVaults=Lock open vaults automatically when quitting application
+preferences.general.autoCloseVaults=Lock vaults without asking when quitting application
preferences.general.debugLogging=Enable debug logging
preferences.general.debugDirectory=Reveal log files
preferences.general.autoStart=Launch Cryptomator on system start
diff --git a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
index abf803e1e..438574b78 100644
--- a/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
+++ b/src/test/java/org/cryptomator/common/keychain/KeychainManagerTest.java
@@ -1,6 +1,7 @@
package org.cryptomator.common.keychain;
+import org.cryptomator.JavaFXUtil;
import org.cryptomator.integrations.keychain.KeychainAccessException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
@@ -13,11 +14,16 @@ import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import java.time.Duration;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
-public class KeychainManagerTest {
+class KeychainManagerTest {
+
+ @BeforeAll
+ public static void startup() throws InterruptedException {
+ var isRunning = JavaFXUtil.startPlatform();
+ Assumptions.assumeTrue(isRunning);
+ }
@Test
public void testStoreAndLoad() throws KeychainAccessException {
@@ -27,15 +33,7 @@ public class KeychainManagerTest {
}
@Nested
- public static class WhenObservingProperties {
-
- @BeforeAll
- public static void startup() throws InterruptedException {
- CountDownLatch latch = new CountDownLatch(1);
- Platform.startup(latch::countDown);
- var javafxStarted = latch.await(5, TimeUnit.SECONDS);
- Assumptions.assumeTrue(javafxStarted);
- }
+ class WhenObservingProperties {
@Test
public void testPropertyChangesWhenStoringPassword() throws KeychainAccessException, InterruptedException {
@@ -43,7 +41,7 @@ public class KeychainManagerTest {
ReadOnlyBooleanProperty property = keychainManager.getPassphraseStoredProperty("test");
Assertions.assertFalse(property.get());
- keychainManager.storePassphrase("test", null,"bar");
+ keychainManager.storePassphrase("test", null, "bar");
AtomicBoolean result = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
diff --git a/src/test/java/org/cryptomator/ui/addvaultwizard/ReadMeGeneratorTest.java b/src/test/java/org/cryptomator/ui/addvaultwizard/ReadMeGeneratorTest.java
index 97f02aee1..10451daa2 100644
--- a/src/test/java/org/cryptomator/ui/addvaultwizard/ReadMeGeneratorTest.java
+++ b/src/test/java/org/cryptomator/ui/addvaultwizard/ReadMeGeneratorTest.java
@@ -15,8 +15,8 @@ public class ReadMeGeneratorTest {
@ParameterizedTest
@CsvSource({ //
"test,test", //
- "t\u00E4st,t\\u228st", //
- "t\uD83D\uDE09st,t\\u55357\\u56841st", //
+ "t\u00E4st,t\\'E4st", //
+ "t\uD83D\uDE09st,t\\uc1\\u55357\\uc1\\u56841st", //
})
public void testEscapeNonAsciiChars(String input, String expectedResult) {
ReadmeGenerator readmeGenerator = new ReadmeGenerator(null);
@@ -40,7 +40,7 @@ public class ReadMeGeneratorTest {
MatcherAssert.assertThat(result, CoreMatchers.startsWith("{\\rtf1\\fbidis\\ansi\\uc0\\fs32"));
MatcherAssert.assertThat(result, CoreMatchers.containsString("{\\sa80 Dear User,}\\par"));
MatcherAssert.assertThat(result, CoreMatchers.containsString("{\\sa80 \\b please don't touch the \"d\" directory.}\\par "));
- MatcherAssert.assertThat(result, CoreMatchers.containsString("{\\sa80 Thank you for your cooperation \\u55357\\u56841}\\par"));
+ MatcherAssert.assertThat(result, CoreMatchers.containsString("{\\sa80 Thank you for your cooperation \\uc1\\u55357\\uc1\\u56841}\\par"));
MatcherAssert.assertThat(result, CoreMatchers.endsWith("}"));
}
diff --git a/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java b/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
index bfe31816e..da9d65117 100644
--- a/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
+++ b/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java
@@ -1,6 +1,6 @@
package org.cryptomator.ui.controls;
-import org.junit.jupiter.api.AfterAll;
+import org.cryptomator.JavaFXUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
@@ -8,20 +8,14 @@ import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
-import javafx.application.Platform;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
public class SecurePasswordFieldTest {
private SecurePasswordField pwField = new SecurePasswordField();
@BeforeAll
public static void initJavaFx() throws InterruptedException {
- CountDownLatch latch = new CountDownLatch(1);
- Platform.startup(latch::countDown);
- var javafxStarted = latch.await(5, TimeUnit.SECONDS);
- Assumptions.assumeTrue(javafxStarted);
+ var isRunning = JavaFXUtil.startPlatform();
+ Assumptions.assumeTrue(isRunning);
}
@Nested