mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 16:51:28 +00:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dd8a28a9d | ||
|
|
39df98ea3c | ||
|
|
2849e39e85 | ||
|
|
9433c22d7f | ||
|
|
5bd38d31bf | ||
|
|
63f64fae03 | ||
|
|
e321994c35 | ||
|
|
f86b27d62f | ||
|
|
cba8bbefc5 | ||
|
|
507e21f8a3 | ||
|
|
676cb10ef0 | ||
|
|
3b3aa4107b | ||
|
|
7edd303f2e | ||
|
|
ea3384d189 | ||
|
|
b2be41e39b | ||
|
|
f1d125bf8d | ||
|
|
028f6ea824 | ||
|
|
30dc8eecb1 | ||
|
|
4d979c26f6 | ||
|
|
4776dbf603 | ||
|
|
0b5e4469b4 | ||
|
|
8ba89a3bf5 | ||
|
|
b68cf71494 | ||
|
|
5569ecbfc7 | ||
|
|
19bc1ed569 | ||
|
|
5aaee7bbf6 | ||
|
|
3187520797 | ||
|
|
bcee1e0d12 | ||
|
|
9fdd2f339c | ||
|
|
ebdf37ed63 | ||
|
|
09c26f5e86 | ||
|
|
def70c5891 | ||
|
|
11396b71e6 | ||
|
|
05ec9b574e | ||
|
|
efac770915 | ||
|
|
f29bcc447c | ||
|
|
5e0ebab587 | ||
|
|
751dbe6b7e | ||
|
|
a72f8ba8ab | ||
|
|
999285617d | ||
|
|
addf488b26 | ||
|
|
cd5e878a26 | ||
|
|
0a671aa9bc | ||
|
|
8cc445a12a | ||
|
|
432beb2a17 | ||
|
|
9fd271ad7b | ||
|
|
72b1ff78c3 | ||
|
|
edfd264e47 | ||
|
|
0cfc3fb7f7 | ||
|
|
ecf29a91b8 | ||
|
|
38884c6dfd | ||
|
|
7813a11381 | ||
|
|
d774546bf8 | ||
|
|
0b64c7ce25 | ||
|
|
0aef60efc4 | ||
|
|
f0fa4fcf3d | ||
|
|
8bfdad38b9 | ||
|
|
19ea81f0e5 | ||
|
|
5e6f343e68 |
@@ -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
|
||||
|
||||
45
README.md
45
README.md
@@ -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
|
||||
[](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.
|
||||
|
||||
[](https://travis-ci.org/totalvoidness/cryptomator)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
25
main/pom.xml
25
main/pom.xml
@@ -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 |
BIN
main/ui/package/macosx/Cryptomator-Volume.icns
Normal file
BIN
main/ui/package/macosx/Cryptomator-Volume.icns
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
76
main/ui/package/macosx/Info.plist
Normal file
76
main/ui/package/macosx/Info.plist
Normal 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 |
@@ -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>
|
||||
|
||||
|
||||
149
main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java
Normal file
149
main/ui/src/main/java/org/cryptomator/ui/Cryptomator.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
88
main/ui/src/main/java/org/cryptomator/ui/MainModule.java
Normal file
88
main/ui/src/main/java/org/cryptomator/ui/MainModule.java
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
@@ -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());
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
199
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
Normal file
199
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String> futureBookName1 = runOnBackgroundThread(restResource::getBookName);
|
||||
*
|
||||
* Future<String> futureBookName2 = runOnBackgroundThread(() -> {
|
||||
* 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<?> futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
|
||||
*
|
||||
* Future<?> futureDone2 = runOnBackgroundThread(() -> {
|
||||
* 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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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 *
|
||||
|
||||
53
main/ui/src/main/resources/fxml/change_password.fxml
Normal file
53
main/ui/src/main/resources/fxml/change_password.fxml
Normal 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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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 |
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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("汉语"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user