diff --git a/main/core/pom.xml b/main/core/pom.xml
index d23e756e7..39044745b 100644
--- a/main/core/pom.xml
+++ b/main/core/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.1.0
+ 0.2.0
core
Cryptomator core I/O module
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 69ec48815..0e819d50f 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java
@@ -21,18 +21,9 @@ import org.slf4j.LoggerFactory;
public final class WebDAVServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
- private static final WebDAVServer INSTANCE = new WebDAVServer();
private static final String LOCALHOST = "127.0.0.1";
private final Server server = new Server();
- private WebDAVServer() {
- // make constructor private
- }
-
- public static WebDAVServer getInstance() {
- return INSTANCE;
- }
-
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
diff --git a/main/crypto-aes/pom.xml b/main/crypto-aes/pom.xml
index c05dee070..e2387264e 100644
--- a/main/crypto-aes/pom.xml
+++ b/main/crypto-aes/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.1.0
+ 0.2.0
crypto-aes
Cryptomator cryptographic module (AES)
diff --git a/main/crypto-api/pom.xml b/main/crypto-api/pom.xml
index 894471358..78c8c3e7a 100644
--- a/main/crypto-api/pom.xml
+++ b/main/crypto-api/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.1.0
+ 0.2.0
crypto-api
Cryptomator cryptographic module API
diff --git a/main/pom.xml b/main/pom.xml
index 3c66b30fb..e4ec9b797 100644
--- a/main/pom.xml
+++ b/main/pom.xml
@@ -11,7 +11,7 @@
4.0.0
org.cryptomator
main
- 0.1.0
+ 0.2.0
pom
Cryptomator
diff --git a/main/ui/pom.xml b/main/ui/pom.xml
index ac5a0ab03..0b1c57673 100644
--- a/main/ui/pom.xml
+++ b/main/ui/pom.xml
@@ -12,7 +12,7 @@
org.cryptomator
main
- 0.1.0
+ 0.2.0
ui
Cryptomator GUI
diff --git a/main/ui/src/main/java/org/cryptomator/ui/AccessController.java b/main/ui/src/main/java/org/cryptomator/ui/AccessController.java
index 47dc60504..75d09fc35 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/AccessController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/AccessController.java
@@ -8,41 +8,28 @@
******************************************************************************/
package org.cryptomator.ui;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
-import java.nio.file.DirectoryStream;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
-import java.nio.file.InvalidPathException;
-import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
+import java.util.concurrent.atomic.AtomicInteger;
-import javafx.application.Platform;
-import javafx.beans.value.ChangeListener;
-import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
-import javafx.scene.control.Button;
-import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
-import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
-import javafx.stage.DirectoryChooser;
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.settings.Settings;
-import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.WebDavMounter;
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
import org.cryptomator.webdav.WebDAVServer;
@@ -52,145 +39,46 @@ import org.slf4j.LoggerFactory;
public class AccessController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(AccessController.class);
+ private static final AtomicInteger ID_GENERATOR = new AtomicInteger();
private final Aes256Cryptor cryptor = new Aes256Cryptor();
- private ResourceBundle localization;
+ private final WebDAVServer server = new WebDAVServer();
+ private final int id = ID_GENERATOR.getAndIncrement();
+ private ResourceBundle rb;
+
@FXML
- private GridPane rootGridPane;
- @FXML
- private TextField workDirTextField;
- @FXML
- private ComboBox usernameBox;
- @FXML
- private SecPasswordField passwordField;
- @FXML
- private Button startServerButton;
+ private GridPane rootPane;
+
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
- this.localization = rb;
- workDirTextField.textProperty().addListener(new WorkDirChangeListener());
- usernameBox.valueProperty().addListener(new UsernameChangeListener());
- workDirTextField.setText(Settings.load().getWebdavWorkDir());
- usernameBox.setValue(Settings.load().getUsername());
+ this.rb = rb;
}
- /**
- * Step 1: Choose encrypted storage:
- */
@FXML
- protected void chooseWorkDir(ActionEvent event) {
- messageLabel.setText(null);
- final File currentFolder = new File(workDirTextField.getText());
- final DirectoryChooser dirChooser = new DirectoryChooser();
- if (currentFolder.exists()) {
- dirChooser.setInitialDirectory(currentFolder);
- }
- final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
- if (file != null) {
- workDirTextField.setText(file.toString());
- }
+ protected void closeVault(ActionEvent event) {
+ this.tryStop();
+ this.rootPane.getScene().getWindow().hide();
}
- private final class WorkDirChangeListener implements ChangeListener {
-
- @Override
- public void changed(ObservableValue extends String> property, String oldValue, String newValue) {
- if (StringUtils.isEmpty(newValue)) {
- usernameBox.setDisable(true);
- usernameBox.setValue(null);
- return;
- }
- boolean storageLocationValid;
- try {
- final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
- final DirectoryStream ds = MasterKeyFilter.filteredDirectory(storagePath);
- 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);
- }
- storageLocationValid = !usernameBox.getItems().isEmpty();
- } catch (InvalidPathException | IOException ex) {
- LOG.trace("Invalid path: " + workDirTextField.getText(), ex);
- storageLocationValid = false;
- }
- // valid encrypted folder?
- if (storageLocationValid) {
- Settings.load().setWebdavWorkDir(workDirTextField.getText());
- Settings.save();
- } else {
- messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
- }
- // enable/disable next controls:
- usernameBox.setDisable(!storageLocationValid);
- if (usernameBox.getItems().size() == 1) {
- usernameBox.setValue(usernameBox.getItems().get(0));
- }
- }
-
- }
-
- /**
- * Step 2: Choose username
- */
- private final class UsernameChangeListener implements ChangeListener {
- @Override
- public void changed(ObservableValue extends String> property, String oldValue, String newValue) {
- if (newValue != null) {
- Settings.load().setUsername(newValue);
- Settings.save();
- }
- passwordField.setDisable(StringUtils.isEmpty(newValue));
- startServerButton.setDisable(StringUtils.isEmpty(newValue));
- Platform.runLater(passwordField::requestFocus);
- }
- }
-
- // step 3: Enter password
-
- /**
- * Step 4: Unlock storage
- */
- @FXML
- protected void startStopServer(ActionEvent event) {
- messageLabel.setText(null);
- if (WebDAVServer.getInstance().isRunning()) {
- this.tryStop();
- cryptor.swipeSensitiveData();
- } else if (this.unlockStorage()) {
- this.tryStart();
- }
- }
-
- private boolean unlockStorage() {
- final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
- final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
- final Path masterKeyPath = storagePath.resolve(masterKeyFileName);
+ 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 (NoSuchFileException e) {
- messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation"));
- LOG.warn("Invalid path: " + storagePath.toString());
- } catch (DecryptFailedException ex) {
- messageLabel.setText(localization.getString("access.messageLabel.decryptionFailed"));
+ } catch (DecryptFailedException | IOException ex) {
+ errorMessageLabel.setText(rb.getString("access.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
- messageLabel.setText(localization.getString("access.messageLabel.wrongPassword"));
+ errorMessageLabel.setText(rb.getString("access.errorMessage.wrongPassword"));
} catch (UnsupportedKeyLengthException ex) {
- messageLabel.setText(localization.getString("access.messageLabel.unsupportedKeyLengthInstallJCE"));
+ errorMessageLabel.setText(rb.getString("access.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
- } catch (IOException ex) {
- LOG.error("I/O Exception", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
@@ -200,28 +88,28 @@ public class AccessController implements Initializable {
private void tryStart() {
final Settings settings = Settings.load();
- final int webdavPort = WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), cryptor);
+ final int webdavPort = server.start(settings.getWebdavWorkDir(), cryptor);
if (webdavPort > 0) {
- startServerButton.setText(localization.getString("access.button.stopServer"));
- passwordField.setDisable(true);
try {
- WebDavMounter.mount(webdavPort);
+ WebDavMounter.mount(webdavPort, id);
+ MainApplication.addShutdownTask(this::tryStop);
} catch (CommandFailedException e) {
- messageLabel.setText(String.format(localization.getString("access.messageLabel.mountFailed"), webdavPort));
+ messageLabel.setText(String.format(rb.getString("access.messageLabel.mountFailed"), webdavPort));
LOG.error("Mounting WebDAV share failed.", e);
}
}
}
- private void tryStop() {
+ public void tryStop() {
try {
- WebDavMounter.unmount(5);
- if (WebDAVServer.getInstance().stop()) {
- startServerButton.setText(localization.getString("access.button.startServer"));
- passwordField.setDisable(false);
+ if (server != null && server.isRunning()) {
+ WebDavMounter.unmount(id, 5);
+ server.stop();
}
} catch (CommandFailedException e) {
LOG.warn("Unmounting WebDAV share failed.", e);
+ } finally {
+ 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 a7083235f..d593253d9 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java
@@ -8,178 +8,70 @@
******************************************************************************/
package org.cryptomator.ui;
-import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
-import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
-import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
-import javafx.event.EventHandler;
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 javafx.scene.layout.GridPane;
-import javafx.stage.DirectoryChooser;
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.util.MasterKeyFilter;
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 = 200;
private ResourceBundle localization;
- @FXML
- private GridPane rootGridPane;
- @FXML
- private TextField workDirTextField;
- @FXML
- private TextField usernameField;
- @FXML
- private SecPasswordField passwordField;
+ private SecPasswordField referencePasswordField;
+ private Path masterKeyPath;
+ private InitializationFinishedCallback callback;
+
@FXML
private SecPasswordField retypePasswordField;
+
@FXML
- private Button initWorkDirButton;
+ private Button okButton;
+
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
- workDirTextField.textProperty().addListener(new WorkDirChangeListener());
- usernameField.addEventFilter(KeyEvent.KEY_TYPED, new AlphaNumericKeyTypeEventFilter());
- usernameField.textProperty().addListener(new UsernameChangeListener());
- usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField));
- passwordField.textProperty().addListener(new PasswordChangeListener());
- passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField));
- retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener());
- retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
+ retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
}
- /**
- * Step 1: Choose a directory, that shall be encrypted. On success, step 2 will be enabled.
- */
- @FXML
- protected void chooseWorkDir(ActionEvent event) {
- final File currentFolder = new File(workDirTextField.getText());
- final DirectoryChooser dirChooser = new DirectoryChooser();
- if (currentFolder.exists()) {
- dirChooser.setInitialDirectory(currentFolder);
- }
- final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow());
- if (file != null && file.canWrite()) {
- workDirTextField.setText(file.toString());
- }
+ private void retypePasswordFieldDidChange(ObservableValue extends String> property, String oldValue, String newValue) {
+ boolean passwordsAreEqual = referencePasswordField.getText().equals(retypePasswordField.getText());
+ okButton.setDisable(!passwordsAreEqual);
}
- private final class WorkDirChangeListener implements ChangeListener {
- @Override
- public void changed(ObservableValue extends String> property, String oldValue, String newValue) {
- if (StringUtils.isEmpty(newValue)) {
- usernameField.setDisable(true);
- return;
- }
- try {
- final Path dir = FileSystems.getDefault().getPath(newValue);
- final boolean containsMasterKeys = MasterKeyFilter.filteredDirectory(dir).iterator().hasNext();
- if (containsMasterKeys) {
- usernameField.setDisable(true);
- messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
- } else {
- usernameField.setDisable(false);
- messageLabel.setText(null);
- }
- } catch (InvalidPathException | IOException e) {
- usernameField.setDisable(true);
- messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
- }
- }
- }
-
- /**
- * Step 2: Choose a valid username
- */
- private static final class AlphaNumericKeyTypeEventFilter implements EventHandler {
- @Override
- public void handle(KeyEvent t) {
- if (t.getCharacter() == null || t.getCharacter().length() == 0) {
- return;
- }
- char c = t.getCharacter().charAt(0);
- if (!CharUtils.isAsciiAlphanumeric(c)) {
- t.consume();
- }
- }
- }
-
- private final class UsernameChangeListener implements ChangeListener {
- @Override
- public void changed(ObservableValue extends String> 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()));
- }
- }
-
- /**
- * Step 3: Defina a password. On success, step 3 will be enabled.
- */
- private final class PasswordChangeListener implements ChangeListener {
- @Override
- public void changed(ObservableValue extends String> property, String oldValue, String newValue) {
- retypePasswordField.setDisable(newValue.isEmpty());
- }
- }
-
- /**
- * Step 4: Retype the password. On success, step 4 will be enabled.
- */
- private final class RetypePasswordChangeListener implements ChangeListener {
- @Override
- public void changed(ObservableValue extends String> property, String oldValue, String newValue) {
- boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
- initWorkDirButton.setDisable(!passwordsAreEqual);
- }
- }
-
- /**
- * Step 5: Generate master password file in working directory. On success, print success message.
- */
@FXML
protected void initWorkDir(ActionEvent event) {
final Aes256Cryptor cryptor = new Aes256Cryptor();
- final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
- final Path masterKeyPath = storagePath.resolve(usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT);
-
- final CharSequence password = passwordField.getCharacters();
+ final CharSequence password = referencePasswordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptMasterKey(masterKeyOutputStream, password);
cryptor.swipeSensitiveData();
- workDirTextField.clear();
+ if (callback != null) {
+ callback.initializationFinished(InitializationResult.SUCCESS);
+ }
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
@@ -187,14 +79,52 @@ public class InitializeController implements Initializable {
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
- swipePasswordFields();
+ retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
- private void swipePasswordFields() {
- passwordField.swipe();
- retypePasswordField.swipe();
+ @FXML
+ protected void cancel(ActionEvent event) {
+ if (callback != null) {
+ callback.initializationFinished(InitializationResult.CANCELED);
+ }
+ }
+
+ /* Getter/Setter */
+
+ public SecPasswordField getReferencePasswordField() {
+ return referencePasswordField;
+ }
+
+ public void setReferencePasswordField(SecPasswordField referencePasswordField) {
+ this.referencePasswordField = referencePasswordField;
+ }
+
+ public Path getMasterKeyPath() {
+ return masterKeyPath;
+ }
+
+ public void setMasterKeyPath(Path masterKeyPath) {
+ this.masterKeyPath = masterKeyPath;
+ }
+
+ public InitializationFinishedCallback getCallback() {
+ return callback;
+ }
+
+ public void setCallback(InitializationFinishedCallback callback) {
+ this.callback = callback;
+ }
+
+ /* Modal callback stuff */
+
+ enum InitializationResult {
+ CANCELED, SUCCESS
+ };
+
+ interface InitializationFinishedCallback {
+ void initializationFinished(InitializationResult result);
}
}
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 f1d914ee0..aacc754fa 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java
@@ -10,6 +10,7 @@ package org.cryptomator.ui;
import java.io.IOException;
import java.util.ResourceBundle;
+import java.util.Set;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
@@ -18,18 +19,16 @@ import javafx.scene.Scene;
import javafx.stage.Stage;
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;
+import org.eclipse.jetty.util.ConcurrentHashSet;
public class MainApplication extends Application {
- private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
+ private static final Set SHUTDOWN_TASKS = new ConcurrentHashSet<>();
+ private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
launch(args);
+ Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
}
@Override
@@ -46,14 +45,27 @@ public class MainApplication extends Application {
@Override
public void stop() throws Exception {
- try {
- WebDavMounter.unmount(5);
- } catch (CommandFailedException e) {
- LOG.warn("Unmounting WebDAV share failed.", e);
- }
- WebDAVServer.getInstance().stop();
+ CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
super.stop();
}
+ static void addShutdownTask(Runnable r) {
+ SHUTDOWN_TASKS.add(r);
+ }
+
+ static void removeShutdownTask(Runnable r) {
+ SHUTDOWN_TASKS.remove(r);
+ }
+
+ private static class CleanShutdownPerformer extends Thread {
+ @Override
+ public void run() {
+ SHUTDOWN_TASKS.forEach(r -> {
+ r.run();
+ });
+ SHUTDOWN_TASKS.clear();
+ }
+ }
+
}
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 e417f1c53..a2871f183 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/MainController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/MainController.java
@@ -8,48 +8,326 @@
******************************************************************************/
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.event.ActionEvent;
import javafx.fxml.FXML;
-import javafx.scene.control.ToggleGroup;
-import javafx.scene.layout.Pane;
-import javafx.scene.layout.VBox;
+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.stage.DirectoryChooser;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
-public class MainController {
+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.settings.Settings;
+import org.cryptomator.ui.util.MasterKeyFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MainController implements Initializable {
+
+ private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
+ private static final int MAX_USERNAME_LENGTH = 200;
+
+ private ResourceBundle rb;
+
+ private Workflow workflow = Workflow.UNKNOWN;
+
+ private enum Workflow {
+ UNKNOWN, INIT, OPEN
+ };
@FXML
- private ToggleGroup toolbarButtonGroup;
+ private GridPane rootPane;
@FXML
- private VBox rootVBox;
+ private TextField workDirTextField;
@FXML
- private Pane initializePanel;
+ private TextField usernameField;
@FXML
- private Pane accessPanel;
+ private ComboBox usernameBox;
@FXML
- private Pane advancedPanel;
+ private SecPasswordField passwordField;
@FXML
- protected void showInitializePane(ActionEvent event) {
- showPanel(initializePanel);
+ 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());
+ }
+
+ // ****************************************
+ // Workdir field
+ // ****************************************
+
+ @FXML
+ protected void chooseWorkDir(ActionEvent event) {
+ final File currentFolder = new File(workDirTextField.getText());
+ final DirectoryChooser dirChooser = new DirectoryChooser();
+ if (currentFolder.exists()) {
+ dirChooser.setInitialDirectory(currentFolder);
+ }
+ final File file = dirChooser.showDialog(rootPane.getScene().getWindow());
+ if (file != null && file.canWrite()) {
+ workDirTextField.setText(file.toString());
+ }
+ }
+
+ private void workDirDidChange(ObservableValue extends String> property, String oldValue, String newValue) {
+ if (StringUtils.isEmpty(newValue)) {
+ usernameField.setDisable(true);
+ usernameBox.setDisable(true);
+ return;
+ }
+ try {
+ final Path dir = FileSystems.getDefault().getPath(newValue);
+ final Iterator masterKeys = MasterKeyFilter.filteredDirectory(dir).iterator();
+ if (masterKeys.hasNext()) {
+ workflow = Workflow.OPEN;
+ showUsernameBox(masterKeys);
+ showOpenButton();
+ } 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"));
+ if (rootPane.getChildren().contains(usernameBox)) {
+ rootPane.getChildren().remove(usernameBox);
+ rootPane.getChildren().add(usernameField);
+ }
+ Platform.runLater(usernameField::requestFocus);
+ }
+
+ private void usernameFieldDidChange(ObservableValue extends String> 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"));
+ if (rootPane.getChildren().contains(usernameField)) {
+ rootPane.getChildren().remove(usernameField);
+ rootPane.getChildren().add(usernameBox);
+ }
+
+ // 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 extends String> 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 extends String> 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() {
+ if (rootPane.getChildren().contains(openButton)) {
+ rootPane.getChildren().remove(openButton);
+ rootPane.getChildren().add(initializeButton);
+ }
}
@FXML
- protected void showAccessPane(ActionEvent event) {
- showPanel(accessPanel);
+ 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() {
+ if (rootPane.getChildren().contains(initializeButton)) {
+ rootPane.getChildren().remove(initializeButton);
+ rootPane.getChildren().add(openButton);
+ }
}
@FXML
- protected void showAdvancedPane(ActionEvent event) {
- showPanel(advancedPanel);
- }
+ 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);
- private void showPanel(Pane panel) {
- rootVBox.getChildren().remove(1);
- rootVBox.getChildren().add(panel);
- rootVBox.getScene().getWindow().sizeToScene();
+ 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);
+ }
+ } catch (IOException e) {
+ LOG.error("Failed to load fxml file.", e);
+ }
}
-
}
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 0d095ec86..6a36bfc5a 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
@@ -53,7 +53,6 @@ public class Settings implements Serializable {
private String webdavWorkDir;
private String username;
- private int port;
private Settings() {
// private constructor
@@ -112,14 +111,4 @@ public class Settings implements Serializable {
this.username = username;
}
- @Deprecated
- public int getPort() {
- return port;
- }
-
- @Deprecated
- public void setPort(int port) {
- this.port = port;
- }
-
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java
index 72c39e757..3aac63a84 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java
@@ -25,17 +25,17 @@ public final class WebDavMounter {
throw new IllegalStateException("not instantiable.");
}
- public static void mount(int localPort) throws CommandFailedException {
+ public static synchronized void mount(int localPort, int uniqueId) throws CommandFailedException {
if (SystemUtils.IS_OS_MAC_OSX) {
- exec("mkdir /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
- exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
- exec("open /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT);
+ exec("mkdir /Volumes/Cryptomator" + uniqueId, CMD_DEFAULT_TIMEOUT);
+ exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator" + uniqueId, CMD_DEFAULT_TIMEOUT);
+ exec("open /Volumes/Cryptomator" + uniqueId, CMD_DEFAULT_TIMEOUT);
}
}
- public static void unmount(int timeout) throws CommandFailedException {
+ public static synchronized void unmount(int uniqueId, int timeout) throws CommandFailedException {
if (SystemUtils.IS_OS_MAC_OSX) {
- exec("umount /Volumes/Cryptomator", timeout);
+ exec("umount /Volumes/Cryptomator" + uniqueId, timeout);
}
}
diff --git a/main/ui/src/main/resources/access.fxml b/main/ui/src/main/resources/access.fxml
index ba4b3e353..d9bf68d70 100644
--- a/main/ui/src/main/resources/access.fxml
+++ b/main/ui/src/main/resources/access.fxml
@@ -12,43 +12,32 @@
-
+
-
+
-
+
-
+
-
-
-
+
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/main/ui/src/main/resources/initialize.fxml b/main/ui/src/main/resources/initialize.fxml
index 17219f511..8666404a0 100644
--- a/main/ui/src/main/resources/initialize.fxml
+++ b/main/ui/src/main/resources/initialize.fxml
@@ -13,46 +13,39 @@
+
-
+
-
+
-
+
-
-
-
+
+
-
-
-
-
-
-
-
+
+
+
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/main/ui/src/main/resources/localization.properties b/main/ui/src/main/resources/localization.properties
index 2394ed932..a1785fd4e 100644
--- a/main/ui/src/main/resources/localization.properties
+++ b/main/ui/src/main/resources/localization.properties
@@ -7,28 +7,27 @@
# Sebastian Stenzel - initial API and implementation
#-------------------------------------------------------------------------------
# main.fxml
-toolbarbutton.initialize=Initialize Vault
-toolbarbutton.access=Access Vault
+
+main.label.workDir=Choose a folder
+main.label.username=Username
+main.label.password=Password
+
+main.messageLabel.invalidPath=Invalid vault directory.
+main.messageLabel.initVaultMessage=Choose username and password to create a new vault.
+main.messageLabel.openVaultMessage=Please enter your password to unlock this vault.
+
+main.primaryButton.initVault=Create
+main.primaryButton.openVault=Open
# initialize.fxml
-initialize.label.workDir=New vault location
-initialize.button.chooseWorkDir=Choose...
-initialize.label.username=Username
-initialize.label.password=Password
-initialize.label.retypePassword=Retype
-initialize.button.initWorkDir=Initialize Vault
-initialize.messageLabel.alreadyInitialized=Vault in this location already exists.
-initialize.messageLabel.invalidPath=Invalid vault location.
+initialize.title=Initialize Vault
+initialize.button.cancel=Cancel
+initialize.button.ok=Confirm
+initialize.label.retypePassword=Retype password
# access.fxml
-access.label.workDir=Vault location
-access.label.username=Username
-access.label.password=Password
-access.button.chooseWorkDir=Choose...
-access.button.startServer=Start Server
-access.button.stopServer=Stop Server
-access.messageLabel.wrongPassword=Wrong password.
-access.messageLabel.invalidStorageLocation=Vault directory invalid.
-access.messageLabel.decryptionFailed=Decryption failed.
-access.messageLabel.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
+access.button.close=Close
+access.errorMessage.wrongPassword=Wrong password.
+access.errorMessage.decryptionFailed=Decryption failed.
+access.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
access.messageLabel.mountFailed=Mounting WebDAV share (Port %d) failed.
diff --git a/main/ui/src/main/resources/main.css b/main/ui/src/main/resources/main.css
index 8cea726c8..debd50cf8 100644
--- a/main/ui/src/main/resources/main.css
+++ b/main/ui/src/main/resources/main.css
@@ -1,32 +1,119 @@
@CHARSET "US-ASCII";
+.root {
+ -fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
+}
+
.text {
-fx-font-smoothing-type: lcd;
}
-.tool-bar {
- -fx-background-color: linear-gradient(to bottom, #888888, #222222);
- -fx-padding: 5.0 10.0 5.0 10.0;
- -fx-border-color: #888888;
- -fx-border-width: 1.0 0.0 1.0 0.0;
- -fx-border-insets: 0.0;
- -fx-alignment: CENTER;
-}
-
-.tool-bar .toggle-button {
- -fx-text-fill: #FFFFFF;
- -fx-background-color: linear-gradient(to bottom, #888888, #222222);
+.button,
+.combo-box {
-fx-border-color: #888888;
-fx-background-insets: 0.0, 1.0;
-fx-background-radius: 4.0, 4.0;
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
- -fx-font-family: "lucida-grande";
- -fx-font-weight: bold;
}
-.tool-bar .toggle-button:armed,
-.tool-bar .toggle-button:selected {
- -fx-background-color: linear-gradient(to bottom, #444444, #555555 30%, #000000);
- -fx-border-color: #FFFFFF;
+.text-field {
+ -fx-border-radius: 3.0;
+ -fx-border-width: 0.5;
+}
+
+.button.green,
+.button.red,
+.split-menu-button.green,
+.split-menu-button.red {
+ -fx-background-radius: 3.0;
+ -fx-background-color: #FFFFFF;
+ -fx-background-insets: 1px 1px 1px 1px;
+}
+
+.button.green,
+.button.red,
+.split-menu-button.green > .label,
+.split-menu-button.red > .label {
+ -fx-text-fill: #FFF;
+ -fx-alignment: CENTER;
+ -fx-font-weight: bold;
+ -fx-font-family: "lucida-grande";
+}
+
+.split-menu-button.green > .arrow-button > .arrow,
+.split-menu-button.red > .arrow-button > .arrow {
+ -fx-background-color: #FFF;
+}
+
+.button.green,
+.split-menu-button.green > .label,
+.split-menu-button.green > .arrow-button {
+ -fx-background-color: linear-gradient(to bottom, #33EE55, #22AA33);
+}
+
+.button.green:hover,
+.split-menu-button.green > .label:hover,
+.split-menu-button.green > .arrow-button:hover {
+ -fx-background-color: linear-gradient(to bottom, #33EE55, #118822);
+}
+
+.button.green:armed,
+.split-menu-button.green:armed > .label,
+.split-menu-button.green > .arrow-button:pressed,
+.split-menu-button.green:showing > .arrow-button {
+ -fx-background-color: linear-gradient(to bottom, #118822, #22AA33 20%, #33EE55);
+}
+
+.button.green:disabled,
+.split-menu-button.green:disabled,
+.split-menu-button.green:disabled > .label,
+.split-menu-button.green:disabled > .arrow-button {
+ -fx-background-color: #22AA33;
+}
+
+.button.red,
+.split-menu-button.red > .label,
+.split-menu-button.red > .arrow-button {
+ -fx-background-color: linear-gradient(to bottom, #EE5533, #AA3322);
+}
+
+.button.red:hover,
+.split-menu-button.red > .label:hover,
+.split-menu-button.red > .arrow-button:hover {
+ -fx-background-color: linear-gradient(to bottom, #EE5533, #882211);
+}
+
+.button.red:armed,
+.split-menu-button.red:armed > .label,
+.split-menu-button.red > .arrow-button:pressed,
+.split-menu-button.red:showing > .arrow-button {
+ -fx-background-color: linear-gradient(to bottom, #882211, #AA3322 20%, #EE5533);
+}
+
+.button.red:disabled,
+.split-menu-button.red:disabled,
+.split-menu-button.red:disabled > .label,
+.split-menu-button.red:disabled > .arrow-button {
+ -fx-background-color: #AA3322;
+}
+
+.split-menu-button .menu-item:focused {
+ -fx-background-color: #CCC;
+}
+
+.split-menu-button .menu-item .label {
+ -fx-text-fill: #000000;
+}
+
+.text-field {
+ -fx-border-radius: 3.0;
+ -fx-border-width: 0.5;
+ -fx-border-color: #888888;
+ -fx-background-color: #FFFFFF;
+ -fx-padding: 4 2 4 2;
+}
+
+.text-field:focused {
+ -fx-background-color: #FFFFFF;
}
diff --git a/main/ui/src/main/resources/main.fxml b/main/ui/src/main/resources/main.fxml
index f1fe478a3..623a1379c 100644
--- a/main/ui/src/main/resources/main.fxml
+++ b/main/ui/src/main/resources/main.fxml
@@ -12,29 +12,67 @@
+
+
+
+
+
-
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/main/ui/src/main/resources/panels.css b/main/ui/src/main/resources/panels.css
index 6ab6b4cf0..f81b52142 100644
--- a/main/ui/src/main/resources/panels.css
+++ b/main/ui/src/main/resources/panels.css
@@ -60,3 +60,4 @@
-fx-background-insets: 0, 0;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 8, 0.0, 0, 0);
}
+