Compare commits

...

59 Commits
0.4.0 ... 0.5.2

Author SHA1 Message Date
Sebastian Stenzel
1dd8a28a9d Merge remote-tracking branch 'origin/master' into 0.5.2
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-03-06 14:56:22 +01:00
Sebastian Stenzel
39df98ea3c Branch 0.5.2 for windows 2015-03-06 14:55:30 +01:00
Sebastian Stenzel
2849e39e85 on-the-fly MAC calculation for better performance (addresses issue #38)
we still need to add some kind of warning on the UI and create an async MAC checker for ranged requests
2015-03-01 22:23:42 +01:00
Sebastian Stenzel
9433c22d7f minor I/O improvements 2015-03-01 20:55:32 +01:00
Sebastian Stenzel
5bd38d31bf Merge branch '0.5.1'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/pom.xml
	main/ui/pom.xml
2015-02-23 14:53:31 +01:00
Sebastian Stenzel
63f64fae03 Fixed performance implications due to slow /dev/random. Now seeding PRNG only once per Cryptor. Fixes #36 2015-02-23 14:51:52 +01:00
Sebastian Stenzel
e321994c35 Update README.md 2015-02-22 23:03:47 +01:00
Sebastian Stenzel
f86b27d62f Updated Version to 0.6.0-SNAPSHOT 2015-02-22 22:19:13 +01:00
Sebastian Stenzel
cba8bbefc5 Beta Version 0.5.0 2015-02-22 22:18:18 +01:00
Sebastian Stenzel
507e21f8a3 - fixes folder creation and automounting on Linux
- using IPv6 address for mounting on Windows only (hostnames on OS X and Linux)
2015-02-22 21:04:46 +01:00
Sebastian Stenzel
676cb10ef0 fixes automount on linux distributions, that do not accept the [::1] literal as localhost
fixes reset of Settings, if a Vault no longer exists upon Cryptomator startup
2015-02-22 18:01:13 +01:00
Sebastian Stenzel
3b3aa4107b fixes #33 2015-02-22 16:46:16 +01:00
Sebastian Stenzel
7edd303f2e Added change password functionality (fixes #20)
Moved controllers to new package
Small UI improvements
2015-02-22 16:10:17 +01:00
Sebastian Stenzel
ea3384d189 removed multi user functionality (see #21)
using fixed masterkey filename now
2015-02-22 15:15:43 +01:00
Sebastian Stenzel
b2be41e39b Refactorings 2015-02-22 14:25:48 +01:00
Sebastian Stenzel
f1d125bf8d reduced public interface complexity of Vault 2015-02-22 14:06:52 +01:00
Sebastian Stenzel
028f6ea824 WebDavMounter warmUp in background thread. 2015-02-22 13:52:28 +01:00
Sebastian Stenzel
30dc8eecb1 - Refactored WebDavMounter (using Guice)
- implemented warm start for windows mounts
2015-02-22 13:21:08 +01:00
Sebastian Stenzel
4d979c26f6 (hopefully) fixed NPE in FXMLLoader.
see http://stackoverflow.com/questions/26434758/npe-in-fxmlloader/26436265#26436265
2015-02-22 12:36:17 +01:00
Sebastian Stenzel
4776dbf603 Renamed volume icon 2015-02-22 12:18:42 +01:00
Sebastian Stenzel
0b5e4469b4 Update .travis.yml 2015-02-20 22:11:00 +01:00
Sebastian Stenzel
8ba89a3bf5 Injecting Cryptor using Guice 2015-02-20 21:30:33 +01:00
Sebastian Stenzel
b68cf71494 - always check HMAC before decryption
- separating AES and CMAC key during SIV mode
2015-02-20 19:47:45 +01:00
Sebastian Stenzel
5569ecbfc7 fixes #23 2015-02-19 19:50:03 +01:00
Sebastian Stenzel
19bc1ed569 using beginning of long filename instead of checksum 2015-02-19 18:54:31 +01:00
Sebastian Stenzel
5aaee7bbf6 - fixed xorend function
- SIV implementation now satisfies all official test vectors
2015-02-15 15:55:49 +01:00
Sebastian Stenzel
3187520797 - fixed special chars in folder names
- fixed IndexOutOfBoundsException
- removal of no longer existing vault directories (at runtime)
2015-02-15 00:48:03 +01:00
Sebastian Stenzel
bcee1e0d12 Filename padding no longer needed: This was done in order to prevent AES-CTR to switch to a stream mode on the last block, which would be highly exploitable. Now we're using SIV mode, which operates on whole blocks. 2015-02-14 19:21:08 +01:00
Sebastian Stenzel
9fdd2f339c - changed file name encryption to SIV mode
- vastly improved exception handling, if decryption of a path name fails
2015-02-14 18:55:33 +01:00
Sebastian Stenzel
ebdf37ed63 RFC 5297 AEAD_AES_SIV_CMAC_256 2015-02-14 18:20:17 +01:00
Sebastian Stenzel
09c26f5e86 Merge pull request #32 from Tillerino/injection
Dependency injection instead of static instances
2015-02-14 16:34:19 +01:00
Tillmann Gaida
def70c5891 Removed static resources in WebDavServer, FXThreads and Settings with
dependency injection. Replaced static references to MainApplication in
the context of closing resources with an injected DeferredCloser. Using
controller factory for dependency injection into FX controllers.
2015-02-14 14:11:55 +01:00
Sebastian Stenzel
11396b71e6 Merge pull request #31 from gitter-badger/gitter-badge
Add a Gitter chat badge to README.md
2015-02-14 12:45:10 +01:00
The Gitter Badger
05ec9b574e Added Gitter badge 2015-02-14 11:44:48 +00:00
Sebastian Stenzel
efac770915 allow adding *.cryptomator files to vault list 2015-02-13 21:22:26 +01:00
Sebastian Stenzel
f29bcc447c - fixed automount on windows 2015-02-13 21:05:16 +01:00
Sebastian Stenzel
5e0ebab587 refactored "add vault" functionality, which fixes #14
removed some dependencies
refactored Main/MainApplication, which fixes #16
2015-02-13 19:46:07 +01:00
Sebastian Stenzel
751dbe6b7e Merge pull request #30 from Tillerino/osxNames
Named mounting (only affects OSX atm)
2015-01-25 13:44:44 +01:00
Tillmann Gaida
a72f8ba8ab Added the new mount name to the web dav mounter interface. Under OSX, we
can now use the name, which fixes #5
2015-01-25 12:42:16 +01:00
Sebastian Stenzel
999285617d Merge pull request #28 from Tillerino/windowsNames
Pretty network drive names on Windows
2015-01-25 12:05:04 +01:00
Sebastian Stenzel
addf488b26 Merge pull request #29 from Tillerino/master
Merged. But we should investigate alternatives to axet's openFileHandler
2015-01-25 12:04:12 +01:00
Tillmann Gaida
cd5e878a26 Bugfix (magic file open handler broke context class loader for event
thread)
2015-01-23 16:25:54 +01:00
Tillmann Gaida
0a671aa9bc Addition of a name to the context path of the WebDAV servlet. The name
will then appear as the name of the network drive on Windows.
The name is "normalized" down to characters, which are certain to be
accepted. I added a field to the unlock controller, which normalizes the
name as you type.
2015-01-23 14:28:22 +01:00
Sebastian Stenzel
8cc445a12a New application icon by Thomas Pähler 2015-01-23 00:20:40 +01:00
Sebastian Stenzel
432beb2a17 - fixed #19 (again): vault-specific prefix is now handled by the servlet context instead of jackrabbit.
- simplified webdav locator, as workspaces and pathPrefixes are not relevant to jackrabbit any longer
2015-01-22 21:48:52 +01:00
Sebastian Stenzel
9fd271ad7b fixed NPE 2015-01-22 21:42:45 +01:00
Sebastian Stenzel
72b1ff78c3 Merge pull request #27 from Tillerino/master
Single Running Instance + Double-clicking folders/files shows in GUI
2015-01-21 20:07:51 +01:00
Tillmann Gaida
edfd264e47 Changes proposed by @totalvoidness in code review 2015-01-21 19:54:10 +01:00
Tillmann Gaida
0cfc3fb7f7 Prevents starting a second instance of the GUI and forwards
main-method-arguments to the running instance. Command line arguments
are treated by showing the corresponding folder in the GUI.

If an argument is a folder, it is shown directly. If an argument is a
.masterkey.json file, the parent directory is shown. If an argument does
not exist, but the folder can be created, the newly created folder is
shown.

It was necessary to move the main function away from the MainApplication
class because running the main method of a class, which extends the
javafx Application class, will start a non-daemon thread. This prevents
the VM from exiting naturally.

OSX needs its own mechanism, which is implemented in OS-specific code.
It is vital that the required handler is added in the main thread of the
application, not the Java FX thread, which is a bit awkward to
implement. Since it is possible to open .cryptomator packages on OSX,
this extension is now hidden in the folder list.
2015-01-21 17:35:25 +01:00
Sebastian Stenzel
ecf29a91b8 Update README.md 2015-01-18 15:35:35 +01:00
Sebastian Stenzel
38884c6dfd - added custom info.plist template for OS X native packages (references #14) kudos to @tillerino 2015-01-17 19:57:15 +01:00
Sebastian Stenzel
7813a11381 - pad filenames with NULL bytes (fixes #24) 2015-01-16 19:55:33 +01:00
Sebastian Stenzel
d774546bf8 - pad file contents to reach a multiple of 16 bytes (so AES/CTR always works on complete blocks) - references #24
- calculate MAC over complete ciphertext (including file length obfuscation trash data)
2015-01-16 19:50:57 +01:00
Sebastian Stenzel
0b64c7ce25 - Updated exception 2015-01-15 12:29:10 +01:00
Sebastian Stenzel
0aef60efc4 - Single Jetty instnace (fixes #19) 2015-01-15 12:27:10 +01:00
Sebastian Stenzel
f0fa4fcf3d Merge branch 'master' of https://github.com/totalvoidness/open-cloud-encryptor 2015-01-14 19:35:04 +01:00
Sebastian Stenzel
8bfdad38b9 - fixed timing attack on MAC (see http://codahale.com/a-lesson-in-timing-attacks/) 2015-01-14 19:34:36 +01:00
Sebastian Stenzel
19ea81f0e5 Update README.md 2015-01-13 13:57:38 +01:00
Sebastian Stenzel
5e6f343e68 - Updated version to 0.5.0-SNAPSHOT 2015-01-13 11:04:58 +01:00
84 changed files with 3851 additions and 1492 deletions

View File

@@ -2,3 +2,10 @@ language: java
jdk:
- oraclejdk8
script: mvn -fmain/pom.xml clean package
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
on_success: change # options: [always|never|change] default: always
on_failure: always # options: [always|never|change] default: always
on_start: false # default: false

View File

@@ -1,47 +1,54 @@
Cryptomator
====================
Multiplatform transparent client-side encryption of your files in the cloud. You need Java 8 in order to run the application. Get the runtime environment here: http://www.oracle.com/technetwork/java/javase/downloads/index.html
[![Join the chat at https://gitter.im/totalvoidness/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/totalvoidness/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
If you want to take a look at the current beta version, go ahead and download [Cryptomator.dmg](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.dmg), [Cryptomator.exe](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.exe) or [Cryptomator.jar](https://github.com/totalvoidness/cryptomator/releases/download/v0.3.0/Cryptomator.jar).
Multiplatform transparent client-side encryption of your files in the cloud.
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
## Features
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
- In fact it works with any directory. You can use it to encrypt as many folders as you like
- AES encryption with up to 256 bit key length
- AES encryption with 256 bit key length
- Client-side. No accounts, no data shared with any online service
- Filenames get encrypted too
- No need to provide credentials for any 3rd party service
- Open Source means: No backdoors. Control is better than trust
- Use as many encrypted folders in your dropbox as you want. Each having individual passwords
## Security
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
- Scrypt key generation
### Privacy
- 256 bit keys (unlimited strength policy bundled with native binaries - 128 bit elsewhere)
- Scrypt key derivation
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
- Sensitive data is swiped from the heap asap
- Lightweight: Complexity kills security
## Consistency
### Consistency
- HMAC over file contents to recognize changed ciphertext before decryption
- I/O operations are transactional and atomic, if the file systems supports it
- ~~Metadata is stored per-folder, so it's not a SPOF~~
- *NEW:* No Metadata at all. Encrypted files can be decrypted even on completely shuffled file systems (if their contents are undamaged).
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no SPOF
## Dependencies
- Java 8
- see pom.xml ;-)
## Building
## TODO
#### Dependencies
* Java 8
* Maven 3
* Optional: OS-dependent build tools for native packaging
* Optional: JCE unlimited strength policy (needed for 256 bit keys)
### UI
- Native L&F
- Drive icons in WebDAV volumes
- Change password functionality
- Better explanations on UI
#### Building on Debian-based OS
```bash
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
git clone https://github.com/totalvoidness/cryptomator.git
cd cryptomator/main
git checkout v0.4.0
mvn clean install
```
## License
Distributed under the MIT X Consortium license license. See the LICENSE file for more info.
Distributed under the MIT X Consortium license. See the LICENSE file for more info.
[![Build Status](https://travis-ci.org/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)

View File

@@ -12,10 +12,10 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.5.2</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator core I/O module</name>
<name>Cryptomator WebDAV and I/O module</name>
<properties>
<jetty.version>9.2.5.v20141112</jetty.version>

View File

@@ -1,83 +0,0 @@
package org.cryptomator.files;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
public class EncryptingFileVisitor extends SimpleFileVisitor<Path> implements CryptorIOSupport {
private final Path rootDir;
private final Cryptor cryptor;
private final EncryptionDecider encryptionDecider;
private Path currentDir;
public EncryptingFileVisitor(Path rootDir, Cryptor cryptor, EncryptionDecider encryptionDecider) {
this.rootDir = rootDir;
this.cryptor = cryptor;
this.encryptionDecider = encryptionDecider;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (rootDir.equals(dir) || encryptionDecider.shouldEncrypt(dir)) {
this.currentDir = dir;
return FileVisitResult.CONTINUE;
} else {
return FileVisitResult.SKIP_SUBTREE;
}
}
@Override
public FileVisitResult visitFile(Path plaintextFile, BasicFileAttributes attrs) throws IOException {
if (encryptionDecider.shouldEncrypt(plaintextFile)) {
final String plaintextName = plaintextFile.getFileName().toString();
final String encryptedName = cryptor.encryptPath(plaintextName, '/', '/', this);
final Path encryptedPath = plaintextFile.resolveSibling(encryptedName);
final InputStream plaintextIn = Files.newInputStream(plaintextFile, StandardOpenOption.READ);
final SeekableByteChannel ciphertextOut = Files.newByteChannel(encryptedPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
cryptor.encryptFile(plaintextIn, ciphertextOut);
Files.delete(plaintextFile);
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (encryptionDecider.shouldEncrypt(dir)) {
final String plaintext = dir.getFileName().toString();
final String encrypted = cryptor.encryptPath(plaintext, '/', '/', this);
final Path newPath = dir.resolveSibling(encrypted);
Files.move(dir, newPath, StandardCopyOption.ATOMIC_MOVE);
}
return FileVisitResult.CONTINUE;
}
@Override
public void writePathSpecificMetadata(String metadataFile, byte[] encryptedMetadata) throws IOException {
final Path path = currentDir.resolve(metadataFile);
Files.write(path, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
}
@Override
public byte[] readPathSpecificMetadata(String metadataFile) throws IOException {
final Path path = currentDir.resolve(metadataFile);
return Files.readAllBytes(path);
}
/* callback */
public interface EncryptionDecider {
boolean shouldEncrypt(Path path);
}
}

View File

@@ -8,16 +8,24 @@
******************************************************************************/
package org.cryptomator.webdav;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.WebDavServlet;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.component.LifeCycle;
import org.eclipse.jetty.util.thread.QueuedThreadPool;
import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
@@ -26,45 +34,37 @@ import org.slf4j.LoggerFactory;
public final class WebDavServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = "::1";
private static final String LOCALHOST = SystemUtils.IS_OS_WINDOWS ? "::1" : "localhost";
private static final int MAX_PENDING_REQUESTS = 200;
private static final int MAX_THREADS = 200;
private static final int MIN_THREADS = 4;
private static final int THREAD_IDLE_SECONDS = 20;
private final Server server;
private int port;
private final ServerConnector localConnector;
private final ContextHandlerCollection servletCollection;
public WebDavServer() {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
server = new Server(tp);
localConnector = new ServerConnector(server);
localConnector.setHost(LOCALHOST);
servletCollection = new ContextHandlerCollection();
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
servletContext.addServlet(servlet, "/");
server.setConnectors(new Connector[] {localConnector});
server.setHandler(servletCollection);
}
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @return <code>true</code> upon success
*/
public synchronized boolean start(final String workDir, final boolean checkFileIntegrity, final Cryptor cryptor) {
final ServerConnector connector = new ServerConnector(server);
connector.setHost(LOCALHOST);
final String contextPath = "/";
final String servletPathSpec = "/*";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(getWebDavServletHolder(workDir, contextPath, checkFileIntegrity, cryptor), servletPathSpec);
context.setContextPath(contextPath);
server.setHandler(context);
public synchronized void start() {
try {
server.setConnectors(new Connector[] {connector});
server.start();
port = connector.getLocalPort();
return true;
LOG.info("Cryptomator is running on port {}", getPort());
} catch (Exception ex) {
LOG.error("Server couldn't be started", ex);
return false;
throw new RuntimeException("Server couldn't be started", ex);
}
}
@@ -72,26 +72,95 @@ public final class WebDavServer {
return server.isRunning();
}
public synchronized boolean stop() {
public synchronized void stop() {
try {
server.stop();
port = 0;
} catch (Exception ex) {
LOG.error("Server couldn't be stopped", ex);
}
return server.isStopped();
}
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final boolean checkFileIntegrity, final Cryptor cryptor) {
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @param name The name of the folder. Must be non-empty and only contain any of
* _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
* @return servlet
*/
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, String name) {
try {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("name empty");
}
if (!StringUtils.containsOnly(name, "_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")) {
throw new IllegalArgumentException("name contains illegal characters: " + name);
}
final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor);
servletContext.addServlet(servlet, "/*");
servletCollection.mapContexts();
LOG.debug("{} available on http:{}", workDir, uri.getRawSchemeSpecificPart());
return new ServletLifeCycleAdapter(servletContext, uri);
} catch (URISyntaxException e) {
throw new IllegalStateException("Invalid hard-coded URI components.", e);
}
}
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);
result.setInitParameter(WebDavServlet.CFG_CHECK_FILE_INTEGRITY, Boolean.toString(checkFileIntegrity));
return result;
}
public int getPort() {
return port;
return localConnector.getLocalPort();
}
/**
* Exposes implementation-specific methods to other modules.
*/
public class ServletLifeCycleAdapter {
private final LifeCycle lifecycle;
private final URI servletUri;
private ServletLifeCycleAdapter(LifeCycle lifecycle, URI servletUri) {
this.lifecycle = lifecycle;
this.servletUri = servletUri;
}
public boolean isRunning() {
return lifecycle.isRunning();
}
public boolean start() {
try {
lifecycle.start();
return true;
} catch (Exception e) {
LOG.error("Failed to start", e);
return false;
}
}
public boolean stop() {
try {
lifecycle.stop();
return true;
} catch (Exception e) {
LOG.error("Failed to stop", e);
return false;
}
}
public URI getServletUri() {
return servletUri;
}
}
}

View File

@@ -0,0 +1,31 @@
/*******************************************************************************
* 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.webdav;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Windows mount attempts will fail, if not all requests on parent paths of a WebDAV resource get served. This servlet will respond to any
* request with status code 200, if the requested resource doesn't match a different servlet.
*/
public class WindowsSucksServlet extends HttpServlet {
private static final long serialVersionUID = -515280795196074354L;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setStatus(HttpServletResponse.SC_OK);
}
}

View File

@@ -0,0 +1,23 @@
package org.cryptomator.webdav.exceptions;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
public class DecryptFailedRuntimeException extends RuntimeException {
private static final long serialVersionUID = -2726689824823439865L;
public DecryptFailedRuntimeException(DecryptFailedException cause) {
super(cause);
}
@Override
public String getMessage() {
return getCause().getMessage();
}
@Override
public String getLocalizedMessage() {
return getCause().getLocalizedMessage();
}
}

View File

@@ -15,31 +15,73 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.apache.commons.collections4.BidiMap;
import org.apache.jackrabbit.webdav.AbstractLocatorFactory;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.util.EncodeUtil;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.SensitiveDataSwipeListener;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
class DavLocatorFactoryImpl implements DavLocatorFactory, SensitiveDataSwipeListener, CryptorIOSupport {
private static final int MAX_CACHED_PATHS = 10000;
private final Path fsRoot;
private final Cryptor cryptor;
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
DavLocatorFactoryImpl(String fsRoot, String httpRoot, Cryptor cryptor) {
super(httpRoot);
DavLocatorFactoryImpl(String fsRoot, Cryptor cryptor) {
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
this.cryptor = cryptor;
cryptor.addSensitiveDataSwipeListener(this);
}
/* DavLocatorFactory */
@Override
public DavResourceLocator createResourceLocator(String prefix, String href) {
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
final String resourcePath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
}
/**
* @throws DecryptFailedRuntimeException, which should a checked exception, but Jackrabbit doesn't allow that.
*/
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
try {
final String resourcePath = (isResourcePath) ? path : getResourcePath(path);
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
} catch (DecryptFailedException e) {
throw new DecryptFailedRuntimeException(e);
}
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
try {
return createResourceLocator(prefix, workspacePath, resourcePath, true);
} catch (DecryptFailedRuntimeException e) {
throw new IllegalStateException("Tried to decrypt resourcePath. Only repositoryPaths can be encrypted.", e);
}
}
/* Encryption/Decryption */
/**
* @return Encrypted absolute paths on the file system.
*/
@Override
protected String getRepositoryPath(String resourcePath, String wspPath) {
private String getRepositoryPath(String resourcePath) {
String encryptedPath = pathCache.get(resourcePath);
if (encryptedPath == null) {
encryptedPath = encryptRepositoryPath(resourcePath);
@@ -59,8 +101,7 @@ class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveD
/**
* @return Decrypted path for use in URIs.
*/
@Override
protected String getResourcePath(String repositoryPath, String wspPath) {
private String getResourcePath(String repositoryPath) throws DecryptFailedException {
String decryptedPath = pathCache.getKey(repositoryPath);
if (decryptedPath == null) {
decryptedPath = decryptResourcePath(repositoryPath);
@@ -69,7 +110,7 @@ class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveD
return decryptedPath;
}
private String decryptResourcePath(String repositoryPath) {
private String decryptResourcePath(String repositoryPath) throws DecryptFailedException {
final Path absRepoPath = FileSystems.getDefault().getPath(repositoryPath);
if (fsRoot.equals(absRepoPath)) {
return null;
@@ -80,24 +121,7 @@ class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveD
}
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
// we don't support workspaces
return super.createResourceLocator(prefix, "", path, isResourcePath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
// we don't support workspaces
return super.createResourceLocator(prefix, "", resourcePath);
}
@Override
public void swipeSensitiveData() {
pathCache.clear();
}
/* Cryptor I/O Support */
/* CryptorIOSupport */
@Override
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
@@ -115,4 +139,104 @@ class DavLocatorFactoryImpl extends AbstractLocatorFactory implements SensitiveD
}
}
/* SensitiveDataSwipeListener */
@Override
public void swipeSensitiveData() {
pathCache.clear();
}
/* Locator */
private class DavResourceLocatorImpl implements DavResourceLocator {
private final String prefix;
private final String resourcePath;
private DavResourceLocatorImpl(String prefix, String resourcePath) {
this.prefix = prefix;
this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true);
}
@Override
public String getPrefix() {
return prefix;
}
@Override
public String getResourcePath() {
return resourcePath;
}
@Override
public String getWorkspacePath() {
return isRootLocation() ? null : "";
}
@Override
public String getWorkspaceName() {
return getPrefix();
}
@Override
public boolean isSameWorkspace(DavResourceLocator locator) {
return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName());
}
@Override
public boolean isSameWorkspace(String workspaceName) {
return getWorkspaceName().equals(workspaceName);
}
@Override
public String getHref(boolean isCollection) {
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
final String href = getPrefix().concat(encodedResourcePath);
if (isCollection && !href.endsWith("/")) {
return href.concat("/");
} else if (!isCollection && href.endsWith("/")) {
return href.substring(0, href.length() - 1);
} else {
return href;
}
}
@Override
public boolean isRootLocation() {
return getResourcePath() == null;
}
@Override
public DavLocatorFactory getFactory() {
return DavLocatorFactoryImpl.this;
}
@Override
public String getRepositoryPath() {
return DavLocatorFactoryImpl.this.getRepositoryPath(getResourcePath());
}
@Override
public int hashCode() {
final HashCodeBuilder builder = new HashCodeBuilder();
builder.append(prefix);
builder.append(resourcePath);
return builder.toHashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof DavResourceLocatorImpl) {
final DavResourceLocatorImpl other = (DavResourceLocatorImpl) obj;
final EqualsBuilder builder = new EqualsBuilder();
builder.append(this.prefix, other.prefix);
builder.append(this.resourcePath, other.resourcePath);
return builder.isEquals();
} else {
return false;
}
}
}
}

View File

@@ -34,11 +34,9 @@ class DavResourceFactoryImpl implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final boolean checkFileIntegrity;
DavResourceFactoryImpl(Cryptor cryptor, boolean checkFileIntegrity) {
DavResourceFactoryImpl(Cryptor cryptor) {
this.cryptor = cryptor;
this.checkFileIntegrity = checkFileIntegrity;
}
@Override
@@ -62,9 +60,9 @@ class DavResourceFactoryImpl implements DavResourceFactory {
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isRegularFile(path)) {
if (path != null && Files.isRegularFile(path)) {
return createFile(locator, session);
} else if (Files.isDirectory(path)) {
} else if (path != null && Files.isDirectory(path)) {
return createDirectory(locator, session);
} else {
return createNonExisting(locator, session);
@@ -72,11 +70,11 @@ class DavResourceFactoryImpl implements DavResourceFactory {
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, checkFileIntegrity);
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, checkFileIntegrity);
return new EncryptedFile(this, locator, session, lockManager, cryptor);
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {

View File

@@ -23,8 +23,6 @@ public class WebDavServlet extends AbstractWebdavServlet {
private static final long serialVersionUID = 7965170007048673022L;
public static final String CFG_FS_ROOT = "cfg.fs.root";
public static final String CFG_HTTP_ROOT = "cfg.http.root";
public static final String CFG_CHECK_FILE_INTEGRITY = "cfg.checkFileIntegrity";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
@@ -42,11 +40,9 @@ public class WebDavServlet extends AbstractWebdavServlet {
davSessionProvider = new DavSessionProviderImpl();
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
final String httpRoot = config.getInitParameter(CFG_HTTP_ROOT);
final boolean checkFileIntegrity = Boolean.parseBoolean(config.getInitParameter(CFG_CHECK_FILE_INTEGRITY));
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, httpRoot, cryptor);
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
this.davResourceFactory = new DavResourceFactoryImpl(cryptor, checkFileIntegrity);
this.davResourceFactory = new DavResourceFactoryImpl(cryptor);
}
@Override

View File

@@ -37,6 +37,7 @@ import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -77,9 +78,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
try (final SeekableByteChannel channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
cryptor.encryptFile(inputContext.getInputStream(), channel);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
@@ -87,7 +86,6 @@ public class EncryptedDir extends AbstractEncryptedNode {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
IOUtils.closeQuietly(inputContext.getInputStream());
}
}
@@ -100,9 +98,14 @@ public class EncryptedDir extends AbstractEncryptedNode {
final List<DavResource> result = new ArrayList<>();
for (final Path childPath : directoryStream) {
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
final DavResource resource = factory.createResource(childLocator, session);
result.add(resource);
try {
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
final DavResource resource = factory.createResource(childLocator, session);
result.add(resource);
} catch (DecryptFailedRuntimeException e) {
LOG.warn("Decryption of resource failed: " + childPath);
continue;
}
}
return new DavResourceIteratorImpl(result);
} catch (IOException e) {
@@ -118,7 +121,9 @@ public class EncryptedDir extends AbstractEncryptedNode {
public void removeMember(DavResource member) throws DavException {
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
if (Files.exists(memberPath)) {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
}
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {

View File

@@ -16,7 +16,6 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
@@ -30,6 +29,7 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
@@ -40,11 +40,8 @@ public class EncryptedFile extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
protected final boolean checkIntegrity;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
this.checkIntegrity = checkIntegrity;
}
@Override
@@ -73,22 +70,20 @@ public class EncryptedFile extends AbstractEncryptedNode {
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
}
outputContext.setContentLength(cryptor.decryptedContentLength(channel));
if (outputContext.hasStream()) {
cryptor.decryptedFile(channel, outputContext.getOutputStream());
cryptor.decryptFile(channel, outputContext.getOutputStream());
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (MacAuthenticationFailedException e) {
LOG.warn("MAC authentication failed, file content {} might be compromised.", getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
@@ -97,12 +92,15 @@ public class EncryptedFile extends AbstractEncryptedNode {
protected void determineProperties() {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
} catch (IOException e) {
LOG.error("Error reading filesize " + path.toString(), e);
throw new IORuntimeException(e);
}
try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
@@ -110,8 +108,6 @@ public class EncryptedFile extends AbstractEncryptedNode {
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}

View File

@@ -9,7 +9,6 @@ import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
@@ -50,8 +49,8 @@ public class EncryptedFilePart extends EncryptedFile {
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, boolean checkIntegrity) {
super(factory, locator, session, lockManager, cryptor, checkIntegrity);
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
@@ -113,12 +112,7 @@ public class EncryptedFilePart extends EncryptedFile {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
if (checkIntegrity && !cryptor.authenticateContent(channel)) {
throw new DecryptFailedException("File content compromised: " + path.toString());
}
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
@@ -133,8 +127,6 @@ public class EncryptedFilePart extends EncryptedFile {
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}

View File

@@ -34,7 +34,7 @@ public class NonExistingNode extends AbstractEncryptedNode {
@Override
public boolean isCollection() {
throw new UnsupportedOperationException("Resource doesn't exist.");
return false;
}
@Override

View File

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

View File

@@ -8,23 +8,24 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.UUID;
import java.util.zip.CRC32;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -39,15 +40,14 @@ import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.AbstractCryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
@@ -58,19 +58,19 @@ import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
/**
* PRNG for cryptographically secure random numbers. Defaults to SHA1-based number generator.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
*/
private static final SecureRandom SECURE_PRNG;
/**
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction
* Policy Files isn't installed. Those files can be downloaded here: http://www.oracle.com/technetwork/java/javase/downloads/.
*/
private static final int AES_KEY_LENGTH_IN_BITS;
/**
* PRNG for cryptographically secure random numbers. Defaults to SHA1-based number generator.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SecureRandom
*/
private final SecureRandom securePrng;
/**
* Jackson JSON-Mapper.
*/
@@ -89,7 +89,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
static {
try {
SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
final int maxKeyLength = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
AES_KEY_LENGTH_IN_BITS = (maxKeyLength >= PREF_MASTER_KEY_LENGTH_IN_BITS) ? PREF_MASTER_KEY_LENGTH_IN_BITS : maxKeyLength;
} catch (NoSuchAlgorithmException e) {
@@ -101,33 +100,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* Creates a new Cryptor with a newly initialized PRNG.
*/
public Aes256Cryptor() {
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
SECURE_PRNG.nextBytes(bytes);
securePrng = SecureRandom.getInstance(PRNG_ALGORITHM);
securePrng.setSeed(securePrng.generateSeed(PRNG_SEED_LENGTH));
securePrng.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
SECURE_PRNG.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} finally {
Arrays.fill(bytes, (byte) 0);
}
}
/**
* Creates a new Cryptor with the given PRNG.<br/>
* <strong>DO NOT USE IN PRODUCTION</strong>. This constructor must only be used in in unit tests. Do not change method visibility.
*
* @param prng Fast, possibly insecure PRNG.
*/
Aes256Cryptor(Random prng) {
byte[] bytes = new byte[AES_KEY_LENGTH_IN_BITS / Byte.SIZE];
try {
prng.nextBytes(bytes);
this.primaryMasterKey = new SecretKeySpec(bytes, AES_KEY_ALGORITHM);
prng.nextBytes(bytes);
securePrng.nextBytes(bytes);
this.hMacMasterKey = new SecretKeySpec(bytes, HMAC_KEY_ALGORITHM);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("PRNG algorithm should exist.", e);
} finally {
Arrays.fill(bytes, (byte) 0);
}
@@ -266,8 +248,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
private byte[] randomData(int length) {
final byte[] result = new byte[length];
SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
SECURE_PRNG.nextBytes(result);
securePrng.nextBytes(result);
return result;
}
@@ -287,23 +268,17 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private long crc32Sum(byte[] source) {
final CRC32 crc32 = new CRC32();
crc32.update(source);
return crc32.getValue();
}
@Override
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
try {
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
for (final String cleartext : cleartextPathComps) {
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, ioSupport);
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, hMacMasterKey, ioSupport);
encryptedPathComps.add(encrypted);
}
return StringUtils.join(encryptedPathComps, encryptedPathSep);
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
} catch (InvalidKeyException | IOException e) {
throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
}
}
@@ -323,21 +298,18 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* These alternative names consist of the checksum, a unique id and a special file extension defined in
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final byte[] mac = hmacSha256(hMacMasterKey).doFinal(cleartext.getBytes());
final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.put(partialIv);
final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.ENCRYPT_MODE);
final byte[] cleartextBytes = cleartext.getBytes(Charsets.UTF_8);
final byte[] encryptedBytes = cipher.doFinal(cleartextBytes);
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(partialIv) + IV_PREFIX_SEPARATOR + ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
private String encryptPathComponent(final String cleartext, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException {
final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8);
// encrypt:
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes);
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String crc32 = Long.toHexString(crc32Sum(ivAndCiphertext.getBytes()));
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final String groupPrefix = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
final String alternativeFileName = crc32 + LONG_NAME_PREFIX_SEPARATOR + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
final String alternativeFileName = groupPrefix + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
this.storeMetadata(ioSupport, metadataFilename, metadata);
return alternativeFileName;
} else {
@@ -346,16 +318,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
try {
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
for (final String encrypted : encryptedPathComps) {
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, ioSupport);
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, hMacMasterKey, ioSupport);
cleartextPathComps.add(new String(cleartext));
}
return StringUtils.join(cleartextPathComps, cleartextPathSep);
} catch (IllegalBlockSizeException | BadPaddingException | IOException e) {
} catch (InvalidKeyException | IOException e) {
throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
}
}
@@ -363,30 +335,26 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
/**
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
*/
private String decryptPathComponent(final String encrypted, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
final String ivAndCiphertext;
private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
final String ciphertext;
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
final String crc32 = StringUtils.substringBefore(basename, LONG_NAME_PREFIX_SEPARATOR);
final String uuid = StringUtils.substringAfter(basename, LONG_NAME_PREFIX_SEPARATOR);
final String metadataFilename = crc32 + METADATA_FILE_EXT;
final String groupPrefix = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
ivAndCiphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
ivAndCiphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
}
final String partialIvStr = StringUtils.substringBefore(ivAndCiphertext, IV_PREFIX_SEPARATOR);
final String ciphertext = StringUtils.substringAfter(ivAndCiphertext, IV_PREFIX_SEPARATOR);
final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
iv.put(ENCRYPTED_FILENAME_CODEC.decode(partialIvStr));
final Cipher cipher = this.aesCtrCipher(key, iv.array(), Cipher.DECRYPT_MODE);
// decrypt:
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
final byte[] cleartextBytes = cipher.doFinal(encryptedBytes);
return new String(cleartextBytes, Charsets.UTF_8);
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes);
return new String(cleartextBytes, StandardCharsets.UTF_8);
}
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
@@ -402,34 +370,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// init mac:
final Mac mac = this.hmacSha256(hMacMasterKey);
// read stored mac:
encryptedFile.position(16);
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
final int numMacBytesRead = encryptedFile.read(macBuffer);
// check validity of header:
if (numMacBytesRead != mac.getMacLength() || fileSize == null) {
throw new IOException("Failed to read file header.");
}
// read all encrypted data and calculate mac:
encryptedFile.position(64);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, mac);
IOUtils.copyLarge(macIn, new NullOutputStream(), 0, fileSize);
// compare:
return Arrays.equals(macBuffer.array(), mac.doFinal());
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
// skip 128bit IV + 256 bit MAC:
@@ -455,18 +395,46 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
private void encryptedContentLength(SeekableByteChannel encryptedFile, Long contentLength) throws IOException {
final ByteBuffer encryptedFileSizeBuffer;
// encrypt content length in ECB mode (content length is less than one block):
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
fileSizeBuffer.putLong(contentLength);
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
encryptedFileSizeBuffer = ByteBuffer.wrap(encryptedFileSize);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
}
// skip 128bit IV + 256 bit MAC:
encryptedFile.position(48);
// write result:
encryptedFile.write(encryptedFileSizeBuffer);
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numIvBytesRead = encryptedFile.read(countingIv);
// init mac:
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
// read stored mac:
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
final int numMacBytesRead = encryptedFile.read(storedMac);
// read file size:
final Long fileSize = decryptedContentLength(encryptedFile);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH || fileSize == null) {
if (numIvBytesRead != AES_BLOCK_LENGTH || numMacBytesRead != calculatedMac.getMacLength() || fileSize == null) {
throw new IOException("Failed to read file header.");
}
@@ -478,12 +446,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
final InputStream macIn = new MacInputStream(in, calculatedMac);
final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
// drain remaining bytes to /dev/null to complete MAC calculation:
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
if (!macMatches) {
// This exception will be thrown AFTER we sent the decrypted content to the user.
// This has two advantages:
// - we don't need to read files twice
// - we can still restore files suffering from non-malicious bit rotting
// Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception.
throw new MacAuthenticationFailedException("MAC authentication failed.");
}
return bytesDecrypted;
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
@@ -520,7 +505,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
countingIv.position(0);
encryptedFile.write(countingIv);
// init crypto stuff:
@@ -531,48 +515,40 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
encryptedFile.write(macBuffer);
// init filesize buffer and skip 16 bytes
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
encryptedFile.write(encryptedFileSizeBuffer);
// encrypt and write "zero length" as a placeholder, which will be read by concurrent requests, as long as encryption didn't finish:
encryptedContentLength(encryptedFile, 0l);
// write content:
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut);
// copy MAC:
macBuffer.position(0);
macBuffer.put(mac.doFinal());
// ensure total byte count is a multiple of the block size, in CTR mode:
final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
// append fake content:
final int randomContentLength = (int) Math.ceil((Math.random() + 1.0) * actualSize / 20.0);
// append a few blocks of fake data:
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
for (int i = 0; i < randomContentLength; i += AES_BLOCK_LENGTH) {
cipheredOut.write(emptyBytes);
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(emptyBytes);
}
cipheredOut.flush();
blockSizeBufferedOut.flush();
// encrypt actualSize
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
fileSizeBuffer.putLong(actualSize);
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
encryptedFileSizeBuffer.position(0);
encryptedFileSizeBuffer.put(encryptedFileSize);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
// write file header
encryptedFile.position(16); // skip already written 128 bit IV
macBuffer.position(0);
// write MAC of total ciphertext:
macBuffer.clear();
macBuffer.put(mac.doFinal());
macBuffer.flip();
encryptedFile.position(16); // right behind the IV
encryptedFile.write(macBuffer); // 256 bit MAC
encryptedFileSizeBuffer.position(0);
encryptedFile.write(encryptedFileSizeBuffer); // 128 bit encrypted file size
return actualSize;
// encrypt and write plaintextSize:
encryptedContentLength(encryptedFile, plaintextSize);
return plaintextSize;
}
@Override

View File

@@ -33,7 +33,7 @@ interface AesCryptographicConfiguration {
/**
* Number of bytes used as seed for the PRNG.
*/
int PRNG_SEED_LENGTH = 16;
int PRNG_SEED_LENGTH = 32;
/**
* Algorithm used for random number generation.
@@ -60,7 +60,8 @@ interface AesCryptographicConfiguration {
String AES_KEYWRAP_CIPHER = "AESWrap";
/**
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.<br/>
* <strong>Important</strong>: As JCE doesn't support a padding, input must be a multiple of the block size.
*
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
*/

View File

@@ -0,0 +1,226 @@
/*******************************************************************************
* 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.crypto.aes256;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.apache.commons.lang3.ArrayUtils;
import org.bouncycastle.crypto.BlockCipher;
import org.bouncycastle.crypto.CipherParameters;
import org.bouncycastle.crypto.Mac;
import org.bouncycastle.crypto.engines.AESFastEngine;
import org.bouncycastle.crypto.macs.CMac;
import org.bouncycastle.crypto.paddings.ISO7816d4Padding;
import org.bouncycastle.crypto.params.KeyParameter;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
/**
* Implements the RFC 5297 SIV mode.
*/
final class AesSivCipherUtil {
private static final byte[] BYTES_ZERO = new byte[16];
private static final byte DOUBLING_CONST = (byte) 0x87;
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivEncrypt(byte[] aesKey, byte[] macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = s2v(macKey, plaintext, additionalData);
final int numBlocks = (plaintext.length + 15) / 16;
// clear out the 31st and 63rd (rightmost) bit:
final byte[] ctr = Arrays.copyOf(iv, 16);
ctr[8] = (byte) (ctr[8] & 0x7F);
ctr[12] = (byte) (ctr[12] & 0x7F);
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
final long initialCtrVal = ctrBuf.getLong(8);
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
aes.reset();
}
final byte[] ciphertext = xor(plaintext, x);
return ArrayUtils.addAll(iv, ciphertext);
}
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException, DecryptFailedException {
final byte[] aesKeyBytes = aesKey.getEncoded();
final byte[] macKeyBytes = macKey.getEncoded();
if (aesKeyBytes == null || macKeyBytes == null) {
throw new IllegalArgumentException("Can't get bytes of given key.");
}
try {
return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
} finally {
Arrays.fill(aesKeyBytes, (byte) 0);
Arrays.fill(macKeyBytes, (byte) 0);
}
}
static byte[] sivDecrypt(byte[] aesKey, byte[] macKey, byte[] ciphertext, byte[]... additionalData) throws DecryptFailedException, InvalidKeyException {
if (aesKey.length != 16 && aesKey.length != 24 && aesKey.length != 32) {
throw new InvalidKeyException("Invalid aesKey length " + aesKey.length);
}
final byte[] iv = Arrays.copyOf(ciphertext, 16);
final byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 16, ciphertext.length);
final int numBlocks = (actualCiphertext.length + 15) / 16;
// clear out the 31st and 63rd (rightmost) bit:
final byte[] ctr = Arrays.copyOf(iv, 16);
ctr[8] = (byte) (ctr[8] & 0x7F);
ctr[12] = (byte) (ctr[12] & 0x7F);
final ByteBuffer ctrBuf = ByteBuffer.wrap(ctr);
final long initialCtrVal = ctrBuf.getLong(8);
final byte[] x = new byte[numBlocks * 16];
final BlockCipher aes = new AESFastEngine();
aes.init(true, new KeyParameter(aesKey));
for (int i = 0; i < numBlocks; i++) {
final long ctrVal = initialCtrVal + i;
ctrBuf.putLong(8, ctrVal);
aes.processBlock(ctrBuf.array(), 0, x, i * 16);
aes.reset();
}
final byte[] plaintext = xor(actualCiphertext, x);
final byte[] control = s2v(macKey, plaintext, additionalData);
if (MessageDigest.isEqual(control, iv)) {
return plaintext;
} else {
throw new DecryptFailedException("Authentication failed");
}
}
static byte[] s2v(byte[] macKey, byte[] plaintext, byte[]... additionalData) {
final CipherParameters params = new KeyParameter(macKey);
final BlockCipher aes = new AESFastEngine();
final CMac mac = new CMac(aes);
mac.init(params);
byte[] d = mac(mac, BYTES_ZERO);
for (byte[] s : additionalData) {
d = xor(dbl(d), mac(mac, s));
}
final byte[] t;
if (plaintext.length >= 16) {
t = xorend(plaintext, d);
} else {
t = xor(dbl(d), pad(plaintext));
}
return mac(mac, t);
}
private static byte[] mac(Mac mac, byte[] in) {
byte[] result = new byte[mac.getMacSize()];
mac.update(in, 0, in.length);
mac.doFinal(result, 0);
return result;
}
/**
* First bit 1, following bits 0.
*/
private static byte[] pad(byte[] in) {
final byte[] result = Arrays.copyOf(in, 16);
new ISO7816d4Padding().addPadding(result, in.length);
return result;
}
/**
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
*/
private static int shiftLeft(byte[] block, byte[] output) {
int i = block.length;
int bit = 0;
while (--i >= 0) {
int b = block[i] & 0xff;
output[i] = (byte) ((b << 1) | bit);
bit = (b >>> 7) & 1;
}
return bit;
}
/**
* Code taken from {@link org.bouncycastle.crypto.macs.CMac}
*/
private static byte[] dbl(byte[] in) {
byte[] ret = new byte[in.length];
int carry = shiftLeft(in, ret);
int xor = 0xff & DOUBLING_CONST;
/*
* NOTE: This construction is an attempt at a constant-time implementation.
*/
ret[in.length - 1] ^= (xor >>> ((1 - carry) << 3));
return ret;
}
private static byte[] xor(byte[] in1, byte[] in2) {
if (in1 == null || in2 == null || in1.length > in2.length) {
throw new IllegalArgumentException("Length of first input must be <= length of second input.");
}
final byte[] result = new byte[in1.length];
for (int i = 0; i < result.length; i++) {
result[i] = (byte) (in1[i] ^ in2[i]);
}
return result;
}
private static byte[] xorend(byte[] in1, byte[] in2) {
if (in1 == null || in2 == null || in1.length < in2.length) {
throw new IllegalArgumentException("Length of first input must be >= length of second input.");
}
final byte[] result = Arrays.copyOf(in1, in1.length);
final int diff = in1.length - in2.length;
for (int i = 0; i < in2.length; i++) {
result[i + diff] = (byte) (result[i + diff] ^ in2[i]);
}
return result;
}
}

View File

@@ -16,11 +16,6 @@ import org.apache.commons.codec.binary.BaseNCodec;
interface FileNamingConventions {
/**
* Extension of masterkey files inside the root directory of the encrypted storage.
*/
String MASTERKEY_FILE_EXT = ".masterkey.json";
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
@@ -48,9 +43,9 @@ interface FileNamingConventions {
String LONG_NAME_FILE_EXT = ".lng.aes";
/**
* Prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
*/
String LONG_NAME_PREFIX_SEPARATOR = "_";
int LONG_NAME_PREFIX_LENGTH = 8;
/**
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some

View File

@@ -32,7 +32,9 @@ class MacInputStream extends FilterInputStream {
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
mac.update(b, off, len);
if (read > 0) {
mac.update(b, off, read);
}
return read;
}

View File

@@ -17,7 +17,6 @@ import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.CryptorIOSupport;
@@ -29,17 +28,15 @@ import org.junit.Test;
public class Aes256CryptorTest {
private static final Random TEST_PRNG = new Random();
@Test
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor decryptor = new Aes256Cryptor();
final InputStream in = new ByteArrayInputStream(out.toByteArray());
decryptor.decryptMasterKey(in, pw);
@@ -50,7 +47,7 @@ public class Aes256CryptorTest {
@Test
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
final String pw = "asd";
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
final ByteArrayOutputStream out = new ByteArrayOutputStream();
cryptor.encryptMasterKey(out, pw);
cryptor.swipeSensitiveData();
@@ -58,7 +55,7 @@ public class Aes256CryptorTest {
// all these passwords are expected to fail.
final String[] wrongPws = {"a", "as", "asdf", "sdf", "das", "dsa", "foo", "bar", "baz"};
final Aes256Cryptor decryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor decryptor = new Aes256Cryptor();
for (final String wrongPw : wrongPws) {
final InputStream in = new ByteArrayInputStream(out.toByteArray());
try {
@@ -72,17 +69,17 @@ public class Aes256CryptorTest {
}
}
@Test
public void testIntegrityAuthentication() throws IOException {
@Test(expected = DecryptFailedException.class)
public void testIntegrityAuthentication() throws IOException, DecryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -90,11 +87,6 @@ public class Aes256CryptorTest {
encryptedData.position(0);
// authenticate unmodified content:
final SeekableByteChannel encryptedIn1 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified1 = cryptor.authenticateContent(encryptedIn1);
Assert.assertTrue(isContentUnmodified1);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
@@ -103,10 +95,10 @@ public class Aes256CryptorTest {
encryptedData.position(0);
// authenticate modified content:
final SeekableByteChannel encryptedIn2 = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean isContentUnmodified2 = cryptor.authenticateContent(encryptedIn2);
Assert.assertFalse(isContentUnmodified2);
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
cryptor.decryptFile(encryptedIn, plaintextOut);
}
@Test
@@ -116,10 +108,10 @@ public class Aes256CryptorTest {
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -134,7 +126,7 @@ public class Aes256CryptorTest {
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
@@ -155,10 +147,10 @@ public class Aes256CryptorTest {
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (64 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -181,9 +173,9 @@ public class Aes256CryptorTest {
}
@Test
public void testEncryptionOfFilenames() throws IOException {
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
final Aes256Cryptor cryptor = new Aes256Cryptor();
// short path components
final String originalPath1 = "foo/bar/baz";
@@ -201,6 +193,14 @@ public class Aes256CryptorTest {
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath2, decryptedPath2);
// block size length path components
final String originalPath3 = "aaaabbbbccccdddd";
final String encryptedPath3a = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
final String encryptedPath3b = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
final String decryptedPath3 = cryptor.decryptPath(encryptedPath3a, '/', '/', ioSupportMock);
Assert.assertEquals(originalPath3, decryptedPath3);
}
private static class CryptoIOSupportMock implements CryptorIOSupport {

View File

@@ -0,0 +1,224 @@
package org.cryptomator.crypto.aes256;
import java.security.InvalidKeyException;
import org.apache.commons.codec.DecoderException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.junit.Assert;
import org.junit.Test;
/**
* Official RFC 5297 test vector taken from https://tools.ietf.org/html/rfc5297#appendix-A.1
*/
public class AesSivCipherUtilTest {
@Test
public void testS2v() throws DecoderException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93};
final byte[] result = AesSivCipherUtil.s2v(macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivEncrypt() throws InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] plaintext = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] expected = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test
public void testSivDecrypt() throws DecryptFailedException, InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0xff};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
@Test(expected = DecryptFailedException.class)
public void testSivDecryptWithInvalidKey() throws DecryptFailedException, InvalidKeyException {
final byte[] macKey = {(byte) 0xff, (byte) 0xfe, (byte) 0xfd, (byte) 0xfc, //
(byte) 0xfb, (byte) 0xfa, (byte) 0xf9, (byte) 0xf8, //
(byte) 0xf7, (byte) 0xf6, (byte) 0xf5, (byte) 0xf4, //
(byte) 0xf3, (byte) 0xf2, (byte) 0xf1, (byte) 0xf0};
final byte[] aesKey = {(byte) 0xf0, (byte) 0xf1, (byte) 0xf2, (byte) 0xf3, //
(byte) 0xf4, (byte) 0xf5, (byte) 0xf6, (byte) 0xf7, //
(byte) 0xf8, (byte) 0xf9, (byte) 0xfa, (byte) 0xfb, //
(byte) 0xfc, (byte) 0xfd, (byte) 0xfe, (byte) 0x00};
final byte[] ad = {(byte) 0x10, (byte) 0x11, (byte) 0x12, (byte) 0x13, //
(byte) 0x14, (byte) 0x15, (byte) 0x16, (byte) 0x17, //
(byte) 0x18, (byte) 0x19, (byte) 0x1a, (byte) 0x1b, //
(byte) 0x1c, (byte) 0x1d, (byte) 0x1e, (byte) 0x1f, //
(byte) 0x20, (byte) 0x21, (byte) 0x22, (byte) 0x23, //
(byte) 0x24, (byte) 0x25, (byte) 0x26, (byte) 0x27};
final byte[] ciphertext = {(byte) 0x85, (byte) 0x63, (byte) 0x2d, (byte) 0x07, //
(byte) 0xc6, (byte) 0xe8, (byte) 0xf3, (byte) 0x7f, //
(byte) 0x95, (byte) 0x0a, (byte) 0xcd, (byte) 0x32, //
(byte) 0x0a, (byte) 0x2e, (byte) 0xcc, (byte) 0x93, //
(byte) 0x40, (byte) 0xc0, (byte) 0x2b, (byte) 0x96, //
(byte) 0x90, (byte) 0xc4, (byte) 0xdc, (byte) 0x04, //
(byte) 0xda, (byte) 0xef, (byte) 0x7f, (byte) 0x6a, //
(byte) 0xfe, (byte) 0x5c};
final byte[] expected = {(byte) 0x11, (byte) 0x22, (byte) 0x33, (byte) 0x44, //
(byte) 0x55, (byte) 0x66, (byte) 0x77, (byte) 0x88, //
(byte) 0x99, (byte) 0xaa, (byte) 0xbb, (byte) 0xcc, //
(byte) 0xdd, (byte) 0xee};
final byte[] result = AesSivCipherUtil.sivDecrypt(aesKey, macKey, ciphertext, ad);
Assert.assertArrayEquals(expected, result);
}
/**
* https://tools.ietf.org/html/rfc5297#appendix-A.2
*/
@Test
public void testNonceBasedAuthenticatedEncryption() throws InvalidKeyException {
final byte[] macKey = {(byte) 0x7f, (byte) 0x7e, (byte) 0x7d, (byte) 0x7c, //
(byte) 0x7b, (byte) 0x7a, (byte) 0x79, (byte) 0x78, //
(byte) 0x77, (byte) 0x76, (byte) 0x75, (byte) 0x74, //
(byte) 0x73, (byte) 0x72, (byte) 0x71, (byte) 0x70};
final byte[] aesKey = {(byte) 0x40, (byte) 0x41, (byte) 0x42, (byte) 0x43, //
(byte) 0x44, (byte) 0x45, (byte) 0x46, (byte) 0x47, //
(byte) 0x48, (byte) 0x49, (byte) 0x4a, (byte) 0x4b, //
(byte) 0x4c, (byte) 0x4d, (byte) 0x4e, (byte) 0x4f};
final byte[] ad1 = {(byte) 0x00, (byte) 0x11, (byte) 0x22, (byte) 0x33, //
(byte) 0x44, (byte) 0x55, (byte) 0x66, (byte) 0x77, //
(byte) 0x88, (byte) 0x99, (byte) 0xaa, (byte) 0xbb, //
(byte) 0xcc, (byte) 0xdd, (byte) 0xee, (byte) 0xff, //
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
(byte) 0xde, (byte) 0xad, (byte) 0xda, (byte) 0xda, //
(byte) 0xff, (byte) 0xee, (byte) 0xdd, (byte) 0xcc, //
(byte) 0xbb, (byte) 0xaa, (byte) 0x99, (byte) 0x88, //
(byte) 0x77, (byte) 0x66, (byte) 0x55, (byte) 0x44, //
(byte) 0x33, (byte) 0x22, (byte) 0x11, (byte) 0x00};
final byte[] ad2 = {(byte) 0x10, (byte) 0x20, (byte) 0x30, (byte) 0x40, //
(byte) 0x50, (byte) 0x60, (byte) 0x70, (byte) 0x80, //
(byte) 0x90, (byte) 0xa0};
final byte[] nonce = {(byte) 0x09, (byte) 0xf9, (byte) 0x11, (byte) 0x02, //
(byte) 0x9d, (byte) 0x74, (byte) 0xe3, (byte) 0x5b, //
(byte) 0xd8, (byte) 0x41, (byte) 0x56, (byte) 0xc5, //
(byte) 0x63, (byte) 0x56, (byte) 0x88, (byte) 0xc0};
final byte[] plaintext = {(byte) 0x74, (byte) 0x68, (byte) 0x69, (byte) 0x73, //
(byte) 0x20, (byte) 0x69, (byte) 0x73, (byte) 0x20, //
(byte) 0x73, (byte) 0x6f, (byte) 0x6d, (byte) 0x65, //
(byte) 0x20, (byte) 0x70, (byte) 0x6c, (byte) 0x61, //
(byte) 0x69, (byte) 0x6e, (byte) 0x74, (byte) 0x65, //
(byte) 0x78, (byte) 0x74, (byte) 0x20, (byte) 0x74, //
(byte) 0x6f, (byte) 0x20, (byte) 0x65, (byte) 0x6e, //
(byte) 0x63, (byte) 0x72, (byte) 0x79, (byte) 0x70, //
(byte) 0x74, (byte) 0x20, (byte) 0x75, (byte) 0x73, //
(byte) 0x69, (byte) 0x6e, (byte) 0x67, (byte) 0x20, //
(byte) 0x53, (byte) 0x49, (byte) 0x56, (byte) 0x2d, //
(byte) 0x41, (byte) 0x45, (byte) 0x53};
final byte[] result = AesSivCipherUtil.sivEncrypt(aesKey, macKey, plaintext, ad1, ad2, nonce);
final byte[] expected = {(byte) 0x7b, (byte) 0xdb, (byte) 0x6e, (byte) 0x3b, //
(byte) 0x43, (byte) 0x26, (byte) 0x67, (byte) 0xeb, //
(byte) 0x06, (byte) 0xf4, (byte) 0xd1, (byte) 0x4b, //
(byte) 0xff, (byte) 0x2f, (byte) 0xbd, (byte) 0x0f, //
(byte) 0xcb, (byte) 0x90, (byte) 0x0f, (byte) 0x2f, //
(byte) 0xdd, (byte) 0xbe, (byte) 0x40, (byte) 0x43, //
(byte) 0x26, (byte) 0x60, (byte) 0x19, (byte) 0x65, //
(byte) 0xc8, (byte) 0x89, (byte) 0xbf, (byte) 0x17, //
(byte) 0xdb, (byte) 0xa7, (byte) 0x7c, (byte) 0xeb, //
(byte) 0x09, (byte) 0x4f, (byte) 0xa6, (byte) 0x63, //
(byte) 0xb7, (byte) 0xa3, (byte) 0xf7, (byte) 0x48, //
(byte) 0xba, (byte) 0x8a, (byte) 0xf8, (byte) 0x29, //
(byte) 0xea, (byte) 0x64, (byte) 0xad, (byte) 0x54, //
(byte) 0x4a, (byte) 0x27, (byte) 0x2e, (byte) 0x9c, //
(byte) 0x48, (byte) 0x5b, (byte) 0x62, (byte) 0xa3, //
(byte) 0xfd, (byte) 0x5c, (byte) 0x0d};
Assert.assertArrayEquals(expected, result);
}
}

View File

@@ -12,12 +12,13 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.5.2</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>
<dependencies>
<!-- commons -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>

View File

@@ -65,13 +65,9 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
* cleartext path is explicitly absolute.
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
*/
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
/**
* @return <code>true</code> If the integrity of the file can be assured.
*/
boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException;
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
@@ -83,7 +79,7 @@ public interface Cryptor extends SensitiveDataSwipeListener {
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
* @throws DecryptFailedException If decryption failed
*/
Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)

View File

@@ -71,25 +71,20 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
}
@Override
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
}
@Override
public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.authenticateContent(encryptedFile);
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.decryptedContentLength(encryptedFile);
}
@Override
public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptedFile(encryptedFile, countingInputStream);
return cryptor.decryptFile(encryptedFile, countingInputStream);
}
@Override

View File

@@ -0,0 +1,11 @@
package org.cryptomator.crypto.exceptions;
public class MacAuthenticationFailedException extends DecryptFailedException {
private static final long serialVersionUID = -5577052361643658772L;
public MacAuthenticationFailedException(String msg) {
super(msg);
}
}

View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.5.2</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -32,8 +32,9 @@
<commons-collections.version>4.0</commons-collections.version>
<commons-lang3.version>3.3.2</commons-lang3.version>
<commons-codec.version>1.10</commons-codec.version>
<jackson-databind.version>2.4.4</jackson-databind.version>
</properties>
<jackson-databind.version>2.4.4</jackson-databind.version>
<mockito.version>1.10.19</mockito.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -102,6 +103,13 @@
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
</dependency>
<!-- JSON -->
<dependency>
@@ -117,6 +125,13 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -137,6 +152,10 @@
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</dependency>
</dependencies>
<modules>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSMinimumSystemVersion</key>
<string>10.7.4</string>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleAllowMixedLocalizations</key>
<true/>
<key>CFBundleExecutable</key>
<string>DEPLOY_LAUNCHER_NAME</string>
<key>CFBundleIconFile</key>
<string>DEPLOY_ICON_FILE</string>
<key>CFBundleIdentifier</key>
<string>DEPLOY_BUNDLE_IDENTIFIER</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>DEPLOY_BUNDLE_NAME</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>DEPLOY_BUNDLE_SHORT_VERSION</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See http://developer.apple.com/library/mac/#releasenotes/General/SubmittingToMacAppStore/_index.html
for list of AppStore categories -->
<key>LSApplicationCategoryType</key>
<string>DEPLOY_BUNDLE_CATEGORY</string>
<key>CFBundleVersion</key>
<string>100</string>
<key>NSHumanReadableCopyright</key>
<string>DEPLOY_BUNDLE_COPYRIGHT</string>
<key>JVMRuntime</key>
<string>DEPLOY_JAVA_RUNTIME_NAME</string>
<key>JVMMainClassName</key>
<string>DEPLOY_LAUNCHER_CLASS</string>
<key>JVMAppClasspath</key>
<string>DEPLOY_APP_CLASSPATH</string>
<key>JVMMainJarName</key>
<string>DEPLOY_MAIN_JAR_NAME</string>
<key>JVMPreferencesID</key>
<string>DEPLOY_PREFERENCES_ID</string>
<key>JVMOptions</key>
<array>
DEPLOY_JVM_OPTIONS
</array>
<key>JVMUserOptions</key>
<dict>
DEPLOY_JVM_USER_OPTIONS
</dict>
<key>NSHighResolutionCapable</key>
<string>true</string>
<!-- register .cryptomator bundle extension -->
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>LSTypeIsPackage</key>
<true/>
<key>CFBundleTypeIconFile</key>
<string>Cryptomator.icns</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>cryptomator</string>
</array>
<key>CFBundleTypeName</key>
<string>org.cryptomator.folder</string>
<key>LSHandlerRank</key>
<string>Owner</string>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -12,16 +12,15 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.4.0</version>
<version>0.5.2</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
<properties>
<javafx.application.name>Cryptomator</javafx.application.name>
<exec.mainClass>org.cryptomator.ui.MainApplication</exec.mainClass>
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
<controlsfx.version>8.20.8</controlsfx.version>
</properties>
<dependencies>
@@ -50,11 +49,10 @@
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- UI -->
<!-- DI -->
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>${controlsfx.version}</version>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
</dependency>
</dependencies>

View File

@@ -0,0 +1,149 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
* Sebastian Stenzel - refactoring
******************************************************************************/
package org.cryptomator.ui;
import java.io.File;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import javafx.application.Application;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Cryptomator {
public static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
public static final CompletableFuture<Consumer<File>> OPEN_FILE_HANDLER = new CompletableFuture<>();
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static void main(String[] args) {
if (SystemUtils.IS_OS_MAC_OSX) {
/*
* On OSX we're in an awkward position. We need to register a handler in the main thread of this application. However, we can't
* even pass objects to the application, so we're forced to use a static CompletableFuture for the handler, which actually opens
* the file in the application.
*
* Code taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
try {
final Class<?> applicationClass = Class.forName("com.apple.eawt.Application");
final Class<?> openFilesHandlerClass = Class.forName("com.apple.eawt.OpenFilesHandler");
final Method getApplication = applicationClass.getMethod("getApplication");
final Object application = getApplication.invoke(null);
final Method setOpenFileHandler = applicationClass.getMethod("setOpenFileHandler", openFilesHandlerClass);
final ClassLoader openFilesHandlerClassLoader = openFilesHandlerClass.getClassLoader();
final OpenFilesHandlerClassHandler openFilesHandlerHandler = new OpenFilesHandlerClassHandler();
final Object openFilesHandlerObject = Proxy.newProxyInstance(openFilesHandlerClassLoader, new Class<?>[] {openFilesHandlerClass}, openFilesHandlerHandler);
setOpenFileHandler.invoke(application, openFilesHandlerObject);
} catch (ReflectiveOperationException | RuntimeException e) {
// Since we're trying to call OS-specific code, we'll just have
// to hope for the best.
LOG.error("exception adding OSX file open handler", e);
}
}
/*
* Perform certain things on VM termination.
*/
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
/*
* Before starting the application, we check if there is already an instance running on this computer. If so, we send our command
* line arguments to that instance and quit.
*/
final Optional<RemoteInstance> remoteInstance = SingleInstanceManager.getRemoteInstance(MainApplication.APPLICATION_KEY);
if (remoteInstance.isPresent()) {
try (RemoteInstance instance = remoteInstance.get()) {
LOG.info("An instance of Cryptomator is already running at {}.", instance.getRemotePort());
for (int i = 0; i < args.length; i++) {
remoteInstance.get().sendMessage(args[i], 100);
}
} catch (Exception e) {
LOG.error("Error forwarding arguments to remote instance", e);
}
} else {
Application.launch(MainApplication.class, args);
}
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
@Override
public void run() {
LOG.debug("Shutting down");
SHUTDOWN_TASKS.forEach(r -> {
try {
r.run();
} catch (RuntimeException e) {
LOG.error("exception while shutting down", e);
}
});
SHUTDOWN_TASKS.clear();
}
}
private static void handleOpenFileRequest(File file) {
try {
OPEN_FILE_HANDLER.get().accept(file);
} catch (Exception e) {
LOG.error("exception handling file open event for file " + file.getAbsolutePath(), e);
throw new RuntimeException(e);
}
}
/**
* Handler class taken from https://github.com/axet/desktop/blob/master/src/main/java/com/github/axet/desktop/os/mac/AppleHandlers.java
*/
private static class OpenFilesHandlerClassHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("openFiles")) {
final Class<?> openFilesEventClass = Class.forName("com.apple.eawt.AppEvent$OpenFilesEvent");
final Method getFiles = openFilesEventClass.getMethod("getFiles");
Object e = args[0];
try {
@SuppressWarnings("unchecked")
final List<File> ff = (List<File>) getFiles.invoke(e);
for (File f : ff) {
handleOpenFileRequest(f);
}
} catch (RuntimeException ee) {
throw ee;
} catch (Exception ee) {
throw new RuntimeException(ee);
}
}
return null;
}
}
}

View File

@@ -1,238 +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.OutputStream;
import java.net.URL;
import java.nio.file.DirectoryStream;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Optional;
import java.util.ResourceBundle;
import java.util.concurrent.Future;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
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.files.EncryptingFileVisitor;
import org.cryptomator.ui.controls.ClearOnDisableListener;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.util.FXThreads;
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 Directory directory;
private InitializationListener listener;
@FXML
private TextField usernameField;
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button okButton;
@FXML
private ProgressIndicator progressIndicator;
@FXML
private Label messageLabel;
@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 = passwordField.getText().equals(retypePasswordField.getText());
okButton.setDisable(!passwordsAreEqual);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
if (!isDirectoryEmpty() && !shouldEncryptExistingFiles()) {
return;
}
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 {
progressIndicator.setVisible(true);
masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
final Future<?> futureDone = FXThreads.runOnBackgroundThread(this::encryptExistingContents);
FXThreads.runOnMainThreadWhenFinished(futureDone, (result) -> {
progressIndicator.setVisible(false);
progressIndicator.setVisible(false);
directory.getCryptor().swipeSensitiveData();
if (listener != null) {
listener.didInitialize(this);
}
});
} catch (FileAlreadyExistsException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
LOG.error("I/O Exception", ex);
} finally {
usernameField.setText(null);
passwordField.swipe();
retypePasswordField.swipe();
IOUtils.closeQuietly(masterKeyOutputStream);
}
}
private void setControlsDisabled(boolean disable) {
usernameField.setDisable(disable);
passwordField.setDisable(disable);
retypePasswordField.setDisable(disable);
okButton.setDisable(disable);
}
private boolean isDirectoryEmpty() {
try {
final DirectoryStream<Path> dirContents = Files.newDirectoryStream(directory.getPath());
return !dirContents.iterator().hasNext();
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
throw new IllegalStateException(e);
}
}
private boolean shouldEncryptExistingFiles() {
final Alert alert = new Alert(AlertType.CONFIRMATION);
alert.setTitle(localization.getString("initialize.alert.directoryIsNotEmpty.title"));
alert.setHeaderText(null);
alert.setContentText(localization.getString("initialize.alert.directoryIsNotEmpty.content"));
final Optional<ButtonType> result = alert.showAndWait();
return ButtonType.OK.equals(result.get());
}
private void encryptExistingContents() {
try {
final FileVisitor<Path> visitor = new EncryptingFileVisitor(directory.getPath(), directory.getCryptor(), this::shouldEncryptExistingFile);
Files.walkFileTree(directory.getPath(), visitor);
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
}
}
private boolean shouldEncryptExistingFile(Path path) {
final String name = path.getFileName().toString();
return !directory.getPath().equals(path) && !name.endsWith(Aes256Cryptor.BASIC_FILE_EXT) && !name.endsWith(Aes256Cryptor.METADATA_FILE_EXT) && !name.endsWith(Aes256Cryptor.MASTERKEY_FILE_EXT);
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
}
public InitializationListener getListener() {
return listener;
}
public void setListener(InitializationListener listener) {
this.listener = listener;
}
/* callback */
interface InitializationListener {
void didInitialize(InitializeController ctrl);
}
}

View File

@@ -9,8 +9,11 @@
package org.cryptomator.ui;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import javafx.application.Application;
import javafx.application.Platform;
@@ -20,26 +23,74 @@ import javafx.scene.Scene;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.MainModule.ControllerFactory;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.SingleInstanceManager;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.cryptomator.ui.util.TrayIconUtil;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
public class MainApplication extends Application {
private static final Set<Runnable> SHUTDOWN_TASKS = new ConcurrentHashSet<>();
private static final CleanShutdownPerformer CLEAN_SHUTDOWN_PERFORMER = new CleanShutdownPerformer();
public static final String APPLICATION_KEY = "CryptomatorGUI";
public static void main(String[] args) {
Application.launch(args);
Runtime.getRuntime().addShutdownHook(CLEAN_SHUTDOWN_PERFORMER);
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private final CleanShutdownPerformer cleanShutdownPerformer = new CleanShutdownPerformer();
private final ExecutorService executorService;
private final ControllerFactory controllerFactory;
private final DeferredCloser closer;
public MainApplication() {
this(getInjector());
}
private static Injector getInjector() {
try {
return Guice.createInjector(new MainModule());
} catch (Exception e) {
throw e;
}
}
public MainApplication(Injector injector) {
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class));
}
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer) {
super();
this.executorService = executorService;
this.controllerFactory = controllerFactory;
this.closer = closer;
}
@Override
public void start(final Stage primaryStage) throws IOException {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
FXMLLoader.setDefaultClassLoader(contextClassLoader);
Platform.runLater(() -> {
/*
* This fixes a bug on OSX where the magic file open handler leads to no context class loader being set in the AppKit (event)
* thread if the application is not started opening a file.
*/
if (Thread.currentThread().getContextClassLoader() == null) {
Thread.currentThread().setContextClassLoader(contextClassLoader);
}
});
Runtime.getRuntime().addShutdownHook(cleanShutdownPerformer);
chooseNativeStylesheet();
final ResourceBundle rb = ResourceBundle.getBundle("localization");
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
final MainController ctrl = loader.getController();
ctrl.setStage(primaryStage);
@@ -53,6 +104,44 @@ public class MainApplication extends Application {
TrayIconUtil.init(primaryStage, rb, () -> {
quit();
});
for (String arg : getParameters().getUnnamed()) {
handleCommandLineArg(ctrl, arg);
}
if (SystemUtils.IS_OS_MAC_OSX) {
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
}
LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService), LocalInstance::close).get().get();
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
}
void handleCommandLineArg(final MainController ctrl, String arg) {
// only open files with our file extension:
if (!arg.endsWith(Vault.VAULT_FILE_EXTENSION)) {
LOG.warn("Invalid vault path %s", arg);
return;
}
// find correct location:
final Path path = FileSystems.getDefault().getPath(arg);
final Path vaultPath;
if (Files.isDirectory(path)) {
vaultPath = path;
} else if (Files.isRegularFile(path) && path.getParent().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultPath = path.getParent();
} else {
LOG.warn("Invalid vault path %s", arg);
return;
}
// add vault to ctrl:
Platform.runLater(() -> {
ctrl.addVault(vaultPath, true);
ctrl.toFront();
});
}
private void chooseNativeStylesheet() {
@@ -67,8 +156,7 @@ public class MainApplication extends Application {
private void quit() {
Platform.runLater(() -> {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
stop();
Platform.exit();
System.exit(0);
});
@@ -76,25 +164,18 @@ public class MainApplication extends Application {
@Override
public void stop() {
CLEAN_SHUTDOWN_PERFORMER.run();
Settings.save();
closer.close();
try {
Runtime.getRuntime().removeShutdownHook(cleanShutdownPerformer);
} catch (Exception e) {
}
}
public static void addShutdownTask(Runnable r) {
SHUTDOWN_TASKS.add(r);
}
public static void removeShutdownTask(Runnable r) {
SHUTDOWN_TASKS.remove(r);
}
private static class CleanShutdownPerformer extends Thread {
private class CleanShutdownPerformer extends Thread {
@Override
public void run() {
SHUTDOWN_TASKS.forEach(r -> {
r.run();
});
SHUTDOWN_TASKS.clear();
closer.close();
}
}

View File

@@ -1,202 +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.File;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.ContextMenu;
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.Stage;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private Stage stage;
@FXML
private ContextMenu directoryContextMenu;
@FXML
private HBox rootPane;
@FXML
private ListView<Directory> directoryList;
@FXML
private Pane contentPane;
private ResourceBundle rb;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
final ObservableList<Directory> items = FXCollections.observableList(Settings.load().getDirectories());
directoryList.setItems(items);
directoryList.setCellFactory(this::createDirecoryListCell);
directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
}
@FXML
private void didClickAddDirectory(ActionEvent event) {
final DirectoryChooser dirChooser = new DirectoryChooser();
final File file = dirChooser.showDialog(stage);
if (file != null && file.canWrite()) {
final Directory dir = new Directory(file.toPath());
if (!directoryList.getItems().contains(dir)) {
directoryList.getItems().add(dir);
}
directoryList.getSelectionModel().select(dir);
}
}
private ListCell<Directory> createDirecoryListCell(ListView<Directory> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(directoryContextMenu);
return cell;
}
private void selectedDirectoryDidChange(ListChangeListener.Change<? extends Directory> change) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
if (selectedDir == null) {
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else {
stage.setTitle(selectedDir.getName());
showDirectory(selectedDir);
}
}
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
final Directory selectedDir = directoryList.getSelectionModel().getSelectedItem();
directoryList.getItems().remove(selectedDir);
directoryList.getSelectionModel().clearSelection();
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showDirectory(Directory directory) {
try {
if (directory.isUnlocked()) {
this.showUnlockedView(directory);
} else if (directory.containsMasterKey()) {
this.showUnlockView(directory);
} else {
this.showInitializeView(directory);
}
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
}
}
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 showWelcomeView() {
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Directory directory) {
final InitializeController ctrl = showView("/fxml/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("/fxml/unlock.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
showUnlockedView(ctrl.getDirectory());
Platform.setImplicitExit(false);
}
private void showUnlockedView(Directory directory) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setDirectory(directory);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
showUnlockView(ctrl.getDirectory());
if (getUnlockedDirectories().isEmpty()) {
Platform.setImplicitExit(true);
}
}
/* Convenience */
public Collection<Directory> getDirectories() {
return directoryList.getItems();
}
public Collection<Directory> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
}

View File

@@ -0,0 +1,88 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.util.Callback;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.model.VaultObjectMapperProvider;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.settings.SettingsProvider;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.DeferredCloser.Closer;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
import org.cryptomator.webdav.WebDavServer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.AbstractModule;
import com.google.inject.Injector;
import com.google.inject.Provider;
import com.google.inject.Provides;
import com.google.inject.name.Names;
public class MainModule extends AbstractModule {
private final DeferredCloser deferredCloser = new DeferredCloser();
public static interface ControllerFactory extends Callback<Class<?>, Object> {
}
@Override
protected void configure() {
bind(DeferredCloser.class).toInstance(deferredCloser);
bind(ObjectMapper.class).annotatedWith(Names.named("VaultJsonMapper")).toProvider(VaultObjectMapperProvider.class);
bind(Settings.class).toProvider(SettingsProvider.class);
bind(WebDavMounter.class).toProvider(WebDavMounterProvider.class).asEagerSingleton();
}
@Provides
@Singleton
ControllerFactory getControllerFactory(Injector injector) {
return cls -> injector.getInstance(cls);
}
@Provides
@Singleton
ExecutorService getExec() {
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
}
@Provides
Cryptor getCryptor() {
return SamplingDecorator.decorate(new Aes256Cryptor());
}
@Provides
@Singleton
VaultFactory getVaultFactory(WebDavServer server, Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
return new VaultFactory(server, cryptorProvider, mounter, closer);
}
@Provides
@Singleton
WebDavServer getServer() {
final WebDavServer webDavServer = new WebDavServer();
webDavServer.start();
return closeLater(webDavServer, WebDavServer::stop);
}
<T> T closeLater(T object, Closer<T> closer) {
return deferredCloser.closeLater(object, closer).get().get();
}
}

View File

@@ -0,0 +1,175 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
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.Button;
import javafx.scene.control.Label;
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.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class ChangePasswordController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private ResourceBundle rb;
private ChangePasswordListener listener;
private Vault vault;
@FXML
private SecPasswordField oldPasswordField;
@FXML
private SecPasswordField newPasswordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button changePasswordButton;
@FXML
private Label messageLabel;
@Inject
public ChangePasswordController() {
super();
}
@Override
public void initialize(URL location, ResourceBundle rb) {
this.rb = rb;
oldPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
newPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
}
// ****************************************
// Password fields
// ****************************************
private void passwordFieldsDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean oldPasswordIsEmpty = oldPasswordField.getText().isEmpty();
boolean newPasswordIsEmpty = newPasswordField.getText().isEmpty();
boolean passwordsAreEqual = newPasswordField.getText().equals(retypePasswordField.getText());
changePasswordButton.setDisable(oldPasswordIsEmpty || newPasswordIsEmpty || !passwordsAreEqual);
}
// ****************************************
// Change password button
// ****************************************
@FXML
private void didClickChangePasswordButton(ActionEvent event) {
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
// decrypt with old password:
final CharSequence oldPassword = oldPasswordField.getCharacters();
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
} catch (DecryptFailedException | IOException ex) {
messageLabel.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} catch (WrongPasswordException e) {
messageLabel.setText(rb.getString("changePassword.errorMessage.wrongPassword"));
newPasswordField.swipe();
retypePasswordField.swipe();
Platform.runLater(oldPasswordField::requestFocus);
return;
} catch (UnsupportedKeyLengthException ex) {
messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} finally {
oldPasswordField.swipe();
}
// when we reach this line, decryption was successful.
// encrypt with new password:
final CharSequence newPassword = newPasswordField.getCharacters();
try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
messageLabel.setText(rb.getString("changePassword.infoMessage.success"));
Platform.runLater(this::didChangePassword);
// At this point the backup is still using the old password.
// It will be changed as soon as the user unlocks the vault the next time.
// This way he can still restore the old password, if he doesn't remember the new one.
} catch (IOException ex) {
LOG.error("Re-encryption failed for technical reasons. Restoring Backup.", ex);
this.restoreBackupQuietly();
} finally {
newPasswordField.swipe();
retypePasswordField.swipe();
}
}
private void restoreBackupQuietly() {
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
try {
Files.copy(masterKeyBackupPath, masterKeyPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException ex) {
LOG.error("Restoring Backup failed.", ex);
}
}
private void didChangePassword() {
if (listener != null) {
listener.didChangePassword(this);
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public void setVault(Vault vault) {
this.vault = vault;
}
public ChangePasswordListener getListener() {
return listener;
}
public void setListener(ChangePasswordListener listener) {
this.listener = listener;
}
/* callback */
interface ChangePasswordListener {
void didChangePassword(ChangePasswordController ctrl);
}
}

View File

@@ -0,0 +1,127 @@
/*******************************************************************************
* 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.controllers;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.nio.file.FileAlreadyExistsException;
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.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InitializeController implements Initializable {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private ResourceBundle localization;
private Vault vault;
private InitializationListener listener;
@FXML
private SecPasswordField passwordField;
@FXML
private SecPasswordField retypePasswordField;
@FXML
private Button okButton;
@FXML
private Label messageLabel;
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
}
// ****************************************
// Password fields
// ****************************************
private void passwordFieldsDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordIsEmpty = passwordField.getText().isEmpty();
boolean passwordsAreEqual = passwordField.getText().equals(retypePasswordField.getText());
okButton.setDisable(passwordIsEmpty || !passwordsAreEqual);
}
// ****************************************
// OK button
// ****************************************
@FXML
protected void initializeVault(ActionEvent event) {
setControlsDisabled(true);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final CharSequence password = passwordField.getCharacters();
try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
if (listener != null) {
listener.didInitialize(this);
}
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {
setControlsDisabled(false);
passwordField.swipe();
retypePasswordField.swipe();
}
}
private void setControlsDisabled(boolean disable) {
passwordField.setDisable(disable);
retypePasswordField.setDisable(disable);
okButton.setDisable(disable);
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public void setVault(Vault vault) {
this.vault = vault;
}
public InitializationListener getListener() {
return listener;
}
public void setListener(InitializationListener listener) {
this.listener = listener;
}
/* callback */
interface InitializationListener {
void didInitialize(InitializeController ctrl);
}
}

View File

@@ -0,0 +1,331 @@
/*******************************************************************************
* 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.controllers;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Side;
import javafx.scene.Parent;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleButton;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import org.cryptomator.ui.MainModule.ControllerFactory;
import org.cryptomator.ui.controllers.ChangePasswordController.ChangePasswordListener;
import org.cryptomator.ui.controllers.InitializeController.InitializationListener;
import org.cryptomator.ui.controllers.UnlockController.UnlockListener;
import org.cryptomator.ui.controllers.UnlockedController.LockListener;
import org.cryptomator.ui.controls.DirectoryListCell;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.model.VaultFactory;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener, ChangePasswordListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
private Stage stage;
@FXML
private ContextMenu vaultListCellContextMenu;
@FXML
private ContextMenu addVaultContextMenu;
@FXML
private HBox rootPane;
@FXML
private ListView<Vault> vaultList;
@FXML
private ToggleButton addVaultButton;
@FXML
private Pane contentPane;
private final ControllerFactory controllerFactory;
private final Settings settings;
private final VaultFactory vaultFactoy;
private ResourceBundle rb;
@Inject
public MainController(ControllerFactory controllerFactory, Settings settings, VaultFactory vaultFactoy) {
super();
this.controllerFactory = controllerFactory;
this.settings = settings;
this.vaultFactoy = vaultFactoy;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
final ObservableList<Vault> items = FXCollections.observableList(settings.getDirectories());
vaultList.setItems(items);
vaultList.setCellFactory(this::createDirecoryListCell);
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
}
@FXML
private void didClickAddVault(ActionEvent event) {
if (addVaultContextMenu.isShowing()) {
addVaultContextMenu.hide();
} else {
addVaultContextMenu.show(addVaultButton, Side.RIGHT, 0.0, 0.0);
}
}
@FXML
private void willShowAddVaultContextMenu(WindowEvent event) {
addVaultButton.setSelected(true);
}
@FXML
private void didHideAddVaultContextMenu(WindowEvent event) {
addVaultButton.setSelected(false);
}
@FXML
private void didClickCreateNewVault(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
final File file = fileChooser.showSaveDialog(stage);
try {
if (file != null) {
final Path vaultDir;
// enforce .cryptomator file extension:
if (!file.getName().endsWith(Vault.VAULT_FILE_EXTENSION)) {
final Path correctedPath = file.toPath().resolveSibling(file.getName() + Vault.VAULT_FILE_EXTENSION);
vaultDir = Files.createDirectory(correctedPath);
} else {
vaultDir = Files.createDirectory(file.toPath());
}
addVault(vaultDir, true);
}
} catch (IOException e) {
LOG.error("Unable to create vault", e);
}
}
@FXML
private void didClickAddExistingVaults(ActionEvent event) {
final FileChooser fileChooser = new FileChooser();
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
final List<File> files = fileChooser.showOpenMultipleDialog(stage);
if (files != null) {
for (final File file : files) {
addVault(file.toPath(), false);
}
}
}
/**
* adds the given directory or selects it if it is already in the list of directories.
*
* @param path non-null, writable, existing directory
*/
public void addVault(final Path path, boolean select) {
if (path == null || !Files.isWritable(path)) {
return;
}
final Path vaultPath;
if (path != null && Files.isDirectory(path)) {
vaultPath = path;
} else if (path != null && Files.isRegularFile(path) && path.getParent().getFileName().toString().endsWith(Vault.VAULT_FILE_EXTENSION)) {
vaultPath = path.getParent();
} else {
return;
}
final Vault vault = vaultFactoy.createVault(vaultPath);
if (!vaultList.getItems().contains(vault)) {
vaultList.getItems().add(vault);
}
vaultList.getSelectionModel().select(vault);
}
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
final DirectoryListCell cell = new DirectoryListCell();
cell.setContextMenu(vaultListCellContextMenu);
return cell;
}
private void selectedVaultDidChange(ListChangeListener.Change<? extends Vault> change) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
if (selectedVault == null) {
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else if (!Files.isDirectory(selectedVault.getPath())) {
Platform.runLater(() -> {
vaultList.getItems().remove(selectedVault);
vaultList.getSelectionModel().clearSelection();
});
stage.setTitle(rb.getString("app.name"));
showWelcomeView();
} else {
stage.setTitle(selectedVault.getName());
showVault(selectedVault);
}
}
@FXML
private void didClickRemoveSelectedEntry(ActionEvent e) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
vaultList.getItems().remove(selectedVault);
vaultList.getSelectionModel().clearSelection();
}
@FXML
private void didClickChangePassword(ActionEvent e) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
showChangePasswordView(selectedVault);
}
// ****************************************
// Subcontroller for right panel
// ****************************************
private void showVault(Vault vault) {
try {
if (vault.isUnlocked()) {
this.showUnlockedView(vault);
} else if (vault.containsMasterKey()) {
this.showUnlockView(vault);
} else {
this.showInitializeView(vault);
}
} catch (IOException e) {
LOG.error("Failed to analyze directory.", e);
}
}
private <T> T showView(String fxml) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
loader.setControllerFactory(controllerFactory);
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 showWelcomeView() {
this.showView("/fxml/welcome.fxml");
}
private void showInitializeView(Vault vault) {
final InitializeController ctrl = showView("/fxml/initialize.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didInitialize(InitializeController ctrl) {
showUnlockView(ctrl.getVault());
}
private void showUnlockView(Vault vault) {
final UnlockController ctrl = showView("/fxml/unlock.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
showUnlockedView(ctrl.getVault());
Platform.setImplicitExit(false);
}
private void showUnlockedView(Vault vault) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
showUnlockView(ctrl.getVault());
if (getUnlockedDirectories().isEmpty()) {
Platform.setImplicitExit(true);
}
}
private void showChangePasswordView(Vault vault) {
final ChangePasswordController ctrl = showView("/fxml/change_password.fxml");
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didChangePassword(ChangePasswordController ctrl) {
showUnlockView(ctrl.getVault());
}
/* Convenience */
public Collection<Vault> getDirectories() {
return vaultList.getItems();
}
public Collection<Vault> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */
public Stage getStage() {
return stage;
}
public void setStage(Stage stage) {
this.stage = stage;
}
/**
* Attempts to make the application window visible.
*/
public void toFront() {
stage.setIconified(false);
stage.show();
stage.toFront();
}
}

View File

@@ -6,16 +6,17 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
package org.cryptomator.ui.controllers;
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.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javafx.application.Platform;
@@ -24,40 +25,36 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.apache.commons.lang3.CharUtils;
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.model.Vault;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
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;
private Vault vault;
@FXML
private SecPasswordField passwordField;
@FXML
private CheckBox checkIntegrity;
private TextField mountName;
@FXML
private Button unlockButton;
@@ -68,22 +65,30 @@ public class UnlockController implements Initializable {
@FXML
private Label messageLabel;
private final ExecutorService exec;
@Inject
public UnlockController(ExecutorService exec) {
super();
this.exec = exec;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
usernameBox.valueProperty().addListener(this::didChooseUsername);
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
}
// ****************************************
// Username box
// Password field
// ****************************************
public void didChooseUsername(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (newValue != null) {
Platform.runLater(passwordField::requestFocus);
}
passwordField.setDisable(StringUtils.isEmpty(newValue));
private void passwordFieldsDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
boolean passwordIsEmpty = passwordField.getText().isEmpty();
unlockButton.setDisable(passwordIsEmpty);
}
// ****************************************
@@ -93,24 +98,23 @@ public class UnlockController implements Initializable {
@FXML
private void didClickUnlockButton(ActionEvent event) {
setControlsDisabled(true);
final String masterKeyFileName = usernameBox.getValue() + Aes256Cryptor.MASTERKEY_FILE_EXT;
final Path masterKeyPath = directory.getPath().resolve(masterKeyFileName);
progressIndicator.setVisible(true);
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
final CharSequence password = passwordField.getCharacters();
InputStream masterKeyInputStream = null;
try {
progressIndicator.setVisible(true);
masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
directory.setVerifyFileIntegrity(checkIntegrity.isSelected());
directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!directory.startServer()) {
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!vault.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
directory.getCryptor().swipeSensitiveData();
vault.getCryptor().swipeSensitiveData();
return;
}
directory.setUnlocked(true);
final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
// at this point we know for sure, that the masterkey can be decrypted, so lets make a backup:
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
vault.setUnlocked(true);
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::didUnlockAndMount);
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, (result) -> {
setControlsDisabled(false);
});
} catch (DecryptFailedException | IOException ex) {
@@ -130,36 +134,15 @@ public class UnlockController implements Initializable {
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} finally {
passwordField.swipe();
IOUtils.closeQuietly(masterKeyInputStream);
}
}
private void setControlsDisabled(boolean disable) {
usernameBox.setDisable(disable);
passwordField.setDisable(disable);
checkIntegrity.setDisable(disable);
mountName.setDisable(disable);
unlockButton.setDisable(disable);
}
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);
}
}
private void didUnlockAndMount(boolean mountSuccess) {
progressIndicator.setVisible(false);
if (listener != null) {
@@ -167,16 +150,32 @@ public class UnlockController implements Initializable {
}
}
/* Getter/Setter */
public Directory getDirectory() {
return directory;
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 setDirectory(Directory directory) {
this.directory = directory;
this.findExistingUsernames();
this.checkIntegrity.setSelected(directory.shouldVerifyFileIntegrity());
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
// newValue is guaranteed to be a-z0-9, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.getMountName());
}
}
/* Getter/Setter */
public Vault getVault() {
return vault;
}
public void setVault(Vault vault) {
this.vault = vault;
this.mountName.setText(vault.getMountName());
}
public UnlockListener getListener() {

View File

@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.ui;
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.ResourceBundle;
@@ -26,15 +26,16 @@ import javafx.scene.control.Label;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
import com.google.inject.Inject;
public class UnlockedController implements Initializable {
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
private ResourceBundle rb;
private LockListener listener;
private Directory directory;
private Vault vault;
private Timeline ioAnimation;
@FXML
@@ -46,16 +47,20 @@ public class UnlockedController implements Initializable {
@FXML
private NumberAxis xAxis;
@Inject
public UnlockedController() {
super();
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
}
@FXML
private void didClickCloseVault(ActionEvent event) {
directory.unmount();
directory.stopServer();
directory.setUnlocked(false);
vault.unmount();
vault.stopServer();
vault.setUnlocked(false);
if (listener != null) {
listener.didLock(this);
}
@@ -117,14 +122,12 @@ public class UnlockedController implements Initializable {
/* Getter/Setter */
public Directory getDirectory() {
return directory;
public Vault getVault() {
return vault;
}
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 void setVault(Vault directory) {
this.vault = directory;
if (directory.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) directory.getCryptor());

View File

@@ -1,30 +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.controls;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextInputControl;
public class ClearOnDisableListener implements ChangeListener<Boolean> {
final TextInputControl control;
public ClearOnDisableListener(TextInputControl control) {
this.control = control;
}
@Override
public void changed(ObservableValue<? extends Boolean> property, Boolean wasDisabled, Boolean isDisabled) {
if (isDisabled) {
control.clear();
}
}
}

View File

@@ -8,9 +8,9 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import org.cryptomator.ui.model.Directory;
import org.cryptomator.ui.model.Vault;
public class DirectoryListCell extends DraggableListCell<Directory> implements ChangeListener<Boolean> {
public class DirectoryListCell extends DraggableListCell<Vault> implements ChangeListener<Boolean> {
// fill: #FD4943, stroke: #E1443F
private static final Color RED_FILL = Color.rgb(253, 73, 67);
@@ -29,8 +29,8 @@ public class DirectoryListCell extends DraggableListCell<Directory> implements C
}
@Override
protected void updateItem(Directory item, boolean empty) {
final Directory oldItem = super.getItem();
protected void updateItem(Vault item, boolean empty) {
final Vault oldItem = super.getItem();
if (oldItem != null) {
oldItem.unlockedProperty().removeListener(this);
}

View File

@@ -1,162 +0,0 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication;
import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
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 Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final Path path;
private boolean verifyFileIntegrity;
private WebDavMount webDavMount;
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(), verifyFileIntegrity, 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 {
webDavMount = WebDavMounter.mount(server.getPort());
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public boolean unmount() {
try {
if (webDavMount != null) {
webDavMount.unmount();
webDavMount = null;
}
return true;
} catch (CommandFailedException e) {
LOG.warn("unmount failed", e);
return false;
}
}
/* Getter/Setter */
public Path getPath() {
return path;
}
public boolean shouldVerifyFileIntegrity() {
return verifyFileIntegrity;
}
public void setVerifyFileIntegrity(boolean verifyFileIntegrity) {
this.verifyFileIntegrity = verifyFileIntegrity;
}
/**
* @return Directory name without preceeding path components
*/
public String getName() {
return path.getFileName().toString();
}
public Cryptor getCryptor() {
return cryptor;
}
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return unlocked.get();
}
public void setUnlocked(boolean unlocked) {
this.unlocked.set(unlocked);
}
public WebDavServer getServer() {
return server;
}
/* hashcode/equals */
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Directory) {
final Directory other = (Directory) obj;
return this.path.equals(other.path);
} else {
return false;
}
}
/* graceful shutdown */
private class ShutdownTask implements Runnable {
@Override
public void run() {
stopServer();
}
}
}

View File

@@ -1,26 +0,0 @@
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);
final Directory dir = new Directory(path);
final boolean verifyFileIntegrity = node.has("checkIntegrity") ? node.get("checkIntegrity").asBoolean() : false;
dir.setVerifyFileIntegrity(verifyFileIntegrity);
return dir;
}
}

View File

@@ -1,20 +0,0 @@
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.writeBooleanField("checkIntegrity", value.shouldVerifyFileIntegrity());
jgen.writeEndObject();
}
}

View File

@@ -0,0 +1,199 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Optional;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredClosable;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Vault implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Vault.class);
public static final String VAULT_FILE_EXTENSION = ".cryptomator";
public static final String VAULT_MASTERKEY_FILE = "masterkey.cryptomator";
public static final String VAULT_MASTERKEY_BACKUP_FILE = "masterkey.cryptomator.bkup";
private final Path path;
private final WebDavServer server;
private final Cryptor cryptor;
private final WebDavMounter mounter;
private final DeferredCloser closer;
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private String mountName;
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
/**
* Package private constructor, use {@link VaultFactory}.
*/
Vault(final Path vaultDirectoryPath, final WebDavServer server, final Cryptor cryptor, final WebDavMounter mounter, final DeferredCloser closer) {
this.path = vaultDirectoryPath;
this.server = server;
this.cryptor = cryptor;
this.mounter = mounter;
this.closer = closer;
try {
setMountName(getName());
} catch (IllegalArgumentException e) {
// mount name needs to be set by the user explicitly later
}
}
public boolean isValidVaultDirectory() {
return Files.isDirectory(path) && path.getFileName().toString().endsWith(VAULT_FILE_EXTENSION);
}
public boolean containsMasterKey() throws IOException {
final Path masterKeyPath = path.resolve(VAULT_MASTERKEY_FILE);
return Files.isRegularFile(masterKeyPath);
}
public synchronized boolean startServer() {
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (o.isPresent() && o.get().isRunning()) {
return false;
}
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, getMountName());
if (servlet.start()) {
webDavServlet = closer.closeLater(servlet, ServletLifeCycleAdapter::stop);
return true;
}
return false;
}
public void stopServer() {
unmount();
webDavServlet.close();
cryptor.swipeSensitiveData();
}
public boolean mount() {
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (!o.isPresent() || !o.get().isRunning()) {
return false;
}
try {
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), getMountName()), WebDavMount::unmount);
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
return false;
}
}
public void unmount() {
webDavMount.close();
}
/* Getter/Setter */
public Path getPath() {
return path;
}
/**
* @return Directory name without preceeding path components and file extension
*/
public String getName() {
return StringUtils.removeEnd(path.getFileName().toString(), VAULT_FILE_EXTENSION);
}
public Cryptor getCryptor() {
return cryptor;
}
public ObjectProperty<Boolean> unlockedProperty() {
return unlocked;
}
public boolean isUnlocked() {
return unlocked.get();
}
public void setUnlocked(boolean unlocked) {
this.unlocked.set(unlocked);
}
public String getMountName() {
return mountName;
}
/**
* Tries to form a similar string using the regular latin alphabet.
*
* @param string
* @return a string composed of a-z, A-Z, 0-9, and _.
*/
public static String normalize(String string) {
String normalized = Normalizer.normalize(string, Form.NFD);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < normalized.length(); i++) {
char c = normalized.charAt(i);
if (Character.isWhitespace(c)) {
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
builder.append('_');
}
} else if (c < 127 && Character.isLetterOrDigit(c)) {
builder.append(c);
} else if (c < 127) {
if (builder.length() == 0 || builder.charAt(builder.length() - 1) != '_') {
builder.append('_');
}
}
}
return builder.toString();
}
/**
* sets the mount name while normalizing it
*
* @param mountName
* @throws IllegalArgumentException if the name is empty after normalization
*/
public void setMountName(String mountName) throws IllegalArgumentException {
mountName = normalize(mountName);
if (StringUtils.isEmpty(mountName)) {
throw new IllegalArgumentException("mount name is empty");
}
this.mountName = mountName;
}
/* hashcode/equals */
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Vault) {
final Vault other = (Vault) obj;
return this.path.equals(other.path);
} else {
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
package org.cryptomator.ui.model;
import java.nio.file.Path;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredCloser;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDavServer;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class VaultFactory {
private final WebDavServer server;
private final Provider<Cryptor> cryptorProvider;
private final WebDavMounter mounter;
private final DeferredCloser closer;
@Inject
public VaultFactory(WebDavServer server, Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
this.server = server;
this.cryptorProvider = cryptorProvider;
this.mounter = mounter;
this.closer = closer;
}
public Vault createVault(Path path) {
return new Vault(path, server, cryptorProvider.get(), mounter, closer);
}
}

View File

@@ -0,0 +1,68 @@
package org.cryptomator.ui.model;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import javax.inject.Inject;
import com.fasterxml.jackson.core.JsonGenerator;
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;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.inject.Provider;
public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
private final VaultFactory vaultFactoy;
@Inject
public VaultObjectMapperProvider(final VaultFactory vaultFactoy) {
this.vaultFactoy = vaultFactoy;
}
@Override
public ObjectMapper get() {
final ObjectMapper om = new ObjectMapper();
final SimpleModule module = new SimpleModule("VaultJsonMapper");
module.addSerializer(Vault.class, new VaultSerializer());
module.addDeserializer(Vault.class, new VaultDeserializer());
om.registerModule(module);
return om;
}
private class VaultSerializer extends JsonSerializer<Vault> {
@Override
public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("path", value.getPath().toString());
jgen.writeStringField("mountName", value.getMountName().toString());
jgen.writeEndObject();
}
}
private class VaultDeserializer extends JsonDeserializer<Vault> {
@Override
public Vault 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);
final Vault vault = vaultFactoy.createVault(path);
if (node.has("mountName")) {
vault.setMountName(node.get("mountName").asText());
}
return vault;
}
}
}

View File

@@ -8,101 +8,38 @@
******************************************************************************/
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.file.FileSystem;
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.List;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.model.Directory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.cryptomator.ui.model.Vault;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.ObjectMapper;
@JsonPropertyOrder(value = {"directories"})
public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
private static final ObjectMapper JSON_OM = new ObjectMapper();
private static Settings INSTANCE = null;
static {
final String appdata = System.getenv("APPDATA");
final FileSystem fs = FileSystems.getDefault();
private List<Vault> directories;
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
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/Cryptomator");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
}
}
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
Settings() {
private List<Directory> directories;
private Settings() {
// private constructor
}
public static synchronized Settings load() {
if (INSTANCE == null) {
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
INSTANCE = JSON_OM.readValue(in, Settings.class);
return INSTANCE;
} catch (IOException e) {
LOG.warn("Failed to load settings, creating new one.");
INSTANCE = Settings.defaultSettings();
}
}
return INSTANCE;
}
public static synchronized void save() {
if (INSTANCE != null) {
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
JSON_OM.writeValue(out, INSTANCE);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}
}
}
private static Settings defaultSettings() {
return new Settings();
}
/* Getter/Setter */
public List<Directory> getDirectories() {
public List<Vault> getDirectories() {
if (directories == null) {
directories = new ArrayList<>();
}
return directories;
}
public void setDirectories(List<Directory> directories) {
public void setDirectories(List<Vault> directories) {
this.directories = directories;
}

View File

@@ -0,0 +1,85 @@
package org.cryptomator.ui.settings;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.DeferredCloser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Provider;
public class SettingsProvider implements Provider<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
private static final Path SETTINGS_DIR;
private static final String SETTINGS_FILE = "settings.json";
static {
final String appdata = System.getenv("APPDATA");
final FileSystem fs = FileSystems.getDefault();
if (SystemUtils.IS_OS_WINDOWS && appdata != null) {
SETTINGS_DIR = fs.getPath(appdata, "Cryptomator");
} else if (SystemUtils.IS_OS_WINDOWS && appdata == null) {
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/Cryptomator");
} else {
// (os.contains("solaris") || os.contains("sunos") || os.contains("linux") || os.contains("unix"))
SETTINGS_DIR = fs.getPath(SystemUtils.USER_HOME, ".Cryptomator");
}
}
private final DeferredCloser deferredCloser;
private final ObjectMapper objectMapper;
@Inject
public SettingsProvider(DeferredCloser deferredCloser, @Named("VaultJsonMapper") ObjectMapper objectMapper) {
this.deferredCloser = deferredCloser;
this.objectMapper = objectMapper;
}
@Override
public Settings get() {
Settings settings = null;
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
settings = objectMapper.readValue(in, Settings.class);
settings.getDirectories().removeIf(v -> !v.isValidVaultDirectory());
} catch (IOException e) {
LOG.warn("Failed to load settings, creating new one.");
settings = new Settings();
}
deferredCloser.closeLater(settings, this::save);
return settings;
}
private void save(Settings settings) {
if (settings == null) {
return;
}
try {
Files.createDirectories(SETTINGS_DIR);
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
objectMapper.writeValue(out, settings);
} catch (IOException e) {
LOG.error("Failed to save settings.", e);
}
}
}

View File

@@ -0,0 +1,43 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Optional;
/**
* Wrapper around an object, which should be closed later - explicitly or by a
* {@link DeferredCloser}. The wrapped object can be accessed as long as the
* resource has not been closed.
*
* @author Tillmann Gaida
*
* @param <T>
* any type
*/
public interface DeferredClosable<T> extends AutoCloseable {
/**
* Returns the wrapped Object.
*
* @return empty if the object has been closed.
*/
public Optional<T> get();
/**
* Quietly closes the Object. If the object was closed before, nothing
* happens.
*/
public void close();
/**
* @return an empty object.
*/
public static <T> DeferredClosable<T> empty() {
return DeferredCloser.empty();
}
}

View File

@@ -0,0 +1,123 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.cryptomator.ui.controllers.MainController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* Tries to bring open-close symmetry in contexts where the resource outlives
* the current scope by introducing a manager, which closes the resources if
* they haven't been closed before.
* </p>
*
* <p>
* If you have a {@link DeferredCloser} instance present, call
* {@link #closeLater(Object, Closer)} immediately after you have opened the
* resource and return a resource handle. If {@link #close()} is called, the
* resource will be closed. Calling {@link DeferredClosable#close()} on the resource
* handle will also close the resource and prevent a second closing by
* {@link #close()}.
* </p>
*
* @author Tillmann Gaida
*/
public class DeferredCloser implements AutoCloseable {
public static interface Closer<T> {
void close(T object) throws Exception;
}
static class EmptyResource<T> implements DeferredClosable<T> {
@Override
public Optional<T> get() {
return Optional.empty();
}
@Override
public void close() {
}
}
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
final Map<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
final AtomicLong counter = new AtomicLong();
public class ManagedResource<T> implements DeferredClosable<T> {
private final long number = counter.incrementAndGet();
private final AtomicReference<T> object = new AtomicReference<>();
private final Closer<T> closer;
ManagedResource(T object, Closer<T> closer) {
super();
this.object.set(object);
this.closer = closer;
}
public void close() {
final T oldObject = object.getAndSet(null);
if (oldObject != null) {
cleanups.remove(number);
try {
closer.close(oldObject);
} catch (Exception e) {
LOG.error("exception closing resource", e);
}
}
}
public Optional<T> get() throws IllegalStateException {
return Optional.ofNullable(object.get());
}
}
/**
* Closes all added objects which have not been closed before.
*/
public void close() {
for (ManagedResource<?> closableProvider : cleanups.values()) {
closableProvider.close();
}
}
public <T> DeferredClosable<T> closeLater(T object, Closer<T> closer) {
Objects.requireNonNull(object);
Objects.requireNonNull(closer);
final ManagedResource<T> resource = new ManagedResource<T>(object, closer);
cleanups.put(resource.number, resource);
return resource;
}
public <T extends AutoCloseable> DeferredClosable<T> closeLater(T object) {
Objects.requireNonNull(object);
final ManagedResource<T> resource = new ManagedResource<T>(object, AutoCloseable::close);
cleanups.put(resource.number, resource);
return resource;
}
private static final EmptyResource<?> EMPTY_RESOURCE = new EmptyResource<>();
@SuppressWarnings("unchecked")
public static <T> DeferredClosable<T> empty() {
return (DeferredClosable<T>) EMPTY_RESOURCE;
}
}

View File

@@ -10,9 +10,8 @@
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.concurrent.Callable;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javafx.application.Platform;
@@ -48,61 +47,14 @@ import javafx.application.Platform;
*/
public final class FXThreads {
private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> {
// ignore.
};
private FXThreads() {
throw new AssertionError("Not instantiable.");
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;String&gt; futureBookName1 = runOnBackgroundThread(restResource::getBookName);
*
* Future&lt;String&gt; futureBookName2 = runOnBackgroundThread(() -&gt; {
* return restResource.getBookName();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static <T> Future<T> runOnBackgroundThread(Callable<T> task) {
return EXECUTOR.submit(task);
}
/**
* Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*
* <pre>
* // examples:
*
* Future&lt;?&gt; futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
*
* Future&lt;?&gt; futureDone2 = runOnBackgroundThread(() -&gt; {
* doSomeComplexCalculation();
* });
* </pre>
*
* @param task The task to be executed on a background thread.
* @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
*/
public static Future<?> runOnBackgroundThread(Runnable task) {
return EXECUTOR.submit(task);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
@@ -112,20 +64,18 @@ public final class FXThreads {
* });
* </pre>
*
* @param executor
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
runOnBackgroundThread(() -> {
return "asd";
});
FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK);
public static <T> void runOnMainThreadWhenFinished(ExecutorService executor, Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
runOnMainThreadWhenFinished(executor, task, successCallback, DUMMY_EXCEPTION_CALLBACK);
}
/**
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
* called. If you are interested in the exception, use
* {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
* // example:
@@ -137,14 +87,16 @@ public final class FXThreads {
* });
* </pre>
*
* @param executor The service to execute the background task on
* @param task The task to wait for.
* @param successCallback The action to perform, when the task finished.
* @param exceptionCallback
*/
public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
assertParamNotNull(task, "task must not be null.");
assertParamNotNull(successCallback, "successCallback must not be null.");
assertParamNotNull(exceptionCallback, "exceptionCallback must not be null.");
EXECUTOR.execute(() -> {
public static <T> void runOnMainThreadWhenFinished(ExecutorService executor, Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
Objects.requireNonNull(task, "task must not be null.");
Objects.requireNonNull(successCallback, "successCallback must not be null.");
Objects.requireNonNull(exceptionCallback, "exceptionCallback must not be null.");
executor.execute(() -> {
try {
final T result = task.get();
Platform.runLater(() -> {
@@ -158,12 +110,6 @@ public final class FXThreads {
});
}
private static void assertParamNotNull(Object param, String msg) {
if (param == null) {
throw new IllegalArgumentException(msg);
}
}
public interface CallbackWhenTaskFinished<T> {
void taskFinished(T result);
}

View File

@@ -0,0 +1,80 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiConsumer;
/**
* Manages and broadcasts events to a set of listeners. The types of the
* listener and event are entirely unbound. Instead, a method must be supplied
* to broadcast an event to a single listener.
*
* @author Tillmann Gaida
*
* @param <LISTENER>
* The type of listener.
* @param <EVENT>
* The type of event.
*/
public class ListenerRegistry<LISTENER, EVENT> {
final BiConsumer<LISTENER, EVENT> listenerCaller;
/**
* Constructs a new registry.
*
* @param listenerCaller
* The method which broadcasts an event to a single listener.
*/
public ListenerRegistry(BiConsumer<LISTENER, EVENT> listenerCaller) {
super();
this.listenerCaller = listenerCaller;
}
/**
* The handle of a registered listener.
*/
public interface ListenerRegistration {
void unregister();
}
final AtomicLong serial = new AtomicLong();
/*
* Since this is a {@link ConcurrentSkipListMap}, we can at the same time
* add to, remove from, and iterate over it. More importantly, a Listener
* can remove itself while being called from the {@link #broadcast(Object)}
* method.
*/
final Map<Long, LISTENER> listeners = new ConcurrentSkipListMap<>();
public ListenerRegistration registerListener(LISTENER listener) {
final long s = serial.incrementAndGet();
listeners.put(s, listener);
return () -> {
listeners.remove(s);
};
}
/**
* Broadcasts the given event to all registered listeners. If a listener
* causes an unchecked exception, that exception is thrown immediately
* without calling the other listeners.
*
* @param event
*/
public void broadcast(EVENT event) {
for (LISTENER listener : listeners.values()) {
listenerCaller.accept(listener, event);
}
}
}

View File

@@ -1,34 +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.util;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Files;
import java.nio.file.Path;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
public class MasterKeyFilter implements Filter<Path> {
public static MasterKeyFilter FILTER = new MasterKeyFilter();
private final String masterKeyExt = Aes256Cryptor.MASTERKEY_FILE_EXT.toLowerCase();
@Override
public boolean accept(Path child) throws IOException {
return child.getFileName().toString().toLowerCase().endsWith(masterKeyExt);
}
public static final DirectoryStream<Path> filteredDirectory(Path dir) throws IOException {
return Files.newDirectoryStream(dir, FILTER);
}
}

View File

@@ -0,0 +1,365 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.WritableByteChannel;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.prefs.Preferences;
import org.apache.commons.io.IOUtils;
import org.cryptomator.ui.Cryptomator;
import org.cryptomator.ui.util.ListenerRegistry.ListenerRegistration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Classes and methods to manage running this application in a mode, which only
* shows one instance.
*
* @author Tillmann Gaida
*/
public class SingleInstanceManager {
private static final Logger LOG = LoggerFactory.getLogger(SingleInstanceManager.class);
/**
* Connection to a running instance
*/
public static class RemoteInstance implements Closeable {
final SocketChannel channel;
RemoteInstance(SocketChannel channel) {
super();
this.channel = channel;
}
/**
* Sends a message to the running instance.
*
* @param string
* May not be longer than 2^16 - 1 bytes.
* @param timeout
* timeout in milliseconds. this should be larger than the
* precision of {@link System#currentTimeMillis()}.
* @return true if the message was sent within the given timeout.
* @throws IOException
*/
public boolean sendMessage(String string, long timeout) throws IOException {
Objects.requireNonNull(string);
byte[] message = string.getBytes();
if (message.length >= 256 * 256) {
throw new IOException("Message too long.");
}
ByteBuffer buf = ByteBuffer.allocate(message.length + 2);
buf.put((byte) (message.length / 256));
buf.put((byte) (message.length % 256));
buf.put(message);
buf.flip();
TimeoutTask.attempt(t -> {
if (channel.write(buf) < 0) {
return true;
}
return !buf.hasRemaining();
}, timeout, 10);
return !buf.hasRemaining();
}
@Override
public void close() throws IOException {
channel.close();
}
public int getRemotePort() throws IOException {
return ((InetSocketAddress) channel.getRemoteAddress()).getPort();
}
}
public static interface MessageListener {
void handleMessage(String message);
}
/**
* Represents a socket making this the main instance of the application.
*/
public static class LocalInstance implements Closeable {
private class ChannelState {
ByteBuffer write = ByteBuffer.wrap(applicationKey.getBytes());
ByteBuffer readLength = ByteBuffer.allocate(2);
ByteBuffer readMessage = null;
}
final ListenerRegistry<MessageListener, String> registry = new ListenerRegistry<>(MessageListener::handleMessage);
final String applicationKey;
final ServerSocketChannel channel;
final Selector selector;
int port = 0;
public LocalInstance(String applicationKey, ServerSocketChannel channel, Selector selector) {
Objects.requireNonNull(applicationKey);
this.applicationKey = applicationKey;
this.channel = channel;
this.selector = selector;
}
/**
* Register a listener for
*
* @param listener
* @return
*/
public ListenerRegistration registerListener(MessageListener listener) {
Objects.requireNonNull(listener);
return registry.registerListener(listener);
}
void handleSelection(SelectionKey key) throws IOException {
if (key.isAcceptable()) {
final SocketChannel accepted = channel.accept();
if (accepted != null) {
LOG.info("accepted incoming connection");
accepted.configureBlocking(false);
accepted.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
}
if (key.attachment() == null) {
key.attach(new ChannelState());
}
ChannelState state = (ChannelState) key.attachment();
if (key.isWritable() && state.write != null) {
((WritableByteChannel) key.channel()).write(state.write);
if (!state.write.hasRemaining()) {
state.write = null;
}
LOG.debug("wrote welcome. switching to read only.");
key.interestOps(SelectionKey.OP_READ);
}
if (key.isReadable()) {
ByteBuffer buffer = state.readLength != null ? state.readLength : state.readMessage;
if (((ReadableByteChannel) key.channel()).read(buffer) < 0) {
key.cancel();
}
if (!buffer.hasRemaining()) {
buffer.flip();
if (state.readLength != null) {
int length = (buffer.get() + 256) % 256;
length = length * 256 + ((buffer.get() + 256) % 256);
state.readLength = null;
state.readMessage = ByteBuffer.allocate(length);
} else {
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes);
state.readMessage = null;
state.readLength = ByteBuffer.allocate(2);
registry.broadcast(new String(bytes, "UTF-8"));
}
}
}
}
public void close() {
IOUtils.closeQuietly(selector);
IOUtils.closeQuietly(channel);
if (getSavedPort(applicationKey).orElse(-1).equals(port)) {
Preferences.userNodeForPackage(Cryptomator.class).remove(applicationKey);
}
}
void selectionLoop() {
try {
final Set<SelectionKey> keysToRemove = new HashSet<>();
while (selector.select() > 0) {
final Set<SelectionKey> keys = selector.selectedKeys();
for (SelectionKey key : keys) {
if (Thread.interrupted()) {
return;
}
try {
handleSelection(key);
} catch (IOException | IllegalStateException e) {
LOG.error("exception in selector", e);
} finally {
keysToRemove.add(key);
}
}
keys.removeAll(keysToRemove);
}
} catch (ClosedSelectorException e) {
return;
} catch (Exception e) {
LOG.error("error while selecting", e);
}
}
}
/**
* Checks if there is a valid port at
* {@link Preferences#userNodeForPackage(Class)} for {@link Cryptomator} under the
* given applicationKey, tries to connect to the port at the loopback
* address and checks if the port identifies with the applicationKey.
*
* @param applicationKey
* key used to load the port and check the identity of the
* connection.
* @return
*/
public static Optional<RemoteInstance> getRemoteInstance(String applicationKey) {
Optional<Integer> port = getSavedPort(applicationKey);
if (!port.isPresent()) {
return Optional.empty();
}
SocketChannel channel = null;
boolean close = true;
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
LOG.info("connecting to instance {}", port.get());
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port.get()));
SocketChannel fChannel = channel;
if (!TimeoutTask.attempt(t -> fChannel.finishConnect(), 1000, 10)) {
return Optional.empty();
}
LOG.info("connected to instance {}", port.get());
final byte[] bytes = applicationKey.getBytes();
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
tryFill(channel, buf, 1000);
if (buf.hasRemaining()) {
return Optional.empty();
}
buf.flip();
for (int i = 0; i < bytes.length; i++) {
if (buf.get() != bytes[i]) {
return Optional.empty();
}
}
close = false;
return Optional.of(new RemoteInstance(channel));
} catch (Exception e) {
return Optional.empty();
} finally {
if (close) {
IOUtils.closeQuietly(channel);
}
}
}
static Optional<Integer> getSavedPort(String applicationKey) {
int port = Preferences.userNodeForPackage(Cryptomator.class).getInt(applicationKey, -1);
if (port == -1) {
LOG.info("no running instance found");
return Optional.empty();
}
return Optional.of(port);
}
/**
* Creates a server socket on a free port and saves the port in
* {@link Preferences#userNodeForPackage(Class)} for {@link Cryptomator} under the
* given applicationKey.
*
* @param applicationKey
* key used to save the port and identify upon connection.
* @param exec
* the task which is submitted is interruptable.
* @return
* @throws IOException
*/
public static LocalInstance startLocalInstance(String applicationKey, ExecutorService exec) throws IOException {
final ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
channel.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
final int port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
Preferences.userNodeForPackage(Cryptomator.class).putInt(applicationKey, port);
LOG.info("InstanceManager bound to port {}", port);
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);
LocalInstance instance = new LocalInstance(applicationKey, channel, selector);
exec.submit(() -> {
try {
instance.port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
} catch (IOException e) {
}
instance.selectionLoop();
});
return instance;
}
/**
* tries to fill the given buffer for the given time
*
* @param channel
* @param buf
* @param timeout
* @throws ClosedChannelException
* @throws IOException
*/
public static <T extends SelectableChannel & ReadableByteChannel> void tryFill(T channel, final ByteBuffer buf, int timeout) throws IOException {
if (channel.isBlocking()) {
throw new IllegalStateException("Channel is in blocking mode.");
}
try (Selector selector = Selector.open()) {
channel.register(selector, SelectionKey.OP_READ);
TimeoutTask.attempt(remainingTime -> {
if (!buf.hasRemaining()) {
return true;
}
if (selector.select(remainingTime) > 0) {
if (channel.read(buf) < 0) {
return true;
}
}
return !buf.hasRemaining();
}, timeout, 1);
}
}
}

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
/**
* A task which is supposed to be repeated until it succeeds.
*
* @author Tillmann Gaida
*
* @param <E>
* The type of checked exception that this task may throw.
*/
public interface TimeoutTask<E extends Exception> {
/**
* Attempts to execute the task.
*
* @param timeout
* The time remaining to finish the task.
* @return true if the task finished, false if it needs to be attempted
* again.
* @throws E
* @throws InterruptedException
*/
boolean attempt(long timeout) throws E, InterruptedException;
/**
* Attempts a task until a timeout occurs. Checks for this timeout are based
* on {@link System#currentTimeMillis()}, so they are very crude. The task
* is guaranteed to be attempted once.
*
* @param task
* the task to perform.
* @param timeout
* time in millis before this method stops attempting to finish
* the task. greater than zero.
* @param sleepTimes
* time in millis to sleep between attempts. greater than zero.
* @return true if the task was finished, false if the task never always
* returned false or as soon as the task throws an
* {@link InterruptedException}.
* @throws E
* From the task.
*/
public static <E extends Exception> boolean attempt(TimeoutTask<E> task, long timeout, long sleepTimes) throws E {
if (timeout <= 0 || sleepTimes <= 0) {
throw new IllegalArgumentException();
}
long currentTime = System.currentTimeMillis();
long tryUntil = currentTime + timeout;
for (;; currentTime = System.currentTimeMillis()) {
if (currentTime >= tryUntil) {
return false;
}
try {
if (task.attempt(tryUntil - currentTime)) {
return true;
}
currentTime = System.currentTimeMillis();
if (currentTime + sleepTimes < tryUntil) {
Thread.sleep(sleepTimes);
} else {
return false;
}
} catch (InterruptedException e) {
return false;
}
}
}
}

View File

@@ -8,6 +8,8 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
/**
* A WebDavMounter acting as fallback if no other mounter works.
*
@@ -21,7 +23,12 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(int localPort) {
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, String name) {
displayMountInstructions();
return new WebDavMount() {
@Override

View File

@@ -9,6 +9,8 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@@ -28,18 +30,23 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
return false;
}
}
@Override
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav://[::1]:$PORT\"",
"xdg-open \"$URI\"")
.addEnv("PORT", String.valueOf(localPort));
"gvfs-mount \"dav:$DAV_SSP\"",
"xdg-open \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
final Script unmountScript = Script.fromLines(
"set -x",
"gvfs-mount -u \"dav://[::1]:$PORT\"")
.addEnv("URI", String.valueOf(localPort));
"gvfs-mount -u \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
mountScript.execute();
return new WebDavMount() {
@Override

View File

@@ -9,6 +9,8 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.net.URI;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@@ -20,14 +22,21 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final String path = "/Volumes/Cryptomator" + localPort;
public void warmUp(int serverPort) {
// no-op
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final String path = "/Volumes/Cryptomator" + uri.getRawPath().replace('/', '_');
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v Cryptomator \"[::1]:$PORT\" \"$MOUNT_PATH\"",
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"",
"open \"$MOUNT_PATH\"")
.addEnv("PORT", String.valueOf(localPort))
.addEnv("MOUNT_PATH", path);
.addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath())
.addEnv("MOUNT_PATH", path)
.addEnv("MOUNT_NAME", name);
final Script unmountScript = Script.fromLines(
"umount $MOUNT_PATH")
.addEnv("MOUNT_PATH", path);

View File

@@ -9,47 +9,18 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
public final class WebDavMounter {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounter.class);
private static final WebDavMounterStrategy[] STRATEGIES = {new WindowsWebDavMounter(), new MacOsXWebDavMounter(), new LinuxGvfsWebDavMounter()};
private static volatile WebDavMounterStrategy choosenStrategy;
public interface WebDavMounter {
/**
* Tries to mount a given webdav share.
*
* @param localPort local TCP port of the webdav share
* @param uri URI of the webdav share
* @param name the name under which the folder is to be mounted. This might be ignored.
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
*/
public static WebDavMount mount(int localPort) throws CommandFailedException {
return chooseStrategy().mount(localPort);
}
private static WebDavMounterStrategy chooseStrategy() {
if (choosenStrategy == null) {
choosenStrategy = getStrategyWhichShouldWork();
}
return choosenStrategy;
}
private static WebDavMounterStrategy getStrategyWhichShouldWork() {
for (WebDavMounterStrategy strategy : STRATEGIES) {
if (strategy.shouldWork()) {
LOG.info("Using {}", strategy.getClass().getSimpleName());
return strategy;
}
}
return new FallbackWebDavMounter();
}
private WebDavMounter() {
throw new IllegalStateException("Class is not instantiable.");
}
WebDavMount mount(URI uri, String name) throws CommandFailedException;
}

View File

@@ -0,0 +1,51 @@
/*******************************************************************************
* 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:
* Markus Kreusch - Refactored to use strategy pattern
* Sebastian Stenzel - Refactored to use Guice provider, added warmup-phase for windows mounts.
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Provider;
public class WebDavMounterProvider implements Provider<WebDavMounter> {
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
private static final WebDavMounterStrategy[] STRATEGIES = {new WindowsWebDavMounter(), new MacOsXWebDavMounter(), new LinuxGvfsWebDavMounter()};
private final WebDavMounterStrategy choosenStrategy;
@Inject
public WebDavMounterProvider(WebDavServer server, ExecutorService executorService) {
this.choosenStrategy = getStrategyWhichShouldWork();
executorService.execute(() -> {
this.choosenStrategy.warmUp(server.getPort());
});
}
@Override
public WebDavMounterStrategy get() {
return this.choosenStrategy;
}
private static WebDavMounterStrategy getStrategyWhichShouldWork() {
for (WebDavMounterStrategy strategy : STRATEGIES) {
if (strategy.shouldWork()) {
LOG.info("Using {}", strategy.getClass().getSimpleName());
return strategy;
}
}
return new FallbackWebDavMounter();
}
}

View File

@@ -9,13 +9,12 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
/**
* A strategy able to mount a webdav share and display it to the user.
*
* @author Markus Kreusch
*/
interface WebDavMounterStrategy {
interface WebDavMounterStrategy extends WebDavMounter {
/**
* @return {@code false} if this {@code WebDavMounterStrategy} can not work on the local machine, {@code true} if it could work
@@ -23,12 +22,9 @@ interface WebDavMounterStrategy {
boolean shouldWork();
/**
* Tries to mount a given webdav share.
*
* @param localPort local TCP port of the webdav share
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
* Invoked when mounting strategy gets chosen. On some operating systems (we don't want to tell names here) mounting might be faster,
* when certain things are prepared before the actual mount attempt.
*/
WebDavMount mount(int localPort) throws CommandFailedException;
void warmUp(int serverPort);
}

View File

@@ -11,6 +11,7 @@ package org.cryptomator.ui.util.mount;
import static org.cryptomator.ui.util.command.Script.fromLines;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -36,8 +37,20 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(int localPort) throws CommandFailedException {
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT% /persistent:no").addEnv("PORT", String.valueOf(localPort));
public void warmUp(int serverPort) {
final URI warmUpUri = URI.create("http://0--1.ipv6-literal.net:" + serverPort + "/bill-gates-mom-uses-goto");
try {
this.mount(warmUpUri, "WarmUpMount");
} catch (CommandFailedException e) {
// will most certainly throw an exception, because this is a fake WebDav path. But now windows has some DNS things cached :)
}
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT%%DAV_PATH% /persistent:no")
.addEnv("PORT", String.valueOf(uri.getPort()))
.addEnv("DAV_PATH", uri.getRawPath());
final CommandResult mountResult = mountScript.execute(30, TimeUnit.SECONDS);
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);

View File

@@ -775,6 +775,13 @@ is being used to size a border should also be in pixels.
-fx-orientation: horizontal;
}
.tool-bar.list-related-toolbar {
-fx-background-color: transparent;
-fx-padding: 0.1em 0;
-fx-spacing: 0;
-fx-alignment: CENTER_LEFT;
}
/*******************************************************************************
* *
* Slider *

View File

@@ -206,7 +206,6 @@
}
.button:armed,
.button:default:armed,
.toggle-button:armed,
.menu-button:armed,
.split-menu-button:armed > .label,
.split-menu-button > .arrow-button:pressed,
@@ -306,7 +305,8 @@
-fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%);
-fx-text-fill: -fx-light-text-color;
}
.button:default:disabled {
.button:default:disabled,
.root.active-window .button:default:disabled {
-fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2;
-fx-background-insets: 0, 1;
-fx-text-fill: -fx-mid-text-color;
@@ -362,6 +362,30 @@
-fx-orientation: vertical;
}
.tool-bar.list-related-toolbar {
-fx-background-color: #B4B4B4, #F7F7F7;
-fx-background-insets: 0, 0 1 1 1;
-fx-padding: 0;
-fx-spacing: 0;
-fx-alignment: CENTER_LEFT;
}
.tool-bar.list-related-toolbar .button,
.tool-bar.list-related-toolbar .toggle-button {
-fx-background-color: transparent;
-fx-background-insets: 0;
-fx-background-radius: 0;
-fx-border-color: transparent #B4B4B4 transparent transparent;
-fx-border-width: 1;
}
.tool-bar.list-related-toolbar .button:armed,
.tool-bar.list-related-toolbar .toggle-button:armed,
.tool-bar.list-related-toolbar .toggle-button:selected {
-fx-background-color: linear-gradient(to bottom, #C0C0C0 0%, #ADADAD 100%);
}
/*******************************************************************************
* *
* ScrollBar *

View File

@@ -358,6 +358,13 @@
-fx-orientation: vertical;
}
.tool-bar.list-related-toolbar {
-fx-background-color: transparent;
-fx-padding: 0.1em 0;
-fx-spacing: 0;
-fx-alignment: CENTER_LEFT;
}
/*******************************************************************************
* *
* ScrollBar *

View File

@@ -0,0 +1,53 @@
<?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?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" 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="%changePassword.label.oldPassword" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<SecPasswordField fx:id="oldPasswordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 1 -->
<Label text="%changePassword.label.newPassword" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<SecPasswordField fx:id="newPasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Label text="%changePassword.label.retypePassword" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 3 -->
<Button fx:id="changePasswordButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -18,7 +18,7 @@
<?import javafx.scene.control.TextField?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.InitializeController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
@@ -30,25 +30,18 @@
<children>
<!-- Row 0 -->
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.username" />
<TextField fx:id="usernameField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.password" />
<SecPasswordField fx:id="passwordField" 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" />
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" />
<!-- Row 2 -->
<Label GridPane.rowIndex="2" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" disable="true" />
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
<!-- Row 3 -->
<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 4 -->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -13,20 +13,28 @@
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ToggleButton?>
<?import javafx.scene.control.ContextMenu?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.Separator?>
<?import javafx.geometry.Insets?>
<HBox fx:id="rootPane" prefHeight="400.0" prefWidth="600.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.controllers.MainController" xmlns:fx="http://javafx.com/fxml">
<padding><Insets top="20" right="20" bottom="20" left="20.0"/></padding>
<fx:define>
<fx:include fx:id="welcomeView" source="welcome.fxml" />
<ContextMenu fx:id="directoryContextMenu">
<ContextMenu fx:id="vaultListCellContextMenu">
<items>
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry" />
<!-- TODO: -->
<MenuItem text="%main.directoryList.contextMenu.addUser" disable="true" />
<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
<MenuItem text="%main.directoryList.contextMenu.changePassword" onAction="#didClickChangePassword" />
</items>
</ContextMenu>
<ContextMenu fx:id="addVaultContextMenu" onShowing="#willShowAddVaultContextMenu" onHidden="#didHideAddVaultContextMenu">
<items>
<MenuItem text="%main.addDirectory.contextMenu.new" onAction="#didClickCreateNewVault" />
<MenuItem text="%main.addDirectory.contextMenu.open" onAction="#didClickAddExistingVaults" />
</items>
</ContextMenu>
</fx:define>
@@ -34,10 +42,10 @@
<children>
<VBox prefWidth="200.0">
<children>
<ListView fx:id="directoryList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<ToolBar VBox.vgrow="NEVER">
<ListView fx:id="vaultList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<ToolBar VBox.vgrow="NEVER" styleClass="list-related-toolbar">
<items>
<Button text="+" onAction="#didClickAddDirectory" />
<ToggleButton text="+" fx:id="addVaultButton" onAction="#didClickAddVault" focusTraversable="false"/>
</items>
</ToolBar>
</children>

View File

@@ -19,7 +19,7 @@
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
@@ -31,25 +31,21 @@
<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" />
<Label text="%unlock.label.password" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- 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" />
<Label text="%unlock.label.mountName" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Label text="%unlock.label.checkIntegrity" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<CheckBox fx:id="checkIntegrity" wrapText="true" text="%unlock.checkbox.checkIntegrity" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" disable="true"/>
<!-- Row 3 -->
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<!-- Row 3-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 4-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 5 -->
<Label fx:id="messageLabel" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@@ -18,7 +18,7 @@
<?import javafx.scene.chart.NumberAxis?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
@@ -30,16 +30,13 @@
<children>
<!-- Row 0 -->
<Label fx:id="messageLabel" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<!-- Row 1 -->
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
<!-- Row 2 -->
<LineChart fx:id="ioGraph" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" animated="false" createSymbols="false" prefHeight="300.0" legendVisible="true" legendSide="BOTTOM" verticalZeroLineVisible="false" verticalGridLinesVisible="false" horizontalGridLinesVisible="true">
<LineChart fx:id="ioGraph" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" animated="false" createSymbols="false" prefHeight="340.0" legendVisible="true" legendSide="BOTTOM" verticalZeroLineVisible="false" verticalGridLinesVisible="false" horizontalGridLinesVisible="true">
<xAxis><NumberAxis fx:id="xAxis" forceZeroInRange="false" tickMarkVisible="false" minorTickVisible="false" tickLabelsVisible="false" autoRanging="false"/></xAxis>
<yAxis><NumberAxis label="%unlocked.ioGraph.yAxis.label" autoRanging="true" forceZeroInRange="true" /></yAxis>
</LineChart>
<!-- Row 1 -->
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
</children>
</GridPane>

View File

@@ -25,9 +25,9 @@
<Label AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="50.0" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<Label AnchorPane.leftAnchor="120.0" AnchorPane.topAnchor="280.0" text="%welcome.addButtonInstructionLabel"/>
<QuadCurve AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
<QuadCurve AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="300.0" startX="200.0" startY="0.0" endX="0.0" endY="80.0" controlX="180.0" controlY="80.0" fill="TRANSPARENT" stroke="BLACK" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="370.0" startX="0.0" endX="10.0" startY="10.0" endY="0.0" strokeWidth="2.0"/>
<Line AnchorPane.leftAnchor="4.0" AnchorPane.topAnchor="380.0" startX="0.0" endX="10.0" startY="0.0" endY="10.0" strokeWidth="2.0"/>
</children>
</AnchorPane>

View File

@@ -11,8 +11,9 @@ app.name=Cryptomator
# main.fxml
main.directoryList.contextMenu.remove=Remove from list
main.directoryList.contextMenu.addUser=Add user
main.directoryList.contextMenu.changePassword=Change password
main.addDirectory.contextMenu.new=Create new vault
main.addDirectory.contextMenu.open=Add existing vault
# welcome.fxml
@@ -21,28 +22,31 @@ welcome.addButtonInstructionLabel=Start by adding a new vault :-)
# initialize.fxml
initialize.label.username=Username
initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
initialize.alert.directoryIsNotEmpty.title=The chosen directory is not empty
initialize.alert.directoryIsNotEmpty.content=All existing files inside this directory will get encrypted. Continue?
# unlock.fxml
unlock.label.username=Username
unlock.label.password=Password
unlock.label.checkIntegrity=File integrity
unlock.checkbox.checkIntegrity=Verify checksums (slower, but detects manipulation)
unlock.label.mountName=Drive name
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.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# change_password.fxml
changePassword.label.oldPassword=Old password
changePassword.label.newPassword=New password
changePassword.label.retypePassword=Retype password
changePassword.button.unlock=Change password
changePassword.errorMessage.wrongPassword=Wrong password.
changePassword.errorMessage.decryptionFailed=Decryption failed.
changePassword.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
changePassword.infoMessage.success=Password changed.
# unlocked.fxml
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
unlocked.button.lock=Lock vault
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,12 @@
package org.cryptomator.ui;
import static org.junit.Assert.*;
import org.junit.Test;
public class MainApplicationTest {
@Test
public void testInjection() throws Exception {
new MainApplication();
}
}

View File

@@ -0,0 +1,18 @@
package org.cryptomator.ui.model;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class VaultTest {
@Test
public void testNormalize() throws Exception {
assertEquals("_", Vault.normalize(" "));
assertEquals("a", Vault.normalize("ä"));
assertEquals("C", Vault.normalize("Ĉ"));
assertEquals("_", Vault.normalize(":"));
assertEquals("", Vault.normalize("汉语"));
}
}

View File

@@ -0,0 +1,48 @@
package org.cryptomator.ui.util;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.io.Closeable;
import org.junit.Test;
public class DeferredCloserTest {
@Test
public void testBasicFunctionality() throws Exception {
DeferredCloser closer = new DeferredCloser();
final Closeable obj = mock(Closeable.class);
final DeferredClosable<Closeable> resource = closer.closeLater(obj);
assertTrue(resource.get().isPresent());
assertTrue(resource.get().get() == obj);
closer.close();
assertFalse(resource.get().isPresent());
verify(obj).close();
}
@Test
public void testAutoremoval() throws Exception {
DeferredCloser closer = new DeferredCloser();
final DeferredClosable<Closeable> resource = closer.closeLater(mock(Closeable.class));
final DeferredClosable<Closeable> resource2 = closer.closeLater(mock(Closeable.class));
resource.close();
assertFalse(resource.get().isPresent());
assertEquals(1, closer.cleanups.size());
assertTrue(resource2.get().isPresent());
closer.close();
assertFalse(resource2.get().isPresent());
assertEquals(0, closer.cleanups.size());
}
}

View File

@@ -0,0 +1,53 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import static org.junit.Assert.*;
import java.util.Iterator;
import java.util.concurrent.ConcurrentSkipListMap;
import org.junit.Test;
public class ListenerRegistryTest {
/**
* This test looks at how concurrent modifications affect the iterator of a
* {@link ConcurrentSkipListMap}. It shows that concurrent modifications
* work just fine, however the state of the iterator including the next
* value are advanced during retrieval of a value, so it's not possible to
* remove the next value.
*
* @throws Exception
*/
@Test
public void testConcurrentSkipListMap() throws Exception {
ConcurrentSkipListMap<Integer, Integer> map = new ConcurrentSkipListMap<>();
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
map.put(5, 5);
final Iterator<Integer> iterator = map.values().iterator();
assertTrue(iterator.hasNext());
assertEquals((Integer) 1, iterator.next());
map.remove(2);
assertTrue(iterator.hasNext());
// iterator returns 2 anyway.
assertEquals((Integer) 2, iterator.next());
assertTrue(iterator.hasNext());
map.remove(4);
assertEquals((Integer) 3, iterator.next());
assertTrue(iterator.hasNext());
// this time we removed 4 before retrieving 3, so it is skipped.
assertEquals((Integer) 5, iterator.next());
}
}

View File

@@ -0,0 +1,200 @@
/*******************************************************************************
* Copyright (c) 2014 cryptomator.org
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Tillmann Gaida - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.TimeUnit;
import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
import org.cryptomator.ui.util.SingleInstanceManager.MessageListener;
import org.cryptomator.ui.util.SingleInstanceManager.RemoteInstance;
import org.junit.Test;
public class SingleInstanceManagerTest {
@Test(timeout = 10000)
public void testTryFillTimeout() throws Exception {
try (final ServerSocket socket = new ServerSocket(0)) {
// we need to asynchronously accept the connection
final ForkJoinTask<?> forked = ForkJoinTask.adapt(() -> {
try {
socket.setSoTimeout(1000);
socket.accept();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).fork();
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), socket.getLocalPort()));
TimeoutTask.attempt(t -> channel.finishConnect(), 1000, 1);
final ByteBuffer buffer = ByteBuffer.allocate(1);
SingleInstanceManager.tryFill(channel, buffer, 1000);
assertTrue(buffer.hasRemaining());
}
forked.join();
}
}
@Test(timeout = 10000)
public void testTryFill() throws Exception {
try (final ServerSocket socket = new ServerSocket(0)) {
// we need to asynchronously accept the connection
final ForkJoinTask<?> forked = ForkJoinTask.adapt(() -> {
try {
socket.setSoTimeout(1000);
socket.accept().getOutputStream().write(1);
} catch (Exception e) {
throw new RuntimeException(e);
}
}).fork();
try (SocketChannel channel = SocketChannel.open()) {
channel.configureBlocking(false);
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), socket.getLocalPort()));
TimeoutTask.attempt(t -> channel.finishConnect(), 1000, 1);
final ByteBuffer buffer = ByteBuffer.allocate(1);
SingleInstanceManager.tryFill(channel, buffer, 1000);
assertFalse(buffer.hasRemaining());
}
forked.join();
}
}
String appKey = "APPKEY";
@Test
public void testOneMessage() throws Exception {
ExecutorService exec = Executors.newCachedThreadPool();
try {
final LocalInstance server = SingleInstanceManager.startLocalInstance(appKey, exec);
final Optional<RemoteInstance> r = SingleInstanceManager.getRemoteInstance(appKey);
CountDownLatch latch = new CountDownLatch(1);
final MessageListener listener = spy(new MessageListener() {
@Override
public void handleMessage(String message) {
latch.countDown();
}
});
server.registerListener(listener);
assertTrue(r.isPresent());
String message = "Is this thing on?";
assertTrue(r.get().sendMessage(message, 1000));
System.out.println("wrote message");
latch.await(10, TimeUnit.SECONDS);
verify(listener).handleMessage(message);
} finally {
exec.shutdownNow();
}
}
@Test(timeout = 60000)
public void testALotOfMessages() throws Exception {
final int connectors = 256;
final int messagesPerConnector = 256;
ExecutorService exec = Executors.newSingleThreadExecutor();
ExecutorService exec2 = Executors.newFixedThreadPool(16);
try (final LocalInstance server = SingleInstanceManager.startLocalInstance(appKey, exec)) {
Set<String> sentMessages = new ConcurrentSkipListSet<>();
Set<String> receivedMessages = new HashSet<>();
CountDownLatch sendLatch = new CountDownLatch(connectors);
CountDownLatch receiveLatch = new CountDownLatch(connectors * messagesPerConnector);
server.registerListener(message -> {
receivedMessages.add(message);
receiveLatch.countDown();
});
Set<RemoteInstance> instances = Collections.synchronizedSet(new HashSet<>());
for (int i = 0; i < connectors; i++) {
exec2.submit(() -> {
try {
final Optional<RemoteInstance> r = SingleInstanceManager.getRemoteInstance(appKey);
assertTrue(r.isPresent());
instances.add(r.get());
for (int j = 0; j < messagesPerConnector; j++) {
exec2.submit(() -> {
try {
for (;;) {
final String message = UUID.randomUUID().toString();
if (!sentMessages.add(message)) {
continue;
}
r.get().sendMessage(message, 1000);
break;
}
} catch (Exception e) {
e.printStackTrace();
}
});
}
sendLatch.countDown();
} catch (Throwable e) {
e.printStackTrace();
}
});
}
assertTrue(sendLatch.await(1, TimeUnit.MINUTES));
exec2.shutdown();
assertTrue(exec2.awaitTermination(1, TimeUnit.MINUTES));
assertTrue(receiveLatch.await(1, TimeUnit.MINUTES));
assertEquals(sentMessages, receivedMessages);
for (RemoteInstance remoteInstance : instances) {
try {
remoteInstance.close();
} catch (Exception e) {
e.printStackTrace();
}
}
} finally {
exec.shutdownNow();
exec2.shutdownNow();
}
}
}