Sebastian Stenzel hace 5 años
padre
commit
fecf9c0423

+ 1 - 1
main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java

@@ -27,7 +27,7 @@ public class AddVaultSuccessController implements FxController {
 	@FXML
 	public void unlockAndClose() {
 		close();
-		fxApplication.showUnlockWindow(vault.get());
+		fxApplication.startUnlockWorkflow(vault.get());
 	}
 
 	@FXML

+ 51 - 0
main/ui/src/main/java/org/cryptomator/ui/common/UserInteractionLock.java

@@ -0,0 +1,51 @@
+package org.cryptomator.ui.common;
+
+import javafx.application.Platform;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class UserInteractionLock<E extends Enum> {
+
+	private final Lock lock = new ReentrantLock();
+	private final Condition condition = lock.newCondition();
+	private final BooleanProperty awaitingInteraction = new SimpleBooleanProperty();
+	private volatile E state;
+	
+	public UserInteractionLock(E initialValue) {
+		state = initialValue;
+	}
+	
+	public void interacted(E result) {
+		assert Platform.isFxApplicationThread();
+		lock.lock();
+		try {
+			state = result;
+			awaitingInteraction.set(false);
+			condition.signal();
+		} finally {
+			lock.unlock();
+		}
+	}
+	
+	public E awaitInteraction() throws InterruptedException {
+		assert !Platform.isFxApplicationThread();
+		lock.lock();
+		try {
+			Platform.runLater(() -> awaitingInteraction.set(true));
+			condition.await();
+			return state;
+		} finally {
+			lock.unlock();
+		}
+	}
+	
+	public ReadOnlyBooleanProperty awaitingInteraction() {
+		return awaitingInteraction;
+	}
+
+}

+ 0 - 166
main/ui/src/main/java/org/cryptomator/ui/common/VaultService.java

@@ -52,62 +52,6 @@ public class VaultService {
 		return task;
 	}
 
-	/**
-	 * Attempts to unlock all given vaults in a background thread using passwords stored in the system keychain.
-	 *
-	 * @param vaults The vaults to unlock
-	 * @implNote No-op if no system keychain is present
-	 */
-	public void attemptAutoUnlock(Collection<Vault> vaults) {
-		if (!keychain.isPresent()) {
-			LOG.debug("No system keychain found. Unable to auto unlock without saved passwords.");
-		} else {
-			List<Task<Vault>> unlockTasks = vaults.stream().map(v -> createAutoUnlockTask(v, keychain.get())).collect(Collectors.toList());
-			Task<Collection<Vault>> runSequentiallyTask = new RunSequentiallyTask(unlockTasks);
-			executorService.execute(runSequentiallyTask);
-		}
-	}
-
-	/**
-	 * Creates but doesn't start an auto-unlock task.
-	 *
-	 * @param vault The vault to unlock
-	 * @param keychainAccess The system keychain holding the passphrase for the vault
-	 * @return The task
-	 */
-	public Task<Vault> createAutoUnlockTask(Vault vault, KeychainAccess keychainAccess) {
-		Task<Vault> task = new AutoUnlockVaultTask(vault, keychainAccess);
-		task.setOnSucceeded(evt -> LOG.info("Auto-unlocked {}", vault.getDisplayableName()));
-		task.setOnFailed(evt -> LOG.error("Failed to auto-unlock " + vault.getDisplayableName(), evt.getSource().getException()));
-		return task;
-	}
-
-	/**
-	 * Unlocks a vault in a background thread
-	 *
-	 * @param vault The vault to unlock
-	 * @param passphrase The password to use - wipe this param asap
-	 * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
-	 */
-	public void unlock(Vault vault, CharSequence passphrase) {
-		executorService.execute(createUnlockTask(vault, passphrase));
-	}
-
-	/**
-	 * Creates but doesn't start an unlock task.
-	 *
-	 * @param vault The vault to unlock
-	 * @param passphrase The password to use - wipe this param asap
-	 * @return The task
-	 * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
-	 */
-	public Task<Vault> createUnlockTask(Vault vault, CharSequence passphrase) {
-		Task<Vault> task = new UnlockVaultTask(vault, passphrase);
-		task.setOnSucceeded(evt -> LOG.info("Unlocked {}", vault.getDisplayableName()));
-		task.setOnFailed(evt -> LOG.error("Failed to unlock " + vault.getDisplayableName(), evt.getSource().getException()));
-		return task;
-	}
-
 	/**
 	 * Locks a vault in a background thread.
 	 *
@@ -209,116 +153,6 @@ public class VaultService {
 		}
 	}
 
-	/**
-	 * A task that runs a list of tasks in their given order
-	 */
-	private static class RunSequentiallyTask extends Task<Collection<Vault>> {
-
-		private final List<Task<Vault>> tasks;
-
-		public RunSequentiallyTask(List<Task<Vault>> tasks) {
-			this.tasks = List.copyOf(tasks);
-		}
-
-		@Override
-		protected List<Vault> call() throws ExecutionException, InterruptedException {
-			List<Vault> completed = new ArrayList<>();
-			for (Task<Vault> task : tasks) {
-				task.run();
-				Vault done = task.get();
-				completed.add(done);
-			}
-			return completed;
-		}
-	}
-
-	private static class AutoUnlockVaultTask extends Task<Vault> {
-
-		private final Vault vault;
-		private final KeychainAccess keychain;
-
-		public AutoUnlockVaultTask(Vault vault, KeychainAccess keychain) {
-			this.vault = vault;
-			this.keychain = keychain;
-		}
-
-		@Override
-		protected Vault call() throws Exception {
-			char[] storedPw = null;
-			try {
-				storedPw = keychain.loadPassphrase(vault.getId());
-				if (storedPw == null) {
-					throw new InvalidPassphraseException();
-				}
-				vault.unlock(CharBuffer.wrap(storedPw));
-			} finally {
-				if (storedPw != null) {
-					Arrays.fill(storedPw, ' ');
-				}
-			}
-			return vault;
-		}
-
-		@Override
-		protected void scheduled() {
-			vault.setState(VaultState.PROCESSING);
-		}
-
-		@Override
-		protected void succeeded() {
-			vault.setState(VaultState.UNLOCKED);
-		}
-
-		@Override
-		protected void failed() {
-			vault.setState(VaultState.LOCKED);
-		}
-	}
-
-	private static class UnlockVaultTask extends Task<Vault> {
-
-		private final Vault vault;
-		private final CharBuffer passphrase;
-
-		/**
-		 * @param vault The vault to unlock
-		 * @param passphrase The password to use - wipe this param asap
-		 * @implNote A copy of the passphrase will be made, which is wiped as soon as the task ran.
-		 */
-		public UnlockVaultTask(Vault vault, CharSequence passphrase) {
-			this.vault = vault;
-			this.passphrase = CharBuffer.allocate(passphrase.length());
-			for (int i = 0; i < passphrase.length(); i++) {
-				this.passphrase.put(i, passphrase.charAt(i));
-			}
-		}
-
-		@Override
-		protected Vault call() throws Exception {
-			try {
-				vault.unlock(passphrase);
-			} finally {
-				Arrays.fill(passphrase.array(), ' ');
-			}
-			return vault;
-		}
-
-		@Override
-		protected void scheduled() {
-			vault.setState(VaultState.PROCESSING);
-		}
-
-		@Override
-		protected void succeeded() {
-			vault.setState(VaultState.UNLOCKED);
-		}
-
-		@Override
-		protected void failed() {
-			vault.setState(VaultState.LOCKED);
-		}
-	}
-
 	/**
 	 * A task that locks a vault
 	 */

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java

@@ -94,8 +94,8 @@ public class NiceSecurePasswordField extends StackPane {
 		passwordField.setPassword(password);
 	}
 
-	public void swipe() {
-		passwordField.swipe();
+	public void wipe() {
+		passwordField.wipe();
 	}
 
 	public void selectAll() {

+ 10 - 10
main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java

@@ -40,7 +40,7 @@ import java.util.Arrays;
  */
 public class SecurePasswordField extends TextField {
 
-	private static final char SWIPE_CHAR = ' ';
+	private static final char WIPE_CHAR = ' ';
 	private static final int INITIAL_BUFFER_SIZE = 50;
 	private static final int GROW_BUFFER_SIZE = 50;
 	private static final String DEFAULT_PLACEHOLDER = "●";
@@ -103,7 +103,7 @@ public class SecurePasswordField extends TextField {
 		if (e.getCode() == KeyCode.CAPS) {
 			updateCapsLocked();
 		} else if (SHORTCUT_BACKSPACE.match(e)) {
-			swipe();
+			wipe();
 		}
 	}
 
@@ -189,7 +189,7 @@ public class SecurePasswordField extends TextField {
 		if (length > content.length) {
 			char[] newContent = new char[length + GROW_BUFFER_SIZE];
 			System.arraycopy(content, 0, newContent, 0, content.length);
-			swipe(content);
+			wipe(content);
 			this.content = newContent;
 		}
 	}
@@ -201,7 +201,7 @@ public class SecurePasswordField extends TextField {
 	 * @implNote The CharSequence will not copy the backing char[].
 	 * Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
 	 * @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
-	 * @see #swipe()
+	 * @see #wipe()
 	 */
 	@Override
 	public CharSequence getCharacters() {
@@ -220,7 +220,7 @@ public class SecurePasswordField extends TextField {
 			buf[i] = password.charAt(i);
 		}
 		setPassword(buf);
-		Arrays.fill(buf, SWIPE_CHAR);
+		Arrays.fill(buf, WIPE_CHAR);
 	}
 
 	/**
@@ -231,7 +231,7 @@ public class SecurePasswordField extends TextField {
 	 * @param password
 	 */
 	public void setPassword(char[] password) {
-		swipe();
+		wipe();
 		content = Arrays.copyOf(password, password.length);
 		length = password.length;
 
@@ -242,14 +242,14 @@ public class SecurePasswordField extends TextField {
 	/**
 	 * Destroys the stored password by overriding each character with a different character.
 	 */
-	public void swipe() {
-		swipe(content);
+	public void wipe() {
+		wipe(content);
 		length = 0;
 		setText(null);
 	}
 
-	private void swipe(char[] buffer) {
-		Arrays.fill(buffer, SWIPE_CHAR);
+	private void wipe(char[] buffer) {
+		Arrays.fill(buffer, WIPE_CHAR);
 	}
 
 	/* Observable Properties */

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

@@ -27,6 +27,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javax.inject.Provider;
 import java.awt.desktop.QuitResponse;
 import java.util.Optional;
 
@@ -38,20 +39,20 @@ public class FxApplication extends Application {
 	private final Settings settings;
 	private final Lazy<MainWindowComponent> mainWindow;
 	private final Lazy<PreferencesComponent> preferencesWindow;
-	private final UnlockComponent.Builder unlockWindowBuilder;
-	private final QuitComponent.Builder quitWindowBuilder;
+	private final Provider<UnlockComponent.Builder> unlockWindowBuilderProvider;
+	private final Provider<QuitComponent.Builder> quitWindowBuilderProvider;
 	private final Optional<MacFunctions> macFunctions;
 	private final VaultService vaultService;
 	private final LicenseHolder licenseHolder;
 	private final BooleanBinding hasVisibleStages;
 
 	@Inject
-	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, UnlockComponent.Builder unlockWindowBuilder, QuitComponent.Builder quitWindowBuilder, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
+	FxApplication(Settings settings, Lazy<MainWindowComponent> mainWindow, Lazy<PreferencesComponent> preferencesWindow, Provider<UnlockComponent.Builder> unlockWindowBuilderProvider, Provider<QuitComponent.Builder> quitWindowBuilderProvider, Optional<MacFunctions> macFunctions, VaultService vaultService, LicenseHolder licenseHolder, ObservableSet<Stage> visibleStages) {
 		this.settings = settings;
 		this.mainWindow = mainWindow;
 		this.preferencesWindow = preferencesWindow;
-		this.unlockWindowBuilder = unlockWindowBuilder;
-		this.quitWindowBuilder = quitWindowBuilder;
+		this.unlockWindowBuilderProvider = unlockWindowBuilderProvider;
+		this.quitWindowBuilderProvider = quitWindowBuilderProvider;
 		this.macFunctions = macFunctions;
 		this.vaultService = vaultService;
 		this.licenseHolder = licenseHolder;
@@ -95,16 +96,16 @@ public class FxApplication extends Application {
 		});
 	}
 
-	public void showUnlockWindow(Vault vault) {
+	public void startUnlockWorkflow(Vault vault) {
 		Platform.runLater(() -> {
-			unlockWindowBuilder.vault(vault).build().showUnlockWindow();
+			unlockWindowBuilderProvider.get().vault(vault).build().startUnlockWorkflow();
 			LOG.debug("Showing UnlockWindow for {}", vault.getDisplayableName());
 		});
 	}
 
 	public void showQuitWindow(QuitResponse response) {
 		Platform.runLater(() -> {
-			quitWindowBuilder.quitResponse(response).build().showQuitWindow();
+			quitWindowBuilderProvider.get().quitResponse(response).build().showQuitWindow();
 			LOG.debug("Showing QuitWindow");
 		});
 	}

+ 5 - 1
main/ui/src/main/java/org/cryptomator/ui/launcher/UiLauncher.java

@@ -64,7 +64,11 @@ public class UiLauncher {
 		// auto unlock
 		Collection<Vault> vaultsWithAutoUnlockEnabled = vaults.filtered(v -> v.getVaultSettings().unlockAfterStartup().get());
 		if (!vaultsWithAutoUnlockEnabled.isEmpty()) {
-			fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> app.getVaultService().attemptAutoUnlock(vaultsWithAutoUnlockEnabled));
+			fxApplicationStarter.get(hasTrayIcon).thenAccept(app -> {
+				for (Vault vault : vaultsWithAutoUnlockEnabled){
+					app.startUnlockWorkflow(vault);
+				}
+			});
 		}
 
 		launchEventHandler.startHandlingLaunchEvents(hasTrayIcon);

+ 1 - 1
main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java

@@ -26,7 +26,7 @@ public class VaultDetailLockedController implements FxController {
 
 	@FXML
 	public void unlock() {
-		application.showUnlockWindow(vault.get());
+		application.startUnlockWorkflow(vault.get());
 	}
 
 	@FXML

+ 1 - 1
main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java

@@ -121,7 +121,7 @@ public class MigrationRunController implements FxController {
 			} else {
 				LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName());
 				vault.setState(VaultState.LOCKED);
-				passwordField.swipe();
+				passwordField.wipe();
 				window.setScene(successScene.get());
 			}
 		}).onError(InvalidPassphraseException.class, e -> {

+ 1 - 4
main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java

@@ -1,8 +1,5 @@
 package org.cryptomator.ui.migration;
 
-import javafx.beans.property.ObjectProperty;
-import javafx.beans.property.ReadOnlyObjectProperty;
-import javafx.event.ActionEvent;
 import javafx.fxml.FXML;
 import javafx.stage.Stage;
 import org.cryptomator.common.vaults.Vault;
@@ -28,7 +25,7 @@ public class MigrationSuccessController implements FxController {
 	@FXML
 	public void unlockAndClose() {
 		close();
-		fxApplication.showUnlockWindow(vault);
+		fxApplication.startUnlockWorkflow(vault);
 	}
 
 	@FXML

+ 1 - 1
main/ui/src/main/java/org/cryptomator/ui/traymenu/TrayMenuController.java

@@ -103,7 +103,7 @@ class TrayMenuController {
 	}
 
 	private void unlockVault(Vault vault) {
-		fxApplicationStarter.get(true).thenAccept(app -> app.showUnlockWindow(vault));
+		fxApplicationStarter.get(true).thenAccept(app -> app.startUnlockWorkflow(vault));
 	}
 
 	private void lockVault(Vault vault) {

+ 12 - 10
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockComponent.java

@@ -14,21 +14,23 @@ import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.common.vaults.Vault;
 
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+
 @UnlockScoped
 @Subcomponent(modules = {UnlockModule.class})
 public interface UnlockComponent {
 
-	@UnlockWindow
-	Stage window();
-
-	@FxmlScene(FxmlFile.UNLOCK)
-	Lazy<Scene> scene();
+	ExecutorService defaultExecutorService();
 
-	default Stage showUnlockWindow() {
-		Stage stage = window();
-		stage.setScene(scene().get());
-		stage.show();
-		return stage;
+	UnlockWorkflow unlockWorkflow();
+	
+	default Future<Boolean> startUnlockWorkflow() {
+		UnlockWorkflow workflow = unlockWorkflow();
+		defaultExecutorService().submit(workflow);
+		return workflow;
 	}
 
 	@Subcomponent.Builder

+ 48 - 110
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java

@@ -1,39 +1,29 @@
 package org.cryptomator.ui.unlock;
 
-import dagger.Lazy;
 import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
 import javafx.beans.binding.ObjectBinding;
 import javafx.beans.property.BooleanProperty;
 import javafx.beans.property.ReadOnlyBooleanProperty;
 import javafx.beans.property.SimpleBooleanProperty;
-import javafx.concurrent.Task;
 import javafx.fxml.FXML;
-import javafx.scene.Scene;
 import javafx.scene.control.CheckBox;
 import javafx.scene.control.ContentDisplay;
 import javafx.stage.Stage;
 import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.common.vaults.VaultState;
-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.FxController;
-import org.cryptomator.ui.common.FxmlFile;
-import org.cryptomator.ui.common.FxmlScene;
-import org.cryptomator.ui.common.VaultService;
+import org.cryptomator.ui.common.UserInteractionLock;
 import org.cryptomator.ui.controls.NiceSecurePasswordField;
 import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
-import java.nio.file.DirectoryNotEmptyException;
-import java.nio.file.NotDirectoryException;
-import java.util.Arrays;
+import javax.inject.Named;
 import java.util.Optional;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 @UnlockScoped
 public class UnlockController implements FxController {
@@ -42,124 +32,67 @@ public class UnlockController implements FxController {
 
 	private final Stage window;
 	private final Vault vault;
-	private final ExecutorService executor;
-	private final ObjectBinding<ContentDisplay> unlockButtonState;
-	private final Optional<KeychainAccess> keychainAccess;
-	private final VaultService vaultService;
-	private final Lazy<Scene> successScene;
-	private final Lazy<Scene> invalidMountPointScene;
-	private final ErrorComponent.Builder errorComponent;
+	private final AtomicReference<char[]> password;
+	private final AtomicBoolean savePassword;
+	private final Optional<char[]> savedPassword;
+	private final UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock;
 	private final ForgetPasswordComponent.Builder forgetPassword;
+	private final Optional<KeychainAccess> keychainAccess;
+	private final ObjectBinding<ContentDisplay> unlockButtonContentDisplay;
+	private final BooleanBinding userInteractionDisabled;
 	private final BooleanProperty unlockButtonDisabled;
 	public NiceSecurePasswordField passwordField;
-	public CheckBox savePassword;
+	public CheckBox savePasswordCheckbox;
 
 	@Inject
-	public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, ExecutorService executor, Optional<KeychainAccess> keychainAccess, VaultService vaultService, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, ForgetPasswordComponent.Builder forgetPassword) {
+	public UnlockController(@UnlockWindow Stage window, @UnlockWindow Vault vault, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<UnlockModule.PasswordEntry> passwordEntryLock, ForgetPasswordComponent.Builder forgetPassword, Optional<KeychainAccess> keychainAccess) {
 		this.window = window;
 		this.vault = vault;
-		this.executor = executor;
-		this.unlockButtonState = Bindings.createObjectBinding(this::getUnlockButtonState, vault.stateProperty());
-		this.keychainAccess = keychainAccess;
-		this.vaultService = vaultService;
-		this.successScene = successScene;
-		this.invalidMountPointScene = invalidMountPointScene;
-		this.errorComponent = errorComponent;
+		this.password = password;
+		this.savePassword = savePassword;
+		this.savedPassword = savedPassword;
+		this.passwordEntryLock = passwordEntryLock;
 		this.forgetPassword = forgetPassword;
+		this.keychainAccess = keychainAccess;
+		this.unlockButtonContentDisplay = Bindings.createObjectBinding(this::getUnlockButtonContentDisplay, passwordEntryLock.awaitingInteraction());
+		this.userInteractionDisabled = passwordEntryLock.awaitingInteraction().not();
 		this.unlockButtonDisabled = new SimpleBooleanProperty();
 	}
 
 	public void initialize() {
-		if (keychainAccess.isPresent()) {
-			loadStoredPassword();
-		} else {
-			savePassword.setSelected(false);
+		savePasswordCheckbox.setSelected(savedPassword.isPresent());
+		if (password.get() != null) {
+			passwordField.setPassword(password.get());
 		}
-		unlockButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.LOCKED).or(passwordField.textProperty().isEmpty()));
+		unlockButtonDisabled.bind(userInteractionDisabled.or(passwordField.textProperty().isEmpty()));
 	}
 
 	@FXML
 	public void cancel() {
 		LOG.debug("Unlock canceled by user.");
 		window.close();
+		passwordEntryLock.interacted(UnlockModule.PasswordEntry.CANCELED);
 	}
 
 	@FXML
 	public void unlock() {
 		LOG.trace("UnlockController.unlock()");
-		CharSequence password = passwordField.getCharacters();
-
-		Task<Vault> task = vaultService.createUnlockTask(vault, password);
-		passwordField.setDisable(true);
-		task.setOnSucceeded(event -> {
-			passwordField.setDisable(false);
-			if (keychainAccess.isPresent() && savePassword.isSelected()) {
-				try {
-					keychainAccess.get().storePassphrase(vault.getId(), password);
-				} catch (KeychainAccessException e) {
-					LOG.error("Failed to store passphrase in system keychain.", e);
-				}
-			}
-			passwordField.swipe();
-			LOG.info("Unlock of '{}' succeeded.", vault.getDisplayableName());
-			window.setScene(successScene.get());
-		});
-		task.setOnFailed(event -> {
-			passwordField.setDisable(false);
-			if (task.getException() instanceof InvalidPassphraseException) {
-				Animations.createShakeWindowAnimation(window).play();
-				passwordField.selectAll();
-				passwordField.requestFocus();
-			} else if (task.getException() instanceof NotDirectoryException || task.getException() instanceof DirectoryNotEmptyException) {
-				LOG.error("Unlock failed. Mount point not an empty directory: {}", task.getException().getMessage());
-				window.setScene(invalidMountPointScene.get());
-			} else {
-				LOG.error("Unlock failed for technical reasons.", task.getException());
-				errorComponent.cause(task.getException()).window(window).returnToScene(window.getScene()).build().showErrorScene();
-			}
-		});
-		executor.execute(task);
+		CharSequence pwFieldContents = passwordField.getCharacters();
+		char[] pw = new char[pwFieldContents.length()];
+		for (int i = 0; i < pwFieldContents.length(); i++) {
+			pw[i] = pwFieldContents.charAt(i);
+		}
+		password.set(pw);
+		passwordEntryLock.interacted(UnlockModule.PasswordEntry.PASSWORD_ENTERED);
 	}
 
 	/* Save Password */
 
 	@FXML
 	private void didClickSavePasswordCheckbox() {
-		if (!savePassword.isSelected() && hasStoredPassword()) {
-			forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePassword.setSelected(!forgotten));
-		}
-	}
-
-	private void loadStoredPassword() {
-		assert keychainAccess.isPresent();
-		char[] storedPw = null;
-		try {
-			storedPw = keychainAccess.get().loadPassphrase(vault.getId());
-			if (storedPw != null) {
-				savePassword.setSelected(true);
-				passwordField.setPassword(storedPw);
-				passwordField.selectRange(storedPw.length, storedPw.length);
-			}
-		} catch (KeychainAccessException e) {
-			LOG.error("Failed to load entry from system keychain.", e);
-		} finally {
-			if (storedPw != null) {
-				Arrays.fill(storedPw, ' ');
-			}
-		}
-	}
-
-	private boolean hasStoredPassword() {
-		char[] storedPw = null;
-		try {
-			storedPw = keychainAccess.get().loadPassphrase(vault.getId());
-			return storedPw != null;
-		} catch (KeychainAccessException e) {
-			return false;
-		} finally {
-			if (storedPw != null) {
-				Arrays.fill(storedPw, ' ');
-			}
+		savePassword.set(savePasswordCheckbox.isSelected());
+		if (!savePasswordCheckbox.isSelected() && savedPassword.isPresent()) {
+			forgetPassword.vault(vault).owner(window).build().showForgetPassword().thenAccept(forgotten -> savePasswordCheckbox.setSelected(!forgotten));
 		}
 	}
 
@@ -169,15 +102,20 @@ public class UnlockController implements FxController {
 		return vault;
 	}
 
-	public ObjectBinding<ContentDisplay> unlockButtonStateProperty() {
-		return unlockButtonState;
+	public ObjectBinding<ContentDisplay> unlockButtonContentDisplayProperty() {
+		return unlockButtonContentDisplay;
+	}
+
+	public ContentDisplay getUnlockButtonContentDisplay() {
+		return passwordEntryLock.awaitingInteraction().get() ? ContentDisplay.TEXT_ONLY : ContentDisplay.LEFT;
+	}
+
+	public BooleanBinding userInteractionDisabledProperty() {
+		return userInteractionDisabled;
 	}
 
-	public ContentDisplay getUnlockButtonState() {
-		return switch (vault.getState()) {
-			case PROCESSING -> ContentDisplay.LEFT;
-			default -> ContentDisplay.TEXT_ONLY;
-		};
+	public boolean isUserInteractionDisabled() {
+		return userInteractionDisabled.get();
 	}
 
 	public ReadOnlyBooleanProperty unlockButtonDisabledProperty() {

+ 49 - 0
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java

@@ -9,6 +9,8 @@ import javafx.scene.image.Image;
 import javafx.stage.Modality;
 import javafx.stage.Stage;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.keychain.KeychainAccess;
+import org.cryptomator.keychain.KeychainAccessException;
 import org.cryptomator.ui.common.DefaultSceneFactory;
 import org.cryptomator.ui.common.FXMLLoaderFactory;
 import org.cryptomator.ui.common.FxController;
@@ -16,17 +18,64 @@ 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.common.UserInteractionLock;
 import org.cryptomator.ui.forgetPassword.ForgetPasswordComponent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.inject.Named;
 import javax.inject.Provider;
+import java.nio.CharBuffer;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 @Module(subcomponents = {ForgetPasswordComponent.class})
 abstract class UnlockModule {
 
+	private static final Logger LOG = LoggerFactory.getLogger(UnlockModule.class);
+
+	public enum PasswordEntry {PASSWORD_ENTERED, CANCELED}
+
+	@Provides
+	@UnlockScoped
+	static UserInteractionLock<PasswordEntry> providePasswordEntryLock() {
+		return new UserInteractionLock<>(null);
+	}
+
+	@Provides
+	@Named("savedPassword")
+	@UnlockScoped
+	static Optional<char[]> provideStoredPassword(Optional<KeychainAccess> keychainAccess, @UnlockWindow Vault vault) {
+		return keychainAccess.map(k -> {
+			try {
+				return k.loadPassphrase(vault.getId());
+			} catch (KeychainAccessException e) {
+				LOG.error("Failed to load entry from system keychain.", e);
+				return null;
+			}
+		});
+	}
+	
+	@Provides
+	@UnlockScoped
+	static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> storedPassword) {
+		return new AtomicReference(storedPassword.orElse(null));
+	}
+
+	@Provides
+	@Named("savePassword")
+	@UnlockScoped
+	static AtomicBoolean provideSavePasswordFlag(@Named("savedPassword") Optional<char[]> storedPassword) {
+		return new AtomicBoolean(storedPassword.isPresent());
+	}
+
 	@Provides
 	@UnlockWindow
 	@UnlockScoped

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 179 - 0
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java


+ 4 - 4
main/ui/src/main/resources/fxml/unlock.fxml

@@ -21,15 +21,15 @@
 	<children>
 		<VBox spacing="6">
 			<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vault.displayableName}" wrapText="true"/>
-			<NiceSecurePasswordField fx:id="passwordField"/>
-			<CheckBox fx:id="savePassword" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.vault.processing}" visible="${controller.keychainAccessAvailable}"/>
+			<NiceSecurePasswordField fx:id="passwordField" disable="${controller.userInteractionDisabled}"/>
+			<CheckBox fx:id="savePasswordCheckbox" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox" disable="${controller.userInteractionDisabled}" visible="${controller.keychainAccessAvailable}"/>
 		</VBox>
 
 		<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
 			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
 				<buttons>
-					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.vault.processing}"/>
-					<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonState}" disable="${controller.unlockButtonDisabled}">
+					<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel" disable="${controller.userInteractionDisabled}"/>
+					<Button text="%unlock.unlockBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#unlock" contentDisplay="${controller.unlockButtonContentDisplay}" disable="${controller.unlockButtonDisabled}">
 						<graphic>
 							<ProgressIndicator progress="-1" prefWidth="12" prefHeight="12"/>
 						</graphic>

+ 1 - 1
main/ui/src/test/java/org/cryptomator/ui/controls/SecurePasswordFieldTest.java

@@ -152,7 +152,7 @@ class SecurePasswordFieldTest {
 
 		CharSequence result1 = pwField.getCharacters();
 		Assertions.assertEquals("topSecret", result1.toString());
-		pwField.swipe();
+		pwField.wipe();
 		CharSequence result2 = pwField.getCharacters();
 		Assertions.assertEquals("         ", result1.toString());
 		Assertions.assertEquals("", result2.toString());