add new (optional) "choose masterkey file" step to unlock dialog

This commit is contained in:
Sebastian Stenzel
2021-01-29 17:44:45 +01:00
parent ff17b60f56
commit b15471b4ff
6 changed files with 193 additions and 10 deletions

View File

@@ -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"), //

View File

@@ -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<PasswordEntry> providePasswordEntryLock() {
return new UserInteractionLock<>(null);
}
@Provides
@UnlockScoped
static UserInteractionLock<MasterkeyFileProvision> provideMasterkeyFileProvisionLock() {
return new UserInteractionLock<>(null);
}
@Provides
@Named("savedPassword")
@UnlockScoped
@@ -62,6 +74,13 @@ abstract class UnlockModule {
}
}
@Provides
@Named("userProvidedMasterkeyPath")
@UnlockScoped
static AtomicReference<Path> provideUserProvidedMasterkeyPath() {
return new AtomicReference();
}
@Provides
@UnlockScoped
static AtomicReference<char[]> providePassword(@Named("savedPassword") Optional<char[]> 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)

View File

@@ -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<Path> masterkeyPath;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final ResourceBundle resourceBundle;
@Inject
public UnlockSelectMasterkeyFileController(@UnlockWindow Stage window, @Named("userProvidedMasterkeyPath") AtomicReference<Path> masterkeyPath, UserInteractionLock<MasterkeyFileProvision> 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();
}
}

View File

@@ -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<Boolean> implements MasterkeyFileLoader
private final AtomicReference<char[]> password;
private final AtomicBoolean savePassword;
private final Optional<char[]> savedPassword;
private final AtomicReference<Path> correctMasterkeyPath;
private final UserInteractionLock<PasswordEntry> passwordEntryLock;
private final UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock;
private final KeychainManager keychain;
private final Lazy<Scene> unlockScene;
private final Lazy<Scene> selectMasterkeyFileScene;
private final Lazy<Scene> successScene;
private final Lazy<Scene> invalidMountPointScene;
private final ErrorComponent.Builder errorComponent;
@@ -69,16 +73,19 @@ public class UnlockWorkflow extends Task<Boolean> implements MasterkeyFileLoader
private boolean didEnterWrongPassphrase = false;
@Inject
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, UserInteractionLock<PasswordEntry> passwordEntryLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> invalidMountPointScene, ErrorComponent.Builder errorComponent, MasterkeyFileAccess masterkeyFileAccess) {
UnlockWorkflow(@UnlockWindow Stage window, @UnlockWindow Vault vault, VaultService vaultService, AtomicReference<char[]> password, @Named("savePassword") AtomicBoolean savePassword, @Named("savedPassword") Optional<char[]> savedPassword, @Named("userProvidedMasterkeyPath") AtomicReference<Path> correctMasterkeyPath, UserInteractionLock<PasswordEntry> passwordEntryLock, UserInteractionLock<MasterkeyFileProvision> masterkeyFileProvisionLock, KeychainManager keychain, @FxmlScene(FxmlFile.UNLOCK) Lazy<Scene> unlockScene, @FxmlScene(FxmlFile.UNLOCK_SELECT_MASTERKEYFILE) Lazy<Scene> selectMasterkeyFileScene, @FxmlScene(FxmlFile.UNLOCK_SUCCESS) Lazy<Scene> successScene, @FxmlScene(FxmlFile.UNLOCK_INVALID_MOUNT_POINT) Lazy<Scene> 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<Boolean> 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<Boolean> 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<Boolean> 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<Boolean> 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);
}
}

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Circle?>
<?import org.cryptomator.ui.controls.FormattedLabel?>
<VBox xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="org.cryptomator.ui.unlock.UnlockSelectMasterkeyFileController"
minWidth="400"
maxWidth="400"
minHeight="145"
spacing="12">
<padding>
<Insets topRightBottomLeft="12"/>
</padding>
<children>
<HBox spacing="12" alignment="CENTER_LEFT" VBox.vgrow="ALWAYS">
<StackPane alignment="CENTER" HBox.hgrow="NEVER">
<Circle styleClass="glyph-icon-primary" radius="24"/>
<FontAwesome5IconView styleClass="glyph-icon-white" glyph="FILE" glyphSize="24"/>
</StackPane>
<VBox spacing="6">
<Label text="Could not find the masterkey file for this vault at its expected location. Please choose the key file manually." wrapText="true" HBox.hgrow="ALWAYS"/>
</VBox>
</HBox>
<VBox alignment="BOTTOM_CENTER" VBox.vgrow="ALWAYS">
<ButtonBar buttonMinWidth="120" buttonOrder="+CX">
<buttons>
<Button text="%generic.button.cancel" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#cancel"/>
<Button text="%generic.button.next" ButtonBar.buttonData="NEXT_FORWARD" defaultButton="true" onAction="#proceed" disable="${controller.proceedButtonDisabled}"/>
</buttons>
</ButtonBar>
</VBox>
</children>
</VBox>

View File

@@ -98,6 +98,8 @@ unlock.title=Unlock Vault
unlock.passwordPrompt=Enter password for "%s":
unlock.savePassword=Save Password
unlock.unlockBtn=Unlock
##
unlock.chooseMasterkey.filePickerTitle=Select Masterkey File
## Success
unlock.success.message=Unlocked "%s" successfully! Your vault is now accessible.
unlock.success.rememberChoice=Remember choice, don't show this again