diff --git a/main/core/pom.xml b/main/core/pom.xml index d23e756e7..39044745b 100644 --- a/main/core/pom.xml +++ b/main/core/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 0.1.0 + 0.2.0 core Cryptomator core I/O module diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java index 69ec48815..0e819d50f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java +++ b/main/core/src/main/java/org/cryptomator/webdav/WebDAVServer.java @@ -21,18 +21,9 @@ import org.slf4j.LoggerFactory; public final class WebDAVServer { private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class); - private static final WebDAVServer INSTANCE = new WebDAVServer(); private static final String LOCALHOST = "127.0.0.1"; private final Server server = new Server(); - private WebDAVServer() { - // make constructor private - } - - public static WebDAVServer getInstance() { - return INSTANCE; - } - /** * @param workDir Path of encrypted folder. * @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams. diff --git a/main/crypto-aes/pom.xml b/main/crypto-aes/pom.xml index c05dee070..e2387264e 100644 --- a/main/crypto-aes/pom.xml +++ b/main/crypto-aes/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 0.1.0 + 0.2.0 crypto-aes Cryptomator cryptographic module (AES) diff --git a/main/crypto-api/pom.xml b/main/crypto-api/pom.xml index 894471358..78c8c3e7a 100644 --- a/main/crypto-api/pom.xml +++ b/main/crypto-api/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 0.1.0 + 0.2.0 crypto-api Cryptomator cryptographic module API diff --git a/main/pom.xml b/main/pom.xml index 3c66b30fb..e4ec9b797 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -11,7 +11,7 @@ 4.0.0 org.cryptomator main - 0.1.0 + 0.2.0 pom Cryptomator diff --git a/main/ui/pom.xml b/main/ui/pom.xml index ac5a0ab03..0b1c57673 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -12,7 +12,7 @@ org.cryptomator main - 0.1.0 + 0.2.0 ui Cryptomator GUI diff --git a/main/ui/src/main/java/org/cryptomator/ui/AccessController.java b/main/ui/src/main/java/org/cryptomator/ui/AccessController.java index 47dc60504..75d09fc35 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/AccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/AccessController.java @@ -8,41 +8,28 @@ ******************************************************************************/ package org.cryptomator.ui; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; -import java.nio.file.DirectoryStream; -import java.nio.file.FileSystems; import java.nio.file.Files; -import java.nio.file.InvalidPathException; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicInteger; -import javafx.application.Platform; -import javafx.beans.value.ChangeListener; -import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.Button; -import javafx.scene.control.ComboBox; import javafx.scene.control.Label; -import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; -import javafx.stage.DirectoryChooser; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.aes256.Aes256Cryptor; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.settings.Settings; -import org.cryptomator.ui.util.MasterKeyFilter; import org.cryptomator.ui.util.WebDavMounter; import org.cryptomator.ui.util.WebDavMounter.CommandFailedException; import org.cryptomator.webdav.WebDAVServer; @@ -52,145 +39,46 @@ import org.slf4j.LoggerFactory; public class AccessController implements Initializable { private static final Logger LOG = LoggerFactory.getLogger(AccessController.class); + private static final AtomicInteger ID_GENERATOR = new AtomicInteger(); private final Aes256Cryptor cryptor = new Aes256Cryptor(); - private ResourceBundle localization; + private final WebDAVServer server = new WebDAVServer(); + private final int id = ID_GENERATOR.getAndIncrement(); + private ResourceBundle rb; + @FXML - private GridPane rootGridPane; - @FXML - private TextField workDirTextField; - @FXML - private ComboBox usernameBox; - @FXML - private SecPasswordField passwordField; - @FXML - private Button startServerButton; + private GridPane rootPane; + @FXML private Label messageLabel; @Override public void initialize(URL url, ResourceBundle rb) { - this.localization = rb; - workDirTextField.textProperty().addListener(new WorkDirChangeListener()); - usernameBox.valueProperty().addListener(new UsernameChangeListener()); - workDirTextField.setText(Settings.load().getWebdavWorkDir()); - usernameBox.setValue(Settings.load().getUsername()); + this.rb = rb; } - /** - * Step 1: Choose encrypted storage: - */ @FXML - protected void chooseWorkDir(ActionEvent event) { - messageLabel.setText(null); - final File currentFolder = new File(workDirTextField.getText()); - final DirectoryChooser dirChooser = new DirectoryChooser(); - if (currentFolder.exists()) { - dirChooser.setInitialDirectory(currentFolder); - } - final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow()); - if (file != null) { - workDirTextField.setText(file.toString()); - } + protected void closeVault(ActionEvent event) { + this.tryStop(); + this.rootPane.getScene().getWindow().hide(); } - private final class WorkDirChangeListener implements ChangeListener { - - @Override - public void changed(ObservableValue property, String oldValue, String newValue) { - if (StringUtils.isEmpty(newValue)) { - usernameBox.setDisable(true); - usernameBox.setValue(null); - return; - } - boolean storageLocationValid; - try { - final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); - final DirectoryStream ds = MasterKeyFilter.filteredDirectory(storagePath); - final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase(); - usernameBox.getItems().clear(); - for (final Path path : ds) { - final String fileName = path.getFileName().toString(); - final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt); - final String baseName = fileName.substring(0, beginOfExt); - usernameBox.getItems().add(baseName); - } - storageLocationValid = !usernameBox.getItems().isEmpty(); - } catch (InvalidPathException | IOException ex) { - LOG.trace("Invalid path: " + workDirTextField.getText(), ex); - storageLocationValid = false; - } - // valid encrypted folder? - if (storageLocationValid) { - Settings.load().setWebdavWorkDir(workDirTextField.getText()); - Settings.save(); - } else { - messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation")); - } - // enable/disable next controls: - usernameBox.setDisable(!storageLocationValid); - if (usernameBox.getItems().size() == 1) { - usernameBox.setValue(usernameBox.getItems().get(0)); - } - } - - } - - /** - * Step 2: Choose username - */ - private final class UsernameChangeListener implements ChangeListener { - @Override - public void changed(ObservableValue property, String oldValue, String newValue) { - if (newValue != null) { - Settings.load().setUsername(newValue); - Settings.save(); - } - passwordField.setDisable(StringUtils.isEmpty(newValue)); - startServerButton.setDisable(StringUtils.isEmpty(newValue)); - Platform.runLater(passwordField::requestFocus); - } - } - - // step 3: Enter password - - /** - * Step 4: Unlock storage - */ - @FXML - protected void startStopServer(ActionEvent event) { - messageLabel.setText(null); - if (WebDAVServer.getInstance().isRunning()) { - this.tryStop(); - cryptor.swipeSensitiveData(); - } else if (this.unlockStorage()) { - this.tryStart(); - } - } - - private boolean unlockStorage() { - final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); - final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT; - final Path masterKeyPath = storagePath.resolve(masterKeyFileName); + public boolean unlockStorage(Path masterKeyPath, SecPasswordField passwordField, Label errorMessageLabel) { final CharSequence password = passwordField.getCharacters(); InputStream masterKeyInputStream = null; try { masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ); cryptor.decryptMasterKey(masterKeyInputStream, password); + tryStart(); return true; - } catch (NoSuchFileException e) { - messageLabel.setText(localization.getString("access.messageLabel.invalidStorageLocation")); - LOG.warn("Invalid path: " + storagePath.toString()); - } catch (DecryptFailedException ex) { - messageLabel.setText(localization.getString("access.messageLabel.decryptionFailed")); + } catch (DecryptFailedException | IOException ex) { + errorMessageLabel.setText(rb.getString("access.errorMessage.decryptionFailed")); LOG.error("Decryption failed for technical reasons.", ex); } catch (WrongPasswordException e) { - messageLabel.setText(localization.getString("access.messageLabel.wrongPassword")); + errorMessageLabel.setText(rb.getString("access.errorMessage.wrongPassword")); } catch (UnsupportedKeyLengthException ex) { - messageLabel.setText(localization.getString("access.messageLabel.unsupportedKeyLengthInstallJCE")); + errorMessageLabel.setText(rb.getString("access.errorMessage.unsupportedKeyLengthInstallJCE")); LOG.error("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); - } catch (IOException ex) { - LOG.error("I/O Exception", ex); } finally { passwordField.swipe(); IOUtils.closeQuietly(masterKeyInputStream); @@ -200,28 +88,28 @@ public class AccessController implements Initializable { private void tryStart() { final Settings settings = Settings.load(); - final int webdavPort = WebDAVServer.getInstance().start(settings.getWebdavWorkDir(), cryptor); + final int webdavPort = server.start(settings.getWebdavWorkDir(), cryptor); if (webdavPort > 0) { - startServerButton.setText(localization.getString("access.button.stopServer")); - passwordField.setDisable(true); try { - WebDavMounter.mount(webdavPort); + WebDavMounter.mount(webdavPort, id); + MainApplication.addShutdownTask(this::tryStop); } catch (CommandFailedException e) { - messageLabel.setText(String.format(localization.getString("access.messageLabel.mountFailed"), webdavPort)); + messageLabel.setText(String.format(rb.getString("access.messageLabel.mountFailed"), webdavPort)); LOG.error("Mounting WebDAV share failed.", e); } } } - private void tryStop() { + public void tryStop() { try { - WebDavMounter.unmount(5); - if (WebDAVServer.getInstance().stop()) { - startServerButton.setText(localization.getString("access.button.startServer")); - passwordField.setDisable(false); + if (server != null && server.isRunning()) { + WebDavMounter.unmount(id, 5); + server.stop(); } } catch (CommandFailedException e) { LOG.warn("Unmounting WebDAV share failed.", e); + } finally { + cryptor.swipeSensitiveData(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java index a7083235f..d593253d9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java @@ -8,178 +8,70 @@ ******************************************************************************/ package org.cryptomator.ui; -import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.net.URL; import java.nio.file.FileAlreadyExistsException; -import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ResourceBundle; -import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; -import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; import javafx.scene.control.Label; -import javafx.scene.control.TextField; -import javafx.scene.input.KeyEvent; -import javafx.scene.layout.GridPane; -import javafx.stage.DirectoryChooser; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.CharUtils; -import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.aes256.Aes256Cryptor; -import org.cryptomator.ui.controls.ClearOnDisableListener; import org.cryptomator.ui.controls.SecPasswordField; -import org.cryptomator.ui.util.MasterKeyFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class InitializeController implements Initializable { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); - private static final int MAX_USERNAME_LENGTH = 200; private ResourceBundle localization; - @FXML - private GridPane rootGridPane; - @FXML - private TextField workDirTextField; - @FXML - private TextField usernameField; - @FXML - private SecPasswordField passwordField; + private SecPasswordField referencePasswordField; + private Path masterKeyPath; + private InitializationFinishedCallback callback; + @FXML private SecPasswordField retypePasswordField; + @FXML - private Button initWorkDirButton; + private Button okButton; + @FXML private Label messageLabel; @Override public void initialize(URL url, ResourceBundle rb) { this.localization = rb; - workDirTextField.textProperty().addListener(new WorkDirChangeListener()); - usernameField.addEventFilter(KeyEvent.KEY_TYPED, new AlphaNumericKeyTypeEventFilter()); - usernameField.textProperty().addListener(new UsernameChangeListener()); - usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField)); - passwordField.textProperty().addListener(new PasswordChangeListener()); - passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField)); - retypePasswordField.textProperty().addListener(new RetypePasswordChangeListener()); - retypePasswordField.disableProperty().addListener(new ClearOnDisableListener(retypePasswordField)); + retypePasswordField.textProperty().addListener(this::retypePasswordFieldDidChange); } - /** - * Step 1: Choose a directory, that shall be encrypted. On success, step 2 will be enabled. - */ - @FXML - protected void chooseWorkDir(ActionEvent event) { - final File currentFolder = new File(workDirTextField.getText()); - final DirectoryChooser dirChooser = new DirectoryChooser(); - if (currentFolder.exists()) { - dirChooser.setInitialDirectory(currentFolder); - } - final File file = dirChooser.showDialog(rootGridPane.getScene().getWindow()); - if (file != null && file.canWrite()) { - workDirTextField.setText(file.toString()); - } + private void retypePasswordFieldDidChange(ObservableValue property, String oldValue, String newValue) { + boolean passwordsAreEqual = referencePasswordField.getText().equals(retypePasswordField.getText()); + okButton.setDisable(!passwordsAreEqual); } - private final class WorkDirChangeListener implements ChangeListener { - @Override - public void changed(ObservableValue property, String oldValue, String newValue) { - if (StringUtils.isEmpty(newValue)) { - usernameField.setDisable(true); - return; - } - try { - final Path dir = FileSystems.getDefault().getPath(newValue); - final boolean containsMasterKeys = MasterKeyFilter.filteredDirectory(dir).iterator().hasNext(); - if (containsMasterKeys) { - usernameField.setDisable(true); - messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); - } else { - usernameField.setDisable(false); - messageLabel.setText(null); - } - } catch (InvalidPathException | IOException e) { - usernameField.setDisable(true); - messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath")); - } - } - } - - /** - * Step 2: Choose a valid username - */ - private static final class AlphaNumericKeyTypeEventFilter implements EventHandler { - @Override - public void handle(KeyEvent t) { - if (t.getCharacter() == null || t.getCharacter().length() == 0) { - return; - } - char c = t.getCharacter().charAt(0); - if (!CharUtils.isAsciiAlphanumeric(c)) { - t.consume(); - } - } - } - - private final class UsernameChangeListener implements ChangeListener { - @Override - public void changed(ObservableValue property, String oldValue, String newValue) { - if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) { - usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH)); - } - passwordField.setDisable(StringUtils.isEmpty(usernameField.getText())); - } - } - - /** - * Step 3: Defina a password. On success, step 3 will be enabled. - */ - private final class PasswordChangeListener implements ChangeListener { - @Override - public void changed(ObservableValue property, String oldValue, String newValue) { - retypePasswordField.setDisable(newValue.isEmpty()); - } - } - - /** - * Step 4: Retype the password. On success, step 4 will be enabled. - */ - private final class RetypePasswordChangeListener implements ChangeListener { - @Override - public void changed(ObservableValue property, String oldValue, String newValue) { - boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText()); - initWorkDirButton.setDisable(!passwordsAreEqual); - } - } - - /** - * Step 5: Generate master password file in working directory. On success, print success message. - */ @FXML protected void initWorkDir(ActionEvent event) { final Aes256Cryptor cryptor = new Aes256Cryptor(); - final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); - final Path masterKeyPath = storagePath.resolve(usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT); - - final CharSequence password = passwordField.getCharacters(); + final CharSequence password = referencePasswordField.getCharacters(); OutputStream masterKeyOutputStream = null; try { masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW); cryptor.encryptMasterKey(masterKeyOutputStream, password); cryptor.swipeSensitiveData(); - workDirTextField.clear(); + if (callback != null) { + callback.initializationFinished(InitializationResult.SUCCESS); + } } catch (FileAlreadyExistsException ex) { messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); } catch (InvalidPathException ex) { @@ -187,14 +79,52 @@ public class InitializeController implements Initializable { } catch (IOException ex) { LOG.error("I/O Exception", ex); } finally { - swipePasswordFields(); + retypePasswordField.swipe(); IOUtils.closeQuietly(masterKeyOutputStream); } } - private void swipePasswordFields() { - passwordField.swipe(); - retypePasswordField.swipe(); + @FXML + protected void cancel(ActionEvent event) { + if (callback != null) { + callback.initializationFinished(InitializationResult.CANCELED); + } + } + + /* Getter/Setter */ + + public SecPasswordField getReferencePasswordField() { + return referencePasswordField; + } + + public void setReferencePasswordField(SecPasswordField referencePasswordField) { + this.referencePasswordField = referencePasswordField; + } + + public Path getMasterKeyPath() { + return masterKeyPath; + } + + public void setMasterKeyPath(Path masterKeyPath) { + this.masterKeyPath = masterKeyPath; + } + + public InitializationFinishedCallback getCallback() { + return callback; + } + + public void setCallback(InitializationFinishedCallback callback) { + this.callback = callback; + } + + /* Modal callback stuff */ + + enum InitializationResult { + CANCELED, SUCCESS + }; + + interface InitializationFinishedCallback { + void initializationFinished(InitializationResult result); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index f1d914ee0..aacc754fa 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -10,6 +10,7 @@ package org.cryptomator.ui; import java.io.IOException; import java.util.ResourceBundle; +import java.util.Set; import javafx.application.Application; import javafx.fxml.FXMLLoader; @@ -18,18 +19,16 @@ import javafx.scene.Scene; import javafx.stage.Stage; import org.cryptomator.ui.settings.Settings; -import org.cryptomator.ui.util.WebDavMounter; -import org.cryptomator.ui.util.WebDavMounter.CommandFailedException; -import org.cryptomator.webdav.WebDAVServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jetty.util.ConcurrentHashSet; public class MainApplication extends Application { - private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class); + private static final Set SHUTDOWN_TASKS = new ConcurrentHashSet<>(); + private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer(); public static void main(String[] args) { launch(args); + Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER); } @Override @@ -46,14 +45,27 @@ public class MainApplication extends Application { @Override public void stop() throws Exception { - try { - WebDavMounter.unmount(5); - } catch (CommandFailedException e) { - LOG.warn("Unmounting WebDAV share failed.", e); - } - WebDAVServer.getInstance().stop(); + CLEAN_SHUTDOWN_PERFORMER.run(); Settings.save(); super.stop(); } + static void addShutdownTask(Runnable r) { + SHUTDOWN_TASKS.add(r); + } + + static void removeShutdownTask(Runnable r) { + SHUTDOWN_TASKS.remove(r); + } + + private static class CleanShutdownPerformer extends Thread { + @Override + public void run() { + SHUTDOWN_TASKS.forEach(r -> { + r.run(); + }); + SHUTDOWN_TASKS.clear(); + } + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/MainController.java index e417f1c53..a2871f183 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainController.java @@ -8,48 +8,326 @@ ******************************************************************************/ package org.cryptomator.ui; +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.util.Iterator; +import java.util.ResourceBundle; + +import javafx.application.Platform; +import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.ToggleGroup; -import javafx.scene.layout.Pane; -import javafx.scene.layout.VBox; +import javafx.fxml.FXMLLoader; +import javafx.fxml.Initializable; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.SplitMenuButton; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.GridPane; +import javafx.stage.DirectoryChooser; +import javafx.stage.Modality; +import javafx.stage.Stage; -public class MainController { +import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.StringUtils; +import org.cryptomator.crypto.aes256.Aes256Cryptor; +import org.cryptomator.ui.InitializeController.InitializationResult; +import org.cryptomator.ui.controls.ClearOnDisableListener; +import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.settings.Settings; +import org.cryptomator.ui.util.MasterKeyFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MainController implements Initializable { + + private static final Logger LOG = LoggerFactory.getLogger(MainController.class); + private static final int MAX_USERNAME_LENGTH = 200; + + private ResourceBundle rb; + + private Workflow workflow = Workflow.UNKNOWN; + + private enum Workflow { + UNKNOWN, INIT, OPEN + }; @FXML - private ToggleGroup toolbarButtonGroup; + private GridPane rootPane; @FXML - private VBox rootVBox; + private TextField workDirTextField; @FXML - private Pane initializePanel; + private TextField usernameField; @FXML - private Pane accessPanel; + private ComboBox usernameBox; @FXML - private Pane advancedPanel; + private SecPasswordField passwordField; @FXML - protected void showInitializePane(ActionEvent event) { - showPanel(initializePanel); + private SplitMenuButton openButton; + + @FXML + private Button initializeButton; + + @FXML + private Label messageLabel; + + @Override + public void initialize(URL url, ResourceBundle rb) { + this.rb = rb; + // attach event handler + workDirTextField.textProperty().addListener(this::workDirDidChange); + usernameField.addEventFilter(KeyEvent.KEY_TYPED, this::filterUsernameKeyEvents); + usernameField.disableProperty().addListener(new ClearOnDisableListener(usernameField)); + usernameField.textProperty().addListener(this::usernameFieldDidChange); + usernameBox.valueProperty().addListener(this::usernameBoxDidChange); + passwordField.disableProperty().addListener(new ClearOnDisableListener(passwordField)); + passwordField.textProperty().addListener(this::passwordFieldDidChange); + passwordField.addEventHandler(KeyEvent.KEY_PRESSED, this::onPasswordFieldKeyPressed); + + // load settings + workDirTextField.setText(Settings.load().getWebdavWorkDir()); + usernameBox.setValue(Settings.load().getUsername()); + } + + // **************************************** + // Workdir field + // **************************************** + + @FXML + protected void chooseWorkDir(ActionEvent event) { + final File currentFolder = new File(workDirTextField.getText()); + final DirectoryChooser dirChooser = new DirectoryChooser(); + if (currentFolder.exists()) { + dirChooser.setInitialDirectory(currentFolder); + } + final File file = dirChooser.showDialog(rootPane.getScene().getWindow()); + if (file != null && file.canWrite()) { + workDirTextField.setText(file.toString()); + } + } + + private void workDirDidChange(ObservableValue property, String oldValue, String newValue) { + if (StringUtils.isEmpty(newValue)) { + usernameField.setDisable(true); + usernameBox.setDisable(true); + return; + } + try { + final Path dir = FileSystems.getDefault().getPath(newValue); + final Iterator masterKeys = MasterKeyFilter.filteredDirectory(dir).iterator(); + if (masterKeys.hasNext()) { + workflow = Workflow.OPEN; + showUsernameBox(masterKeys); + showOpenButton(); + } else { + workflow = Workflow.INIT; + showUsernameField(); + showInitializeButton(); + } + usernameField.setDisable(false); + usernameBox.setDisable(false); + Settings.load().setWebdavWorkDir(newValue); + } catch (InvalidPathException | IOException e) { + usernameField.setDisable(true); + usernameBox.setDisable(true); + messageLabel.setText(rb.getString("main.messageLabel.invalidPath")); + } + } + + // **************************************** + // Username field + // **************************************** + + private void showUsernameField() { + messageLabel.setText(rb.getString("main.messageLabel.initVaultMessage")); + if (rootPane.getChildren().contains(usernameBox)) { + rootPane.getChildren().remove(usernameBox); + rootPane.getChildren().add(usernameField); + } + Platform.runLater(usernameField::requestFocus); + } + + private void usernameFieldDidChange(ObservableValue property, String oldValue, String newValue) { + if (StringUtils.length(newValue) > MAX_USERNAME_LENGTH) { + usernameField.setText(newValue.substring(0, MAX_USERNAME_LENGTH)); + } + passwordField.setDisable(StringUtils.isEmpty(usernameField.getText())); + } + + private void filterUsernameKeyEvents(KeyEvent t) { + if (t.getCharacter() == null || t.getCharacter().length() == 0) { + return; + } + char c = t.getCharacter().charAt(0); + if (!CharUtils.isAsciiAlphanumeric(c)) { + t.consume(); + } + } + + // **************************************** + // Username box + // **************************************** + + private void showUsernameBox(Iterator foundMasterKeys) { + messageLabel.setText(rb.getString("main.messageLabel.openVaultMessage")); + if (rootPane.getChildren().contains(usernameField)) { + rootPane.getChildren().remove(usernameField); + rootPane.getChildren().add(usernameBox); + } + + // update usernameBox options: + usernameBox.getItems().clear(); + final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase(); + foundMasterKeys.forEachRemaining(path -> { + final String fileName = path.getFileName().toString(); + final int beginOfExt = fileName.toLowerCase().lastIndexOf(masterKeyExt); + final String baseName = fileName.substring(0, beginOfExt); + usernameBox.getItems().add(baseName); + }); + + // autochoose user, if possible: + if (usernameBox.getItems().size() == 1) { + usernameBox.setValue(usernameBox.getItems().get(0)); + } + } + + private void usernameBoxDidChange(ObservableValue property, String oldValue, String newValue) { + if (!Workflow.OPEN.equals(workflow)) { + return; + } + if (newValue != null) { + Settings.load().setUsername(newValue); + } + passwordField.setDisable(StringUtils.isEmpty(newValue)); + Platform.runLater(passwordField::requestFocus); + } + + // **************************************** + // Password field + // **************************************** + + private void passwordFieldDidChange(ObservableValue property, String oldValue, String newValue) { + initializeButton.setDisable(StringUtils.isEmpty(newValue)); + openButton.setDisable(StringUtils.isEmpty(newValue)); + } + + public void onPasswordFieldKeyPressed(KeyEvent event) { + if (KeyCode.ENTER.equals(event.getCode())) { + switch (workflow) { + case OPEN: + openButton.fire(); + break; + case INIT: + initializeButton.fire(); + break; + default: + break; + } + } + } + + // **************************************** + // Initialize vault button + // **************************************** + + private void showInitializeButton() { + if (rootPane.getChildren().contains(openButton)) { + rootPane.getChildren().remove(openButton); + rootPane.getChildren().add(initializeButton); + } } @FXML - protected void showAccessPane(ActionEvent event) { - showPanel(accessPanel); + protected void showInitializationDialog(ActionEvent event) { + final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); + final String masterKeyFileName = usernameField.getText() + Aes256Cryptor.MASTERKEY_FILE_EXT; + final Path masterKeyPath = storagePath.resolve(masterKeyFileName); + + try { + final FXMLLoader loader = new FXMLLoader(getClass().getResource("/initialize.fxml"), rb); + final Parent initDialog = loader.load(); + final Scene dialogScene = new Scene(initDialog); + final Stage dialog = new Stage(); + final InitializeController ctrl = loader.getController(); + ctrl.setReferencePasswordField(passwordField); + ctrl.setMasterKeyPath(masterKeyPath); + ctrl.setCallback(result -> { + if (InitializationResult.SUCCESS.equals(result)) { + this.initializationSucceeded(); + } + dialog.close(); + }); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.initOwner(rootPane.getScene().getWindow()); + dialog.setTitle(rb.getString("initialize.title")); + dialog.setScene(dialogScene); + dialog.sizeToScene(); + dialog.setResizable(false); + dialog.show(); + } catch (IOException e) { + LOG.error("Failed to load fxml file.", e); + } + } + + private void initializationSucceeded() { + // trigger re-evaluation of work dir. there should be a masterkey file now. + this.workDirDidChange(workDirTextField.textProperty(), workDirTextField.getText(), workDirTextField.getText()); + } + + // **************************************** + // Open vault button + // **************************************** + + private void showOpenButton() { + if (rootPane.getChildren().contains(initializeButton)) { + rootPane.getChildren().remove(initializeButton); + rootPane.getChildren().add(openButton); + } } @FXML - protected void showAdvancedPane(ActionEvent event) { - showPanel(advancedPanel); - } + protected void openVault(ActionEvent event) { + final Path storagePath = FileSystems.getDefault().getPath(workDirTextField.getText()); + final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT; + final Path masterKeyPath = storagePath.resolve(masterKeyFileName); - private void showPanel(Pane panel) { - rootVBox.getChildren().remove(1); - rootVBox.getChildren().add(panel); - rootVBox.getScene().getWindow().sizeToScene(); + try { + final FXMLLoader loader = new FXMLLoader(getClass().getResource("/access.fxml"), rb); + final Parent accessDialog = loader.load(); + final Scene dialogScene = new Scene(accessDialog); + final AccessController ctrl = loader.getController(); + if (ctrl.unlockStorage(masterKeyPath, passwordField, messageLabel)) { + passwordField.swipe(); + final Stage dialog = new Stage(); + dialog.initModality(Modality.NONE); + dialog.initOwner(rootPane.getScene().getWindow()); + dialog.setTitle(storagePath.getFileName().toString()); + dialog.setScene(dialogScene); + dialog.sizeToScene(); + dialog.setResizable(false); + dialog.show(); + dialog.setOnCloseRequest(windowEvent -> { + ctrl.tryStop(); + }); + } else { + Platform.runLater(passwordField::requestFocus); + } + } catch (IOException e) { + LOG.error("Failed to load fxml file.", e); + } } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java index 0d095ec86..6a36bfc5a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java +++ b/main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java @@ -53,7 +53,6 @@ public class Settings implements Serializable { private String webdavWorkDir; private String username; - private int port; private Settings() { // private constructor @@ -112,14 +111,4 @@ public class Settings implements Serializable { this.username = username; } - @Deprecated - public int getPort() { - return port; - } - - @Deprecated - public void setPort(int port) { - this.port = port; - } - } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java b/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java index 72c39e757..3aac63a84 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/WebDavMounter.java @@ -25,17 +25,17 @@ public final class WebDavMounter { throw new IllegalStateException("not instantiable."); } - public static void mount(int localPort) throws CommandFailedException { + public static synchronized void mount(int localPort, int uniqueId) throws CommandFailedException { if (SystemUtils.IS_OS_MAC_OSX) { - exec("mkdir /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT); - exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT); - exec("open /Volumes/Cryptomator", CMD_DEFAULT_TIMEOUT); + exec("mkdir /Volumes/Cryptomator" + uniqueId, CMD_DEFAULT_TIMEOUT); + exec("mount_webdav -S -v Cryptomator localhost:" + localPort + " /Volumes/Cryptomator" + uniqueId, CMD_DEFAULT_TIMEOUT); + exec("open /Volumes/Cryptomator" + uniqueId, CMD_DEFAULT_TIMEOUT); } } - public static void unmount(int timeout) throws CommandFailedException { + public static synchronized void unmount(int uniqueId, int timeout) throws CommandFailedException { if (SystemUtils.IS_OS_MAC_OSX) { - exec("umount /Volumes/Cryptomator", timeout); + exec("umount /Volumes/Cryptomator" + uniqueId, timeout); } } diff --git a/main/ui/src/main/resources/access.fxml b/main/ui/src/main/resources/access.fxml index ba4b3e353..d9bf68d70 100644 --- a/main/ui/src/main/resources/access.fxml +++ b/main/ui/src/main/resources/access.fxml @@ -12,43 +12,32 @@ - + - + - + - + - - - + - diff --git a/main/ui/src/main/resources/initialize.fxml b/main/ui/src/main/resources/initialize.fxml index 17219f511..8666404a0 100644 --- a/main/ui/src/main/resources/initialize.fxml +++ b/main/ui/src/main/resources/initialize.fxml @@ -13,46 +13,39 @@ + - + - + - + - - - + + - - - + diff --git a/main/ui/src/main/resources/panels.css b/main/ui/src/main/resources/panels.css index 6ab6b4cf0..f81b52142 100644 --- a/main/ui/src/main/resources/panels.css +++ b/main/ui/src/main/resources/panels.css @@ -60,3 +60,4 @@ -fx-background-insets: 0, 0; -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.6), 8, 0.0, 0, 0); } +