diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 310e11747..d79237baa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -28,6 +28,7 @@ public enum FxmlFile { REMOVE_VAULT("/fxml/remove_vault.fxml"), // UNLOCK("/fxml/unlock.fxml"), UNLOCK_INVALID_MOUNT_POINT("/fxml/unlock_invalid_mount_point.fxml"), // + UNLOCK_SELECT_MASTERKEYFILE("/fxml/unlock_select_masterkeyfile.fxml"), // UNLOCK_SUCCESS("/fxml/unlock_success.fxml"), // VAULT_OPTIONS("/fxml/vault_options.fxml"), // VAULT_STATISTICS("/fxml/stats.fxml"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java index da5e2884d..97821ab25 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockModule.java @@ -24,6 +24,7 @@ import javax.inject.Provider; import javafx.scene.Scene; import javafx.stage.Modality; import javafx.stage.Stage; +import java.nio.file.Path; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; @@ -40,12 +41,23 @@ abstract class UnlockModule { CANCELED } + public enum MasterkeyFileProvision { + MASTERKEYFILE_PROVIDED, + CANCELED + } + @Provides @UnlockScoped static UserInteractionLock providePasswordEntryLock() { return new UserInteractionLock<>(null); } + @Provides + @UnlockScoped + static UserInteractionLock provideMasterkeyFileProvisionLock() { + return new UserInteractionLock<>(null); + } + @Provides @Named("savedPassword") @UnlockScoped @@ -62,6 +74,13 @@ abstract class UnlockModule { } } + @Provides + @Named("userProvidedMasterkeyPath") + @UnlockScoped + static AtomicReference provideUserProvidedMasterkeyPath() { + return new AtomicReference(); + } + @Provides @UnlockScoped static AtomicReference providePassword(@Named("savedPassword") Optional storedPassword) { @@ -105,6 +124,13 @@ abstract class UnlockModule { return fxmlLoaders.createScene("/fxml/unlock.fxml"); } + @Provides + @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) + @UnlockScoped + static Scene provideUnlockSelectMasterkeyFileScene(@UnlockWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/unlock_select_masterkeyfile.fxml"); + } + @Provides @FxmlScene(FxmlFile.UNLOCK_SUCCESS) @UnlockScoped @@ -126,6 +152,11 @@ abstract class UnlockModule { @FxControllerKey(UnlockController.class) abstract FxController bindUnlockController(UnlockController controller); + @Binds + @IntoMap + @FxControllerKey(UnlockSelectMasterkeyFileController.class) + abstract FxController bindUnlockSelectMasterkeyFileController(UnlockSelectMasterkeyFileController controller); + @Binds @IntoMap @FxControllerKey(UnlockSuccessController.class) diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSelectMasterkeyFileController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSelectMasterkeyFileController.java new file mode 100644 index 000000000..656f39b6d --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSelectMasterkeyFileController.java @@ -0,0 +1,76 @@ +package org.cryptomator.ui.unlock; + +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.UserInteractionLock; +import org.cryptomator.ui.unlock.UnlockModule.MasterkeyFileProvision; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javax.inject.Named; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.fxml.FXML; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; +import java.io.File; +import java.nio.file.Path; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; + +@UnlockScoped +public class UnlockSelectMasterkeyFileController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(UnlockSelectMasterkeyFileController.class); + + private final BooleanProperty proceedButtonDisabled = new SimpleBooleanProperty(); + private final Stage window; + private final AtomicReference masterkeyPath; + private final UserInteractionLock masterkeyFileProvisionLock; + private final ResourceBundle resourceBundle; + + @Inject + public UnlockSelectMasterkeyFileController(@UnlockWindow Stage window, @Named("userProvidedMasterkeyPath") AtomicReference masterkeyPath, UserInteractionLock masterkeyFileProvisionLock, ResourceBundle resourceBundle) { + this.window = window; + this.masterkeyPath = masterkeyPath; + this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; + this.resourceBundle = resourceBundle; + this.window.setOnHiding(this::windowClosed); + } + + @FXML + public void cancel() { + window.close(); + } + + private void windowClosed(WindowEvent windowEvent) { + // if not already interacted, mark this workflow as cancelled: + if (masterkeyFileProvisionLock.awaitingInteraction().get()) { + LOG.debug("Unlock canceled by user."); + masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.CANCELED); + } + } + + @FXML + public void proceed() { + LOG.trace("proceed()"); + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(resourceBundle.getString("unlock.chooseMasterkey.filePickerTitle")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator")); + File masterkeyFile = fileChooser.showOpenDialog(window); + if (masterkeyFile != null) { + LOG.debug("Chose masterkey file: {}", masterkeyFile); + masterkeyPath.set(masterkeyFile.toPath()); + masterkeyFileProvisionLock.interacted(MasterkeyFileProvision.MASTERKEYFILE_PROVIDED); + } + } + + public BooleanProperty proceedButtonDisabledProperty() { + return proceedButtonDisabled; + } + + public boolean isProceedButtonDisabled() { + return proceedButtonDisabled.get(); + } +} 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 index 8c250665a..2c6e3be65 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockWorkflow.java @@ -20,6 +20,7 @@ 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.MasterkeyFileProvision; import org.cryptomator.ui.unlock.UnlockModule.PasswordEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,9 +59,12 @@ public class UnlockWorkflow extends Task implements MasterkeyFileLoader private final AtomicReference password; private final AtomicBoolean savePassword; private final Optional savedPassword; + private final AtomicReference correctMasterkeyPath; private final UserInteractionLock passwordEntryLock; + private final UserInteractionLock masterkeyFileProvisionLock; private final KeychainManager keychain; private final Lazy unlockScene; + private final Lazy selectMasterkeyFileScene; private final Lazy successScene; private final Lazy invalidMountPointScene; private final ErrorComponent.Builder errorComponent; @@ -69,16 +73,19 @@ public class UnlockWorkflow extends Task implements MasterkeyFileLoader private boolean didEnterWrongPassphrase = false; @Inject - UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, UserInteractionLock passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent, MasterkeyFileAccess masterkeyFileAccess) { + UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional savedPassword, @Named("userProvidedMasterkeyPath") AtomicReference correctMasterkeyPath, UserInteractionLock passwordEntryLock, UserInteractionLock masterkeyFileProvisionLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy unlockScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy selectMasterkeyFileScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy invalidMountPointScene, ErrorComponent.Builder errorComponent, MasterkeyFileAccess masterkeyFileAccess) { this.window = window; this.vault = vault; this.vaultService = vaultService; this.password = password; this.savePassword = savePassword; this.savedPassword = savedPassword; + this.correctMasterkeyPath = correctMasterkeyPath; this.passwordEntryLock = passwordEntryLock; + this.masterkeyFileProvisionLock = masterkeyFileProvisionLock; this.keychain = keychain; this.unlockScene = unlockScene; + this.selectMasterkeyFileScene = selectMasterkeyFileScene; this.successScene = successScene; this.invalidMountPointScene = invalidMountPointScene; this.errorComponent = errorComponent; @@ -101,7 +108,7 @@ public class UnlockWorkflow extends Task implements MasterkeyFileLoader attemptUnlock(keyLoader, 0); handleSuccess(); return true; - } catch (PasswordEntryCancelledException e) { + } catch (UnlockCancelledException e) { cancel(false); // set Tasks state to cancelled return false; } finally { @@ -123,12 +130,35 @@ public class UnlockWorkflow extends Task implements MasterkeyFileLoader @Override public Path getCorrectMasterkeyFilePath(String masterkeyFilePath) { - LOG.warn("Did not find masterkey file at expected path: {}", masterkeyFilePath); - throw new MasterkeyLoadingFailedException(masterkeyFilePath + " not found.", new UnsupportedOperationException("getCorrectMasterkeyFilePath() not implemented")); + try { + if (askForCorrectMasterkeyFile() == MasterkeyFileProvision.MASTERKEYFILE_PROVIDED) { + return correctMasterkeyPath.get(); + } else { + throw new UnlockCancelledException("Password entry cancelled."); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new UnlockCancelledException("Password entry interrupted", e); + } + } + + private MasterkeyFileProvision askForCorrectMasterkeyFile() throws InterruptedException { + Platform.runLater(() -> { + window.setScene(selectMasterkeyFileScene.get()); + window.show(); + Window owner = window.getOwner(); + if (owner != null) { + window.setX(owner.getX() + (owner.getWidth() - window.getWidth()) / 2); + window.setY(owner.getY() + (owner.getHeight() - window.getHeight()) / 2); + } else { + window.centerOnScreen(); + } + }); + return masterkeyFileProvisionLock.awaitInteraction(); } @Override - public CharSequence getPassphrase(Path path) throws PasswordEntryCancelledException { + public CharSequence getPassphrase(Path path) throws UnlockCancelledException { if (password.get() != null) { // e.g. pre-filled from keychain return CharBuffer.wrap(password.get()); } @@ -139,11 +169,11 @@ public class UnlockWorkflow extends Task implements MasterkeyFileLoader assert password.get() != null; return CharBuffer.wrap(password.get()); } else { - throw new PasswordEntryCancelledException("Password entry cancelled."); + throw new UnlockCancelledException("Password entry cancelled."); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new PasswordEntryCancelledException("Password entry interrupted", e); + throw new UnlockCancelledException("Password entry interrupted", e); } } @@ -255,12 +285,12 @@ public class UnlockWorkflow extends Task implements MasterkeyFileLoader vault.setState(VaultState.LOCKED); } - private static class PasswordEntryCancelledException extends MasterkeyLoadingFailedException { - public PasswordEntryCancelledException(String message) { + private static class UnlockCancelledException extends MasterkeyLoadingFailedException { + public UnlockCancelledException(String message) { super(message); } - public PasswordEntryCancelledException(String message, Throwable cause) { + public UnlockCancelledException(String message, Throwable cause) { super(message, cause); } } diff --git a/main/ui/src/main/resources/fxml/unlock_select_masterkeyfile.fxml b/main/ui/src/main/resources/fxml/unlock_select_masterkeyfile.fxml new file mode 100644 index 000000000..91a643ef6 --- /dev/null +++ b/main/ui/src/main/resources/fxml/unlock_select_masterkeyfile.fxml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +