revealButtonState;
private final BooleanProperty revealButtonDisabled;
+
+ public CheckBox rememberChoiceCheckbox;
@Inject
public UnlockSuccessController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, VaultService vaultService) {
@@ -44,6 +48,9 @@ public class UnlockSuccessController implements FxController {
public void close() {
LOG.trace("UnlockSuccessController.close()");
window.close();
+ if (rememberChoiceCheckbox.isSelected()) {
+ vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.IGNORE);
+ }
}
@FXML
@@ -64,6 +71,9 @@ public class UnlockSuccessController implements FxController {
revealButtonDisabled.set(false);
});
executor.execute(revealTask);
+ if (rememberChoiceCheckbox.isSelected()) {
+ vault.getVaultSettings().actionAfterUnlock().setValue(WhenUnlocked.REVEAL);
+ }
}
/* Getter/Setter */
diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
new file mode 100644
index 000000000..c2f6e47f4
--- /dev/null
+++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java
@@ -0,0 +1,190 @@
+package org.cryptomator.ui.unlock;
+
+import dagger.Lazy;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultState;
+import org.cryptomator.common.vaults.Volume;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.keychain.KeychainAccess;
+import org.cryptomator.keychain.KeychainAccessException;
+import org.cryptomator.ui.common.Animations;
+import org.cryptomator.ui.common.ErrorComponent;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.UserInteractionLock;
+import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import java.io.IOException;
+import java.nio.CharBuffer;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileSystemException;
+import java.nio.file.NotDirectoryException;
+import java.util.Arrays;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A multi-step task that consists of background activities as well as user interaction.
+ *
+ * This class runs the unlock process and controls when to display which UI.
+ */
+@UnlockScoped
+public class UnlockWorkflow extends Task {
+
+ private static final Logger LOG = LoggerFactory.getLogger(UnlockWorkflow.class);
+
+ private final Stage window;
+ private final Vault vault;
+ private final VaultService vaultService;
+ private final AtomicReference password;
+ private final AtomicBoolean savePassword;
+ private final Optional savedPassword;
+ private final UserInteractionLock passwordEntryLock;
+ private final Optional keychain;
+ private final Lazy unlockScene;
+ private final Lazy successScene;
+ private final Lazy invalidMountPointScene;
+ private final ErrorComponent.Builder errorComponent;
+
+ @Inject
+ UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, Optional keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent) {
+ this.window = window;
+ this.vault = vault;
+ this.vaultService = vaultService;
+ this.password = password;
+ this.savePassword = savePassword;
+ this.savedPassword = savedPassword;
+ this.passwordEntryLock = passwordEntryLock;
+ this.keychain = keychain;
+ this.unlockScene = unlockScene;
+ this.successScene = successScene;
+ this.invalidMountPointScene = invalidMountPointScene;
+ this.errorComponent = errorComponent;
+ }
+
+ @Override
+ protected Boolean call() throws InterruptedException, IOException, Volume.VolumeException {
+ try {
+ if (attemptUnlock()) {
+ handleSuccess();
+ return true;
+ } else {
+ cancel(false); // set Tasks state to cancelled
+ return false;
+ }
+ } catch (NotDirectoryException | DirectoryNotEmptyException e) {
+ handleInvalidMountPoint(e);
+ throw e; // rethrow to trigger correct exception handling in Task
+ } catch (CryptoException | Volume.VolumeException | IOException e) {
+ handleGenericError(e);
+ throw e; // rethrow to trigger correct exception handling in Task
+ } finally {
+ wipePassword(password.get());
+ wipePassword(savedPassword.orElse(null));
+ }
+ }
+
+ private boolean attemptUnlock() throws InterruptedException, IOException, Volume.VolumeException {
+ boolean proceed = password.get() != null || askForPassword(false) == PasswordEntry.PASSWORD_ENTERED;
+ while (proceed) {
+ try {
+ vault.unlock(CharBuffer.wrap(password.get()));
+ return true;
+ } catch (InvalidPassphraseException e) {
+ proceed = askForPassword(true) == PasswordEntry.PASSWORD_ENTERED;
+ }
+ }
+ return false;
+ }
+
+ private PasswordEntry askForPassword(boolean animateShake) throws InterruptedException {
+ Platform.runLater(() -> {
+ window.setScene(unlockScene.get());
+ window.show();
+ if (animateShake) {
+ Animations.createShakeWindowAnimation(window).play();
+ }
+ });
+ return passwordEntryLock.awaitInteraction();
+ }
+
+ private void handleSuccess() {
+ LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
+ if (savePassword.get()) {
+ savePasswordToSystemkeychain();
+ }
+ switch (vault.getVaultSettings().actionAfterUnlock().get()) {
+ case ASK -> Platform.runLater(() -> {
+ window.setScene(successScene.get());
+ window.show();
+ });
+ case REVEAL -> {
+ Platform.runLater(window::close);
+ vaultService.reveal(vault);
+ }
+ case IGNORE -> Platform.runLater(window::close);
+ }
+ }
+
+ private void savePasswordToSystemkeychain() {
+ if (keychain.isPresent()) {
+ try {
+ keychain.get().storePassphrase(vault.getId(), CharBuffer.wrap(password.get()));
+ } catch (KeychainAccessException e) {
+ LOG.error("Failed to store passphrase in system keychain.", e);
+ }
+ }
+ }
+
+ private void handleInvalidMountPoint(FileSystemException e) {
+ LOG.error("Unlock failed. Mount point not an empty directory: {}", e.getMessage());
+ Platform.runLater(() -> {
+ window.setScene(invalidMountPointScene.get());
+ });
+ }
+
+ private void handleGenericError(Exception e) {
+ LOG.error("Unlock failed for technical reasons.", e);
+ Platform.runLater(() -> {
+ errorComponent.cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene();
+ });
+ }
+
+ private void wipePassword(char[] pw) {
+ if (pw != null) {
+ Arrays.fill(pw, ' ');
+ }
+ }
+
+ @Override
+ protected void scheduled() {
+ vault.setState(VaultState.PROCESSING);
+ }
+
+ @Override
+ protected void succeeded() {
+ vault.setState(VaultState.UNLOCKED);
+ }
+
+ @Override
+ protected void failed() {
+ vault.setState(VaultState.LOCKED);
+ }
+
+ @Override
+ protected void cancelled() {
+ vault.setState(VaultState.LOCKED);
+ }
+
+}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
index 2507997c9..ddb6e0553 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java
@@ -2,24 +2,56 @@ package org.cryptomator.ui.vaultoptions;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
+import javafx.scene.control.ChoiceBox;
+import javafx.util.StringConverter;
+import org.cryptomator.common.settings.UiTheme;
+import org.cryptomator.common.settings.WhenUnlocked;
import org.cryptomator.common.vaults.Vault;
import org.cryptomator.ui.common.FxController;
import javax.inject.Inject;
+import java.util.ResourceBundle;
@VaultOptionsScoped
public class GeneralVaultOptionsController implements FxController {
private final Vault vault;
+ private final ResourceBundle resourceBundle;
+
public CheckBox unlockOnStartupCheckbox;
+ public ChoiceBox actionAfterUnlockChoiceBox;
@Inject
- GeneralVaultOptionsController(@VaultOptionsWindow Vault vault) {
+ GeneralVaultOptionsController(@VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) {
this.vault = vault;
+ this.resourceBundle = resourceBundle;
}
@FXML
public void initialize() {
unlockOnStartupCheckbox.selectedProperty().bindBidirectional(vault.getVaultSettings().unlockAfterStartup());
+ actionAfterUnlockChoiceBox.getItems().addAll(WhenUnlocked.values());
+ actionAfterUnlockChoiceBox.valueProperty().bindBidirectional(vault.getVaultSettings().actionAfterUnlock());
+ actionAfterUnlockChoiceBox.setConverter(new WhenUnlockedConverter(resourceBundle));
}
+
+ private static class WhenUnlockedConverter extends StringConverter {
+
+ private final ResourceBundle resourceBundle;
+
+ public WhenUnlockedConverter(ResourceBundle resourceBundle) {
+ this.resourceBundle = resourceBundle;
+ }
+
+ @Override
+ public String toString(WhenUnlocked obj) {
+ return resourceBundle.getString(obj.getDisplayName());
+ }
+
+ @Override
+ public WhenUnlocked fromString(String string) {
+ throw new UnsupportedOperationException();
+ }
+ }
+
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
index 1556d0f74..81753d83a 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/MountOptionsController.java
@@ -85,7 +85,7 @@ public class MountOptionsController implements FxController {
driveLetterSelection.setConverter(new WinDriveLetterLabelConverter(windowsDriveLetters, resourceBundle));
driveLetterSelection.setValue(vault.getVaultSettings().winDriveLetter().get());
- if (vault.getVaultSettings().usesIndividualMountPath().get()) {
+ if (vault.getVaultSettings().useCustomMountPath().get()) {
mountPoint.selectToggle(mountPointCustomDir);
} else if (!Strings.isNullOrEmpty(vault.getVaultSettings().winDriveLetter().get())) {
mountPoint.selectToggle(mountPointWinDriveLetter);
@@ -93,7 +93,7 @@ public class MountOptionsController implements FxController {
mountPoint.selectToggle(mountPointAuto);
}
- vault.getVaultSettings().usesIndividualMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
+ vault.getVaultSettings().useCustomMountPath().bind(mountPoint.selectedToggleProperty().isEqualTo(mountPointCustomDir));
vault.getVaultSettings().winDriveLetter().bind( //
Bindings.when(mountPoint.selectedToggleProperty().isEqualTo(mountPointWinDriveLetter)) //
.then(driveLetterSelection.getSelectionModel().selectedItemProperty()) //
@@ -126,14 +126,14 @@ public class MountOptionsController implements FxController {
}
File file = directoryChooser.showDialog(window);
if (file != null) {
- vault.getVaultSettings().individualMountPath().set(file.getAbsolutePath());
+ vault.getVaultSettings().customMountPath().set(file.getAbsolutePath());
} else {
- vault.getVaultSettings().individualMountPath().set(null);
+ vault.getVaultSettings().customMountPath().set(null);
}
}
private void toggleMountPoint(@SuppressWarnings("unused") ObservableValue extends Toggle> observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) {
- if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().individualMountPath().get())) {
+ if (mountPointCustomDir.equals(newValue) && Strings.isNullOrEmpty(vault.getVaultSettings().customMountPath().get())) {
chooseCustomMountPoint();
}
}
@@ -186,11 +186,11 @@ public class MountOptionsController implements FxController {
}
public StringProperty customMountPathProperty() {
- return vault.getVaultSettings().individualMountPath();
+ return vault.getVaultSettings().customMountPath();
}
public String getCustomMountPath() {
- return vault.getVaultSettings().individualMountPath().get();
+ return vault.getVaultSettings().customMountPath().get();
}
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
index 639f5b36b..d0aa49281 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/VaultOptionsModule.java
@@ -16,6 +16,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
@@ -38,15 +39,14 @@ abstract class VaultOptionsModule {
@Provides
@VaultOptionsWindow
@VaultOptionsScoped
- static Stage provideStage(@MainWindow Stage owner, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
- Stage stage = new Stage();
+ static Stage provideStage(StageFactory factory, @MainWindow Stage owner, @VaultOptionsWindow Vault vault) {
+ Stage stage = factory.create();
stage.setTitle(vault.getDisplayableName());
stage.setResizable(true);
stage.setMinWidth(400);
stage.setMinHeight(300);
stage.initModality(Modality.WINDOW_MODAL);
stage.initOwner(owner);
- stage.getIcons().addAll(windowIcons);
return stage;
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java b/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
index 7c64a079d..cca3b0269 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/wrongfilealert/WrongFileAlertModule.java
@@ -14,6 +14,7 @@ import org.cryptomator.ui.common.FxController;
import org.cryptomator.ui.common.FxControllerKey;
import org.cryptomator.ui.common.FxmlFile;
import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.common.StageFactory;
import org.cryptomator.ui.mainwindow.MainWindow;
import javax.inject.Named;
@@ -35,13 +36,12 @@ abstract class WrongFileAlertModule {
@Provides
@WrongFileAlertWindow
@WrongFileAlertScoped
- static Stage provideStage(@MainWindow Stage mainWindow, ResourceBundle resourceBundle, @Named("windowIcons") List windowIcons) {
- Stage stage = new Stage();
+ static Stage provideStage(StageFactory factory, @MainWindow Stage mainWindow, ResourceBundle resourceBundle) {
+ Stage stage = factory.create();
stage.setTitle(resourceBundle.getString("wrongFileAlert.title"));
stage.setResizable(false);
stage.initOwner(mainWindow);
stage.initModality(Modality.WINDOW_MODAL);
- stage.getIcons().addAll(windowIcons);
return stage;
}
diff --git a/main/ui/src/main/resources/fxml/preferences_general.fxml b/main/ui/src/main/resources/fxml/preferences_general.fxml
index 04b8ce9b0..e3ace69f6 100644
--- a/main/ui/src/main/resources/fxml/preferences_general.fxml
+++ b/main/ui/src/main/resources/fxml/preferences_general.fxml
@@ -34,7 +34,10 @@
-
+
+
+
+
diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml
index e2b2217ff..099a04148 100644
--- a/main/ui/src/main/resources/fxml/unlock.fxml
+++ b/main/ui/src/main/resources/fxml/unlock.fxml
@@ -21,15 +21,15 @@
-
-
+
+
-
-