소스 검색

- Completely redesigned and much simpler user interface.
- Support for multiple simultaneous mounts
- Added shutdown hooks for secure unmounting

Sebastian Stenzel 10 년 전
부모
커밋
e7ba6f5c92

+ 1 - 1
main/core/pom.xml

@@ -12,7 +12,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>0.1.0</version>
+		<version>0.2.0</version>
 	</parent>
 	<artifactId>core</artifactId>
 	<name>Cryptomator core I/O module</name>

+ 0 - 9
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.

+ 1 - 1
main/crypto-aes/pom.xml

@@ -12,7 +12,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>0.1.0</version>
+		<version>0.2.0</version>
 	</parent>
 	<artifactId>crypto-aes</artifactId>
 	<name>Cryptomator cryptographic module (AES)</name>

+ 1 - 1
main/crypto-api/pom.xml

@@ -12,7 +12,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>0.1.0</version>
+		<version>0.2.0</version>
 	</parent>
 	<artifactId>crypto-api</artifactId>
 	<name>Cryptomator cryptographic module API</name>

+ 1 - 1
main/pom.xml

@@ -11,7 +11,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>main</artifactId>
-	<version>0.1.0</version>
+	<version>0.2.0</version>
 	<packaging>pom</packaging>
 	<name>Cryptomator</name>
 	

+ 1 - 1
main/ui/pom.xml

@@ -12,7 +12,7 @@
 	<parent>
 		<groupId>org.cryptomator</groupId>
 		<artifactId>main</artifactId>
-		<version>0.1.0</version>
+		<version>0.2.0</version>
 	</parent>
 	<artifactId>ui</artifactId>
 	<name>Cryptomator GUI</name>

+ 28 - 140
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;
-	@FXML
-	private GridPane rootGridPane;
-	@FXML
-	private TextField workDirTextField;
-	@FXML
-	private ComboBox<String> usernameBox;
-	@FXML
-	private SecPasswordField passwordField;
+	private final WebDAVServer server = new WebDAVServer();
+	private final int id = ID_GENERATOR.getAndIncrement();
+	private ResourceBundle rb;
+
 	@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());
-	}
-
-	/**
-	 * 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());
-		}
-	}
-
-	private final class WorkDirChangeListener implements ChangeListener<String> {
-
-		@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<Path> 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));
-			}
-		}
-
+		this.rb = rb;
 	}
 
-	/**
-	 * Step 2: Choose username
-	 */
-	private final class UsernameChangeListener implements ChangeListener<String> {
-		@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();
-		}
+	protected void closeVault(ActionEvent event) {
+		this.tryStop();
+		this.rootPane.getScene().getWindow().hide();
 	}
 
-	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();
 		}
 	}
 

+ 57 - 127
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));
-	}
-
-	/**
-	 * 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 final class WorkDirChangeListener implements ChangeListener<String> {
-		@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"));
-			}
-		}
+		retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
 	}
 
-	/**
-	 * Step 2: Choose a valid username
-	 */
-	private static final class AlphaNumericKeyTypeEventFilter implements EventHandler<KeyEvent> {
-		@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 void retypePasswordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
+		boolean passwordsAreEqual = referencePasswordField.getText().equals(retypePasswordField.getText());
+		okButton.setDisable(!passwordsAreEqual);
 	}
 
-	private final class UsernameChangeListener implements ChangeListener<String> {
-		@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<String> {
-		@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<String> {
-		@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);
 	}
 
 }

+ 24 - 12
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<Runnable> 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();
+		}
+	}
+
 }

+ 297 - 19
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;
+
+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;
 
-public class MainController {
+	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<String> 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 showAccessPane(ActionEvent event) {
-		showPanel(accessPanel);
+	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<Path> 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<Path> 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 showAdvancedPane(ActionEvent event) {
-		showPanel(advancedPanel);
+	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 showPanel(Pane panel) {
-		rootVBox.getChildren().remove(1);
-		rootVBox.getChildren().add(panel);
-		rootVBox.getScene().getWindow().sizeToScene();
+	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 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);
+			}
+		} catch (IOException e) {
+			LOG.error("Failed to load fxml file.", e);
+		}
+	}
 }

+ 0 - 11
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;
-	}
-
 }

+ 6 - 6
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);
 		}
 	}
 

+ 11 - 22
main/ui/src/main/resources/access.fxml

@@ -12,43 +12,32 @@
 <?import javafx.scene.control.*?>
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.*?>
-<?import org.cryptomator.ui.controls.*?>
+<?import java.lang.String?>
 
 
-<GridPane fx:id="rootGridPane" fx:controller="org.cryptomator.ui.AccessController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
+<GridPane vgap="6.0" hgap="6.0" fx:id="rootPane" fx:controller="org.cryptomator.ui.AccessController" xmlns:fx="http://javafx.com/fxml">
 	<stylesheets>
-		<URL value="@panels.css" />
+		<URL value="@main.css" />
 	</stylesheets>
 
 	<padding>
-		<Insets top="10" right="10" bottom="10" left="10" />
+		<Insets top="12.0" right="12.0" bottom="12.0" left="12.0" />
 	</padding>
 
 	<columnConstraints>
-		<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
-		<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
-		<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
+		<ColumnConstraints minWidth="300" />
 	</columnConstraints>
 
 	<children>
 		<!-- Row 0 -->
-		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%access.label.workDir" GridPane.halignment="RIGHT" />
-		<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
-		<Button GridPane.rowIndex="0" GridPane.columnIndex="2" text="%access.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false"  />
+		<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.halignment="CENTER"/>
 		
 		<!-- Row 1 -->
-		<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%access.label.username" GridPane.halignment="RIGHT" />
-		<ComboBox fx:id="usernameBox" GridPane.rowIndex="1" GridPane.columnIndex="1" promptText="$access.label.username" disable="true" />
-		
-		<!-- Row 2 -->
-		<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%access.label.password" GridPane.halignment="RIGHT" />
-		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
-		
-		<!-- Row 3 -->
-		<Button fx:id="startServerButton" text="%access.button.startServer" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" defaultButton="true" onAction="#startStopServer" focusTraversable="false" />
-		
-		<!-- Row 4 -->
-		<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" textAlignment="CENTER" />
+		<Button text="%access.button.close" GridPane.rowIndex="1" GridPane.columnIndex="0" prefWidth="120.0" GridPane.halignment="CENTER" onAction="#closeVault" focusTraversable="false">
+			<styleClass>
+				<String fx:value="red"/>
+			</styleClass>
+		</Button>
 	</children>
 </GridPane>
 

+ 16 - 23
main/ui/src/main/resources/initialize.fxml

@@ -13,46 +13,39 @@
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.*?>
 <?import org.cryptomator.ui.controls.*?>
+<?import javafx.scene.layout.HBox?>
 
 
-<GridPane fx:id="rootGridPane" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml" styleClass="root" gridLinesVisible="false" vgap="5" hgap="5" prefWidth="480">
+<GridPane vgap="6.0" hgap="6.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
 	<stylesheets>
-		<URL value="@panels.css" />
+		<URL value="@main.css" />
 	</stylesheets>
 
 	<padding>
-		<Insets top="10" right="10" bottom="10" left="10" />
+		<Insets top="12.0" right="12.0" bottom="12.0" left="12.0" />
 	</padding>
 
 	<columnConstraints>
-		<ColumnConstraints minWidth="150" maxWidth="150" hgrow="NEVER" />
-		<ColumnConstraints minWidth="200" hgrow="ALWAYS" />
-		<ColumnConstraints minWidth="50" maxWidth="120" hgrow="NEVER" />
+		<ColumnConstraints minWidth="100" />
+		<ColumnConstraints minWidth="200" />
 	</columnConstraints>
 
 	<children>
-		<!-- Row 0 -->
-		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.workDir" GridPane.halignment="RIGHT" />
-		<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
-		<Button fx:id="chooseWorkDirButton" GridPane.rowIndex="0" GridPane.columnIndex="2" text="%initialize.button.chooseWorkDir" onAction="#chooseWorkDir" focusTraversable="false" />
-		
 		<!-- Row 1 -->
-		<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.username" GridPane.halignment="RIGHT" />
-		<TextField fx:id="usernameField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
+		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.retypePassword" GridPane.halignment="RIGHT" />
+		<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
+		
 		
 		<!-- Row 2 -->
-		<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.password" GridPane.halignment="RIGHT" />
-		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
+		<Label fx:id="messageLabel" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
 		
 		<!-- Row 3 -->
-		<Label GridPane.rowIndex="3" GridPane.columnIndex="0" text="%initialize.label.retypePassword" GridPane.halignment="RIGHT" />
-		<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="3" GridPane.columnIndex="1" disable="true" />
-		
-		<!-- Row 4 -->
-		<Button fx:id="initWorkDirButton" text="%initialize.button.initWorkDir" GridPane.rowIndex="4" GridPane.columnIndex="1" defaultButton="true" onAction="#initWorkDir" disable="true" focusTraversable="false"/>
-		
-		<!-- Row 5 -->
-		<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
+		<HBox GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS" spacing="6.0" alignment="BOTTOM_RIGHT">
+			<children>
+					<Button text="%initialize.button.cancel" prefWidth="80.0" cancelButton="true" onAction="#cancel" focusTraversable="false"/>
+					<Button fx:id="okButton" text="%initialize.button.ok" prefWidth="80.0" defaultButton="true" onAction="#initWorkDir" disable="true" focusTraversable="false"/>
+			</children>
+		</HBox>
 	</children>
 </GridPane>
 

+ 19 - 20
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.

+ 104 - 17
main/ui/src/main/resources/main.css

@@ -1,32 +1,119 @@
 @CHARSET "US-ASCII";
 
-.text {
-    -fx-font-smoothing-type: lcd;
+.root {
+	-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
 }
 
-.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;
+.text {
+    -fx-font-smoothing-type: lcd;
 }
 
-.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";
+}
+
+.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;
 }
 
-.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:focused {
+	-fx-background-color: #FFFFFF;
 }

+ 52 - 14
main/ui/src/main/resources/main.fxml

@@ -12,29 +12,67 @@
 <?import javafx.scene.control.*?>
 <?import javafx.scene.layout.*?>
 <?import javafx.scene.text.*?>
+<?import javafx.scene.control.SplitMenuButton?>
+<?import javafx.scene.control.MenuItem?>
+<?import java.lang.String?>
+<?import org.cryptomator.ui.controls.*?>
+<?import javafx.scene.control.Button?>
 
-<VBox fx:id="rootVBox" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
+<GridPane vgap="6.0" hgap="6.0" fx:id="rootPane" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
 	<stylesheets>
 		<URL value="@main.css" />
 	</stylesheets>
 
+	<padding>
+		<Insets top="12.0" right="12.0" bottom="12.0" left="12.0" />
+	</padding>
+
 	<fx:define>
-		<fx:include fx:id="initializePanel" source="initialize.fxml" />
-		<fx:include fx:id="accessPanel" source="access.fxml" />
+		<ComboBox fx:id="usernameBox" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS" maxWidth="Infinity" promptText="$access.label.username" disable="true" focusTraversable="false" />
+			
+		<SplitMenuButton fx:id="openButton" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" prefWidth="120.0" GridPane.halignment="CENTER" text="%main.primaryButton.openVault" onAction="#openVault">
+			<styleClass>
+				<String fx:value="green" />
+			</styleClass>
+			<items>
+				<MenuItem text="Add User" />
+				<MenuItem text="Change Password" />
+			</items>
+		</SplitMenuButton>
 	</fx:define>
 
+	<columnConstraints>
+		<ColumnConstraints minWidth="100" />
+		<ColumnConstraints minWidth="200" />
+		<ColumnConstraints minWidth="25" />
+	</columnConstraints>
+
 	<children>
-		<ToolBar>
-			<items>
-				<fx:define>
-					<ToggleGroup fx:id="toolbarButtonGroup" />
-				</fx:define>
-				<ToggleButton text="%toolbarbutton.initialize" toggleGroup="$toolbarButtonGroup" onAction="#showInitializePane" />
-				<ToggleButton text="%toolbarbutton.access" toggleGroup="$toolbarButtonGroup" onAction="#showAccessPane" selected="true" />
-			</items>
-		</ToolBar>
-		<fx:reference source="accessPanel"/>
+		<!-- Row 0 -->
+		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%main.label.workDir" GridPane.halignment="RIGHT" />
+		<TextField fx:id="workDirTextField" GridPane.rowIndex="0" GridPane.columnIndex="1" editable="true" />
+		<Button GridPane.rowIndex="0" GridPane.columnIndex="2" text="..." GridPane.hgrow="ALWAYS" maxWidth="Infinity" focusTraversable="false" onAction="#chooseWorkDir" />
+
+		<!-- Row 1 -->
+		<Label fx:id="messageLabel" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="3" GridPane.hgrow="ALWAYS" maxWidth="Infinity" alignment="CENTER" />
+
+		<!-- Row 2 -->
+		<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%main.label.username" GridPane.halignment="RIGHT" />
+		<TextField fx:id="usernameField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.columnSpan="2" disable="true" />
+
+		<!-- Row 3 -->
+		<Label GridPane.rowIndex="3" GridPane.columnIndex="0" text="%main.label.password" GridPane.halignment="RIGHT" />
+		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="3" GridPane.columnIndex="1" GridPane.columnSpan="2" disable="true" />
+
+		<!-- Row 4 -->
+		<Button fx:id="initializeButton" text="%main.primaryButton.initVault" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" prefWidth="120.0" GridPane.halignment="CENTER" onAction="#showInitializationDialog"
+			disable="true" focusTraversable="false">
+			<styleClass>
+				<String fx:value="green" />
+			</styleClass>
+		</Button>
+
 	</children>
-</VBox>
+</GridPane>
 
 

+ 1 - 0
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);
 }
+