From d0f0c095858dfc2b6759ba7ccca90e49e12dd50d Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 9 Dec 2014 10:50:09 +0100 Subject: [PATCH] - Improved shutdown hooks - Redesigned UI, now a single-window application (todo: minimize to tray) --- .../org/cryptomator/webdav/WebDAVServer.java | 17 +- .../org/cryptomator/ui/AccessController.java | 113 ------ .../cryptomator/ui/InitializeController.java | 122 ++++-- .../org/cryptomator/ui/MainApplication.java | 9 +- .../org/cryptomator/ui/MainController.java | 378 +++++------------- .../org/cryptomator/ui/UnlockController.java | 154 +++++++ .../cryptomator/ui/UnlockedController.java | 71 ++++ .../ui/controls/DirectoryListCell.java | 19 + .../org/cryptomator/ui/model/Directory.java | 127 ++++++ .../ui/model/DirectoryDeserializer.java | 23 ++ .../ui/model/DirectorySerializer.java | 19 + .../org/cryptomator/ui/settings/Settings.java | 28 +- main/ui/src/main/resources/initialize.fxml | 38 +- .../main/resources/localization.properties | 37 +- main/ui/src/main/resources/main.fxml | 98 ++--- main/ui/src/main/resources/panels.css | 63 --- main/ui/src/main/resources/unlock.fxml | 47 +++ .../resources/{access.fxml => unlocked.fxml} | 22 +- main/ui/src/main/resources/welcome.fxml | 26 ++ 19 files changed, 785 insertions(+), 626 deletions(-) delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/AccessController.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/UnlockController.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/model/Directory.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java delete mode 100644 main/ui/src/main/resources/panels.css create mode 100644 main/ui/src/main/resources/unlock.fxml rename main/ui/src/main/resources/{access.fxml => unlocked.fxml} (52%) create mode 100644 main/ui/src/main/resources/welcome.fxml diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java index 21d8c108f..798a872bb 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java +++ b/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java @@ -32,6 +32,7 @@ public final class WebDAVServer { private static final int MIN_THREADS = 4; private static final int THREAD_IDLE_SECONDS = 20; private final Server server; + private int port; public WebDAVServer() { final BlockingQueue queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS); @@ -42,9 +43,9 @@ public final class WebDAVServer { /** * @param workDir Path of encrypted folder. * @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams. - * @return port, on which the server did start + * @return true upon success */ - public int start(final String workDir, final Cryptor cryptor) { + public synchronized boolean start(final String workDir, final Cryptor cryptor) { final ServerConnector connector = new ServerConnector(server); connector.setHost(LOCALHOST); @@ -58,20 +59,22 @@ public final class WebDAVServer { try { server.setConnectors(new Connector[] {connector}); server.start(); + port = connector.getLocalPort(); + return true; } catch (Exception ex) { LOG.error("Server couldn't be started", ex); + return false; } - - return connector.getLocalPort(); } public boolean isRunning() { return server.isRunning(); } - public boolean stop() { + public synchronized boolean stop() { try { server.stop(); + port = 0; } catch (Exception ex) { LOG.error("Server couldn't be stopped", ex); } @@ -85,4 +88,8 @@ public final class WebDAVServer { return result; } + public int getPort() { + return port; + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/AccessController.java b/main/ui/src/main/java/org/cryptomator/ui/AccessController.java deleted file mode 100644 index 4b7f9c6e0..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/AccessController.java +++ /dev/null @@ -1,113 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2014 Sebastian Stenzel - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - ******************************************************************************/ -package org.cryptomator.ui; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.ResourceBundle; - -import javafx.event.ActionEvent; -import javafx.fxml.FXML; -import javafx.fxml.Initializable; -import javafx.scene.control.Label; -import javafx.scene.layout.GridPane; - -import org.apache.commons.io.IOUtils; -import org.cryptomator.crypto.aes256.Aes256Cryptor; -import org.cryptomator.crypto.exceptions.DecryptFailedException; -import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; -import org.cryptomator.crypto.exceptions.WrongPasswordException; -import org.cryptomator.ui.controls.SecPasswordField; -import org.cryptomator.ui.settings.Settings; -import org.cryptomator.ui.util.WebDavMounter; -import org.cryptomator.ui.util.WebDavMounter.CommandFailedException; -import org.cryptomator.webdav.WebDAVServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AccessController implements Initializable { - - private static final Logger LOG = LoggerFactory.getLogger(AccessController.class); - - private final Aes256Cryptor cryptor = new Aes256Cryptor(); - private final WebDAVServer server = new WebDAVServer(); - private ResourceBundle rb; - private String unmountCmd; - - @FXML - private GridPane rootPane; - - @FXML - private Label messageLabel; - - @Override - public void initialize(URL url, ResourceBundle rb) { - this.rb = rb; - } - - @FXML - protected void closeVault(ActionEvent event) { - this.tryStop(); - this.rootPane.getScene().getWindow().hide(); - } - - public boolean unlockStorage(Path masterKeyPath, SecPasswordField passwordField, Label errorMessageLabel) { - final CharSequence password = passwordField.getCharacters(); - InputStream masterKeyInputStream = null; - try { - masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ); - cryptor.decryptMasterKey(masterKeyInputStream, password); - tryStart(); - return true; - } catch (DecryptFailedException | IOException ex) { - errorMessageLabel.setText(rb.getString("access.errorMessage.decryptionFailed")); - LOG.error("Decryption failed for technical reasons.", ex); - } catch (WrongPasswordException e) { - errorMessageLabel.setText(rb.getString("access.errorMessage.wrongPassword")); - } catch (UnsupportedKeyLengthException ex) { - errorMessageLabel.setText(rb.getString("access.errorMessage.unsupportedKeyLengthInstallJCE")); - LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); - } finally { - passwordField.swipe(); - IOUtils.closeQuietly(masterKeyInputStream); - } - return false; - } - - private void tryStart() { - final Settings settings = Settings.load(); - final int webdavPort = server.start(settings.getWebdavWorkDir(), cryptor); - if (webdavPort > 0) { - try { - unmountCmd = WebDavMounter.mount(webdavPort); - MainApplication.addShutdownTask(this::tryStop); - } catch (CommandFailedException e) { - messageLabel.setText(String.format(rb.getString("access.messageLabel.mountFailed"), webdavPort)); - LOG.error("Mounting WebDAV share failed.", e); - } - } - } - - public void tryStop() { - if (server != null && server.isRunning()) { - try { - WebDavMounter.unmount(unmountCmd); - } catch (CommandFailedException e) { - LOG.warn("Unmounting WebDAV share failed.", e); - } - server.stop(); - cryptor.swipeSensitiveData(); - } - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java index bd722a003..5ff50dad8 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java @@ -24,21 +24,33 @@ import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyEvent; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.aes256.Aes256Cryptor; +import org.cryptomator.ui.controls.ClearOnDisableListener; import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.model.Directory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class InitializeController implements Initializable { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); + private static final int MAX_USERNAME_LENGTH = 250; private ResourceBundle localization; - private SecPasswordField referencePasswordField; - private Path masterKeyPath; - private InitializationFinishedCallback callback; + private Directory directory; + private InitializationListener listener; + + @FXML + private TextField usernameField; + + @FXML + private SecPasswordField passwordField; @FXML private SecPasswordField retypePasswordField; @@ -52,26 +64,69 @@ public class InitializeController implements Initializable { @Override public void initialize(URL url, ResourceBundle rb) { this.localization = rb; + usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); + usernameField.textProperty().addListener(this::usernameFieldDidChange); + passwordField.textProperty().addListener(this::passwordFieldDidChange); retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange); + retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField)); + } + // **************************************** + // Username field + // **************************************** + + public void filterAlphanumericKeyEvents(KeyEvent t) { + if (t.getCharacter() == null || t.getCharacter().length() == 0) { + return; + } + char c = t.getCharacter().charAt(0); + if (!CharUtils.isAsciiAlphanumeric(c)) { + t.consume(); + } + } + + public void usernameFieldDidChange(ObservableValue property, String oldValue, String newValue) { + if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) { + usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH)); + } + passwordField.setDisable(StringUtils.isEmpty(newValue)); + } + + // **************************************** + // Password field + // **************************************** + + private void passwordFieldDidChange(ObservableValue property, String oldValue, String newValue) { + retypePasswordField.setDisable(StringUtils.isEmpty(newValue)); + } + + // **************************************** + // Retype password field + // **************************************** + private void retypePasswordFieldDidChange(ObservableValue property, String oldValue, String newValue) { - boolean passwordsAreEqual = referencePasswordField.getText().equals(retypePasswordField.getText()); + boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText()); okButton.setDisable(!passwordsAreEqual); } + // **************************************** + // OK button + // **************************************** + @FXML - protected void initWorkDir(ActionEvent event) { - final Aes256Cryptor cryptor = new Aes256Cryptor(); - final CharSequence password = referencePasswordField.getCharacters(); + protected void initializeVault(ActionEvent event) { + final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT; + final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName); + final CharSequence password = passwordField.getCharacters(); OutputStream masterKeyOutputStream = null; try { masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); - cryptor.randomizeMasterKey(); - cryptor.encryptMasterKey(masterKeyOutputStream, password); - cryptor.swipeSensitiveData(); - if (callback != null) { - callback.initializationFinished(InitializationResult.SUCCESS); + directory.getCryptor().randomizeMasterKey(); + directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password); + directory.getCryptor().swipeSensitiveData(); + if (listener != null) { + listener.didInitialize(this); } } catch (FileAlreadyExistsException ex) { messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); @@ -80,52 +135,35 @@ public class InitializeController implements Initializable { } catch (IOException ex) { LOG.error("I/O Exception", ex); } finally { + usernameField.setText(null); + passwordField.swipe(); retypePasswordField.swipe(); IOUtils.closeQuietly(masterKeyOutputStream); } } - @FXML - protected void cancel(ActionEvent event) { - if (callback != null) { - callback.initializationFinished(InitializationResult.CANCELED); - } - } - /* Getter/Setter */ - public SecPasswordField getReferencePasswordField() { - return referencePasswordField; + public Directory getDirectory() { + return directory; } - public void setReferencePasswordField(SecPasswordField referencePasswordField) { - this.referencePasswordField = referencePasswordField; + public void setDirectory(Directory directory) { + this.directory = directory; } - public Path getMasterKeyPath() { - return masterKeyPath; + public InitializationListener getListener() { + return listener; } - public void setMasterKeyPath(Path masterKeyPath) { - this.masterKeyPath = masterKeyPath; + public void setListener(InitializationListener listener) { + this.listener = listener; } - public InitializationFinishedCallback getCallback() { - return callback; - } + /* callback */ - public void setCallback(InitializationFinishedCallback callback) { - this.callback = callback; - } - - /* Modal callback stuff */ - - enum InitializationResult { - CANCELED, SUCCESS - }; - - interface InitializationFinishedCallback { - void initializationFinished(InitializationResult result); + interface InitializationListener { + void didInitialize(InitializeController ctrl); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index aacc754fa..21db728b3 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -34,7 +34,10 @@ public class MainApplication extends Application { @Override public void start(final Stage primaryStage) throws IOException { final ResourceBundle localizations = ResourceBundle.getBundle("localization"); - final Parent root = FXMLLoader.load(getClass().getResource("/main.fxml"), localizations); + final FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"), localizations); + final Parent root = loader.load(); + final MainController ctrl = loader.getController(); + ctrl.setStage(primaryStage); final Scene scene = new Scene(root); primaryStage.setTitle("Cryptomator"); primaryStage.setScene(scene); @@ -50,11 +53,11 @@ public class MainApplication extends Application { super.stop(); } - static void addShutdownTask(Runnable r) { + public static void addShutdownTask(Runnable r) { SHUTDOWN_TASKS.add(r); } - static void removeShutdownTask(Runnable r) { + public static void removeShutdownTask(Runnable r) { SHUTDOWN_TASKS.remove(r); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/MainController.java index 02afd3c19..a88b71acc 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainController.java @@ -11,315 +11,143 @@ package org.cryptomator.ui; import java.io.File; import java.io.IOException; import java.net.URL; -import java.nio.file.FileSystems; -import java.nio.file.InvalidPathException; -import java.nio.file.Path; -import java.util.Iterator; import java.util.ResourceBundle; -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; +import javafx.collections.ListChangeListener; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; import javafx.fxml.Initializable; import javafx.scene.Parent; -import javafx.scene.Scene; -import javafx.scene.control.Button; -import javafx.scene.control.ComboBox; -import javafx.scene.control.Label; -import javafx.scene.control.SplitMenuButton; -import javafx.scene.control.TextField; -import javafx.scene.input.KeyCode; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.GridPane; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Pane; import javafx.stage.DirectoryChooser; -import javafx.stage.Modality; import javafx.stage.Stage; -import org.apache.commons.lang3.CharUtils; -import org.apache.commons.lang3.StringUtils; -import org.cryptomator.crypto.aes256.Aes256Cryptor; -import org.cryptomator.ui.InitializeController.InitializationResult; -import org.cryptomator.ui.controls.ClearOnDisableListener; -import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.InitializeController.InitializationListener; +import org.cryptomator.ui.UnlockController.UnlockListener; +import org.cryptomator.ui.UnlockedController.LockListener; +import org.cryptomator.ui.controls.DirectoryListCell; +import org.cryptomator.ui.model.Directory; import org.cryptomator.ui.settings.Settings; -import org.cryptomator.ui.util.MasterKeyFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class MainController implements Initializable { +public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener { private static final Logger LOG = LoggerFactory.getLogger(MainController.class); - private static final int MAX_USERNAME_LENGTH = 200; + + private Stage stage; + + @FXML + private HBox rootPane; + + @FXML + private ListView directoryList; + + @FXML + private Pane contentPane; private ResourceBundle rb; - private Workflow workflow = Workflow.UNKNOWN; - - private enum Workflow { - UNKNOWN, INIT, OPEN - }; - - @FXML - private GridPane rootPane; - - @FXML - private TextField workDirTextField; - - @FXML - private TextField usernameField; - - @FXML - private ComboBox usernameBox; - - @FXML - private SecPasswordField passwordField; - - @FXML - private SplitMenuButton openButton; - - @FXML - private Button initializeButton; - - @FXML - private Label messageLabel; - @Override public void initialize(URL url, ResourceBundle rb) { this.rb = rb; - // attach event handler - workDirTextField.textProperty().addListener(this::workDirDidChange); - usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterUsernameKeyEvents); - usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField)); - usernameField.textProperty().addListener(this::usernameFieldDidChange); - usernameBox.valueProperty().addListener(this::usernameBoxDidChange); - passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField)); - passwordField.textProperty().addListener(this::passwordFieldDidChange); - passwordField.addEventHandler(KeyEvent.KEY_PRESSED, this::onPasswordFieldKeyPressed); - - // load settings - workDirTextField.setText(Settings.load().getWebdavWorkDir()); - usernameBox.setValue(Settings.load().getUsername()); + directoryList.setCellFactory(this::createDirecoryListCell); + directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange); + directoryList.getItems().addAll(Settings.load().getDirectories()); } - // **************************************** - // Workdir field - // **************************************** - @FXML - protected void chooseWorkDir(ActionEvent event) { - final File currentFolder = new File(workDirTextField.getText()); + private void didClickAddDirectory(ActionEvent event) { final DirectoryChooser dirChooser = new DirectoryChooser(); - if (currentFolder.exists()) { - dirChooser.setInitialDirectory(currentFolder); - } - final File file = dirChooser.showDialog(rootPane.getScene().getWindow()); + final File file = dirChooser.showDialog(stage); if (file != null && file.canWrite()) { - workDirTextField.setText(file.toString()); + final Directory dir = new Directory(file.toPath()); + directoryList.getItems().add(dir); + Settings.load().getDirectories().clear(); + Settings.load().getDirectories().addAll(directoryList.getItems()); + directoryList.getSelectionModel().selectLast(); } } - private void workDirDidChange(ObservableValue property, String oldValue, String newValue) { - if (StringUtils.isEmpty(newValue)) { - usernameField.setDisable(true); - usernameBox.setDisable(true); - return; - } + private ListCell createDirecoryListCell(ListView param) { + return new DirectoryListCell(); + } + + private void selectedDirectoryDidChange(ListChangeListener.Change change) { + final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem(); + stage.setTitle(selectedDir.getName()); try { - final Path dir = FileSystems.getDefault().getPath(newValue); - final Iterator masterKeys = MasterKeyFilter.filteredDirectory(dir).iterator(); - if (masterKeys.hasNext()) { - workflow = Workflow.OPEN; - showUsernameBox(masterKeys); - showOpenButton(); + if (selectedDir.containsMasterKey()) { + this.showUnlockView(selectedDir); } else { - workflow = Workflow.INIT; - showUsernameField(); - showInitializeButton(); - } - usernameField.setDisable(false); - usernameBox.setDisable(false); - Settings.load().setWebdavWorkDir(newValue); - } catch (InvalidPathException | IOException e) { - usernameField.setDisable(true); - usernameBox.setDisable(true); - messageLabel.setText(rb.getString("main.messageLabel.invalidPath")); - } - } - - // **************************************** - // Username field - // **************************************** - - private void showUsernameField() { - messageLabel.setText(rb.getString("main.messageLabel.initVaultMessage")); - usernameBox.setVisible(false); - usernameField.setVisible(true); - Platform.runLater(usernameField::requestFocus); - } - - private void usernameFieldDidChange(ObservableValue property, String oldValue, String newValue) { - if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) { - usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH)); - } - passwordField.setDisable(StringUtils.isEmpty(usernameField.getText())); - } - - private void filterUsernameKeyEvents(KeyEvent t) { - if (t.getCharacter() == null || t.getCharacter().length() == 0) { - return; - } - char c = t.getCharacter().charAt(0); - if (!CharUtils.isAsciiAlphanumeric(c)) { - t.consume(); - } - } - - // **************************************** - // Username box - // **************************************** - - private void showUsernameBox(Iterator foundMasterKeys) { - messageLabel.setText(rb.getString("main.messageLabel.openVaultMessage")); - usernameField.setVisible(false); - usernameBox.setVisible(true); - - // update usernameBox options: - usernameBox.getItems().clear(); - final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase(); - foundMasterKeys.forEachRemaining(path -> { - final String fileName = path.getFileName().toString(); - final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt); - final String baseName = fileName.substring(0, beginOfExt); - usernameBox.getItems().add(baseName); - }); - - // autochoose user, if possible: - if (usernameBox.getItems().size() == 1) { - usernameBox.setValue(usernameBox.getItems().get(0)); - } - } - - private void usernameBoxDidChange(ObservableValue property, String oldValue, String newValue) { - if (!Workflow.OPEN.equals(workflow)) { - return; - } - if (newValue != null) { - Settings.load().setUsername(newValue); - } - passwordField.setDisable(StringUtils.isEmpty(newValue)); - Platform.runLater(passwordField::requestFocus); - } - - // **************************************** - // Password field - // **************************************** - - private void passwordFieldDidChange(ObservableValue property, String oldValue, String newValue) { - initializeButton.setDisable(StringUtils.isEmpty(newValue)); - openButton.setDisable(StringUtils.isEmpty(newValue)); - } - - public void onPasswordFieldKeyPressed(KeyEvent event) { - if (KeyCode.ENTER.equals(event.getCode())) { - switch (workflow) { - case OPEN: - openButton.fire(); - break; - case INIT: - initializeButton.fire(); - break; - default: - break; - } - } - } - - // **************************************** - // Initialize vault button - // **************************************** - - private void showInitializeButton() { - openButton.setVisible(false); - initializeButton.setVisible(true); - } - - @FXML - protected void showInitializationDialog(ActionEvent event) { - final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); - final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT; - final Path masterKeyPath = storagePath.resolve(masterKeyFileName); - - try { - final FXMLLoader loader = new FXMLLoader(getClass().getResource("/initialize.fxml"), rb); - final Parent initDialog = loader.load(); - final Scene dialogScene = new Scene(initDialog); - final Stage dialog = new Stage(); - final InitializeController ctrl = loader.getController(); - ctrl.setReferencePasswordField(passwordField); - ctrl.setMasterKeyPath(masterKeyPath); - ctrl.setCallback(result -> { - if (InitializationResult.SUCCESS.equals(result)) { - this.initializationSucceeded(); - } - dialog.close(); - }); - dialog.initModality(Modality.APPLICATION_MODAL); - dialog.initOwner(rootPane.getScene().getWindow()); - dialog.setTitle(rb.getString("initialize.title")); - dialog.setScene(dialogScene); - dialog.sizeToScene(); - dialog.setResizable(false); - dialog.show(); - } catch (IOException e) { - LOG.error("Failed to load fxml file.", e); - } - } - - private void initializationSucceeded() { - // trigger re-evaluation of work dir. there should be a masterkey file now. - this.workDirDidChange(workDirTextField.textProperty(), workDirTextField.getText(), workDirTextField.getText()); - } - - // **************************************** - // Open vault button - // **************************************** - - private void showOpenButton() { - initializeButton.setVisible(false); - openButton.setVisible(true); - } - - @FXML - protected void openVault(ActionEvent event) { - final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); - final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT; - final Path masterKeyPath = storagePath.resolve(masterKeyFileName); - - try { - final FXMLLoader loader = new FXMLLoader(getClass().getResource("/access.fxml"), rb); - final Parent accessDialog = loader.load(); - final Scene dialogScene = new Scene(accessDialog); - final AccessController ctrl = loader.getController(); - if (ctrl.unlockStorage(masterKeyPath, passwordField, messageLabel)) { - passwordField.swipe(); - final Stage dialog = new Stage(); - dialog.initModality(Modality.NONE); - dialog.initOwner(rootPane.getScene().getWindow()); - dialog.setTitle(storagePath.getFileName().toString()); - dialog.setScene(dialogScene); - dialog.sizeToScene(); - dialog.setResizable(false); - dialog.show(); - dialog.setOnCloseRequest(windowEvent -> { - ctrl.tryStop(); - }); - } else { - Platform.runLater(passwordField::requestFocus); + this.showInitializeView(selectedDir); } } catch (IOException e) { - LOG.error("Failed to load fxml file.", e); + LOG.error("Failed to analyze directory.", e); } } + + // **************************************** + // Subcontroller for right panel + // **************************************** + + private T showView(String fxml) { + try { + final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb); + final Parent root = loader.load(); + contentPane.getChildren().clear(); + contentPane.getChildren().add(root); + return loader.getController(); + } catch (IOException e) { + throw new IllegalStateException("Failed to load fxml file.", e); + } + } + + private void showInitializeView(Directory directory) { + final InitializeController ctrl = showView("/initialize.fxml"); + ctrl.setDirectory(directory); + ctrl.setListener(this); + } + + @Override + public void didInitialize(InitializeController ctrl) { + showUnlockView(ctrl.getDirectory()); + } + + private void showUnlockView(Directory directory) { + final UnlockController ctrl = showView("/unlock.fxml"); + ctrl.setDirectory(directory); + ctrl.setListener(this); + } + + @Override + public void didUnlock(UnlockController ctrl) { + showUnlockedView(ctrl.getDirectory()); + } + + private void showUnlockedView(Directory directory) { + final UnlockedController ctrl = showView("/unlocked.fxml"); + ctrl.setDirectory(directory); + ctrl.setListener(this); + } + + @Override + public void didLock(UnlockedController ctrl) { + showUnlockView(ctrl.getDirectory()); + } + + /* public Getter/Setter */ + + public Stage getStage() { + return stage; + } + + public void setStage(Stage stage) { + this.stage = stage; + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java new file mode 100644 index 000000000..8edb1d547 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2014 Sebastian Stenzel + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + ******************************************************************************/ +package org.cryptomator.ui; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ResourceBundle; + +import javafx.application.Platform; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.crypto.aes256.Aes256Cryptor; +import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.WrongPasswordException; +import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.model.Directory; +import org.cryptomator.ui.util.MasterKeyFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class UnlockController implements Initializable { + + private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); + + private ResourceBundle rb; + private UnlockListener listener; + private Directory directory; + + @FXML + private ComboBox usernameBox; + + @FXML + private SecPasswordField passwordField; + + @FXML + private Label messageLabel; + + @Override + public void initialize(URL url, ResourceBundle rb) { + this.rb = rb; + + usernameBox.valueProperty().addListener(this::didChooseUsername); + } + + // **************************************** + // Username box + // **************************************** + + public void didChooseUsername(ObservableValue property, String oldValue, String newValue) { + if (newValue != null) { + Platform.runLater(passwordField::requestFocus); + } + passwordField.setDisable(StringUtils.isEmpty(newValue)); + } + + // **************************************** + // Unlock button + // **************************************** + + @FXML + protected void didClickUnlockButton(ActionEvent event) { + final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT; + final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName); + final CharSequence password = passwordField.getCharacters(); + InputStream masterKeyInputStream = null; + try { + masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ); + directory.getCryptor().decryptMasterKey(masterKeyInputStream, password); + if (!directory.startServer()) { + messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed")); + return; + } + directory.mount(); + if (listener != null) { + listener.didUnlock(this); + } + } catch (DecryptFailedException | IOException ex) { + messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed")); + LOG.error("Decryption failed for technical reasons.", ex); + } catch (WrongPasswordException e) { + messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword")); + } catch (UnsupportedKeyLengthException ex) { + messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE")); + LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); + } finally { + passwordField.swipe(); + IOUtils.closeQuietly(masterKeyInputStream); + } + } + + private void findExistingUsernames() { + try { + DirectoryStream ds = MasterKeyFilter.filteredDirectory(directory.getPath()); + final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase(); + usernameBox.getItems().clear(); + for (final Path path : ds) { + final String fileName = path.getFileName().toString(); + final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt); + final String baseName = fileName.substring(0, beginOfExt); + usernameBox.getItems().add(baseName); + } + if (usernameBox.getItems().size() == 1) { + usernameBox.getSelectionModel().selectFirst(); + } + } catch (IOException e) { + LOG.trace("Invalid path: " + directory.getPath(), e); + } + } + + /* Getter/Setter */ + + public Directory getDirectory() { + return directory; + } + + public void setDirectory(Directory directory) { + this.directory = directory; + this.findExistingUsernames(); + } + + public UnlockListener getListener() { + return listener; + } + + public void setListener(UnlockListener listener) { + this.listener = listener; + } + + /* callback */ + + interface UnlockListener { + void didUnlock(UnlockController ctrl); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java new file mode 100644 index 000000000..f12880686 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2014 Sebastian Stenzel + * This file is licensed under the terms of the MIT license. + * See the LICENSE.txt file for more info. + * + * Contributors: + * Sebastian Stenzel - initial API and implementation + ******************************************************************************/ +package org.cryptomator.ui; + +import java.net.URL; +import java.util.ResourceBundle; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Label; + +import org.cryptomator.ui.model.Directory; + +public class UnlockedController implements Initializable { + + private ResourceBundle rb; + private LockListener listener; + private Directory directory; + + @FXML + private Label messageLabel; + + @Override + public void initialize(URL url, ResourceBundle rb) { + this.rb = rb; + + } + + @FXML + protected void closeVault(ActionEvent event) { + directory.unmount(); + directory.stopServer(); + if (listener != null) { + listener.didLock(this); + } + } + + /* Getter/Setter */ + + public Directory getDirectory() { + return directory; + } + + public void setDirectory(Directory directory) { + this.directory = directory; + final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort()); + messageLabel.setText(msg); + } + + public LockListener getListener() { + return listener; + } + + public void setListener(LockListener listener) { + this.listener = listener; + } + + /* callback */ + + interface LockListener { + void didLock(UnlockedController ctrl); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java b/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java new file mode 100644 index 000000000..bed87ef0c --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/DirectoryListCell.java @@ -0,0 +1,19 @@ +package org.cryptomator.ui.controls; + +import javafx.scene.control.ListCell; + +import org.cryptomator.ui.model.Directory; + +public class DirectoryListCell extends ListCell { + + @Override + protected void updateItem(Directory item, boolean empty) { + super.updateItem(item, empty); + if (item == null) { + setText(null); + } else { + setText(item.getName()); + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java new file mode 100644 index 000000000..0ace7a4d9 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Directory.java @@ -0,0 +1,127 @@ +package org.cryptomator.ui.model; + +import java.io.IOException; +import java.io.Serializable; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.crypto.aes256.Aes256Cryptor; +import org.cryptomator.ui.MainApplication; +import org.cryptomator.ui.util.MasterKeyFilter; +import org.cryptomator.ui.util.WebDavMounter; +import org.cryptomator.ui.util.WebDavMounter.CommandFailedException; +import org.cryptomator.webdav.WebDAVServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +@JsonSerialize(using = DirectorySerializer.class) +@JsonDeserialize(using = DirectoryDeserializer.class) +public class Directory implements Serializable { + + private static final long serialVersionUID = 3754487289683599469L; + private static final Logger LOG = LoggerFactory.getLogger(Directory.class); + + private final WebDAVServer server = new WebDAVServer(); + private final Aes256Cryptor cryptor = new Aes256Cryptor(); + private final Path path; + private boolean unlocked; + private String unmountCommand; + private final Runnable shutdownTask = new ShutdownTask(); + + public Directory(final Path path) { + if (!Files.isDirectory(path)) { + throw new IllegalArgumentException("Not a directory: " + path); + } + this.path = path; + } + + public boolean containsMasterKey() throws IOException { + return MasterKeyFilter.filteredDirectory(path).iterator().hasNext(); + } + + public synchronized boolean startServer() { + if (server.start(path.toString(), cryptor)) { + MainApplication.addShutdownTask(shutdownTask); + return true; + } else { + return false; + } + } + + public synchronized void stopServer() { + if (server.isRunning()) { + MainApplication.removeShutdownTask(shutdownTask); + this.unmount(); + server.stop(); + cryptor.swipeSensitiveData(); + } + } + + public boolean mount() { + try { + unmountCommand = WebDavMounter.mount(server.getPort()); + return true; + } catch (CommandFailedException e) { + LOG.warn("mount failed", e); + return false; + } + } + + public boolean unmount() { + try { + if (StringUtils.isNotEmpty(unmountCommand)) { + WebDavMounter.unmount(unmountCommand); + unmountCommand = null; + } + return true; + } catch (CommandFailedException e) { + LOG.warn("unmount failed", e); + return false; + } + } + + /* Getter/Setter */ + + public Path getPath() { + return path; + } + + /** + * @return Directory name without preceeding path components + */ + public String getName() { + return path.getFileName().toString(); + } + + public Aes256Cryptor getCryptor() { + return cryptor; + } + + public boolean isUnlocked() { + return unlocked; + } + + public void setUnlocked(boolean unlocked) { + this.unlocked = unlocked; + } + + public WebDAVServer getServer() { + return server; + } + + /* graceful shutdown */ + + private class ShutdownTask implements Runnable { + + @Override + public void run() { + stopServer(); + } + + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java b/main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java new file mode 100644 index 000000000..a5d942688 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/DirectoryDeserializer.java @@ -0,0 +1,23 @@ +package org.cryptomator.ui.model; + +import java.io.IOException; +import java.nio.file.FileSystems; +import java.nio.file.Path; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +public class DirectoryDeserializer extends JsonDeserializer { + + @Override + public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + final JsonNode node = jp.readValueAsTree(); + final String pathStr = node.get("path").asText(); + final Path path = FileSystems.getDefault().getPath(pathStr); + return new Directory(path); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java b/main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java new file mode 100644 index 000000000..83eef3839 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/model/DirectorySerializer.java @@ -0,0 +1,19 @@ +package org.cryptomator.ui.model; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class DirectorySerializer extends JsonSerializer { + + @Override + public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeStartObject(); + jgen.writeStringField("path", value.getPath().toString()); + jgen.writeEndObject(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 6a36bfc5a..b4f7a20b9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -17,8 +17,11 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.ui.model.Directory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,18 +43,18 @@ public class Settings implements Serializable { final FileSystem fs = FileSystems.getDefault(); if (SystemUtils.IS_OS_WINDOWS && appdata != null) { - SETTINGS_DIR = fs.getPath(appdata, "opencloudencryptor"); + SETTINGS_DIR = fs.getPath(appdata, "Cryptomator"); } else if (SystemUtils.IS_OS_WINDOWS && appdata == null) { - SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor"); + SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator"); } else if (SystemUtils.IS_OS_MAC_OSX) { - SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/opencloudencryptor"); + SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator"); } else { // (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix")) - SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor"); + SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator"); } } - private String webdavWorkDir; + private Collection directories; private String username; private Settings() { @@ -88,19 +91,20 @@ public class Settings implements Serializable { } private static Settings defaultSettings() { - final Settings result = new Settings(); - result.setWebdavWorkDir(System.getProperty("user.home", ".")); - return result; + return new Settings(); } /* Getter/Setter */ - public String getWebdavWorkDir() { - return webdavWorkDir; + public Collection getDirectories() { + if (directories == null) { + directories = new ArrayList<>(); + } + return directories; } - public void setWebdavWorkDir(String webdavWorkDir) { - this.webdavWorkDir = webdavWorkDir; + public void setDirectories(Collection directories) { + this.directories = directories; } public String getUsername() { diff --git a/main/ui/src/main/resources/initialize.fxml b/main/ui/src/main/resources/initialize.fxml index 8666404a0..3ebbf7a0e 100644 --- a/main/ui/src/main/resources/initialize.fxml +++ b/main/ui/src/main/resources/initialize.fxml @@ -14,38 +14,38 @@ + + - - - - - + - + - + - - + + - -