- Improved shutdown hooks

- Redesigned UI, now a single-window application (todo: minimize to tray)
This commit is contained in:
Sebastian Stenzel 2014-12-09 10:50:09 +01:00
parent 884b894e04
commit d0f0c09585
19 changed files with 785 additions and 626 deletions

View File

@ -32,6 +32,7 @@ public final class WebDAVServer {
private static final int MIN_THREADS = 4;
private static final int THREAD_IDLE_SECONDS = 20;
private final Server server;
private int port;
public WebDAVServer() {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
@ -42,9 +43,9 @@ public final class WebDAVServer {
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @return port, on which the server did start
* @return <code>true</code> upon success
*/
public int start(final String workDir, final Cryptor cryptor) {
public synchronized boolean start(final String workDir, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost(LOCALHOST);
@ -58,20 +59,22 @@ public final class WebDAVServer {
try {
server.setConnectors(new Connector[] {connector});
server.start();
port = connector.getLocalPort();
return true;
} catch (Exception ex) {
LOG.error("Server couldn't be started", ex);
return false;
}
return connector.getLocalPort();
}
public boolean isRunning() {
return server.isRunning();
}
public boolean stop() {
public synchronized boolean stop() {
try {
server.stop();
port = 0;
} catch (Exception ex) {
LOG.error("Server couldn't be stopped", ex);
}
@ -85,4 +88,8 @@ public final class WebDAVServer {
return result;
}
public int getPort() {
return port;
}
}

View File

@ -1,113 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.WebDavMounter;
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
import org.cryptomator.webdav.WebDAVServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AccessController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(AccessController.class);
private final Aes256Cryptor cryptor = new Aes256Cryptor();
private final WebDAVServer server = new WebDAVServer();
private ResourceBundle rb;
private String unmountCmd;
@FXML
private GridPane rootPane;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
protected void closeVault(ActionEvent event) {
this.tryStop();
this.rootPane.getScene().getWindow().hide();
}
public boolean unlockStorage(Path masterKeyPath, SecPasswordField passwordField, Label errorMessageLabel) {
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
cryptor.decryptMasterKey(masterKeyInputStream, password);
tryStart();
return true;
} catch (DecryptFailedException | IOException ex) {
errorMessageLabel.setText(rb.getString("access.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
errorMessageLabel.setText(rb.getString("access.errorMessage.wrongPassword"));
} catch (UnsupportedKeyLengthException ex) {
errorMessageLabel.setText(rb.getString("access.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
}
return false;
}
private void tryStart() {
final Settings settings = Settings.load();
final int webdavPort = server.start(settings.getWebdavWorkDir(), cryptor);
if (webdavPort > 0) {
try {
unmountCmd = WebDavMounter.mount(webdavPort);
MainApplication.addShutdownTask(this::tryStop);
} catch (CommandFailedException e) {
messageLabel.setText(String.format(rb.getString("access.messageLabel.mountFailed"), webdavPort));
LOG.error("Mounting WebDAV share failed.", e);
}
}
}
public void tryStop() {
if (server != null && server.isRunning()) {
try {
WebDavMounter.unmount(unmountCmd);
} catch (CommandFailedException e) {
LOG.warn("Unmounting WebDAV share failed.", e);
}
server.stop();
cryptor.swipeSensitiveData();
}
}
}

View File

@ -24,21 +24,33 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InitializeController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private static final int MAX_USERNAME_LENGTH = 250;
private ResourceBundle localization;
private SecPasswordField referencePasswordField;
private Path masterKeyPath;
private InitializationFinishedCallback callback;
private Directory directory;
private InitializationListener listener;
@FXML
private TextField usernameField;
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@ -52,26 +64,69 @@ public class InitializeController implements Initializable {
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
usernameField.textProperty().addListener(this::usernameFieldDidChange);
passwordField.textProperty().addListener(this::passwordFieldDidChange);
retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange);
retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField));
}
// ****************************************
// Username field
// ****************************************
public void filterAlphanumericKeyEvents(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;
}
char c = t.getCharacter().charAt(0);
if (!CharUtils.isAsciiAlphanumeric(c)) {
t.consume();
}
}
public void usernameFieldDidChange(ObservableValue<? 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(newValue));
}
// ****************************************
// Password field
// ****************************************
private void passwordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
retypePasswordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Retype password field
// ****************************************
private void retypePasswordFieldDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordsAreEqual = referencePasswordField.getText().equals(retypePasswordField.getText());
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
okButton.setDisable(!passwordsAreEqual);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initWorkDir(ActionEvent event) {
final Aes256Cryptor cryptor = new Aes256Cryptor();
final CharSequence password = referencePasswordField.getCharacters();
protected void initializeVault(ActionEvent event) {
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
OutputStream masterKeyOutputStream = null;
try {
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.randomizeMasterKey();
cryptor.encryptMasterKey(masterKeyOutputStream, password);
cryptor.swipeSensitiveData();
if (callback != null) {
callback.initializationFinished(InitializationResult.SUCCESS);
directory.getCryptor().randomizeMasterKey();
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
@ -80,52 +135,35 @@ public class InitializeController implements Initializable {
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
usernameField.setText(null);
passwordField.swipe();
retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
@FXML
protected void cancel(ActionEvent event) {
if (callback != null) {
callback.initializationFinished(InitializationResult.CANCELED);
}
}
/* Getter/Setter */
public SecPasswordField getReferencePasswordField() {
return referencePasswordField;
public Directory getDirectory() {
return directory;
}
public void setReferencePasswordField(SecPasswordField referencePasswordField) {
this.referencePasswordField = referencePasswordField;
public void setDirectory(Directory directory) {
this.directory = directory;
}
public Path getMasterKeyPath() {
return masterKeyPath;
public InitializationListener getListener() {
return listener;
}
public void setMasterKeyPath(Path masterKeyPath) {
this.masterKeyPath = masterKeyPath;
public void setListener(InitializationListener listener) {
this.listener = listener;
}
public InitializationFinishedCallback getCallback() {
return callback;
}
/* callback */
public void setCallback(InitializationFinishedCallback callback) {
this.callback = callback;
}
/* Modal callback stuff */
enum InitializationResult {
CANCELED, SUCCESS
};
interface InitializationFinishedCallback {
void initializationFinished(InitializationResult result);
interface InitializationListener {
void didInitialize(InitializeController ctrl);
}
}

View File

@ -34,7 +34,10 @@ public class MainApplication extends Application {
@Override
public void start(final Stage primaryStage) throws IOException {
final ResourceBundle localizations = ResourceBundle.getBundle("localization");
final Parent root = FXMLLoader.load(getClass().getResource("/main.fxml"), localizations);
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/main.fxml"), localizations);
final Parent root = loader.load();
final MainController ctrl = loader.getController();
ctrl.setStage(primaryStage);
final Scene scene = new Scene(root);
primaryStage.setTitle("Cryptomator");
primaryStage.setScene(scene);
@ -50,11 +53,11 @@ public class MainApplication extends Application {
super.stop();
}
static void addShutdownTask(Runnable r) {
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
static void removeShutdownTask(Runnable r) {
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}

View File

@ -11,315 +11,143 @@ package org.cryptomator.ui;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.SplitMenuButton;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.InitializeController.InitializationResult;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.InitializeController.InitializationListener;
import org.cryptomator.ui.UnlockController.UnlockListener;
import org.cryptomator.ui.UnlockedController.LockListener;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MainController implements Initializable {
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private static final int MAX_USERNAME_LENGTH = 200;
private Stage stage;
@FXML
private HBox rootPane;
@FXML
private ListView<Directory> directoryList;
@FXML
private Pane contentPane;
private ResourceBundle rb;
private Workflow workflow = Workflow.UNKNOWN;
private enum Workflow {
UNKNOWN, INIT, OPEN
};
@FXML
private GridPane rootPane;
@FXML
private TextField workDirTextField;
@FXML
private TextField usernameField;
@FXML
private ComboBox<String> usernameBox;
@FXML
private SecPasswordField passwordField;
@FXML
private SplitMenuButton openButton;
@FXML
private Button initializeButton;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
// attach event handler
workDirTextField.textProperty().addListener(this::workDirDidChange);
usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterUsernameKeyEvents);
usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField));
usernameField.textProperty().addListener(this::usernameFieldDidChange);
usernameBox.valueProperty().addListener(this::usernameBoxDidChange);
passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField));
passwordField.textProperty().addListener(this::passwordFieldDidChange);
passwordField.addEventHandler(KeyEvent.KEY_PRESSED, this::onPasswordFieldKeyPressed);
// load settings
workDirTextField.setText(Settings.load().getWebdavWorkDir());
usernameBox.setValue(Settings.load().getUsername());
directoryList.setCellFactory(this::createDirecoryListCell);
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
directoryList.getItems().addAll(Settings.load().getDirectories());
}
// ****************************************
// Workdir field
// ****************************************
@FXML
protected void chooseWorkDir(ActionEvent event) {
final File currentFolder = new File(workDirTextField.getText());
private void didClickAddDirectory(ActionEvent event) {
final DirectoryChooser dirChooser = new DirectoryChooser();
if (currentFolder.exists()) {
dirChooser.setInitialDirectory(currentFolder);
}
final File file = dirChooser.showDialog(rootPane.getScene().getWindow());
final File file = dirChooser.showDialog(stage);
if (file != null && file.canWrite()) {
workDirTextField.setText(file.toString());
final Directory dir = new Directory(file.toPath());
directoryList.getItems().add(dir);
Settings.load().getDirectories().clear();
Settings.load().getDirectories().addAll(directoryList.getItems());
directoryList.getSelectionModel().selectLast();
}
}
private void workDirDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (StringUtils.isEmpty(newValue)) {
usernameField.setDisable(true);
usernameBox.setDisable(true);
return;
}
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
return new DirectoryListCell();
}
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
stage.setTitle(selectedDir.getName());
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();
if (selectedDir.containsMasterKey()) {
this.showUnlockView(selectedDir);
} else {
workflow = Workflow.INIT;
showUsernameField();
showInitializeButton();
}
usernameField.setDisable(false);
usernameBox.setDisable(false);
Settings.load().setWebdavWorkDir(newValue);
} catch (InvalidPathException | IOException e) {
usernameField.setDisable(true);
usernameBox.setDisable(true);
messageLabel.setText(rb.getString("main.messageLabel.invalidPath"));
}
}
// ****************************************
// Username field
// ****************************************
private void showUsernameField() {
messageLabel.setText(rb.getString("main.messageLabel.initVaultMessage"));
usernameBox.setVisible(false);
usernameField.setVisible(true);
Platform.runLater(usernameField::requestFocus);
}
private void usernameFieldDidChange(ObservableValue<? 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"));
usernameField.setVisible(false);
usernameBox.setVisible(true);
// update usernameBox options:
usernameBox.getItems().clear();
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
foundMasterKeys.forEachRemaining(path -> {
final String fileName = path.getFileName().toString();
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
final String baseName = fileName.substring(0, beginOfExt);
usernameBox.getItems().add(baseName);
});
// autochoose user, if possible:
if (usernameBox.getItems().size() == 1) {
usernameBox.setValue(usernameBox.getItems().get(0));
}
}
private void usernameBoxDidChange(ObservableValue<? 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() {
openButton.setVisible(false);
initializeButton.setVisible(true);
}
@FXML
protected void showInitializationDialog(ActionEvent event) {
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = storagePath.resolve(masterKeyFileName);
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/initialize.fxml"), rb);
final Parent initDialog = loader.load();
final Scene dialogScene = new Scene(initDialog);
final Stage dialog = new Stage();
final InitializeController ctrl = loader.getController();
ctrl.setReferencePasswordField(passwordField);
ctrl.setMasterKeyPath(masterKeyPath);
ctrl.setCallback(result -> {
if (InitializationResult.SUCCESS.equals(result)) {
this.initializationSucceeded();
}
dialog.close();
});
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.initOwner(rootPane.getScene().getWindow());
dialog.setTitle(rb.getString("initialize.title"));
dialog.setScene(dialogScene);
dialog.sizeToScene();
dialog.setResizable(false);
dialog.show();
} catch (IOException e) {
LOG.error("Failed to load fxml file.", e);
}
}
private void initializationSucceeded() {
// trigger re-evaluation of work dir. there should be a masterkey file now.
this.workDirDidChange(workDirTextField.textProperty(), workDirTextField.getText(), workDirTextField.getText());
}
// ****************************************
// Open vault button
// ****************************************
private void showOpenButton() {
initializeButton.setVisible(false);
openButton.setVisible(true);
}
@FXML
protected void openVault(ActionEvent event) {
final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText());
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = storagePath.resolve(masterKeyFileName);
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/access.fxml"), rb);
final Parent accessDialog = loader.load();
final Scene dialogScene = new Scene(accessDialog);
final AccessController ctrl = loader.getController();
if (ctrl.unlockStorage(masterKeyPath, passwordField, messageLabel)) {
passwordField.swipe();
final Stage dialog = new Stage();
dialog.initModality(Modality.NONE);
dialog.initOwner(rootPane.getScene().getWindow());
dialog.setTitle(storagePath.getFileName().toString());
dialog.setScene(dialogScene);
dialog.sizeToScene();
dialog.setResizable(false);
dialog.show();
dialog.setOnCloseRequest(windowEvent -> {
ctrl.tryStop();
});
} else {
Platform.runLater(passwordField::requestFocus);
this.showInitializeView(selectedDir);
}
} catch (IOException e) {
LOG.error("Failed to load fxml file.", e);
LOG.error("Failed to analyze directory.", e);
}
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private <T> T showView(String fxml) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
final Parent root = loader.load();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
return loader.getController();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
private void showInitializeView(Directory directory) {
final InitializeController ctrl = showView("/initialize.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didInitialize(InitializeController ctrl) {
showUnlockView(ctrl.getDirectory());
}
private void showUnlockView(Directory directory) {
final UnlockController ctrl = showView("/unlock.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
showUnlockedView(ctrl.getDirectory());
}
private void showUnlockedView(Directory directory) {
final UnlockedController ctrl = showView("/unlocked.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
showUnlockView(ctrl.getDirectory());
}
/* public Getter/Setter */
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@ -0,0 +1,154 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class UnlockController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private ResourceBundle rb;
private UnlockListener listener;
private Directory directory;
@FXML
private ComboBox<String> usernameBox;
@FXML
private SecPasswordField passwordField;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
usernameBox.valueProperty().addListener(this::didChooseUsername);
}
// ****************************************
// Username box
// ****************************************
public void didChooseUsername(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (newValue != null) {
Platform.runLater(passwordField::requestFocus);
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
}
// ****************************************
// Unlock button
// ****************************************
@FXML
protected void didClickUnlockButton(ActionEvent event) {
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!directory.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
return;
}
directory.mount();
if (listener != null) {
listener.didUnlock(this);
}
} catch (DecryptFailedException | IOException ex) {
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
} catch (UnsupportedKeyLengthException ex) {
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
}
}
private void findExistingUsernames() {
try {
DirectoryStream<Path> ds = MasterKeyFilter.filteredDirectory(directory.getPath());
final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
usernameBox.getItems().clear();
for (final Path path : ds) {
final String fileName = path.getFileName().toString();
final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt);
final String baseName = fileName.substring(0, beginOfExt);
usernameBox.getItems().add(baseName);
}
if (usernameBox.getItems().size() == 1) {
usernameBox.getSelectionModel().selectFirst();
}
} catch (IOException e) {
LOG.trace("Invalid path: " + directory.getPath(), e);
}
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
this.findExistingUsernames();
}
public UnlockListener getListener() {
return listener;
}
public void setListener(UnlockListener listener) {
this.listener = listener;
}
/* callback */
interface UnlockListener {
void didUnlock(UnlockController ctrl);
}
}

View File

@ -0,0 +1,71 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import org.cryptomator.ui.model.Directory;
public class UnlockedController implements Initializable {
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
protected void closeVault(ActionEvent event) {
directory.unmount();
directory.stopServer();
if (listener != null) {
listener.didLock(this);
}
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), directory.getServer().getPort());
messageLabel.setText(msg);
}
public LockListener getListener() {
return listener;
}
public void setListener(LockListener listener) {
this.listener = listener;
}
/* callback */
interface LockListener {
void didLock(UnlockedController ctrl);
}
}

View File

@ -0,0 +1,19 @@
package org.cryptomator.ui.controls;
import javafx.scene.control.ListCell;
import org.cryptomator.ui.model.Directory;
public class DirectoryListCell extends ListCell<Directory> {
@Override
protected void updateItem(Directory item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setText(null);
} else {
setText(item.getName());
}
}
}

View File

@ -0,0 +1,127 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.WebDavMounter;
import org.cryptomator.ui.util.WebDavMounter.CommandFailedException;
import org.cryptomator.webdav.WebDAVServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@JsonSerialize(using = DirectorySerializer.class)
@JsonDeserialize(using = DirectoryDeserializer.class)
public class Directory implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
private final WebDAVServer server = new WebDAVServer();
private final Aes256Cryptor cryptor = new Aes256Cryptor();
private final Path path;
private boolean unlocked;
private String unmountCommand;
private final Runnable shutdownTask = new ShutdownTask();
public Directory(final Path path) {
if (!Files.isDirectory(path)) {
throw new IllegalArgumentException("Not a directory: " + path);
}
this.path = path;
}
public boolean containsMasterKey() throws IOException {
return MasterKeyFilter.filteredDirectory(path).iterator().hasNext();
}
public synchronized boolean startServer() {
if (server.start(path.toString(), cryptor)) {
MainApplication.addShutdownTask(shutdownTask);
return true;
} else {
return false;
}
}
public synchronized void stopServer() {
if (server.isRunning()) {
MainApplication.removeShutdownTask(shutdownTask);
this.unmount();
server.stop();
cryptor.swipeSensitiveData();
}
}
public boolean mount() {
try {
unmountCommand = WebDavMounter.mount(server.getPort());
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public boolean unmount() {
try {
if (StringUtils.isNotEmpty(unmountCommand)) {
WebDavMounter.unmount(unmountCommand);
unmountCommand = null;
}
return true;
} catch (CommandFailedException e) {
LOG.warn("unmount failed", e);
return false;
}
}
/* Getter/Setter */
public Path getPath() {
return path;
}
/**
* @return Directory name without preceeding path components
*/
public String getName() {
return path.getFileName().toString();
}
public Aes256Cryptor getCryptor() {
return cryptor;
}
public boolean isUnlocked() {
return unlocked;
}
public void setUnlocked(boolean unlocked) {
this.unlocked = unlocked;
}
public WebDAVServer getServer() {
return server;
}
/* graceful shutdown */
private class ShutdownTask implements Runnable {
@Override
public void run() {
stopServer();
}
}
}

View File

@ -0,0 +1,23 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
public class DirectoryDeserializer extends JsonDeserializer<Directory> {
@Override
public Directory deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
final JsonNode node = jp.readValueAsTree();
final String pathStr = node.get("path").asText();
final Path path = FileSystems.getDefault().getPath(pathStr);
return new Directory(path);
}
}

View File

@ -0,0 +1,19 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
public class DirectorySerializer extends JsonSerializer<Directory> {
@Override
public void serialize(Directory value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeEndObject();
}
}

View File

@ -17,8 +17,11 @@ import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collection;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.model.Directory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -40,18 +43,18 @@ public class Settings implements Serializable {
final FileSystem fs = FileSystems.getDefault();
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "opencloudencryptor");
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor");
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
} else if (SystemUtils.IS_OS_MAC_OSX) {
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/opencloudencryptor");
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, "Library/Application Support/Cryptomator");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".opencloudencryptor");
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
}
}
private String webdavWorkDir;
private Collection<Directory> directories;
private String username;
private Settings() {
@ -88,19 +91,20 @@ public class Settings implements Serializable {
}
private static Settings defaultSettings() {
final Settings result = new Settings();
result.setWebdavWorkDir(System.getProperty("user.home", "."));
return result;
return new Settings();
}
/* Getter/Setter */
public String getWebdavWorkDir() {
return webdavWorkDir;
public Collection<Directory> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setWebdavWorkDir(String webdavWorkDir) {
this.webdavWorkDir = webdavWorkDir;
public void setDirectories(Collection<Directory> directories) {
this.directories = directories;
}
public String getUsername() {

View File

@ -14,38 +14,38 @@
<?import javafx.scene.text.*?>
<?import org.cryptomator.ui.controls.*?>
<?import javafx.scene.layout.HBox?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.TextField?>
<GridPane vgap="6.0" hgap="6.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
<stylesheets>
<URL value="@main.css" />
</stylesheets>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="12.0" right="12.0" bottom="12.0" left="12.0" />
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="100" />
<ColumnConstraints minWidth="200" />
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 1 -->
<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 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.username" />
<TextField fx:id="usernameField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<!-- Row 1 -->
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.password" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" disable="true" />
<!-- Row 2 -->
<Label fx:id="messageLabel" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="5" textOverrun="ELLIPSIS" />
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<!-- Row 3 -->
<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>
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@ -6,28 +6,29 @@
# Contributors:
# Sebastian Stenzel - initial API and implementation
#-------------------------------------------------------------------------------
# main.fxml
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.
# welcome.fxml
welcome.welcomeLabel=Welcome to Cryptomator
main.primaryButton.initVault=Create
main.primaryButton.openVault=Open
# initialize.fxml
initialize.title=Initialize Vault
initialize.button.cancel=Cancel
initialize.button.ok=Confirm
initialize.label.username=Username
initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
# access.fxml
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.
# unlock.fxml
unlock.label.username=Username
unlock.label.password=Password
unlock.button.unlock=Unlock vault
unlock.errorMessage.wrongPassword=Wrong password.
unlock.errorMessage.decryptionFailed=Decryption failed.
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# unlocked.fxml
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
unlocked.button.lock=Lock vault

View File

@ -1,69 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2014 Sebastian Stenzel This file is licensed under the terms of the MIT license. See the LICENSE.txt file for more info. Contributors: Sebastian Stenzel - initial API and implementation -->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?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.*?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.URL?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Button?>
<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>
<columnConstraints>
<ColumnConstraints minWidth="100" />
<ColumnConstraints minWidth="200" />
<ColumnConstraints minWidth="25" />
</columnConstraints>
<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<fx:define>
<fx:include fx:id="welcomeView" source="welcome.fxml" />
</fx:define>
<children>
<!-- 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" />
<ComboBox fx:id="usernameBox" visible="false" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.columnSpan="2" GridPane.hgrow="ALWAYS" maxWidth="Infinity" promptText="$access.label.username" 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>
<SplitMenuButton fx:id="openButton" visible="false" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="3" prefWidth="120.0" GridPane.halignment="CENTER" text="%main.primaryButton.openVault"
onAction="#openVault" focusTraversable="false">
<styleClass>
<String fx:value="green" />
</styleClass>
<items>
<MenuItem text="Add User" />
<MenuItem text="Change Password" />
</items>
</SplitMenuButton>
<VBox prefWidth="200.0">
<children>
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" />
<ToolBar VBox.vgrow="NEVER">
<items>
<Button text="+" onAction="#didClickAddDirectory" />
</items>
</ToolBar>
</children>
</VBox>
<Pane fx:id="contentPane">
<children>
<fx:reference source="welcomeView"/>
</children>
</Pane>
</children>
</GridPane>
</HBox>

View File

@ -1,63 +0,0 @@
@CHARSET "US-ASCII";
.root {
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
}
.text {
-fx-font-smoothing-type: lcd;
}
.label {
-fx-alignment: CENTER;
-fx-font-family: "lucida-grande";
}
.button,
.combo-box {
-fx-text-fill: #000000;
-fx-background-color: linear-gradient(to bottom, #FFFFFF, #DDDDDD);
-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: normal;
}
.text-field {
-fx-border-radius: 3.0;
-fx-border-width: 0.5;
-fx-border-color: #888888;
-fx-focus-color: #FF0000;
-fx-background-color: transparent;
-fx-padding: 5 2 5 2;
}
.text-field:focused {
-fx-background-color: #DDDDDD;
}
.button:armed,
.button:selected,
.combo-box:armed,
.combo-box:selected {
-fx-background-color: linear-gradient(to bottom, #DDDDDD, #CCCCCC 30%, #EEEEEE);
}
.combo-box .list-cell {
-fx-background-color: transparent;
-fx-text-fill: -fx-text-base-color;
}
.combo-box .list-cell:hover {
-fx-background-color: #DDDDDD;
}
.combo-box-popup .list-view {
-fx-padding: 0 0 0 0;
-fx-background-insets: 0, 0;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 8, 0.0, 0, 0);
}

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 0 -->
<Label text="%unlock.label.username" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<ComboBox fx:id="usernameBox" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" promptText="$access.label.username" />
<!-- Row 1 -->
<Label text="%unlock.label.password" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Button text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" focusTraversable="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@ -13,31 +13,25 @@
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import javafx.scene.control.Label?>
<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="@main.css" />
</stylesheets>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="12.0" right="12.0" bottom="12.0" left="12.0" />
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints minWidth="300" />
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 0 -->
<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.halignment="CENTER"/>
<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<!-- Row 1 -->
<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>
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#closeVault" focusTraversable="false"/>
</children>
</GridPane>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<children>
<Label fx:id="messageLabel" AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" text="%welcome.welcomeLabel"/>
</children>
</AnchorPane>