diff --git a/.gitignore b/.gitignore index 5ddd9fc95..7051b0cd0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,7 @@ test-output/ out/ .idea_modules/ *.iws +*.iml + +# Temporary file created by test launcher +main/launcher/.ipcPort.tmp diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fb80dfd30..4889efd2a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,20 +4,9 @@ - Ensure you're running the latest version of Cryptomator. - Ensure the bug is related to the desktop version of Cryptomator. Bugs concerning the Cryptomator iOS and Android app can be reported on the [Cryptomator for iOS issues list](https://github.com/cryptomator/cryptomator-ios/issues) and [Cryptomator for Android issues list](https://github.com/cryptomator/cryptomator-android/issues) respectively. -- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [knowledge base](https://cryptomator.freshdesk.com/support/solutions) and our [Wiki](https://github.com/cryptomator/cryptomator/wiki). +- Ensure the bug was not [already reported](https://github.com/cryptomator/cryptomator/issues). You can also check out our [FAQ](https://community.cryptomator.org/c/faq). - If you're unable to find an open issue addressing the problem, [submit a new one](https://github.com/cryptomator/cryptomator/issues/new). -## Do you have questions? - -- Ask questions by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new). -- [Contact us](https://cryptomator.org/contact/) directly by writing an email. Wir sprechen auch Deutsch! -- Have a chat with us on [Gitter](https://gitter.im/cryptomator/cryptomator). - -## Do you miss a feature? - -- Ensure the feature was not [already requested](https://github.com/cryptomator/cryptomator/issues). -- You're welcome to suggest a feature by [submitting a new issue](https://github.com/cryptomator/cryptomator/issues/new). - ## Did you write a patch that fixes a bug? - Open a new pull request with the patch. @@ -29,7 +18,7 @@ ## Code of Conduct -Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md). +Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/develop/CODE_OF_CONDUCT.md). ## Above all, thank you for your contributions diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 0aa4dfbbd..f0908e880 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,24 +1,15 @@ -To tick a checkbox replace [ ] with [x]. Make sure to replace placeholders (…) accordingly. - ## Issue Checklist Before creating a new issue make sure that you -- [ ] searched [existing (and closed) issues](https://github.com/cryptomator/cryptomator/issues). -- [ ] searched the [knowledge base](https://cryptomator.freshdesk.com/support/solutions). -- [ ] have read the [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md). -- [ ] have read the [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md). +- searched existing (and closed) issues: https://github.com/cryptomator/cryptomator/issues +- searched the knowledge base: https://community.cryptomator.org/c/kb +- have read the support guide: https://github.com/cryptomator/cryptomator/blob/develop/SUPPORT.md +- have read the contribution guide: https://github.com/cryptomator/cryptomator/blob/develop/CONTRIBUTING.md +- have read the code of conduct: https://github.com/cryptomator/cryptomator/blob/develop/CODE_OF_CONDUCT.md ## Basic Info -This is a -- [ ] bug report. -- [ ] feature request. -- [ ] question or something else. - -I'm using -- [ ] Windows in version: … -- [ ] macOS in version: … -- [ ] Linux in version: … +I'm using Windows / macOS / Linux / … in version: … I'm running Cryptomator in version: … (You can check the version in the Cryptomator settings.) @@ -30,7 +21,9 @@ I'm running Cryptomator in version: … ## Attachments (optional) -If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the [debug mode](https://cryptomator.freshdesk.com/support/solutions/articles/16000046480) first and reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file. +If you want to add the log file or screenshots, please add them as attachments. If your log file seems empty and doesn't show any errors, you may enable the debug mode first. Here is how to do that: https://community.cryptomator.org/t/how-do-i-enable-debug-mode/36 + +Then reproduce the problem to ensure all important information is contained in there. You may use test data or redact sensitive information from the log file. You can find the log file - on Windows: %appdata%/Cryptomator/cryptomator.log diff --git a/README.md b/README.md index cd3bbc835..235d688a3 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ [![Twitter](https://img.shields.io/badge/twitter-@Cryptomator-blue.svg?style=flat)](http://twitter.com/Cryptomator) [![POEditor](https://img.shields.io/badge/POEditor-Help%20Translate-blue.svg?style=flat)](https://poeditor.com/join/project/bHwbvJmx0E) [![Latest Release](https://img.shields.io/github/release/cryptomator/cryptomator.svg)](https://github.com/cryptomator/cryptomator/releases/latest) +[![Community](https://img.shields.io/badge/help-Community-orange.svg)](https://community.cryptomator.org) Multi-platform transparent client-side encryption of your files in the cloud. @@ -13,7 +14,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator ## Features -- Works with Dropbox, Google Drive, OneDrive, Nextcloud and any other cloud storage service which synchronizes with a local directory +- Works with Dropbox, Google Drive, OneDrive, ownCloud, Nextcloud and any other cloud storage service which synchronizes with a local directory - Open Source means: No backdoors, control is better than trust - Client-side: No accounts, no data shared with any online service - Totally transparent: Just work on the virtual drive as if it were a USB flash drive @@ -21,6 +22,7 @@ Download native binaries of Cryptomator on [cryptomator.org](https://cryptomator - File names get encrypted - Folder structure gets obfuscated - Use as many vaults in your Dropbox as you want, each having individual passwords +- One thousand commits for the security of your data!! :tada: ### Privacy @@ -58,14 +60,6 @@ mvn clean install -Prelease An executable jar file will be created inside `main/uber-jar/target`. -## Contributing to Cryptomator - -Please read our [contribution guide](https://github.com/cryptomator/cryptomator/blob/master/CONTRIBUTING.md), if you would like to report a bug, ask a question or help us with coding. - -## Code of Conduct - -Help us keep Cryptomator open and inclusive. Please read and follow our [Code of Conduct](https://github.com/cryptomator/cryptomator/blob/master/CODE_OF_CONDUCT.md). - ## License This project is dual-licensed under the GPLv3 for FOSS projects as well as a commercial license for independent software vendors and resellers. If you want to modify this application under different conditions, feel free to contact our support team. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 000000000..cdbc8ff88 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,10 @@ +# Support for Cryptomator + +For development-related topics, GitHub is the right place. + +For _everything else_, please visit our official [Cryptomator Community](https://community.cryptomator.org) (we are there, too :wink:). Amongst others, you will find: + +- Installation manuals +- Usage guides +- Help with problems +- Tips & tricks diff --git a/main/ant-kit/pom.xml b/main/ant-kit/pom.xml index f269efdaf..2fdd401d6 100644 --- a/main/ant-kit/pom.xml +++ b/main/ant-kit/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 ant-kit pom diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 85b2d0333..3ac49a6a8 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 commons Cryptomator Commons @@ -34,6 +34,15 @@ com.google.dagger dagger + + com.google.dagger + dagger-compiler + + + + com.google.dagger + dagger-compiler + diff --git a/main/jacoco-report/pom.xml b/main/jacoco-report/pom.xml index 26a7d39ac..47aa199f3 100644 --- a/main/jacoco-report/pom.xml +++ b/main/jacoco-report/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 jacoco-report Cryptomator Code Coverage Report diff --git a/main/keychain/pom.xml b/main/keychain/pom.xml index b07ee5788..e2ebca5a1 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 keychain System Keychain Access @@ -34,6 +34,10 @@ com.google.dagger dagger + + com.google.dagger + dagger-compiler + diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml index 59e578075..a821ea627 100644 --- a/main/launcher/pom.xml +++ b/main/launcher/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 launcher Cryptomator Launcher @@ -34,6 +34,10 @@ com.google.dagger dagger + + com.google.dagger + dagger-compiler + diff --git a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java index ccb24a528..e9108cb75 100644 --- a/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java +++ b/main/launcher/src/main/java/org/cryptomator/launcher/InterProcessCommunicator.java @@ -9,6 +9,9 @@ import java.io.Closeable; import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; @@ -17,6 +20,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.rmi.ConnectException; +import java.rmi.ConnectIOException; import java.rmi.NotBoundException; import java.rmi.Remote; import java.rmi.RemoteException; @@ -55,22 +59,19 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt // visible for testing static InterProcessCommunicator start(Path portFilePath, InterProcessCommunicationProtocol endpoint) throws IOException { System.setProperty("java.rmi.server.hostname", "localhost"); - // try to connect to existing server: - int port = readPort(portFilePath); - LOG.debug("Connecting to running process on TCP port {}...", port); try { - ClientCommunicator client = new ClientCommunicator(port); + // try to connect to existing server: + ClientCommunicator client = new ClientCommunicator(portFilePath); LOG.trace("Connected to running process."); return client; - } catch (ConnectException | NotBoundException e) { - LOG.debug("Did not find running process."); + } catch (ConnectException | ConnectIOException | NotBoundException e) { + LOG.debug("Could not connect to running process."); // continue } // spawn a new server: LOG.trace("Spawning new server..."); - ServerCommunicator server = new ServerCommunicator(endpoint); - writePort(portFilePath, server.getPort()); + ServerCommunicator server = new ServerCommunicator(endpoint, portFilePath); LOG.debug("Server listening on port {}.", server.getPort()); return server; } @@ -79,7 +80,7 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt final String settingsPathProperty = System.getProperty("cryptomator.ipcPortPath"); if (settingsPathProperty == null) { LOG.warn("System property cryptomator.ipcPortPath not set."); - return Paths.get("ipcPort.tmp"); + return Paths.get(".ipcPort.tmp"); } else { return Paths.get(replaceHomeDir(settingsPathProperty)); } @@ -97,12 +98,30 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt private final IpcProtocolRemote remote; - private ClientCommunicator(int port) throws ConnectException, NotBoundException, RemoteException { - if (port == 0) { - throw new ConnectException("Can not connect to port 0."); + private ClientCommunicator(Path portFilePath) throws ConnectException, NotBoundException, RemoteException { + if (Files.notExists(portFilePath)) { + throw new ConnectException("No IPC port file."); + } + try { + int port = ClientCommunicator.readPort(portFilePath); + LOG.debug("Connecting to port {}...", port); + Registry registry = LocateRegistry.getRegistry("localhost", port, new ClientSocketFactory()); + this.remote = (IpcProtocolRemote) registry.lookup(RMI_NAME); + } catch (IOException e) { + throw new ConnectException("Error reading IPC port file."); + } + } + + private static int readPort(Path portFilePath) throws IOException { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + try (ReadableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.READ)) { + if (ch.read(buf) == Integer.BYTES) { + buf.flip(); + return buf.getInt(); + } else { + throw new IOException("Invalid IPC port file."); + } } - Registry registry = LocateRegistry.getRegistry("localhost", port); - this.remote = (IpcProtocolRemote) registry.lookup(RMI_NAME); } @Override @@ -131,8 +150,9 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt private final ServerSocket socket; private final Registry registry; private final IpcProtocolRemoteImpl remote; + private final Path portFilePath; - private ServerCommunicator(InterProcessCommunicationProtocol delegate) throws IOException { + private ServerCommunicator(InterProcessCommunicationProtocol delegate, Path portFilePath) throws IOException { this.socket = new ServerSocket(0, Byte.MAX_VALUE, InetAddress.getByName("localhost")); RMIClientSocketFactory csf = RMISocketFactory.getDefaultSocketFactory(); SingletonServerSocketFactory ssf = new SingletonServerSocketFactory(socket); @@ -140,6 +160,20 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt this.remote = new IpcProtocolRemoteImpl(delegate); UnicastRemoteObject.exportObject(remote, 0); registry.rebind(RMI_NAME, remote); + this.portFilePath = portFilePath; + ServerCommunicator.writePort(portFilePath, socket.getLocalPort()); + } + + private static void writePort(Path portFilePath, int port) throws IOException { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + buf.putInt(port); + buf.flip(); + MoreFiles.createParentDirectories(portFilePath); + try (WritableByteChannel ch = Files.newByteChannel(portFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + if (ch.write(buf) != Integer.BYTES) { + throw new IOException("Did not write expected number of bytes."); + } + } } @Override @@ -162,6 +196,7 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt registry.unbind(RMI_NAME); UnicastRemoteObject.unexportObject(remote, true); socket.close(); + Files.deleteIfExists(portFilePath); LOG.debug("Server shut down."); } catch (NotBoundException | IOException e) { LOG.warn("Failed to close IPC Server.", e); @@ -210,31 +245,30 @@ abstract class InterProcessCommunicator implements InterProcessCommunicationProt } - private static int readPort(Path path) throws IOException { - if (Files.notExists(path)) { - return 0; - } - ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); - try (ReadableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.READ)) { - if (ch.read(buf) == Integer.BYTES) { - buf.flip(); - return buf.getInt(); - } else { - return 0; - } + /** + * Creates client sockets with short timeouts. + */ + private static class ClientSocketFactory implements RMIClientSocketFactory { + + @Override + public Socket createSocket(String host, int port) throws IOException { + return new SocketWithFixedTimeout(host, port, 1000); } + } - private static void writePort(Path path, int port) throws IOException { - ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); - buf.putInt(port); - buf.flip(); - MoreFiles.createParentDirectories(path); - try (WritableByteChannel ch = Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - if (ch.write(buf) != Integer.BYTES) { - throw new IOException("Did not write expected number of bytes."); - } + private static class SocketWithFixedTimeout extends Socket { + + public SocketWithFixedTimeout(String host, int port, int timeoutInMs) throws UnknownHostException, IOException { + super(host, port); + super.setSoTimeout(timeoutInMs); } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + // do nothing, timeout is fixed + } + } } diff --git a/main/pom.xml b/main/pom.xml index 1a7d5d575..60c9a0caf 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator main - 1.3.1 + 1.3.2 pom Cryptomator @@ -24,9 +24,9 @@ UTF-8 - 1.1.5 - 1.4.1 - 0.6.2 + 1.1.7 + 1.4.5 + 1.0.3 1.0.2 2.5 @@ -35,16 +35,16 @@ 1.0.3 - 22.0 + 23.5-jre 2.11 - 2.8.1 + 2.8.2 1.7.25 - 1.2.2 + 1.2.3 4.12 4.12.1 - 2.7.21 + 2.11.0 1.3 @@ -164,6 +164,12 @@ dagger ${dagger.version} + + com.google.dagger + dagger-compiler + ${dagger.version} + true + com.google.code.gson gson @@ -264,7 +270,7 @@ maven-dependency-plugin - 3.0.1 + 3.0.2 copy-libs @@ -302,17 +308,10 @@ maven-compiler-plugin - 3.6.1 + 3.7.0 1.8 1.8 - - - com.google.dagger - dagger-compiler - ${dagger.version} - - diff --git a/main/uber-jar/pom.xml b/main/uber-jar/pom.xml index 1bc18341f..4bc28572b 100644 --- a/main/uber-jar/pom.xml +++ b/main/uber-jar/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 uber-jar Single über jar with all dependencies diff --git a/main/ui/pom.xml b/main/ui/pom.xml index 0692c6732..aea587541 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.3.1 + 1.3.2 ui Cryptomator GUI @@ -72,6 +72,10 @@ com.google.dagger dagger + + com.google.dagger + dagger-compiler + @@ -86,5 +90,13 @@ slf4j-simple test + + + + com.google.jimfs + jimfs + 1.1 + test + diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 3c4f03dd8..12a838139 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -119,6 +119,11 @@ public class ChangePasswordController implements ViewController { return root; } + @Override + public void focus() { + oldPasswordField.requestFocus(); + } + void setVault(Vault vault) { this.vault = Objects.requireNonNull(vault); // trigger "default" change to refresh key bindings: diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index a718cd4eb..5c7c0dc5f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -10,7 +10,6 @@ package org.cryptomator.ui.controllers; import java.io.IOException; -import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.util.Objects; import java.util.Optional; @@ -106,6 +105,11 @@ public class InitializeController implements ViewController { return root; } + @Override + public void focus() { + passwordField.requestFocus(); + } + void setVault(Vault vault) { this.vault = Objects.requireNonNull(vault); // trigger "default" change to refresh key bindings: @@ -125,8 +129,6 @@ public class InitializeController implements ViewController { listener.ifPresent(this::invokeListenerLater); } catch (FileAlreadyExistsException ex) { messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized")); - } catch (DirectoryNotEmptyException ex) { - messageLabel.setText(localization.getString("initialize.messageLabel.notEmpty")); } catch (IOException ex) { LOG.error("I/O Exception", ex); messageLabel.setText(localization.getString("initialize.messageLabel.initializationFailed")); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index cd661655f..9c1a8fe83 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -9,6 +9,8 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; +import static org.cryptomator.ui.util.DialogBuilderUtil.buildErrorDialog; + import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -20,6 +22,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ExecutorService; +import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Named; @@ -63,12 +66,16 @@ import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.control.Button; import javafx.scene.control.ButtonType; +import javafx.scene.control.Cell; import javafx.scene.control.ContextMenu; import javafx.scene.control.ListCell; import javafx.scene.control.ListView; import javafx.scene.control.MenuItem; import javafx.scene.control.ToggleButton; import javafx.scene.image.Image; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.text.Font; @@ -162,6 +169,7 @@ public class MainController implements ViewController { @Override public void initialize() { vaultList.setItems(vaults); + vaultList.setOnKeyReleased(this::didPressKeyOnList); vaultList.setCellFactory(this::createDirecoryListCell); activeController.set(viewControllerLoader.load("/fxml/welcome.fxml")); selectedVault.bind(vaultList.getSelectionModel().selectedItemProperty()); @@ -236,6 +244,7 @@ public class MainController implements ViewController { private ListCell createDirecoryListCell(ListView param) { final DirectoryListCell cell = new DirectoryListCell(); cell.setVaultContextMenu(vaultListCellContextMenu); + cell.setOnMouseClicked(this::didClickOnListCell); return cell; } @@ -261,7 +270,18 @@ public class MainController implements ViewController { } try { final Path vaultDir = file.toPath(); - if (!Files.exists(vaultDir)) { + if (Files.exists(vaultDir)) { + try (Stream stream = Files.list(vaultDir)) { + if (stream.filter(this::isNotHidden).findAny().isPresent()) { + buildErrorDialog( // + localization.getString("main.createVault.nonEmptyDir.title"), // + localization.getString("main.createVault.nonEmptyDir.header"), // + localization.getString("main.createVault.nonEmptyDir.content"), // + ButtonType.OK).show(); + return; + } + } + } else { Files.createDirectory(vaultDir); } addVault(vaultDir, true); @@ -270,6 +290,10 @@ public class MainController implements ViewController { } } + private boolean isNotHidden(Path file) { + return !file.getFileName().toString().startsWith("."); + } + @FXML private void didClickAddExistingVaults(ActionEvent event) { final FileChooser fileChooser = new FileChooser(); @@ -309,6 +333,7 @@ public class MainController implements ViewController { } if (select) { vaultList.getSelectionModel().select(vault); + activeController.get().focus(); } } @@ -325,6 +350,8 @@ public class MainController implements ViewController { vaults.remove(selectedVault.get()); if (vaults.isEmpty()) { activeController.set(viewControllerLoader.load("/fxml/welcome.fxml")); + } else { + activeController.get().focus(); } } } @@ -375,6 +402,18 @@ public class MainController implements ViewController { } } + private void didPressKeyOnList(KeyEvent e) { + if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.SPACE) { + activeController.get().focus(); + } + } + + private void didClickOnListCell(MouseEvent e) { + if (MouseEvent.MOUSE_CLICKED.equals(e.getEventType()) && e.getSource() instanceof Cell && ((Cell) e.getSource()).isSelected()) { + activeController.get().focus(); + } + } + // **************************************** // Public Bindings // **************************************** @@ -408,6 +447,7 @@ public class MainController implements ViewController { public void didInitialize() { showUnlockView(); + activeController.get().focus(); } private void showUpgradeView() { @@ -419,6 +459,7 @@ public class MainController implements ViewController { public void didUpgrade() { showUnlockView(); + activeController.get().focus(); } private void showUnlockView() { @@ -446,6 +487,7 @@ public class MainController implements ViewController { public void didLock(UnlockedController ctrl) { unlockedVaults.remove(ctrl.getVault()); showUnlockView(); + activeController.get().focus(); } private void showChangePasswordView() { @@ -453,10 +495,12 @@ public class MainController implements ViewController { ctrl.setVault(selectedVault.get()); ctrl.setListener(this::didChangePassword); activeController.set(ctrl); + Platform.runLater(ctrl::focus); } public void didChangePassword() { showUnlockView(); + activeController.get().focus(); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 95fb5ed1d..6d30a7e2f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -155,6 +155,11 @@ public class UnlockController implements ViewController { return root; } + @Override + public void focus() { + passwordField.requestFocus(); + } + void setVault(Vault vault) { vaultSubs.unsubscribe(); vaultSubs = Subscription.EMPTY; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java index aafd46085..2d7cb7fa9 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java @@ -84,6 +84,11 @@ public class UpgradeController implements ViewController { return root; } + @Override + public void focus() { + passwordField.requestFocus(); + } + void setVault(Vault vault) { this.vault = Objects.requireNonNull(vault); errorLabel.setText(null); diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ViewController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ViewController.java index 1d27a0598..1f1ef817e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ViewController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ViewController.java @@ -24,4 +24,8 @@ public interface ViewController extends Initializable { // no-op } + default void focus() { + // no-op + } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java index a6339ed24..19d01cfbd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java @@ -11,6 +11,9 @@ package org.cryptomator.ui.controls; import java.util.Arrays; import javafx.scene.control.PasswordField; +import javafx.scene.input.DragEvent; +import javafx.scene.input.Dragboard; +import javafx.scene.input.TransferMode; /** * Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap. @@ -19,6 +22,27 @@ public class SecPasswordField extends PasswordField { private static final char SWIPE_CHAR = ' '; + public SecPasswordField() { + this.onDragOverProperty().set(this::handleDragOver); + this.onDragDroppedProperty().set(this::handleDragDropped); + } + + private void handleDragOver(DragEvent event) { + Dragboard dragboard = event.getDragboard(); + if (dragboard.hasString() && dragboard.getString() != null) { + event.acceptTransferModes(TransferMode.COPY); + } + event.consume(); + } + + private void handleDragDropped(DragEvent event) { + Dragboard dragboard = event.getDragboard(); + if (dragboard.hasString() && dragboard.getString() != null) { + insertText(getCaretPosition(), dragboard.getString()); + } + event.consume(); + } + /** * {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[]. * The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars. diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java index 426a9b200..29c51ff2b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java @@ -112,13 +112,8 @@ public abstract class UpgradeStrategy { public boolean isApplicable(Vault vault) { final Path masterkeyFile = vault.getPath().resolve(MASTERKEY_FILENAME); try { - if (Files.isRegularFile(masterkeyFile)) { - byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); - return KeyFile.parse(masterkeyFileContents).getVersion() == vaultVersionBeforeUpgrade; - } else { - LOG.warn("Not a file: {}", masterkeyFile); - return false; - } + byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile); + return KeyFile.parse(masterkeyFileContents).getVersion() == vaultVersionBeforeUpgrade; } catch (IOException e) { LOG.warn("Could not determine, whether upgrade is applicable.", e); return false; diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java index 3dd4b6010..3c9e6aa2e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java @@ -42,9 +42,10 @@ class UpgradeVersion3to4 extends UpgradeStrategy { private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class); private static final Pattern LVL1_DIR_PATTERN = Pattern.compile("[A-Z2-7]{2}"); private static final Pattern LVL2_DIR_PATTERN = Pattern.compile("[A-Z2-7]{30}"); - private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})_"); + private static final Pattern BASE32_PATTERN = Pattern.compile("^(([A-Z2-7]{8})*[A-Z2-7=]{8})"); + private static final Pattern BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN = Pattern.compile(BASE32_PATTERN.pattern() + "_"); private static final int FILE_MIN_SIZE = 88; // vault version 3 files have a header of 88 bytes (assuming no chunks at all) - private static final String LONG_FILENAME_SUFFIX = ".lng"; + private static final String LONG_FILENAME_EXT = ".lng"; private static final String OLD_FOLDER_SUFFIX = "_"; private static final String NEW_FOLDER_PREFIX = "0"; @@ -96,10 +97,12 @@ class UpgradeVersion3to4 extends UpgradeStrategy { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { String name = file.getFileName().toString(); - if (name.endsWith(LONG_FILENAME_SUFFIX)) { + if (attrs.size() > FILE_MIN_SIZE) { + LOG.trace("Skipping non-directory file {}.", file); + } else if (name.endsWith(LONG_FILENAME_EXT)) { migrateLong(metadataDir, file); } else { - migrate(file, attrs); + migrate(file); } return FileVisitResult.CONTINUE; } @@ -112,14 +115,13 @@ class UpgradeVersion3to4 extends UpgradeStrategy { LOG.info("Migration finished."); } - private void migrate(Path file, BasicFileAttributes attrs) throws IOException { + private void migrate(Path file) throws IOException { String name = file.getFileName().toString(); - long size = attrs.size(); Matcher m = BASE32_FOLLOWED_BY_UNDERSCORE_PATTERN.matcher(name); - if (attrs.isRegularFile() && m.find(0) && size < FILE_MIN_SIZE) { + if (m.find(0)) { String base32 = m.group(1); String suffix = name.substring(m.end()); - String renamed = NEW_FOLDER_PREFIX + base32 + (suffix.isEmpty() ? "" : " " + suffix); + String renamed = NEW_FOLDER_PREFIX + base32 + suffix; renameWithoutOverwriting(file, renamed); } } @@ -135,22 +137,35 @@ class UpgradeVersion3to4 extends UpgradeStrategy { private void migrateLong(Path metadataDir, Path path) throws IOException { String oldName = path.getFileName().toString(); - Path oldMetadataFile = metadataDir.resolve(oldName.substring(0, 2)).resolve(oldName.substring(2, 4)).resolve(oldName); - if (Files.isRegularFile(oldMetadataFile)) { - String oldContent = new String(Files.readAllBytes(oldMetadataFile), UTF_8); - if (oldContent.endsWith(OLD_FOLDER_SUFFIX)) { - String newContent = NEW_FOLDER_PREFIX + StringUtils.removeEnd(oldContent, OLD_FOLDER_SUFFIX); - String newName = base32.encodeAsString(sha1.digest(newContent.getBytes(UTF_8))) + LONG_FILENAME_SUFFIX; + assert oldName.endsWith(LONG_FILENAME_EXT); + String oldNameBase = StringUtils.removeEnd(oldName, LONG_FILENAME_EXT); + + Matcher m = BASE32_PATTERN.matcher(oldNameBase); + if (m.find(0)) { + String oldNameBase32 = m.group(1); + String oldNameSuffix = oldNameBase.substring(m.end()); + String oldCanonicalName = oldNameBase32 + LONG_FILENAME_EXT; + + Path oldMetadataFile = metadataDir.resolve(oldCanonicalName.substring(0, 2)).resolve(oldCanonicalName.substring(2, 4)).resolve(oldCanonicalName); + if (!Files.isReadable(oldMetadataFile)) { + LOG.warn("Found uninflatable long file name. Expected: {}", oldMetadataFile); + return; + } + + String oldLongName = new String(Files.readAllBytes(oldMetadataFile), UTF_8); + if (oldLongName.endsWith(OLD_FOLDER_SUFFIX)) { + String newLongName = NEW_FOLDER_PREFIX + StringUtils.removeEnd(oldLongName, OLD_FOLDER_SUFFIX); + String newCanonicalBase32 = base32.encodeAsString(sha1.digest(newLongName.getBytes(UTF_8))); + String newCanonicalName = newCanonicalBase32 + LONG_FILENAME_EXT; + Path newMetadataFile = metadataDir.resolve(newCanonicalName.substring(0, 2)).resolve(newCanonicalName.substring(2, 4)).resolve(newCanonicalName); + String newName = newCanonicalBase32 + oldNameSuffix + LONG_FILENAME_EXT; Path newPath = path.resolveSibling(newName); - Path newMetadataFile = metadataDir.resolve(newName.substring(0, 2)).resolve(newName.substring(2, 4)).resolve(newName); Files.move(path, newPath); Files.createDirectories(newMetadataFile.getParent()); - Files.write(newMetadataFile, newContent.getBytes(UTF_8)); - Files.delete(oldMetadataFile); - LOG.info("Renaming {} to {}\nDeleting {}\nCreating {}", path, newName, oldMetadataFile, newMetadataFile); + Files.write(newMetadataFile, newLongName.getBytes(UTF_8)); + LOG.info("Renaming {} to {}.", path, newName); + LOG.info("Creating {}.", newMetadataFile); } - } else { - LOG.warn("Found uninflatable long file name. Expected: {}", oldMetadataFile); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java index ae26f75da..89b2847d1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion4to5.java @@ -83,7 +83,7 @@ class UpgradeVersion4to5 extends UpgradeStrategy { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (attrs.isRegularFile() && BASE32_PATTERN.matcher(file.getFileName().toString()).find() && attrs.size() > cryptor.fileHeaderCryptor().headerSize()) { + if (BASE32_PATTERN.matcher(file.getFileName().toString()).find() && attrs.size() > cryptor.fileHeaderCryptor().headerSize()) { migrate(file, attrs, cryptor); } else { LOG.info("Skipping irrelevant file {}.", file); diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index 23c76429b..2b57d14ab 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -9,8 +9,8 @@ package org.cryptomator.ui.model; import java.io.IOException; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.DirectoryStream; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -60,6 +60,7 @@ public class Vault { public static final Predicate NOT_LOCKED = hasState(State.LOCKED).negate(); private static final Logger LOG = LoggerFactory.getLogger(Vault.class); private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; + private static final String LOCALHOST_ALIAS = "cryptomator-vault"; private final Settings settings; private final VaultSettings vaultSettings; @@ -99,13 +100,6 @@ public class Vault { } public void create(CharSequence passphrase) throws IOException { - try (DirectoryStream stream = Files.newDirectoryStream(getPath())) { - for (Path file : stream) { - if (!file.getFileName().toString().startsWith(".")) { - throw new DirectoryNotEmptyException(getPath().toString()); - } - } - } if (!isValidVaultDirectory()) { CryptoFileSystemProvider.initialize(getPath(), MASTERKEY_FILENAME, passphrase); } else { @@ -137,6 +131,7 @@ public class Vault { MountParams mountParams = MountParams.create() // .withWindowsDriveLetter(vaultSettings.winDriveLetter().get()) // .withPreferredGvfsScheme(settings.preferredGvfsScheme().get()) // + .withWebdavHostname(getLocalhostAliasOrNull()) // .build(); Platform.runLater(() -> { @@ -148,6 +143,19 @@ public class Vault { }); } + private String getLocalhostAliasOrNull() { + try { + InetAddress alias = InetAddress.getByName(LOCALHOST_ALIAS); + if (alias.getHostAddress().equals("127.0.0.1")) { + return LOCALHOST_ALIAS; + } else { + return null; + } + } catch (UnknownHostException e) { + return null; + } + } + public synchronized void unmount() throws CommandFailedException { unmount(Function.identity()); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java b/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java index 52b854530..056ba8f97 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/PasswordStrengthUtil.java @@ -14,9 +14,9 @@ import java.util.List; import javax.inject.Inject; import javax.inject.Singleton; -import org.apache.commons.lang3.StringUtils; import org.cryptomator.ui.l10n.Localization; +import com.google.common.base.Strings; import com.nulabinc.zxcvbn.Zxcvbn; import javafx.geometry.Insets; @@ -41,8 +41,10 @@ public class PasswordStrengthUtil { } public int computeRate(String password) { - if (StringUtils.isEmpty(password)) { + if (Strings.isNullOrEmpty(password)) { return -1; + } else if (password.length() > 100) { + return 4; // assume this is strong. zxcvbn memory and runtime depends vastly on the password length } else { return zxcvbn.measure(password, sanitizedInputs).getScore(); } diff --git a/main/ui/src/main/resources/css/linux_theme.css b/main/ui/src/main/resources/css/linux_theme.css index fdb8d341b..91a7de6d8 100644 --- a/main/ui/src/main/resources/css/linux_theme.css +++ b/main/ui/src/main/resources/css/linux_theme.css @@ -108,6 +108,13 @@ -fx-text-fill: COLOR_TEXT_DISABLED; } +.button:focused, +.toggle-button:focused { + -fx-border-color: black; + -fx-border-insets: 2px; + -fx-border-style: dotted inside; +} + /**************************************************************************** * * * Segmented Buttons * @@ -157,6 +164,10 @@ -fx-padding: 1; } +.list-view:focused { + -fx-border-style: dotted inside; +} + .list-cell { -fx-padding: 0.5em; -fx-text-fill: COLOR_TEXT; @@ -243,6 +254,12 @@ -fx-background-color: COLOR_VGRAD_DARK; } +.tool-bar.list-related-toolbar .toggle-button:focused, +.tool-bar.list-related-toolbar .button:focused { + -fx-border-style: dotted inside; + -fx-border-color: black; +} + /******************************************************************************* * * * PopupMenu * @@ -333,6 +350,10 @@ .check-box:selected > .box > .mark { -fx-background-color: COLOR_TEXT; } +/* focused */ +.check-box:focused { + -fx-border-style: dotted inside; +} /* disabled: */ .check-box:disabled > .box { -fx-background-color: COLOR_BORDER, COLOR_BACKGROUND; @@ -355,6 +376,10 @@ -fx-text-fill: COLOR_TEXT; } +.choice-box:focused { + -fx-border-style: dotted inside; +} + .choice-box > .open-button > .arrow { -fx-background-color: transparent, COLOR_TEXT; -fx-background-insets: 0 0 -1 0, 0; diff --git a/main/ui/src/main/resources/css/win_theme.css b/main/ui/src/main/resources/css/win_theme.css index 6bf949b6c..cdf67b4b0 100644 --- a/main/ui/src/main/resources/css/win_theme.css +++ b/main/ui/src/main/resources/css/win_theme.css @@ -157,36 +157,48 @@ .list-view { -fx-background-color: COLOR_BORDER, #FFF; + -fx-border-style: dotted inside; + -fx-border-color: transparent; -fx-background-insets: 0, 1; -fx-padding: 1; } +.list-view:focused { + -fx-border-color: black; +} + .list-view > .virtual-flow > .scroll-bar:vertical{ -fx-background-insets: 0, 0 0 0 1; -fx-padding: -1 -1 -1 0; } + .list-view > .virtual-flow > .scroll-bar:horizontal{ -fx-background-insets: 0, 1 0 0 0; -fx-padding: 0 -1 -1 -1; } + .list-view > .virtual-flow > .corner { -fx-background-color: COLOR_BORDER, COLOR_CONTROL_BASE; -fx-background-insets: 0, 1 0 0 1; } + .list-cell { -fx-background-color: #FFF; -fx-padding: 0.6em; -fx-text-fill: COLOR_TEXT; -fx-opacity: 1; } + .list-view:focused > .virtual-flow > .clipped-container > .sheet > .list-cell:focused { -fx-background-color: COLOR_BORDER_FOCUS, #FFF; -fx-background-insets: 0, 1; } + .list-view > .virtual-flow > .clipped-container > .sheet > .list-cell:filled:selected { -fx-background-color: #DEDEDE, #F7F7F7; -fx-background-insets: 0, 1; } + .list-cell:filled:hover { -fx-background-color: #F7F7F7; } @@ -364,6 +376,8 @@ .check-box { -fx-label-padding: 0 0 0 6px; -fx-text-fill: COLOR_TEXT; + -fx-border-style: dotted inside; + -fx-border-color: transparent; } .check-box > .box { -fx-padding: 1px; @@ -380,6 +394,10 @@ .check-box:armed > .box { -fx-border-color: COLOR_BORDER_FOCUS; } +/* focused */ +.check-box:focused { + -fx-border-color: black; +} /* selected: */ .check-box:selected > .box > .mark { -fx-background-color: black; @@ -406,6 +424,10 @@ -fx-text-fill: COLOR_TEXT; } +.choice-box:focused { + -fx-border-style: dotted inside; +} + .choice-box > .open-button > .arrow { -fx-background-color: transparent, COLOR_TEXT; -fx-background-insets: 0 0 -1 0, 0; diff --git a/main/ui/src/main/resources/fxml/main.fxml b/main/ui/src/main/resources/fxml/main.fxml index afc023ea2..93444fc9e 100644 --- a/main/ui/src/main/resources/fxml/main.fxml +++ b/main/ui/src/main/resources/fxml/main.fxml @@ -53,7 +53,7 @@ - + - -