- Completely redesigned and much simpler user interface.

- Support for multiple simultaneous mounts
- Added shutdown hooks for secure unmounting
This commit is contained in:
Sebastian Stenzel
2014-12-04 22:04:04 +01:00
parent c322b0488e
commit e7ba6f5c92
18 changed files with 623 additions and 428 deletions

View File

@@ -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>

View File

@@ -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.

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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<String> 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<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));
}
}
}
/**
* 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();
}
}
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();
}
}

View File

@@ -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<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"));
}
}
}
/**
* 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 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);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<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 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 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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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);
}