Compare commits

...

61 Commits
0.7.0 ... 0.9.0

Author SHA1 Message Date
Sebastian Stenzel
44475fa3f1 Version 0.9.0 2015-10-16 13:52:03 +02:00
Sebastian Stenzel
f430f3c579 just fixes some warnings in eclipse [ci skip] 2015-10-16 13:24:52 +02:00
Sebastian Stenzel
3efa23987f reverted previous commit (fixing travis setting) 2015-10-15 17:29:55 +02:00
Sebastian Stenzel
1dce871354 trying out travis container-based builds 2015-10-15 17:27:58 +02:00
Sebastian Stenzel
d919c727cf trying out travis container-based builds 2015-10-15 17:24:45 +02:00
Sebastian Stenzel
b691e374eb fixes #74 2015-10-15 17:19:27 +02:00
Sebastian Stenzel
ca88e05849 Preparations for #74 2015-10-10 17:01:25 +02:00
Sebastian Stenzel
104c3b64f6 - fixes #75
- restored progress indicator, broken by previous commit
2015-10-04 22:01:01 +02:00
Sebastian Stenzel
1bef4e786d - Fixed "return" key in unlock view
- Fixed password field focus
- Don't show unlock error messages from one vault, when switching to another vault
- Hide advanced mount options by default (preparation for things like #74)
2015-10-04 15:38:41 +02:00
Sebastian Stenzel
c1f32105d8 last byte in request range inclusive (!!) 2015-10-03 14:07:41 +02:00
Sebastian Stenzel
09b4130c3e - fixed size obfuscation padding
- fixed behaviour when serving invalid content ranges, thus improving random access performance (thats why we created the 0.8.2 workaround)
- reduced loglevels of some frequent messages
2015-10-03 13:10:28 +02:00
Sebastian Stenzel
6d1e0fe609 loading settings only once 2015-10-03 12:07:24 +02:00
Sebastian Stenzel
e65c84ca1d Merge tag '0.8.2'
Conflicts:
	main/core/pom.xml
	main/crypto-aes/pom.xml
	main/crypto-api/pom.xml
	main/installer-debian/pom.xml
	main/installer-osx/pom.xml
	main/installer-win-portable/pom.xml
	main/installer-win/pom.xml
	main/pom.xml
	main/uber-jar/pom.xml
	main/ui/pom.xml
2015-10-02 17:38:40 +02:00
Sebastian Stenzel
095f60ec03 Update 0.8.2
Fixes slow range requests on OS X, as WebDAVFS sometimes processes responses only after closing the connection, even though "keep-alive" is requested.
2015-10-02 17:36:04 +02:00
Sebastian Stenzel
485df3aa71 fixes #65 2015-09-26 16:39:18 +02:00
Sebastian Stenzel
6b073c1499 small I/O tweaks 2015-08-25 10:55:46 +02:00
Sebastian Stenzel
71983cc3a8 - added copyright information for the OS X progress indicator
- fixed spinning direction (now clockwise)
- use default progress indicator style for win + tux
[ci skip]
2015-08-16 11:55:42 +02:00
Sebastian Stenzel
db2297d2f1 fixed unit test 2015-08-15 23:19:34 +02:00
Sebastian Stenzel
38ab167fa4 clear password field when switching vault before unlocking [ci skip] 2015-08-15 23:14:05 +02:00
Sebastian Stenzel
f87e8f55f1 CSS cleanup 2015-08-15 23:05:53 +02:00
Sebastian Stenzel
a4e6365e0b New UI style for Linux builds 2015-08-15 16:51:36 +02:00
Sebastian Stenzel
06034fd95b code cleanup 2015-08-15 12:07:29 +02:00
Sebastian Stenzel
571fee9524 simplified DI, where possible 2015-08-09 00:38:23 +02:00
Sebastian Stenzel
af9deffa6d moved from guice to dagger 2015-08-08 19:13:16 +02:00
Sebastian Stenzel
48b319ec99 started working on 0.9.0-SNAPSHOT 2015-08-08 19:12:44 +02:00
Sebastian Stenzel
9ea9cb6eb2 Removed JVM flag for windows builds, as it may prevent the application from starting. This is a hotfix release for Windows only. 2015-08-06 22:42:59 +02:00
Sebastian Stenzel
301ba9cdb7 Update .travis.yml 2015-08-04 20:29:00 +02:00
Sebastian Stenzel
740c4c2ba9 Update .travis.yml 2015-08-04 18:01:13 +02:00
Sebastian Stenzel
18e7dcd91f Beta 0.8.0 2015-08-04 17:44:19 +02:00
Sebastian Stenzel
95133152f9 using 1GB heap size on 32 bit windows systems, due to http://www.oracle.com/technetwork/java/hotspotfaq-138619.html#gc_heap_32bit 2015-08-04 17:38:42 +02:00
Sebastian Stenzel
4cd243e32a Build with unlimited strength JCE policy files on Travis 2015-08-04 15:23:50 +02:00
Sebastian Stenzel
f454f48248 updated travis configuration 2015-08-04 15:06:59 +02:00
Sebastian Stenzel
ad3801b223 - Allow up to 2GB RAM to be used by native packages.
- Copy dependency jars instead of bundling them (allows more restrictively licensed dependencies)
2015-08-03 22:49:55 +02:00
Sebastian Stenzel
3f946d1c82 Creating directories lazily now. This fixes an issue, where a "ui" folder appeared inside the vault, just because Windows Explorer queries the existence of "ui/SwDRM.dll". 2015-08-03 12:33:11 +02:00
Sebastian Stenzel
ecb178d5b2 simplified code 2015-08-02 15:13:56 +02:00
Sebastian Stenzel
ed7dc60f5e slow start, increasing number of blocks worked on per thread, reusing ciphers for multiple consecutive blocks 2015-08-02 00:37:26 +02:00
Sebastian Stenzel
6bbfacd794 small changes for easier debugging 2015-08-01 21:34:47 +02:00
Sebastian Stenzel
5a06d01ef5 moved to ByteBuffers 2015-07-31 10:56:34 +02:00
Sebastian Stenzel
aac9ead633 - mutlithreaded encryption
- moved to bytebuffer (experimental)
2015-07-27 22:25:24 +02:00
Sebastian Stenzel
cdcc1626ce multithreaded decryption using producer/consumer pattern 2015-07-26 22:22:50 +02:00
Sebastian Stenzel
738d2dfc34 first multithreaded decryption test (PoC quality) 2015-07-25 17:28:00 +02:00
Sebastian Stenzel
9771c6d1e7 don't decrypt file size obfucscation padding 2015-07-25 16:18:02 +02:00
Sebastian Stenzel
bc0a26b0ad building pre-release versions
(trying out https://github.com/travis-ci/dpl/issues/234#issuecomment-123309684)
2015-07-25 01:58:25 +02:00
Tobias Hagemann
7349ef754e Unsatisfiable content range in partial file requests 2015-07-25 01:56:06 +02:00
Sebastian Stenzel
e8e80f306b WebDAV range request refinements 2015-07-25 01:52:37 +02:00
Sebastian Stenzel
e1ce400bcd fixed wrong long-to-bytearray conversion 2015-07-14 15:20:50 +02:00
Sebastian Stenzel
8c4d5a9614 non-interchangable chunks by adding additional data to MACs 2015-07-14 14:34:17 +02:00
Sebastian Stenzel
93a87c86a4 sensitive header data is now encrypted using a single enc_cbc(concat(fileSize, fileKey)) instead of encrypting both data separately 2015-07-13 17:37:06 +02:00
Sebastian Stenzel
685e347524 New MAC authentication warning, preventing CCAs, but allowing to force-decrypt unauthentic files. 2015-07-09 17:16:43 +02:00
Sebastian Stenzel
9d2d847727 fixed build 2015-07-05 20:57:15 +02:00
Sebastian Stenzel
a00086ff2d - simplified range request handling
- correct handling of HTTP 416 responses
- moved unit test to apache httpclient (old version 3.1 due to jackrabbit's dependency)
2015-07-04 20:47:23 +02:00
Sebastian Stenzel
d76154c8d1 - reduced size of chunks, a MAC is calculated for (not final yet)
- faster range requests due to reduced chunk size, thus faster video playback start
- fixed range requests
- making file locks optional (if not supported by file system)
2015-07-03 19:30:49 +02:00
Sebastian Stenzel
bc76ab285d fixed unit test 2015-06-27 08:09:12 +02:00
Sebastian Stenzel
0d3a5b4e70 - increased vault version
- Showing "per vault" MAC authentication failure dialogs
2015-06-26 23:35:24 +02:00
Sebastian Stenzel
48f544ef91 - support for http range requests in new schema 2015-06-21 22:11:15 +02:00
Sebastian Stenzel
45cf87d089 new, more secure encryption scheme
- fixed flaw reported by Stan Drapkin (SecurityDriven.NET) reducing effective key size to 96 bit
- multiple file content MACs for 1MB blocks, preventing chosen ciphertext attacks, as authentication now happens before decryption
- allowing files bigger than 64GiB
2015-06-21 18:51:39 +02:00
Sebastian Stenzel
d7186bb2dd Merge tag '0.7.2' 2015-06-05 15:20:09 +02:00
Sebastian Stenzel
85f3487cf0 fixes #64 by attempting to mount the more common "localhost", which is more likely to be excluded from proxy autoconfiguration scripts. 2015-06-05 15:17:33 +02:00
Sebastian Stenzel
4a754d6a6c Beginning development of 0.8.0 2015-05-31 00:04:26 +02:00
Sebastian Stenzel
abf9920caf its getting late... 2015-05-30 22:29:59 +02:00
Sebastian Stenzel
dd2863da5b 0.7.1 (fixed debian build)
updated travis script (requires git release tags to be equal to maven version, so starting with this tag we drop the preceeding "v")
2015-05-30 22:22:50 +02:00
103 changed files with 4454 additions and 5997 deletions

View File

@@ -1,7 +1,8 @@
language: java
jdk:
- oraclejdk8
script: mvn -fmain/pom.xml clean package
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
script: mvn -fmain/pom.xml -Puber-jar clean package
notifications:
webhooks:
urls:
@@ -11,9 +12,10 @@ notifications:
on_start: false
deploy:
provider: releases
prerelease: true
api_key:
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
file: main/ui/target/dist/Cryptomator.jar
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
skip_cleanup: true
on:
repo: cryptomator/cryptomator

View File

@@ -4,53 +4,14 @@ Copyright (c) 2014, Sebastian Stenzel
Cryptomator is licensed under the MIT license. The details can be found in the accompanying license file.
## Third party softwares
Cryptomator uses third party softwares that may be licensed under different licenses.
### AquaFX
The ProgressIndicator in ui/src/main/resource/css/mac_theme.css contains code from the AquaFX project.
### Jackson
Jackson is a high-performance, Free/Open Source JSON processing library.
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
been in development since 2007.
It is currently developed by a community of developers, as well as supported
commercially by FasterXML.com.
Copyright 2013 Claudine Zillmann (http://aquafx-project.com/)
**Licensing:** Jackson core and extension components may licensed under different licenses.
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
For more information, including possible other licensing options, contact
FasterXML.com (http://fasterxml.com).
**Credits:** A list of contributors may be found from CREDITS file, which is included
in some artifacts (usually source distributions); but is always available
from the source code management (SCM) system project uses.
### Jetty
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
and Apache License v2.0 which accompanies this distribution.
The UnixCrypt.java code implements the one way cryptography used by
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
modified April 2001 by Iris Van den Broeke, Daniel Deville.
Permission to use, copy, modify and distribute UnixCrypt
for non-commercial or commercial purposes and without fee is
granted provided that the copyright notice appears in all copies.
### Jackrabbit WebDAV Library
Copyright 2004-2014 The Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
Based on source code originally developed by Day Software (http://www.day.com/).
### Apache Jakarta HttpClient
Copyright 1999-2007 The Apache Software Foundation
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
Licensed under the accompanying BSD license file.
### Apache Commons Collections
Copyright 2001-2013 The Apache Software Foundation
@@ -83,6 +44,17 @@ Copyright (c) 2013, ControlsFX
Licensed under the accompanying BSD license file.
### Dagger 2
Copyright 2014 Google, Inc.
Copyright 2012 Square, Inc.
Licensed under the Apache License, Version 2.0
### Apache Jakarta HttpClient
Copyright 1999-2007 The Apache Software Foundation
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
### Apache Log4j
Copyright 1999-2012 Apache Software Foundation
@@ -90,7 +62,44 @@ This product includes software developed at The Apache Software Foundation (http
ResolverUtil.java Copyright 2005-2006 Tim Fennell
### Jackrabbit WebDAV Library
Copyright 2004-2014 The Apache Software Foundation
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
Based on source code originally developed by Day Software (http://www.day.com/).
### Jackson
Jackson is a high-performance, Free/Open Source JSON processing library.
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
been in development since 2007.
It is currently developed by a community of developers, as well as supported
commercially by FasterXML.com.
**Licensing:** Jackson core and extension components may licensed under different licenses.
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
For more information, including possible other licensing options, contact
FasterXML.com (http://fasterxml.com).
**Credits:** A list of contributors may be found from CREDITS file, which is included
in some artifacts (usually source distributions); but is always available
from the source code management (SCM) system project uses.
### Jetty
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
and Apache License v2.0 which accompanies this distribution.
The UnixCrypt.java code implements the one way cryptography used by
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
modified April 2001 by Iris Van den Broeke, Daniel Deville.
Permission to use, copy, modify and distribute UnixCrypt
for non-commercial or commercial purposes and without fee is
granted provided that the copyright notice appears in all copies.
### JUnit
Copyright (c) 2000-2006, www.hamcrest.org
Licensed under the accompanying BSD license file.
Licensed under the accompanying BSD license file.

View File

@@ -46,7 +46,7 @@ If you want to take a look at the current beta version, go ahead and get your co
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
git clone https://github.com/cryptomator/cryptomator.git
cd cryptomator/main
git checkout v0.6.0
git checkout 0.7.1
mvn clean install -Pdebian
```

View File

@@ -12,16 +12,14 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator WebDAV and I/O module</name>
<properties>
<jetty.version>9.2.10.v20150310</jetty.version>
<jackrabbit.version>2.10.1</jackrabbit.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
<jetty.version>9.3.3.v20150827</jetty.version>
<jackrabbit.version>2.11.0</jackrabbit.version>
</properties>
<dependencies>
@@ -29,6 +27,11 @@
<groupId>org.cryptomator</groupId>
<artifactId>crypto-api</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>crypto-aes</artifactId>
<scope>test</scope>
</dependency>
<!-- Jetty (Servlet Container) -->
<dependency>
@@ -41,6 +44,11 @@
<artifactId>jetty-webapp</artifactId>
<version>${jetty.version}</version>
</dependency>
<dependency>
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<scope>test</scope>
</dependency>
<!-- Jackrabbit -->
<dependency>
@@ -48,13 +56,13 @@
<artifactId>jackrabbit-webdav</artifactId>
<version>${jackrabbit.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- I/O -->
<dependency>
<groupId>commons-io</groupId>
@@ -64,7 +72,7 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>

View File

@@ -52,9 +52,11 @@ public final class WebDavServer {
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, "/");
if (SystemUtils.IS_OS_WINDOWS) {
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);
@@ -84,13 +86,11 @@ public final class WebDavServer {
/**
* @param workDir Path of encrypted folder.
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC
* authentication fails.
* @param name The name of the folder. Must be non-empty and only contain any of
* _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
* @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, final Collection<String> failingMacCollection, final String name) {
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection, final String name) {
try {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("name empty");
@@ -101,7 +101,7 @@ public final class WebDavServer {
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, failingMacCollection);
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
servletContext.addServlet(servlet, "/*");
servletCollection.mapContexts();
@@ -113,8 +113,8 @@ public final class WebDavServer {
}
}
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection));
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
return result;
}

View File

@@ -79,7 +79,7 @@ public class CleartextLocatorFactory implements DavLocatorFactory {
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
final String fullPrefix = pathPrefix.endsWith("/") ? pathPrefix : pathPrefix + "/";
final String href = fullPrefix.concat(encodedResourcePath);
assert !href.endsWith("/");
assert href.equals(fullPrefix) || !href.endsWith("/");
if (isCollection) {
return href.concat("/");
} else {

View File

@@ -5,10 +5,13 @@ import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.ExecutorService;
import java.nio.file.attribute.FileTime;
import java.time.format.DateTimeParseException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
@@ -21,22 +24,25 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.apache.logging.log4j.util.Strings;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
private static final String RANGE_BYTE_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final ExecutorService backgroundTaskExecutor;
private final Path dataRoot;
private final FilenameTranslator filenameTranslator;
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) {
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.backgroundTaskExecutor = backgroundTaskExecutor;
this.dataRoot = vaultRootPath.resolve("d");
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
}
@@ -47,20 +53,36 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
return createRootDirectory(locator, request.getDavSession());
}
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession(), dirFilePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request, filePath);
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession(), filePath);
} else {
// e.g. for MOVE operations:
return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath);
try {
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
// DIRECTORY
return createDirectory(locator, request.getDavSession(), dirFilePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
// FILE RANGE
final Pair<String, String> requestRange = getRequestRange(rangeHeader);
response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), requestRange, filePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && !isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
// FULL FILE (if-range not fulfilled)
return createFile(locator, request.getDavSession(), filePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && !isRangeSatisfiable(rangeHeader)) {
// FULL FILE (unsatisfiable range)
response.setStatus(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
final EncryptedFile file = createFile(locator, request.getDavSession(), filePath);
response.addHeader(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + file.getContentLength());
return file;
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
// FULL FILE (as requested)
return createFile(locator, request.getDavSession(), filePath);
}
} catch (NonExistingParentException e) {
// return non-existing
}
return createNonExisting(locator, request.getDavSession());
}
@Override
@@ -69,16 +91,18 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
return createRootDirectory(locator, session);
}
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
if (Files.exists(dirFilePath)) {
return createDirectory(locator, session, dirFilePath);
} else if (Files.exists(filePath)) {
return createFile(locator, session, filePath);
} else {
// e.g. for MOVE operations:
return createNonExisting(locator, session, filePath, dirFilePath);
try {
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
if (Files.exists(dirFilePath)) {
return createDirectory(locator, session, dirFilePath);
} else if (Files.exists(filePath)) {
return createFile(locator, session, filePath);
}
} catch (NonExistingParentException e) {
// return non-existing
}
return createNonExisting(locator, session);
}
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
@@ -90,42 +114,109 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws IOException
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
*/
private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException {
private boolean isIfRangePreconditionFulfilled(String ifRangeHeader, Path filePath) throws DavException {
if (ifRangeHeader == null) {
// no header set -> fulfilled implicitly
return true;
} else {
try {
final FileTime expectedTime = FileTimeUtils.fromRfc1123String(ifRangeHeader);
final FileTime actualTime = Files.getLastModifiedTime(filePath);
return expectedTime.compareTo(actualTime) == 0;
} catch (DateTimeParseException e) {
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
}
/**
* @return <code>true</code> if and only if exactly one byte range has been requested.
*/
private boolean isRangeSatisfiable(String rangeHeader) {
assert rangeHeader != null;
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
return false;
}
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length != 1) {
return false;
}
return true;
}
/**
* Processes the given range header field, if it is supported. Only headers containing a single byte range are supported.<br/>
* <code>
* bytes=100-200<br/>
* bytes=-500<br/>
* bytes=1000-
* </code>
*
* @return Tuple of left and right range.
* @throws DavException HTTP statuscode 400 for malformed requests.
* @throws IllegalArgumentException If the given rangeHeader is not satisfiable. Check with {@link #isRangeSatisfiable(String)} before.
*/
private Pair<String, String> getRequestRange(String rangeHeader) throws DavException {
assert rangeHeader != null;
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
}
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length != 1) {
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
}
final String byteRange = byteRanges[0];
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "malformed range header: " + rangeHeader);
}
return new ImmutablePair<>(bytePos[0], bytePos[1]);
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws NonExistingParentException If one ancestor of the enrypted path is missing
*/
Path getEncryptedFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
throw new IORuntimeException(e);
}
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws IOException
* @throws NonExistingParentException If one ancestor of the enrypted path is missing
*/
private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException {
Path getEncryptedDirectoryFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
throw new IORuntimeException(e);
}
}
/**
* @param createNonExisting if <code>false</code>, a {@link NonExistingParentException} will be thrown for missing ancestors.
* @return Absolute directory path for a given cleartext directory resourcePath.
* @throws IOException
* @throws NonExistingParentException if one ancestor directory is missing.
*/
private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException {
private Path getEncryptedDirectoryPath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
try {
final Path result;
@@ -135,10 +226,13 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
result = dataRoot.resolve(fixedRootDirectory);
} else {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
final Path directoryFile = parent.resolve(encryptedFilename);
if (!createNonExisting && !Files.exists(directoryFile)) {
throw new NonExistingParentException();
}
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(directory);
@@ -146,12 +240,12 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
Files.createDirectories(result);
return result;
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
throw new IORuntimeException(e);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath);
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, Path filePath) {
return new EncryptedFilePart(this, locator, session, requestRange, lockManager, cryptor, cryptoWarningHandler, filePath);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {
@@ -178,8 +272,14 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
}
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session, Path filePath, Path dirFilePath) {
return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath);
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
return new NonExistingNode(this, locator, session, lockManager, cryptor);
}
static class NonExistingParentException extends Exception {
private static final long serialVersionUID = 4421121746624627094L;
}
}

View File

@@ -5,15 +5,22 @@ import java.util.Collection;
class CryptoWarningHandler {
private final Collection<String> resourcesWithInvalidMac;
private final Collection<String> whitelistedResources;
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac) {
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
this.whitelistedResources = whitelistedResources;
}
public void macAuthFailed(String resourceName) {
if (!resourcesWithInvalidMac.contains(resourceName)) {
resourcesWithInvalidMac.add(resourceName);
public void macAuthFailed(String resourcePath) {
// collection might be a list, but we don't want duplicates:
if (!resourcesWithInvalidMac.contains(resourcePath)) {
resourcesWithInvalidMac.add(resourcePath);
}
}
public boolean ignoreMac(String resourcePath) {
return whitelistedResources.contains(resourcePath);
}
}

View File

@@ -12,7 +12,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
@@ -20,6 +19,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
@@ -156,7 +157,10 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
final Path filePath = dirPath.resolve(ciphertextFilename);
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); final FileLock lock = c.lock(0L, FILE_HEADER_LENGTH, false)) {
final Path tmpFilePath = Files.createTempFile(dirPath, null, null);
// encrypt to tmp file:
try (final FileChannel c = FileChannel.open(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
cryptor.encryptFile(inputContext.getInputStream(), c);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
@@ -169,6 +173,13 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
} finally {
IOUtils.closeQuietly(inputContext.getInputStream());
}
// mv tmp to target file:
try {
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING);
}
Files.setLastModifiedTime(filePath, FileTime.from(Instant.now()));
} catch (IOException e) {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
@@ -260,7 +271,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getDirFilePath();
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
} else {
dstPath = dest.filePath;
}
@@ -278,7 +289,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
final Path dstDirFilePath;
if (dest instanceof NonExistingNode) {
dstDirFilePath = ((NonExistingNode) dest).getDirFilePath();
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
} else {
dstDirFilePath = dest.filePath;
}
@@ -289,7 +300,8 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
final String dstDirId = UUID.randomUUID().toString();
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
}

View File

@@ -11,9 +11,7 @@ package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -31,7 +29,6 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
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;
@@ -44,6 +41,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
protected final CryptoWarningHandler cryptoWarningHandler;
protected final Long contentLength;
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
@@ -51,9 +49,10 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
throw new IllegalArgumentException("filePath must not be null");
}
this.cryptoWarningHandler = cryptoWarningHandler;
Long contentLength = null;
if (Files.isRegularFile(filePath)) {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.tryLock(0L, FILE_HEADER_LENGTH, true)) {
final Long contentLength = cryptor.decryptedContentLength(c);
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
contentLength = cryptor.decryptedContentLength(c);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
@@ -61,14 +60,19 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
} catch (OverlappingFileLockException e) {
// file header currently locked, report -1 for unknown size.
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
} catch (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
} catch (MacAuthenticationFailedException e) {
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
// don't add content length DAV property
} catch (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
}
}
this.contentLength = contentLength;
}
public Long getContentLength() {
return contentLength;
}
@Override
@@ -96,20 +100,19 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
if (Files.isRegularFile(filePath)) {
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
final Long contentLength = cryptor.decryptedContentLength(c);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
}
if (outputContext.hasStream()) {
cryptor.decryptFile(channel, outputContext.getOutputStream());
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
outputContext.getOutputStream().flush();
}
} catch (EOFException e) {
LOG.warn("Unexpected end of stream (possibly client hung up).");
} catch (MacAuthenticationFailedException e) {
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + filePath.toString(), e);
}
}
}
@@ -119,7 +122,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getFilePath();
dstPath = ((NonExistingNode) dest).materializeFilePath();
} else {
dstPath = dest.filePath;
}
@@ -136,7 +139,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getFilePath();
dstPath = ((NonExistingNode) dest).materializeFilePath();
} else {
dstPath = dest.filePath;
}

View File

@@ -2,34 +2,22 @@ package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* Delivers only the requested range of bytes from a file.
*
@@ -38,157 +26,65 @@ import com.google.common.cache.CacheBuilder;
class EncryptedFilePart extends EncryptedFile {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
private static final String BYTE_UNIT_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
private static final Cache<DavResourceLocator, MacAuthenticationJob> cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
/**
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
*/
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
private final Pair<Long, Long> range;
/**
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
*/
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor, Path filePath) {
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, LockManager lockManager, Cryptor cryptor,
CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
}
determineByteRanges(rangeHeader);
synchronized (cachedMacAuthenticationJobs) {
if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
cachedMacAuthenticationJobs.put(locator, macAuthJob);
backgroundTaskExecutor.submit(macAuthJob);
}
}
}
private void determineByteRanges(String rangeHeader) {
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length == 0) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
for (final String byteRange : byteRanges) {
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
if (lower > upper) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
}
}
/**
* @return One range, that spans all requested ranges.
*/
private Pair<Long, Long> getUnionRange(Long fileSize) {
final long lastByte = fileSize - 1;
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
for (Pair<Long, Long> range : requestedContentRanges) {
final long left;
final long right;
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
left = lastByte - range.getRight();
right = lastByte;
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
left = range.getLeft();
right = lastByte;
try {
final Long lower = requestRange.getLeft().isEmpty() ? null : Long.valueOf(requestRange.getLeft());
final Long upper = requestRange.getRight().isEmpty() ? null : Long.valueOf(requestRange.getRight());
if (lower == null) {
range = new ImmutablePair<Long, Long>(contentLength - upper, contentLength - 1);
} else if (upper == null) {
range = new ImmutablePair<Long, Long>(lower, contentLength - 1);
} else {
left = range.getLeft();
right = range.getRight();
}
if (result.getLeft() == null || left < result.getLeft()) {
result.setLeft(left);
}
if (result.getRight() == null || right > result.getRight()) {
result.setRight(right);
range = new ImmutablePair<Long, Long>(lower, Math.min(upper, contentLength - 1));
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
}
return result;
}
@Override
public void spool(OutputContext outputContext) throws IOException {
assert Files.isRegularFile(filePath);
assert contentLength != null;
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
if (rangeLength <= 0 || range.getLeft() > contentLength - 1) {
// unsatisfiable content range:
outputContext.setContentLength(0);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
LOG.debug("Requested content range unsatisfiable: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
return;
} else {
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
}
assert range.getLeft() > 0;
assert range.getLeft() < contentLength;
assert range.getRight() < contentLength;
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
if (outputContext.hasStream()) {
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
outputContext.getOutputStream().flush();
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + filePath.toString(), e);
}
}
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
}
private class MacAuthenticationJob implements Runnable {
private final DavResourceLocator locator;
public MacAuthenticationJob(final DavResourceLocator locator) {
if (locator == null) {
throw new IllegalArgumentException("locator must not be null.");
}
this.locator = locator;
}
@Override
public void run() {
assert Files.isRegularFile(filePath);
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + filePath.toString(), e);
}
}
@Override
public int hashCode() {
return locator.hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof MacAuthenticationJob) {
final MacAuthenticationJob other = (MacAuthenticationJob) obj;
return this.locator.equals(other.locator);
} else {
return false;
}
}
return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength);
}
}

View File

@@ -21,7 +21,7 @@ interface FileConstants {
/**
* Number of bytes in the file header.
*/
long FILE_HEADER_LENGTH = 96;
long FILE_HEADER_LENGTH = 104;
/**
* Allow range requests for files > 32MiB.

View File

@@ -5,7 +5,6 @@ import java.io.IOException;
import java.io.Serializable;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
@@ -130,13 +129,14 @@ class FilenameTranslator implements FileConstants {
/* Locked I/O */
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
c.write(ByteBuffer.wrap(bytes));
}
}
private byte[] readAllBytesAtomically(Path path) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
return buffer.array();

View File

@@ -21,16 +21,12 @@ import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
class NonExistingNode extends AbstractEncryptedNode {
private final Path filePath;
private final Path dirFilePath;
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) {
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor, null);
this.filePath = filePath;
this.dirFilePath = dirFilePath;
}
@Override
@@ -83,12 +79,26 @@ class NonExistingNode extends AbstractEncryptedNode {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
public Path getFilePath() {
return filePath;
/**
* @return lazily resolved file path, e.g. needed during MOVE operations.
*/
public Path materializeFilePath() {
try {
return factory.getEncryptedFilePath(locator.getResourcePath(), true);
} catch (NonExistingParentException e) {
throw new IllegalStateException(e);
}
}
public Path getDirFilePath() {
return dirFilePath;
/**
* @return lazily resolved directory file path, e.g. needed during MOVE operations.
*/
public Path materializeDirFilePath() {
try {
return factory.getEncryptedDirectoryFilePath(locator.getResourcePath(), true);
} catch (NonExistingParentException e) {
throw new IllegalStateException(e);
}
}
}

View File

@@ -0,0 +1,56 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.OverlappingFileLockException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Instances of this class wrap a file lock, that is created upon construction and destroyed by {@link #close()}.
*
* If the construction fails (e.g. if the file system does not support locks) no exception will be thrown and no lock is created.
*/
class SilentlyFailingFileLock implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(SilentlyFailingFileLock.class);
private final FileLock lock;
/**
* Invokes #SilentlyFailingFileLock(FileChannel, long, long, boolean) with a position of 0 and a size of {@link Long#MAX_VALUE}.
*/
SilentlyFailingFileLock(FileChannel channel, boolean shared) {
this(channel, 0L, Long.MAX_VALUE, shared);
}
/**
* @throws NonReadableChannelException If shared is true this channel was not opened for reading
* @throws NonWritableChannelException If shared is false but this channel was not opened for writing
* @see FileChannel#lock(long, long, boolean)
*/
SilentlyFailingFileLock(FileChannel channel, long position, long size, boolean shared) {
FileLock lock = null;
try {
lock = channel.tryLock(position, size, shared);
} catch (IOException | OverlappingFileLockException e) {
if (LOG.isDebugEnabled()) {
LOG.trace("Unable to lock file.");
}
} finally {
this.lock = lock;
}
}
@Override
public void close() throws IOException {
if (lock != null) {
lock.close();
}
}
}

View File

@@ -8,63 +8,50 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
import org.apache.jackrabbit.webdav.WebdavResponse;
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebDavServlet extends AbstractWebdavServlet {
private static final long serialVersionUID = 7965170007048673022L;
private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
public static final String CFG_FS_ROOT = "cfg.fs.root";
private DavSessionProvider davSessionProvider;
private DavLocatorFactory davLocatorFactory;
private DavResourceFactory davResourceFactory;
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private ExecutorService backgroundTaskExecutor;
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
super();
this.cryptor = cryptor;
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
backgroundTaskExecutor = Executors.newCachedThreadPool();
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot);
}
@Override
public void destroy() {
backgroundTaskExecutor.shutdown();
try {
final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS);
if (!tasksFinished) {
backgroundTaskExecutor.shutdownNow();
}
} catch (InterruptedException e) {
backgroundTaskExecutor.shutdownNow();
Thread.currentThread().interrupt();
} finally {
super.destroy();
}
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
}
@Override
@@ -102,4 +89,30 @@ public class WebDavServlet extends AbstractWebdavServlet {
this.davResourceFactory = resourceFactory;
}
@Override
protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
long t0 = System.nanoTime();
super.doPut(request, response, resource);
if (LOG.isDebugEnabled()) {
long t1 = System.nanoTime();
LOG.trace("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
}
}
@Override
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
long t0 = System.nanoTime();
try {
super.doGet(request, response, resource);
} catch (MacAuthenticationFailedException e) {
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
if (LOG.isDebugEnabled()) {
long t1 = System.nanoTime();
LOG.trace("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
}
}
}

View File

@@ -0,0 +1,271 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager;
import org.apache.commons.httpclient.methods.ByteArrayRequestEntity;
import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PutMethod;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.io.Files;
public class RangeRequestTest {
private static final Logger LOG = LoggerFactory.getLogger(RangeRequestTest.class);
private static final Aes256Cryptor CRYPTOR = new Aes256Cryptor();
private static final WebDavServer SERVER = new WebDavServer();
private static final File TMP_VAULT = Files.createTempDir();
private static ServletLifeCycleAdapter SERVLET;
private static URI VAULT_BASE_URI;
@BeforeClass
public static void startServer() throws URISyntaxException {
SERVER.start();
SERVLET = SERVER.createServlet(TMP_VAULT.toPath(), CRYPTOR, new ArrayList<String>(), new ArrayList<String>(), "JUnitTestVault");
SERVLET.start();
VAULT_BASE_URI = new URI("http", SERVLET.getServletUri().getSchemeSpecificPart() + "/", null);
Assert.assertTrue(SERVLET.isRunning());
Assert.assertNotNull(VAULT_BASE_URI);
}
@AfterClass
public static void stopServer() {
SERVLET.stop();
SERVER.stop();
FileUtils.deleteQuietly(TMP_VAULT);
}
@Test
public void testFullFileDecryption() throws IOException, URISyntaxException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "fullFileDecryptionTestFile.txt");
final HttpClient client = new HttpClient();
// prepare 64MiB test data:
final byte[] plaintextData = new byte[16777216 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 16777216; i++) {
bbIn.putInt(i);
}
final InputStream plaintextDataInputStream = new ByteArrayInputStream(plaintextData);
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// get request:
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
final int statusCode = client.executeMethod(getMethod);
Assert.assertEquals(200, statusCode);
// final byte[] received = new byte[plaintextData.length];
// IOUtils.read(getMethod.getResponseBodyAsStream(), received);
// Assert.assertArrayEquals(plaintextData, received);
Assert.assertTrue(IOUtils.contentEquals(plaintextDataInputStream, getMethod.getResponseBodyAsStream()));
getMethod.releaseConnection();
}
@Test
public void testAsyncRangeRequests() throws IOException, URISyntaxException, InterruptedException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "asyncRangeRequestTestFile.txt");
final MultiThreadedHttpConnectionManager cm = new MultiThreadedHttpConnectionManager();
cm.getParams().setDefaultMaxConnectionsPerHost(50);
final HttpClient client = new HttpClient(cm);
// prepare 8MiB test data:
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 2097152; i++) {
bbIn.putInt(i);
}
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// multiple async range requests:
final List<ForkJoinTask<?>> tasks = new ArrayList<>();
final Random generator = new Random(System.currentTimeMillis());
final AtomicBoolean success = new AtomicBoolean(true);
// 10 full interrupted requests:
for (int i = 0; i < 10; i++) {
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
final int statusCode = client.executeMethod(getMethod);
if (statusCode != 200) {
LOG.error("Invalid status code for interrupted full request");
success.set(false);
}
getMethod.getResponseBodyAsStream().read();
getMethod.getResponseBodyAsStream().close();
getMethod.releaseConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 50 crappy interrupted range requests:
for (int i = 0; i < 50; i++) {
final int lower = generator.nextInt(plaintextData.length);
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
final int statusCode = client.executeMethod(getMethod);
if (statusCode != 206) {
LOG.error("Invalid status code for interrupted range request");
success.set(false);
}
getMethod.getResponseBodyAsStream().read();
getMethod.getResponseBodyAsStream().close();
getMethod.releaseConnection();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 50 normal open range requests:
for (int i = 0; i < 50; i++) {
final int lower = generator.nextInt(plaintextData.length - 512);
final int upper = plaintextData.length - 1;
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-");
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
final int statusCode = client.executeMethod(getMethod);
final byte[] responseBody = new byte[upper - lower + 10];
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
getMethod.releaseConnection();
if (statusCode != 206) {
LOG.error("Invalid status code for open range request");
success.set(false);
} else if (upper - lower + 1 != bytesRead) {
LOG.error("Invalid response length for open range request");
success.set(false);
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
LOG.error("Invalid response body for open range request");
success.set(false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
// 200 normal closed range requests:
for (int i = 0; i < 200; i++) {
final int pos1 = generator.nextInt(plaintextData.length - 512);
final int pos2 = pos1 + 512;
final ForkJoinTask<?> task = ForkJoinTask.adapt(() -> {
try {
final int lower = Math.min(pos1, pos2);
final int upper = Math.max(pos1, pos2);
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "bytes=" + lower + "-" + upper);
final byte[] expected = Arrays.copyOfRange(plaintextData, lower, upper + 1);
final int statusCode = client.executeMethod(getMethod);
final byte[] responseBody = new byte[upper - lower + 1];
final int bytesRead = IOUtils.read(getMethod.getResponseBodyAsStream(), responseBody);
getMethod.releaseConnection();
if (statusCode != 206) {
LOG.error("Invalid status code for closed range request");
success.set(false);
} else if (upper - lower + 1 != bytesRead) {
LOG.error("Invalid response length for closed range request");
success.set(false);
} else if (!Arrays.equals(expected, Arrays.copyOfRange(responseBody, 0, bytesRead))) {
LOG.error("Invalid response body for closed range request");
success.set(false);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
});
tasks.add(task);
}
Collections.shuffle(tasks, generator);
final ForkJoinPool pool = new ForkJoinPool(4);
for (ForkJoinTask<?> task : tasks) {
pool.execute(task);
}
for (ForkJoinTask<?> task : tasks) {
task.join();
}
pool.shutdown();
cm.shutdown();
Assert.assertTrue(success.get());
}
@Test
public void testUnsatisfiableRangeRequest() throws IOException, URISyntaxException {
final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "unsatisfiableRangeRequestTestFile.txt");
final HttpClient client = new HttpClient();
// prepare file content:
final byte[] fileContent = "This is some test file content.".getBytes();
// put request:
final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString());
putMethod.setRequestEntity(new ByteArrayRequestEntity(fileContent));
final int putResponse = client.executeMethod(putMethod);
putMethod.releaseConnection();
Assert.assertEquals(201, putResponse);
// get request:
final HttpMethod getMethod = new GetMethod(testResourceUrl.toString());
getMethod.addRequestHeader("Range", "chunks=1-2");
final int getResponse = client.executeMethod(getMethod);
final byte[] response = new byte[fileContent.length];
IOUtils.read(getMethod.getResponseBodyAsStream(), response);
getMethod.releaseConnection();
Assert.assertEquals(416, getResponse);
Assert.assertArrayEquals(fileContent, response);
}
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - log4j config for WebDAV unit tests
-->
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG" />
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

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

View File

@@ -8,11 +8,12 @@
******************************************************************************/
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.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
@@ -21,39 +22,38 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(Aes256Cryptor.class);
/**
* 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/.
@@ -211,7 +211,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
} catch (InvalidKeyException ex) {
throw new IllegalArgumentException("Invalid key.", ex);
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
throw new IllegalStateException("Algorithm/Padding should exist.", ex);
}
}
@@ -308,8 +308,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
// read header:
encryptedFile.position(0);
final ByteBuffer headerBuf = ByteBuffer.allocate(64);
final int headerBytesRead = encryptedFile.read(headerBuf);
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
return null;
}
@@ -319,20 +319,20 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
headerBuf.position(0);
headerBuf.get(iv);
// read content length:
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
headerBuf.position(16);
headerBuf.get(encryptedContentLengthBytes);
// read sensitive header data:
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
headerBuf.position(24);
headerBuf.get(encryptedSensitiveHeaderContentBytes);
// read stored header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(32);
headerBuf.position(72);
headerBuf.get(storedHeaderMac);
// calculate mac over first 32 bytes of header:
// calculate mac over first 72 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.rewind();
headerBuf.limit(32);
headerBuf.limit(72);
headerMac.update(headerBuf);
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
@@ -340,76 +340,38 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
throw new MacAuthenticationFailedException("MAC authentication failed.");
}
return decryptContentLength(encryptedContentLengthBytes, iv);
// decrypt sensitive header data:
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
final Long fileSize = sensitiveHeaderContentBuf.getLong();
return fileSize;
}
private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) {
private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) {
try {
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes);
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
return fileSizeBuffer.getLong();
return sizeCipher.doFinal(ciphertextBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException(e);
}
}
private byte[] encryptContentLength(long contentLength, byte[] iv) {
private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
try {
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
fileSizeBuffer.putLong(contentLength);
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
return sizeCipher.doFinal(fileSizeBuffer.array());
return sizeCipher.doFinal(plaintextBytes);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
}
}
@Override
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
final int headerBytesRead = encryptedFile.read(headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
// read header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(32);
headerBuf.get(storedHeaderMac);
// read content mac:
final byte[] storedContentMac = new byte[32];
headerBuf.position(64);
headerBuf.get(storedContentMac);
// calculate mac over first 32 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(32);
headerMac.update(headerBuf);
// calculate mac over content:
encryptedFile.position(96l);
final Mac contentMac = this.hmacSha256(hMacMasterKey);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, contentMac);
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal());
return headerMacMatches && contentMacMatches;
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
final int headerBytesRead = encryptedFile.read(headerBuf);
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
@@ -419,134 +381,361 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
headerBuf.position(0);
headerBuf.get(iv);
// read content length:
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
// read nonce:
final byte[] nonce = new byte[8];
headerBuf.position(16);
headerBuf.get(encryptedContentLengthBytes);
final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv);
headerBuf.get(nonce);
// read sensitive header data:
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
headerBuf.position(24);
headerBuf.get(encryptedSensitiveHeaderContentBytes);
// read header mac:
final byte[] headerMac = new byte[32];
headerBuf.position(32);
headerBuf.get(headerMac);
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(72);
headerBuf.get(storedHeaderMac);
// read content mac:
final byte[] contentMac = new byte[32];
headerBuf.position(64);
headerBuf.get(contentMac);
// decrypt content
encryptedFile.position(96l);
final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey);
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, calculatedContentMac);
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(contentMac, calculatedContentMac.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.");
// calculate mac over first 72 bytes of header:
if (authenticate) {
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(72);
headerMac.update(headerBuf);
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
}
}
return bytesDecrypted;
// decrypt sensitive header data:
final byte[] fileKeyBytes = new byte[32];
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
final Long fileSize = sensitiveHeaderContentBuf.getLong();
sensitiveHeaderContentBuf.get(fileKeyBytes);
// prepare content decryption:
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
final LengthLimitingOutputStream paddingRemovingOutputStream = new LengthLimitingOutputStream(plaintextFile, fileSize);
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
return new DecryptWorker(lock, blockDone, currentBlock, inputQueue, authenticate, Channels.newChannel(paddingRemovingOutputStream)) {
@Override
protected Cipher initCipher(long startBlockNum) {
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
nonceAndCounterBuf.put(nonce);
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
}
@Override
protected Mac initMac() {
return hmacSha256(hMacMasterKey);
}
@Override
protected void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException {
mac.update(iv);
mac.update(longToByteArray(blockNum));
mac.update(ciphertextBuf);
final byte[] calculatedMac = mac.doFinal();
final byte[] storedMac = new byte[mac.getMacLength()];
macBuf.get(storedMac);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
@Override
protected void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException {
assert plaintextBuf.remaining() >= cipher.getOutputSize(ciphertextBuf.remaining());
try {
cipher.update(ciphertextBuf, plaintextBuf);
} catch (ShortBufferException e) {
throw new DecryptFailedException(e);
}
}
};
});
// read as many blocks from file as possible, but wait if queue is full:
encryptedFile.position(104l);
final int maxNumBlocks = 64;
int numBlocks = 1;
int bytesRead = 0;
long blockNumber = 0;
do {
if (numBlocks < maxNumBlocks) {
numBlocks++;
}
final int inBufSize = numBlocks * (CONTENT_MAC_BLOCK + 32);
final ByteBuffer buf = ByteBuffer.allocate(inBufSize);
bytesRead = readFromChannel(encryptedFile, buf);
buf.flip();
final int blocksRead = (int) Math.ceil(bytesRead / (double) (CONTENT_MAC_BLOCK + 32));
final boolean consumedInTime = executor.offer(new BlocksData(buf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
if (!consumedInTime) {
break;
}
blockNumber += numBlocks;
} while (bytesRead == numBlocks * (CONTENT_MAC_BLOCK + 32));
// wait for decryption workers to finish:
try {
executor.waitUntilDone();
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
LOG.error("Unexpected exception", e);
}
} finally {
destroyQuietly(fileKey);
}
return paddingRemovingOutputStream.getBytesWritten();
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
// read iv:
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
// read header:
encryptedFile.position(0l);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numIvBytesRead = encryptedFile.read(countingIv);
// check validity of header:
if (numIvBytesRead != AES_BLOCK_LENGTH) {
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
// read iv:
final byte[] iv = new byte[AES_BLOCK_LENGTH];
headerBuf.position(0);
headerBuf.get(iv);
// fast forward stream:
encryptedFile.position(96l + beginOfFirstRelevantBlock);
// read nonce:
final byte[] nonce = new byte[8];
headerBuf.position(16);
headerBuf.get(nonce);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
// read sensitive header data:
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
headerBuf.position(24);
headerBuf.get(encryptedSensitiveHeaderContentBytes);
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
// read header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(72);
headerBuf.get(storedHeaderMac);
// calculate mac over first 72 bytes of header:
if (authenticate) {
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(72);
headerMac.update(headerBuf);
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
}
}
// decrypt sensitive header data:
final byte[] fileKeyBytes = new byte[32];
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
final Long fileSize = sensitiveHeaderContentBuf.getLong();
sensitiveHeaderContentBuf.get(fileKeyBytes);
assert pos + length - 1 < fileSize;
// find first relevant block:
final long startBlock = pos / CONTENT_MAC_BLOCK; // floor
final long startByte = startBlock * (CONTENT_MAC_BLOCK + 32) + 104;
final long offsetFromFirstBlock = pos - startBlock * CONTENT_MAC_BLOCK;
// append correct counter value to nonce:
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
nonceAndCounterBuf.put(nonce);
nonceAndCounterBuf.putLong(startBlock * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
// content decryption:
encryptedFile.position(startByte);
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
final Mac contentMac = this.hmacSha256(hMacMasterKey);
try {
// reading ciphered input and MACs interleaved:
long bytesWritten = 0;
final ByteBuffer buf = ByteBuffer.allocate(CONTENT_MAC_BLOCK + 32);
int n = 0;
long blockNum = startBlock;
while ((n = readFromChannel(encryptedFile, buf)) > 0 && bytesWritten < length) {
if (n < 32) {
throw new DecryptFailedException("Invalid file content, missing MAC.");
}
buf.flip();
final ByteBuffer ciphertextBuf = buf.asReadOnlyBuffer();
ciphertextBuf.limit(n - 32);
// check MAC of current block:
if (authenticate) {
final byte[] storedMac = new byte[contentMac.getMacLength()];
final ByteBuffer storedMacBuf = buf.asReadOnlyBuffer();
storedMacBuf.position(n - 32);
storedMacBuf.get(storedMac);
contentMac.update(iv);
contentMac.update(longToByteArray(blockNum));
contentMac.update(ciphertextBuf);
ciphertextBuf.rewind();
final byte[] calculatedMac = contentMac.doFinal();
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
// decrypt block:
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(ciphertextBuf.remaining()));
cipher.update(ciphertextBuf, plaintextBuf);
plaintextBuf.flip();
final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
final long pending = length - bytesWritten;
final int available = plaintextBuf.remaining() - offset;
final int currentBatch = (int) Math.min(pending, available);
plaintextFile.write(plaintextBuf.array(), offset, currentBatch);
bytesWritten += currentBatch;
blockNum++;
buf.rewind();
}
return bytesWritten;
} catch (ShortBufferException e) {
throw new IllegalStateException("Output buffer size known to fit.", e);
} finally {
destroyQuietly(fileKey);
}
}
/**
* header = {16 byte iv, 16 byte filesize, 32 byte headerMac, 32 byte contentMac}
* header = {16 byte iv, 8 byte nonce, 48 byte sensitive header data (file size + file key + padding), 32 byte headerMac}
*/
@Override
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
// truncate file
encryptedFile.truncate(0l);
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
final byte[] iv = ivBuf.array();
// choose a random header IV:
final byte[] iv = randomData(AES_BLOCK_LENGTH);
// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
headerBuf.limit(96);
// chosse 8 byte random nonce and 8 byte counter set to zero:
final byte[] nonce = randomData(8);
// choose a random content key:
final byte[] fileKeyBytes = randomData(32);
// 104 byte header buffer (16 header IV, 8 content nonce, 48 sensitive header data, 32 headerMac), filled after writing the content
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
headerBuf.limit(104);
encryptedFile.write(headerBuf);
// content encryption:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
final Mac contentMac = this.hmacSha256(hMacMasterKey);
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream macOut = new MacOutputStream(out, contentMac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
final Long plaintextSize;
try {
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
} catch (CounterAwareInputLimitReachedException ex) {
encryptedFile.truncate(0l);
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
}
// prepare content encryption:
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
return new EncryptWorker(lock, blockDone, currentBlock, inputQueue, encryptedFile) {
// add random length padding to obfuscate file length:
final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final long minAdditionalBlocks = 4;
final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs)
final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks
final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks);
@Override
protected Cipher initCipher(long startBlockNum) {
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
nonceAndCounterBuf.put(nonce);
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.ENCRYPT_MODE);
}
@Override
protected Mac initMac() {
return hmacSha256(hMacMasterKey);
}
@Override
protected byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf) {
mac.update(iv);
mac.update(longToByteArray(blockNum));
mac.update(ciphertextBuf);
return mac.doFinal();
}
@Override
protected void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException {
try {
assert ciphertextBuf.remaining() >= cipher.getOutputSize(plaintextBuf.remaining());
cipher.update(plaintextBuf, ciphertextBuf);
} catch (ShortBufferException e) {
throw new EncryptFailedException(e);
}
}
};
});
// read as many blocks from file as possible, but wait if queue is full:
final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(randomPadding);
final LengthObfuscatingInputStream in = new LengthObfuscatingInputStream(plaintextFile, randomPadding);
final ReadableByteChannel channel = Channels.newChannel(in);
int bytesRead = 0;
long blockNumber = 0;
final int maxNumBlocks = 64;
int numBlocks = 0;
do {
if (numBlocks < maxNumBlocks) {
numBlocks++;
}
final int inBufSize = numBlocks * CONTENT_MAC_BLOCK;
final ByteBuffer inBuf = ByteBuffer.allocate(inBufSize);
bytesRead = readFromChannel(channel, inBuf);
inBuf.flip();
final int blocksRead = (int) Math.ceil(bytesRead / (double) CONTENT_MAC_BLOCK);
final boolean consumedInTime = executor.offer(new BlocksData(inBuf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
if (!consumedInTime) {
break;
}
blockNumber += numBlocks;
} while (bytesRead == numBlocks * CONTENT_MAC_BLOCK);
// wait for encryption workers to finish:
try {
executor.waitUntilDone();
} catch (ExecutionException e) {
final Throwable cause = e.getCause();
if (cause instanceof IOException) {
throw (IOException) cause;
} else if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else {
LOG.error("Unexpected exception", e);
}
} finally {
destroyQuietly(fileKey);
}
blockSizeBufferedOut.flush();
// create and write header:
final long plaintextSize = in.getRealInputLength();
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.allocate(Long.BYTES + fileKeyBytes.length);
sensitiveHeaderContentBuf.putLong(plaintextSize);
sensitiveHeaderContentBuf.put(fileKeyBytes);
headerBuf.clear();
headerBuf.put(iv);
headerBuf.put(encryptContentLength(plaintextSize, iv));
headerBuf.put(nonce);
headerBuf.put(encryptHeaderData(sensitiveHeaderContentBuf.array(), iv));
headerBuf.flip();
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerMac.update(headerBuf);
headerBuf.limit(96);
headerBuf.limit(104);
headerBuf.put(headerMac.doFinal());
headerBuf.put(contentMac.doFinal());
headerBuf.flip();
encryptedFile.position(0);
encryptedFile.write(headerBuf);
@@ -554,4 +743,33 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
return plaintextSize;
}
private byte[] longToByteArray(long lng) {
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
}
/**
* Reads bytes from a ReadableByteChannel.
* <p>
* This implementation guarantees that it will read as many bytes
* as possible before giving up; this may not always be the case for
* subclasses of {@link ReadableByteChannel}.
*
* @param input the byte channel to read
* @param buffer byte buffer destination
* @return the actual length read; may be less than requested if EOF was reached
* @throws IOException if a read error occurs
* @see
* <a href="http://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/IOUtils.html">Apache Commons IOUtils 2.5</a>
*/
public static int readFromChannel(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException {
final int length = buffer.remaining();
while (buffer.remaining() > 0) {
final int count = input.read(buffer);
if (count == -1) { // EOF
break;
}
}
return length - buffer.remaining();
}
}

View File

@@ -81,6 +81,11 @@ interface AesCryptographicConfiguration {
*/
int AES_BLOCK_LENGTH = 16;
/**
* Number of bytes, a content block over which a MAC is calculated consists of.
*/
int CONTENT_MAC_BLOCK = 32 * 1024;
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/

View File

@@ -0,0 +1,22 @@
package org.cryptomator.crypto.aes256;
import java.nio.ByteBuffer;
class BlocksData {
public static final int MAX_NUM_BLOCKS = 128;
final ByteBuffer buffer;
final long startBlockNum;
final int numBlocks;
BlocksData(ByteBuffer buffer, long startBlockNum, int numBlocks) {
if (numBlocks > MAX_NUM_BLOCKS) {
throw new IllegalArgumentException("Too many blocks to process at once: " + numBlocks);
}
this.buffer = buffer;
this.startBlockNum = startBlockNum;
this.numBlocks = numBlocks;
}
}

View File

@@ -1,57 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicLong;
/**
* Throws an exception, if more than (2^32)-1 16 byte blocks will be encrypted (would result in an counter overflow).<br/>
* From https://tools.ietf.org/html/rfc3686: <cite> Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks</cite>
*/
class CounterAwareInputStream extends FilterInputStream {
static final long SIXTY_FOUR_GIGABYE = ((1l << 32) - 1) * 16;
private final AtomicLong counter;
/**
* @param in Stream from which to read contents, which will update the Mac.
*/
public CounterAwareInputStream(InputStream in) {
super(in);
this.counter = new AtomicLong(0l);
}
@Override
public int read() throws IOException {
int b = in.read();
if (b != -1) {
final long currentValue = counter.incrementAndGet();
failWhen64GibReached(currentValue);
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
if (read > 0) {
final long currentValue = counter.addAndGet(read);
failWhen64GibReached(currentValue);
}
return read;
}
private void failWhen64GibReached(long currentValue) throws CounterAwareInputLimitReachedException {
if (currentValue > SIXTY_FOUR_GIGABYE) {
throw new CounterAwareInputLimitReachedException();
}
}
static class CounterAwareInputLimitReachedException extends IOException {
private static final long serialVersionUID = -1905012809288019359L;
}
}

View File

@@ -0,0 +1,64 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import org.cryptomator.crypto.exceptions.CryptingException;
abstract class CryptoWorker implements Callable<Void> {
static final BlocksData POISON = new BlocksData(ByteBuffer.allocate(0), -1L, 0);
final Lock lock;
final Condition blockDone;
final AtomicLong currentBlock;
final BlockingQueue<BlocksData> queue;
public CryptoWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue) {
this.lock = lock;
this.blockDone = blockDone;
this.currentBlock = currentBlock;
this.queue = queue;
}
@Override
public final Void call() throws IOException {
try {
while (!Thread.currentThread().isInterrupted()) {
final BlocksData blocksData = queue.take();
if (blocksData == POISON) {
// put poison back in for other threads:
break;
}
final ByteBuffer processedBytes = this.process(blocksData);
lock.lock();
try {
while (currentBlock.get() != blocksData.startBlockNum) {
blockDone.await();
}
assert currentBlock.get() == blocksData.startBlockNum;
// yay, its my turn!
this.write(processedBytes);
// signal worker working on next block:
currentBlock.set(blocksData.startBlockNum + blocksData.numBlocks);
blockDone.signalAll();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
protected abstract ByteBuffer process(BlocksData block) throws CryptingException;
protected abstract void write(ByteBuffer processedBytes) throws IOException;
}

View File

@@ -0,0 +1,112 @@
package org.cryptomator.crypto.aes256;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class CryptoWorkerExecutor {
private static final Logger LOG = LoggerFactory.getLogger(CryptoWorkerExecutor.class);
private final int numWorkers;
private final Lock lock;
private final Condition blockDone;
private final AtomicLong currentBlock;
private final BlockingQueue<BlocksData> inputQueue;
private final ExecutorService executorService;
private final CompletionService<Void> completionService;
private boolean acceptWork;
/**
* Starts as many {@link CryptoWorker} as specified in the constructor, that start working immediately on the items submitted via {@link #offer(BlocksData, long, TimeUnit)}.
*/
public CryptoWorkerExecutor(int numWorkers, WorkerFactory workerFactory) {
this.numWorkers = numWorkers;
this.lock = new ReentrantLock();
this.blockDone = lock.newCondition();
this.currentBlock = new AtomicLong();
this.inputQueue = new LinkedBlockingQueue<>(numWorkers * 2); // one cycle read-ahead
this.executorService = Executors.newFixedThreadPool(numWorkers);
this.completionService = new ExecutorCompletionService<>(executorService);
this.acceptWork = true;
// start workers:
for (int i = 0; i < numWorkers; i++) {
final CryptoWorker worker = workerFactory.createWorker(lock, blockDone, currentBlock, inputQueue);
completionService.submit(worker);
}
}
/**
* Adds work to the work queue. On timeout all workers will be shut down.
*
* @see BlockingQueue#offer(Object, long, TimeUnit)
* @return <code>true</code> if the work has been added in time. <code>false</code> in any other case.
*/
public boolean offer(BlocksData data, long timeout, TimeUnit unit) {
if (!acceptWork) {
return false;
}
try {
final boolean success = inputQueue.offer(data, timeout, unit);
if (!success) {
this.acceptWork = false;
inputQueue.clear();
poisonWorkers();
}
return success;
} catch (InterruptedException e) {
LOG.error("Interrupted thread.", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
return false;
}
/**
* Graceful shutdown of this executor, waiting for all jobs to finish (normally or by throwing exceptions).
*
* @throws ExecutionException If any of the workers failed.
*/
public void waitUntilDone() throws ExecutionException {
this.acceptWork = false;
try {
poisonWorkers();
// now workers will one after another finish their work, potentially throwing an ExecutionException:
for (int i = 0; i < numWorkers; i++) {
completionService.take().get();
}
} catch (InterruptedException e) {
LOG.error("Interrupted thread.", e);
Thread.currentThread().interrupt();
} finally {
// shutdown either after normal decryption or if ANY worker threw an exception:
executorService.shutdownNow();
}
}
private void poisonWorkers() throws InterruptedException {
// add enough poison for each worker:
for (int i = 0; i < numWorkers; i++) {
inputQueue.put(CryptoWorker.POISON);
}
}
@FunctionalInterface
interface WorkerFactory {
CryptoWorker createWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> inputQueue);
}
}

View File

@@ -0,0 +1,75 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import org.cryptomator.crypto.exceptions.CryptingException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
abstract class DecryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
private final boolean shouldAuthenticate;
private final WritableByteChannel out;
public DecryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, boolean shouldAuthenticate, WritableByteChannel out) {
super(lock, blockDone, currentBlock, queue);
this.shouldAuthenticate = shouldAuthenticate;
this.out = out;
}
@Override
protected ByteBuffer process(BlocksData data) throws CryptingException {
final Cipher cipher = initCipher(data.startBlockNum);
final Mac mac = initMac();
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(CONTENT_MAC_BLOCK) * data.numBlocks);
final ByteBuffer ciphertextBuf = data.buffer.asReadOnlyBuffer();
final ByteBuffer macBuf = data.buffer.asReadOnlyBuffer();
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
assert (blockNum - data.startBlockNum) < BlocksData.MAX_NUM_BLOCKS;
assert (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK < Integer.MAX_VALUE;
final int pos = (int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength());
ciphertextBuf.limit(Math.min(data.buffer.limit() - mac.getMacLength(), pos + CONTENT_MAC_BLOCK));
ciphertextBuf.position(pos);
try {
macBuf.limit(ciphertextBuf.limit() + mac.getMacLength());
macBuf.position(ciphertextBuf.limit());
} catch (IllegalArgumentException e) {
throw new DecryptFailedException("Invalid file content, missing MAC.");
}
if (shouldAuthenticate) {
checkMac(mac, blockNum, ciphertextBuf, macBuf);
}
ciphertextBuf.position(pos);
decrypt(cipher, ciphertextBuf, plaintextBuf);
}
plaintextBuf.flip();
return plaintextBuf;
}
@Override
protected void write(ByteBuffer processedBytes) throws IOException {
out.write(processedBytes);
}
protected abstract Cipher initCipher(long startBlockNum);
protected abstract Mac initMac();
protected abstract void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException;
protected abstract void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException;
}

View File

@@ -0,0 +1,61 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.WritableByteChannel;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import org.cryptomator.crypto.exceptions.CryptingException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
abstract class EncryptWorker extends CryptoWorker implements AesCryptographicConfiguration {
private final WritableByteChannel out;
public EncryptWorker(Lock lock, Condition blockDone, AtomicLong currentBlock, BlockingQueue<BlocksData> queue, WritableByteChannel out) {
super(lock, blockDone, currentBlock, queue);
this.out = out;
}
@Override
protected ByteBuffer process(BlocksData data) throws CryptingException {
final Cipher cipher = initCipher(data.startBlockNum);
final Mac mac = initMac();
final ByteBuffer ciphertextBuf = ByteBuffer.allocate((cipher.getOutputSize(CONTENT_MAC_BLOCK) + mac.getMacLength()) * data.numBlocks);
final ByteBuffer plaintextBuf = data.buffer.asReadOnlyBuffer();
for (long blockNum = data.startBlockNum; blockNum < data.startBlockNum + data.numBlocks; blockNum++) {
final int pos = (int) (blockNum - data.startBlockNum) * CONTENT_MAC_BLOCK;
plaintextBuf.limit(Math.min(data.buffer.limit(), pos + CONTENT_MAC_BLOCK));
encrypt(cipher, plaintextBuf, ciphertextBuf);
final ByteBuffer toMac = ciphertextBuf.asReadOnlyBuffer();
toMac.limit(ciphertextBuf.position());
toMac.position((int) (blockNum - data.startBlockNum) * (CONTENT_MAC_BLOCK + mac.getMacLength()));
ciphertextBuf.put(calcMac(mac, blockNum, toMac));
}
ciphertextBuf.flip();
return ciphertextBuf;
}
@Override
protected void write(ByteBuffer processedBytes) throws IOException {
out.write(processedBytes);
}
protected abstract Cipher initCipher(long startBlockNum);
protected abstract Mac initMac();
protected abstract byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf);
protected abstract void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException;
}

View File

@@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
public class KeyFile implements Serializable {
static final Integer CURRENT_VERSION = 1;
static final Integer CURRENT_VERSION = 2;
private static final long serialVersionUID = 8578363158959619885L;
private Integer version;

View File

@@ -0,0 +1,47 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class LengthLimitingOutputStream extends FilterOutputStream {
private final long limit;
private volatile long bytesWritten;
public LengthLimitingOutputStream(OutputStream out, long limit) {
super(out);
this.limit = limit;
this.bytesWritten = 0;
}
@Override
public void write(int b) throws IOException {
if (bytesWritten < limit) {
out.write(b);
increaseNumberOfWrittenBytes(1);
}
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
final long bytesAvailable = limit - bytesWritten;
final int adjustedLen = (int) Math.min(len, bytesAvailable);
if (adjustedLen > 0) {
out.write(b, off, adjustedLen);
increaseNumberOfWrittenBytes(adjustedLen);
}
}
public long getBytesWritten() {
return bytesWritten;
}
private void increaseNumberOfWrittenBytes(int amount) throws IOException {
bytesWritten += amount;
if (bytesWritten >= limit) {
out.flush();
}
}
}

View File

@@ -0,0 +1,117 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
/**
* Not thread-safe!
*/
public class LengthObfuscatingInputStream extends FilterInputStream {
private final byte[] padding;
private int paddingLength = -1;
private long inputBytesRead = 0;
private int paddingBytesRead = 0;
LengthObfuscatingInputStream(InputStream in, byte[] padding) {
super(in);
this.padding = padding;
}
long getRealInputLength() {
return inputBytesRead;
}
private void choosePaddingLengthOnce() {
if (paddingLength == -1) {
long upperBound = Math.min(Math.max(inputBytesRead / 10, 4096), 16 * 1024 * 1024); // 10% of original bytes (at least 4KiB), but not more than 16MiBs
paddingLength = (int) (Math.random() * upperBound);
}
}
@Override
public int read() throws IOException {
final int b = in.read();
if (b != -1) {
// stream available:
inputBytesRead++;
return b;
} else {
choosePaddingLengthOnce();
return readFromPadding();
}
}
private int readFromPadding() {
if (paddingLength == -1) {
throw new IllegalStateException("No padding length chosen yet.");
}
if (paddingBytesRead < paddingLength) {
// padding available:
return padding[paddingBytesRead++ % padding.length];
} else {
// end of stream AND padding
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
final int bytesRead = IOUtils.read(in, b, off, len); // 0 on EOF
inputBytesRead += bytesRead;
if (bytesRead == len) {
return bytesRead;
} else if (bytesRead < len) {
choosePaddingLengthOnce();
final int additionalBytesNeeded = len - bytesRead;
final int additionalBytesRead = readFromPadding(b, off + bytesRead, additionalBytesNeeded);
return (bytesRead == 0 && additionalBytesRead == 0) ? -1 : bytesRead + additionalBytesRead;
} else {
// bytesRead > len:
throw new IllegalStateException("Read more bytes than requested.");
}
}
/**
* @return bytes read from padding (0, if fully read)
*/
private int readFromPadding(byte[] b, int off, int len) {
if (len < 0) {
throw new IllegalArgumentException("Length must not be negative");
}
if (paddingLength == -1) {
throw new IllegalStateException("No padding length chosen yet.");
}
final int remainingPadding = paddingLength - paddingBytesRead;
if (remainingPadding > len) {
// padding available:
for (int i = 0; i < len; i++) {
b[off + i] = padding[paddingBytesRead++ % padding.length];
}
return len;
} else {
// partly available:
for (int i = 0; i < remainingPadding; i++) {
b[off + i] = padding[paddingBytesRead++ % padding.length];
}
return remainingPadding;
}
}
@Override
public long skip(long n) throws IOException {
throw new IOException("Skip not supported");
}
@Override
public boolean markSupported() {
return false;
}
}

View File

@@ -1,43 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes read from this stream.
*/
class MacInputStream extends FilterInputStream {
private final Mac mac;
/**
* @param in Stream from which to read contents, which will update the Mac.
* @param mac Mac to be updated during writes.
*/
public MacInputStream(InputStream in, Mac mac) {
super(in);
this.mac = mac;
}
@Override
public int read() throws IOException {
int b = in.read();
if (b != -1) {
mac.update((byte) b);
}
return b;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
int read = in.read(b, off, len);
if (read > 0) {
mac.update(b, off, read);
}
return read;
}
}

View File

@@ -1,37 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes written to this stream.
*/
class MacOutputStream extends FilterOutputStream {
private final Mac mac;
/**
* @param out Stream to redirect contents to after updating the mac.
* @param mac Mac to be updated during writes.
*/
public MacOutputStream(OutputStream out, Mac mac) {
super(out);
this.mac = mac;
}
@Override
public void write(int b) throws IOException {
mac.update((byte) b);
out.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
mac.update(b, off, len);
out.write(b, off, len);
}
}

View File

@@ -70,38 +70,6 @@ public class Aes256CryptorTest {
}
}
@Test
public void testIntegrityAuthentication() throws IOException, DecryptFailedException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = "Hello World".getBytes();
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
// init cryptor:
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
IOUtils.closeQuietly(encryptedOut);
encryptedData.position(0);
// toggle one bit inf first content byte:
encryptedData.position(64);
final byte fifthByte = encryptedData.get();
encryptedData.position(64);
encryptedData.put((byte) (fifthByte ^ 0x01));
encryptedData.position(0);
// check mac (should return false)
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final boolean authentic = cryptor.isAuthentic(encryptedIn);
Assert.assertFalse(authentic);
}
@Test(expected = DecryptFailedException.class)
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
// our test plaintext data:
@@ -112,7 +80,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -131,7 +99,7 @@ public class Aes256CryptorTest {
// decrypt modified content (should fail with DecryptFailedException):
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
cryptor.decryptFile(encryptedIn, plaintextOut);
cryptor.decryptFile(encryptedIn, plaintextOut, true);
}
@Test
@@ -144,7 +112,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096 + 32); // header + content + maximum possible size obfuscation padding + 32 bytes mac (per each 32k)
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -159,7 +127,7 @@ public class Aes256CryptorTest {
// decrypt:
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut);
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
@@ -171,10 +139,10 @@ public class Aes256CryptorTest {
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
// our test plaintext data:
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
// 8MiB test plaintext data:
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 65536; i++) {
for (int i = 0; i < 2097152; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
@@ -183,7 +151,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (96 + plaintextData.length * 1.2));
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (104 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -194,14 +162,14 @@ public class Aes256CryptorTest {
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES, true);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
final byte[] expected = Arrays.copyOfRange(plaintextData, 260000 * Integer.BYTES, 264000 * Integer.BYTES);
Assert.assertArrayEquals(expected, result);
}

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!--
Copyright (c) 2014 Markus Kreusch
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - log4j config for WebDAV unit tests
-->
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="DENY" onMismatch="ACCEPT" />
</Console>
<Console name="StdErr" target="SYSTEM_ERR">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
</Appenders>
<Loggers>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG" />
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
</Root>
</Loggers>
</Configuration>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>crypto-api</artifactId>
<name>Cryptomator cryptographic module API</name>

View File

@@ -53,18 +53,13 @@ public class AbstractCryptorDecorator implements Cryptor {
}
@Override
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.isAuthentic(encryptedFile);
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length);
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
}
@Override

View File

@@ -75,16 +75,11 @@ public interface Cryptor extends Destroyable {
*/
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
/**
* @return true, if the stored MAC matches the calculated one.
*/
boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException;
/**
* @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 decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @param pos First byte (inclusive)
@@ -92,7 +87,7 @@ public interface Cryptor extends Destroyable {
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
* @throws DecryptFailedException If decryption failed
*/
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException;
/**
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.

View File

@@ -45,15 +45,15 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
/* Cryptor */
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptFile(encryptedFile, countingInputStream);
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
}
@Override

View File

@@ -0,0 +1,15 @@
package org.cryptomator.crypto.exceptions;
import java.io.IOException;
public class CryptingException extends IOException {
private static final long serialVersionUID = -6622699014483319376L;
public CryptingException(String string) {
super(string);
}
public CryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class DecryptFailedException extends StorageCryptingException {
public class DecryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public DecryptFailedException(Throwable t) {

View File

@@ -1,8 +1,12 @@
package org.cryptomator.crypto.exceptions;
public class EncryptFailedException extends StorageCryptingException {
public class EncryptFailedException extends CryptingException {
private static final long serialVersionUID = -3855673600374897828L;
public EncryptFailedException(Throwable t) {
super("Encryption failed.", t);
}
public EncryptFailedException(String msg) {
super(msg);
}

View File

@@ -0,0 +1,11 @@
package org.cryptomator.crypto.exceptions;
public class MasterkeyDecryptionException extends Exception {
private static final long serialVersionUID = -6241452734672333206L;
public MasterkeyDecryptionException(String string) {
super(string);
}
}

View File

@@ -1,13 +0,0 @@
package org.cryptomator.crypto.exceptions;
public class StorageCryptingException extends Exception {
private static final long serialVersionUID = -6622699014483319376L;
public StorageCryptingException(String string) {
super(string);
}
public StorageCryptingException(String string, Throwable t) {
super(string, t);
}
}

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class UnsupportedKeyLengthException extends StorageCryptingException {
public class UnsupportedKeyLengthException extends MasterkeyDecryptionException {
private static final long serialVersionUID = 8114147446419390179L;
private final int requestedLength;

View File

@@ -1,6 +1,6 @@
package org.cryptomator.crypto.exceptions;
public class WrongPasswordException extends StorageCryptingException {
public class WrongPasswordException extends MasterkeyDecryptionException {
private static final long serialVersionUID = -602047799678568780L;
public WrongPasswordException() {

View File

@@ -1,90 +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.crypto.io;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
public class SeekableByteChannelInputStream extends InputStream {
private final SeekableByteChannel channel;
private volatile long markedPos = 0;
public SeekableByteChannelInputStream(SeekableByteChannel channel) {
this.channel = channel;
}
@Override
public int read() throws IOException {
final ByteBuffer buffer = ByteBuffer.allocate(1);
final int read = channel.read(buffer);
if (read == 1) {
return buffer.get(0);
} else {
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
return channel.read(buffer);
}
@Override
public int available() throws IOException {
long available = channel.size() - channel.position();
if (available > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
} else {
return (int) available;
}
}
@Override
public long skip(long n) throws IOException {
final long pos = channel.position();
final long max = channel.size();
final long maxSkip = max - pos;
final long actualSkip = Math.min(n, maxSkip);
channel.position(channel.position() + actualSkip);
return actualSkip;
}
@Override
public void close() throws IOException {
channel.close();
super.close();
}
@Override
public synchronized void mark(int readlimit) {
try {
markedPos = channel.position();
} catch (IOException e) {
markedPos = 0;
}
}
@Override
public synchronized void reset() throws IOException {
channel.position(markedPos);
}
public synchronized void resetTo(long position) throws IOException {
channel.position(position);
}
@Override
public boolean markSupported() {
return true;
}
}

View File

@@ -1,64 +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.crypto.io;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
public class SeekableByteChannelOutputStream extends OutputStream {
private final SeekableByteChannel channel;
public SeekableByteChannelOutputStream(SeekableByteChannel channel) {
this.channel = channel;
}
@Override
public void write(int b) throws IOException {
final byte actualByte = (byte) (b & 0x000000FF);
final ByteBuffer buffer = ByteBuffer.allocate(1);
buffer.put(actualByte);
channel.write(buffer);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
channel.write(buffer);
}
@Override
public void close() throws IOException {
channel.close();
}
/**
* @see SeekableByteChannel#truncate(long)
*/
public void truncate(long size) throws IOException {
channel.truncate(size);
}
/**
* @see SeekableByteChannel#position()
*/
public long position() throws IOException {
return channel.position();
}
/**
* @see SeekableByteChannel#position(long)
*/
public void position(long newPosition) throws IOException {
channel.position(newPosition);
}
}

View File

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>installer-debian</artifactId>
<packaging>pom</packaging>
@@ -24,6 +24,15 @@
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
@@ -37,15 +46,34 @@
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="deb" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="logPath" value="~/.Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx2048m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>installer-osx</artifactId>
<packaging>pom</packaging>
@@ -24,6 +24,15 @@
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
@@ -37,15 +46,34 @@
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="dmg" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="logPath" value="~/Library/Logs/Cryptomator/cryptomator.log" />
<fx:jvmarg value="-Xmx2048m"/>
</fx:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>installer-win-portable</artifactId>
<packaging>pom</packaging>
@@ -24,6 +24,15 @@
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
@@ -37,16 +46,34 @@
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="settingsPath" value="./settings.json" />
<fx:property name="logPath" value="cryptomator.log" />
</fx:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="false" menu="false" shortcut="false" />

View File

@@ -3,7 +3,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>installer-win</artifactId>
<packaging>pom</packaging>
@@ -24,6 +24,15 @@
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.7</version>
@@ -37,15 +46,33 @@
<configuration>
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
<!-- Define application to build -->
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<!-- Create main application jar -->
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
<fx:application refid="fxApp" />
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
<fx:resources>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
</fx:resources>
<fx:manifest>
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
<fx:attribute name="Implementation-Version" value="${project.version}" />
</fx:manifest>
</fx:jar>
<!-- Create native package -->
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:application refid="fxApp"/>
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0" >
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
</fx:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />

View File

@@ -11,7 +11,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -112,7 +112,7 @@
<version>${commons-codec.version}</version>
</dependency>
<dependency>
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
<groupId>commons-httpclient</groupId>
<artifactId>commons-httpclient</artifactId>
<version>${commons-httpclient.version}</version>
@@ -127,9 +127,15 @@
<!-- DI -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<version>3.0</version>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>2.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSON -->
@@ -139,14 +145,13 @@
<version>${jackson-databind.version}</version>
</dependency>
<!-- JUnit -->
<!-- JUnit / Mockito -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@@ -211,9 +216,35 @@
<module>installer-win-portable</module>
</modules>
</profile>
<profile>
<id>uber-jar</id>
<modules>
<module>uber-jar</module>
</modules>
</profile>
</profiles>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-libs</id>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}/libs</outputDirectory>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>

57
main/uber-jar/pom.xml Normal file
View File

@@ -0,0 +1,57 @@
<?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
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.9.0</version>
</parent>
<artifactId>uber-jar</artifactId>
<packaging>pom</packaging>
<name>Single über jar with all dependencies</name>
<dependencies>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>ui</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<finalName>Cryptomator-${project.parent.version}</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.7.0</version>
<version>0.9.0</version>
</parent>
<artifactId>ui</artifactId>
<name>Cryptomator GUI</name>
@@ -32,6 +32,12 @@
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- apache commons -->
<dependency>
@@ -49,39 +55,13 @@
<!-- DI -->
<dependency>
<groupId>com.google.inject</groupId>
<artifactId>guice</artifactId>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<outputDirectory>${project.parent.build.directory}</outputDirectory>
<finalName>Cryptomator-${project.parent.version}</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,20 @@
package org.cryptomator.ui;
import java.util.concurrent.ExecutorService;
import javax.inject.Singleton;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.util.DeferredCloser;
import dagger.Component;
@Singleton
@Component(modules = CryptomatorModule.class)
interface CryptomatorComponent {
ExecutorService executorService();
DeferredCloser deferredCloser();
MainController mainController();
}

View File

@@ -0,0 +1,101 @@
package org.cryptomator.ui;
import java.util.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingCryptorDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
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.SemVerComparator;
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 dagger.Module;
import dagger.Provides;
import javafx.application.Application;
@Module
class CryptomatorModule {
private final Application application;
private final DeferredCloser deferredCloser;
public CryptomatorModule(Application application) {
this.application = application;
this.deferredCloser = new DeferredCloser();
}
@Provides
@Singleton
Application provideApplication() {
return application;
}
@Provides
@Singleton
DeferredCloser provideDeferredCloser() {
return deferredCloser;
}
@Provides
@Singleton
@Named("SemVer")
Comparator<String> provideSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
@Named("VaultJsonMapper")
ObjectMapper provideVaultObjectMapper(VaultObjectMapperProvider vaultObjectMapperProvider) {
return vaultObjectMapperProvider.get();
}
@Provides
@Singleton
Settings provideSettings(SettingsProvider settingsProvider) {
return settingsProvider.get();
}
@Provides
@Singleton
ExecutorService provideExecutorService() {
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
}
@Provides
@Singleton
WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) {
return webDavMounterProvider.get();
}
@Provides
@Singleton
WebDavServer provideWebDavServer() {
final WebDavServer webDavServer = new WebDavServer();
webDavServer.start();
return closeLater(webDavServer, WebDavServer::stop);
}
@Provides
Cryptor provideCryptor() {
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
}
private <T> T closeLater(T object, Closer<T> closer) {
return deferredCloser.closeLater(object, closer).get().get();
}
}

View File

@@ -15,15 +15,7 @@ import java.nio.file.Path;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.MainModule.ControllerFactory;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
@@ -34,8 +26,10 @@ import org.cryptomator.ui.util.TrayIconUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Guice;
import com.google.inject.Injector;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
public class MainApplication extends Application {
@@ -44,28 +38,15 @@ public class MainApplication extends Application {
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
private final ExecutorService executorService;
private final ControllerFactory controllerFactory;
private final DeferredCloser closer;
private final MainController mainCtrl;
public MainApplication() {
this(getInjector());
}
private static Injector getInjector() {
return Guice.createInjector(new MainModule());
}
public MainApplication(Injector injector) {
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class), injector.getInstance(MainApplicationReference.class));
}
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer, MainApplicationReference appRef) {
super();
this.executorService = executorService;
this.controllerFactory = controllerFactory;
this.closer = closer;
final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this)).build();
this.executorService = comp.executorService();
this.closer = comp.deferredCloser();
this.mainCtrl = comp.mainController();
Cryptomator.addShutdownTask(closer::close);
appRef.set(this);
}
@Override
@@ -83,37 +64,32 @@ public class MainApplication extends Application {
});
chooseNativeStylesheet();
mainCtrl.initStage(primaryStage);
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);
final Scene scene = new Scene(root);
primaryStage.setTitle(rb.getString("app.name"));
primaryStage.setScene(scene);
primaryStage.sizeToScene();
primaryStage.setResizable(false);
primaryStage.show();
ActiveWindowStyleSupport.startObservingFocus(primaryStage);
TrayIconUtil.init(primaryStage, rb, () -> {
quit();
});
for (String arg : getParameters().getUnnamed()) {
handleCommandLineArg(ctrl, arg);
handleCommandLineArg(arg);
}
if (SystemUtils.IS_OS_MAC_OSX) {
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(file.getAbsolutePath()));
}
LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService), LocalInstance::close).get().get();
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(arg));
}
void handleCommandLineArg(final MainController ctrl, String arg) {
private void handleCommandLineArg(String arg) {
// only open files with our file extension:
if (!arg.endsWith(Vault.VAULT_FILE_EXTENSION)) {
LOG.warn("Invalid vault path %s", arg);
@@ -134,8 +110,8 @@ public class MainApplication extends Application {
// add vault to ctrl:
Platform.runLater(() -> {
ctrl.addVault(vaultPath, true);
ctrl.toFront();
mainCtrl.addVault(vaultPath, true);
mainCtrl.toFront();
});
}
@@ -162,25 +138,4 @@ public class MainApplication extends Application {
closer.close();
}
/**
* Needed to inject MainApplication. Problem: Application needs to be set asap after injector creation.
*/
static class MainApplicationReference {
private Application application;
private void set(Application application) {
this.application = application;
}
public Application get() {
if (application == null) {
throw new IllegalStateException("not yet ready.");
} else {
return application;
}
}
}
}

View File

@@ -1,111 +0,0 @@
/*******************************************************************************
* 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.Comparator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javafx.application.Application;
import javafx.util.Callback;
import javax.inject.Named;
import javax.inject.Singleton;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.SamplingCryptorDecorator;
import org.cryptomator.crypto.aes256.Aes256Cryptor;
import org.cryptomator.ui.MainApplication.MainApplicationReference;
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.SemVerComparator;
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
MainApplicationReference getApplicationBinding() {
return new MainApplicationReference();
}
@Provides
Application getApplication(MainApplicationReference ref) {
return ref.get();
}
@Provides
@Named("SemVer")
@Singleton
Comparator<String> getSemVerComparator() {
return new SemVerComparator();
}
@Provides
@Singleton
ExecutorService getExec() {
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
}
@Provides
Cryptor getCryptor() {
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
}
@Provides
@Singleton
VaultFactory getVaultFactory(WebDavServer server, Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
return new VaultFactory(server, cryptorProvider, mounter, closer);
}
@Provides
@Singleton
WebDavServer getServer() {
final WebDavServer webDavServer = new WebDavServer();
webDavServer.start();
return closeLater(webDavServer, WebDavServer::stop);
}
<T> T closeLater(T object, Closer<T> closer) {
return deferredCloser.closeLater(object, closer).get().get();
}
}

View File

@@ -0,0 +1,105 @@
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
* Controller presenting a single view.
*/
abstract class AbstractFXMLViewController implements Initializable {
private Parent fxmlRoot;
/**
* URL from #initialize(URL, ResourceBundle)
*/
protected URL rootUrl;
/**
* ResourceBundle from #initialize(URL, ResourceBundle)
*/
protected ResourceBundle resourceBundle;
/**
* Gets the URL to the FXML file describing the view presented by this controller.<br/>
*
* A default implementation would look like this:<br/>
* <code>
* return getClass().getResource("/myView.fxml");
* </code>
*
* @return FXML resource URL
*/
protected abstract URL getFxmlResourceUrl();
/**
* @return Localization bundle for the FXML labels or <code>null</code>.
*/
protected abstract ResourceBundle getFxmlResourceBundle();
@Override
public final void initialize(URL location, ResourceBundle resources) {
this.rootUrl = location;
this.resourceBundle = resources;
this.initialize();
}
protected void initialize() {
}
/**
* Creates a FXML loader used in {@link #loadFxml()}. This method can be overwritten for further loader customization.
*
* @return Configured loader ready to load.
*/
protected FXMLLoader createFxmlLoader() {
final URL fxmlUrl = getFxmlResourceUrl();
final ResourceBundle rb = getFxmlResourceBundle();
final FXMLLoader loader = new FXMLLoader(fxmlUrl, rb);
loader.setController(this);
return loader;
}
/**
* Loads the view presented by this controller from the FXML file return by {@link #getFxmlResourceUrl()}. This method can only be invoked once.
*
* @return Parent view element.
*/
protected final synchronized Parent loadFxml() {
if (fxmlRoot == null) {
final FXMLLoader loader = createFxmlLoader();
try {
fxmlRoot = loader.load();
} catch (IOException e) {
throw new IllegalStateException("Could not load FXML file from location: " + loader.getLocation(), e);
}
}
return fxmlRoot;
}
/**
* Creates a new scene with the root node from the FXML file and applies it to the given stage.
*/
public void initStage(Stage stage) {
final Parent root = loadFxml();
stage.setScene(new Scene(root));
stage.sizeToScene();
}
/**
* @return Creates a new stage and calls {@link #initStage(Stage)}.
*/
public Stage createStage() {
final Stage stage = new Stage();
initStage(stage);
return stage;
}
}

View File

@@ -10,17 +10,9 @@ import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.ResourceBundle;
import javafx.application.Application;
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.Hyperlink;
import javafx.scene.text.Text;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -29,13 +21,20 @@ import org.cryptomator.ui.model.Vault;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
public class ChangePasswordController implements Initializable {
@Singleton
public class ChangePasswordController extends AbstractFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
private ResourceBundle rb;
private ChangePasswordListener listener;
private Vault vault;
@@ -66,9 +65,17 @@ public class ChangePasswordController implements Initializable {
}
@Override
public void initialize(URL location, ResourceBundle rb) {
this.rb = rb;
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/change_password.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
oldPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
newPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
@@ -109,20 +116,20 @@ public class ChangePasswordController implements Initializable {
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) {
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
} catch (IOException ex) {
messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
return;
} catch (WrongPasswordException e) {
messageText.setText(rb.getString("changePassword.errorMessage.wrongPassword"));
messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
newPasswordField.swipe();
retypePasswordField.swipe();
Platform.runLater(oldPasswordField::requestFocus);
return;
} catch (UnsupportedKeyLengthException ex) {
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
newPasswordField.swipe();
retypePasswordField.swipe();
@@ -130,9 +137,9 @@ public class ChangePasswordController implements Initializable {
} catch (UnsupportedVaultException e) {
downloadsPageLink.setVisible(true);
if (e.isVaultOlderThanSoftware()) {
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
}
newPasswordField.swipe();
retypePasswordField.swipe();
@@ -147,7 +154,7 @@ public class ChangePasswordController implements Initializable {
final CharSequence newPassword = newPasswordField.getCharacters();
try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
messageText.setText(rb.getString("changePassword.infoMessage.success"));
messageText.setText(resourceBundle.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.

View File

@@ -19,23 +19,25 @@ 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 javax.inject.Inject;
import javax.inject.Singleton;
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 {
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
@Singleton
public class InitializeController extends AbstractFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
private ResourceBundle localization;
private Vault vault;
private InitializationListener listener;
@@ -51,9 +53,22 @@ public class InitializeController implements Initializable {
@FXML
private Label messageLabel;
@Inject
public InitializeController() {
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.localization = rb;
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/initialize.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
}
@@ -88,9 +103,9 @@ public class InitializeController implements Initializable {
listener.didInitialize(this);
}
} catch (FileAlreadyExistsException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
messageLabel.setText(resourceBundle.getString("initialize.messageLabel.alreadyInitialized"));
} catch (InvalidPathException ex) {
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
messageLabel.setText(resourceBundle.getString("initialize.messageLabel.invalidPath"));
} catch (IOException ex) {
LOG.error("I/O Exception", ex);
} finally {

View File

@@ -1,33 +1,97 @@
package org.cryptomator.ui.controllers;
import javafx.application.Application;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.WeakListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import javax.inject.Inject;
public class MacWarningsController {
import org.cryptomator.ui.model.Vault;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class MacWarningsController extends AbstractFXMLViewController {
@FXML
private ListView<String> warningsList;
private ListView<Warning> warningsList;
private Stage stage;
@FXML
private Button whitelistButton;
private final Application application;
private final ObservableList<Warning> warnings = FXCollections.observableArrayList();
private final ListChangeListener<String> unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange;
private final ChangeListener<Boolean> stageVisibilityChangeListener = this::windowVisibilityDidChange;
private Stage stage;
private Vault vault;
@Inject
public MacWarningsController(Application application) {
this.application = application;
}
@Override
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/mac_warnings.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
warnings.addListener(this::warningsDidInvalidate);
warningsList.setItems(warnings);
warningsList.setCellFactory(CheckBoxListCell.forListView(Warning::selectedProperty, new StringConverter<Warning>() {
@Override
public String toString(Warning object) {
return object.getName();
}
@Override
public Warning fromString(String string) {
return null;
}
}));
}
@Override
public void initStage(Stage stage) {
super.initStage(stage);
this.stage = stage;
stage.showingProperty().addListener(new WeakChangeListener<>(stageVisibilityChangeListener));
}
@FXML
private void didClickDismissButton(ActionEvent event) {
stage.hide();
private void didClickWhitelistButton(ActionEvent event) {
warnings.filtered(w -> w.isSelected()).stream().forEach(w -> {
final String resourceToBeWhitelisted = w.getName();
vault.getWhitelistedResourcesWithInvalidMac().add(resourceToBeWhitelisted);
vault.getNamesOfResourcesWithInvalidMac().remove(resourceToBeWhitelisted);
});
warnings.removeIf(w -> w.isSelected());
}
@FXML
@@ -35,24 +99,65 @@ public class MacWarningsController {
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
}
public void setMacWarnings(ObservableList<String> macWarnings) {
this.warningsList.setItems(macWarnings);
this.warningsList.getItems().addListener(new WeakListChangeListener<String>(this::warningsDidChange));
}
// closes this window automatically, if all warnings disappeared (e.g. due to an unmount event)
private void warningsDidChange(Change<? extends String> change) {
if (change.getList().isEmpty()) {
stage.hide();
private void unauthenticatedResourcesDidChange(Change<? extends String> change) {
while (change.next()) {
if (change.wasAdded()) {
warnings.addAll(change.getAddedSubList().stream().map(Warning::new).collect(Collectors.toList()));
} else if (change.wasRemoved()) {
change.getRemoved().forEach(str -> {
warnings.removeIf(w -> str.equals(w.name.get()));
});
}
}
}
public Stage getStage() {
return stage;
private void warningsDidInvalidate(Observable observable) {
disableWhitelistButtonIfNothingSelected();
}
public void setStage(Stage stage) {
this.stage = stage;
private void windowVisibilityDidChange(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (Boolean.TRUE.equals(newValue)) {
stage.setTitle(String.format(resourceBundle.getString("macWarnings.windowTitle"), vault.getName()));
warnings.addAll(vault.getNamesOfResourcesWithInvalidMac().stream().map(Warning::new).collect(Collectors.toList()));
vault.getNamesOfResourcesWithInvalidMac().addListener(this.unauthenticatedResourcesChangeListener);
} else {
vault.getNamesOfResourcesWithInvalidMac().clear();
vault.getNamesOfResourcesWithInvalidMac().removeListener(this.unauthenticatedResourcesChangeListener);
}
}
private void disableWhitelistButtonIfNothingSelected() {
whitelistButton.setDisable(warnings.filtered(w -> w.isSelected()).isEmpty());
}
public void setVault(Vault vault) {
this.vault = vault;
}
private class Warning {
private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
private final BooleanProperty selected = new SimpleBooleanProperty(false);
public Warning(String name) {
this.name.set(name);
this.selectedProperty().addListener(change -> {
disableWhitelistButtonIfNothingSelected();
});
}
public String getName() {
return name.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public boolean isSelected() {
return selected.get();
}
}
}

View File

@@ -13,25 +13,35 @@ import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.ResourceBundle;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
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 dagger.Lazy;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.SetChangeListener;
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.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
@@ -42,23 +52,8 @@ 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.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.ObservableSetAggregator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener, ChangePasswordListener {
@Singleton
public class MainController extends AbstractFXMLViewController implements InitializationListener, UnlockListener, LockListener, ChangePasswordListener {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
@@ -82,36 +77,50 @@ public class MainController implements Initializable, InitializationListener, Un
@FXML
private Pane contentPane;
private final ControllerFactory controllerFactory;
private final Settings settings;
private final VaultFactory vaultFactoy;
private final ObservableList<String> aggregatedMacWarnings;
private final SetChangeListener<String> macWarningsAggregator;
private final AtomicBoolean macWarningsWindowVisible;
private ResourceBundle rb;
private final Lazy<WelcomeController> welcomeController;
private final Lazy<InitializeController> initializeController;
private final Lazy<UnlockController> unlockController;
private final Provider<UnlockedController> unlockedController;
private final Lazy<ChangePasswordController> changePasswordController;
@Inject
public MainController(ControllerFactory controllerFactory, Settings settings, VaultFactory vaultFactoy) {
public MainController(Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController, Lazy<InitializeController> initializeController, Lazy<UnlockController> unlockController,
Provider<UnlockedController> unlockedController, Lazy<ChangePasswordController> changePasswordController) {
super();
this.controllerFactory = controllerFactory;
this.settings = settings;
this.vaultFactoy = vaultFactoy;
this.aggregatedMacWarnings = FXCollections.observableList(new ArrayList<>());
this.macWarningsAggregator = new ObservableSetAggregator<>(this.aggregatedMacWarnings);
this.macWarningsWindowVisible = new AtomicBoolean();
this.welcomeController = welcomeController;
this.initializeController = initializeController;
this.unlockController = unlockController;
this.unlockedController = unlockedController;
this.changePasswordController = changePasswordController;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/main.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
final ObservableList<Vault> items = FXCollections.observableList(settings.getDirectories());
vaultList.setItems(items);
vaultList.setCellFactory(this::createDirecoryListCell);
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
this.showWelcomeView();
}
aggregatedMacWarnings.addListener(this::macWarningsDidChange);
@Override
public void initStage(Stage stage) {
super.initStage(stage);
this.stage = stage;
}
@FXML
@@ -205,14 +214,14 @@ public class MainController implements Initializable, InitializationListener, Un
private void selectedVaultDidChange(ListChangeListener.Change<? extends Vault> change) {
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
if (selectedVault == null) {
stage.setTitle(rb.getString("app.name"));
stage.setTitle(resourceBundle.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"));
stage.setTitle(resourceBundle.getString("app.name"));
showWelcomeView();
} else {
stage.setTitle(selectedVault.getName());
@@ -233,12 +242,6 @@ public class MainController implements Initializable, InitializationListener, Un
showChangePasswordView(selectedVault);
}
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
if (aggregatedMacWarnings.size() > 0) {
Platform.runLater(this::showMacWarningsWindow);
}
}
// ****************************************
// Subcontroller for right panel
// ****************************************
@@ -257,25 +260,17 @@ public class MainController implements Initializable, InitializationListener, Un
}
}
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");
final Parent root = welcomeController.get().loadFxml();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
}
private void showInitializeView(Vault vault) {
final InitializeController ctrl = showView("/fxml/initialize.fxml");
final InitializeController ctrl = initializeController.get();
final Parent root = ctrl.loadFxml();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
ctrl.setVault(vault);
ctrl.setListener(this);
}
@@ -286,35 +281,42 @@ public class MainController implements Initializable, InitializationListener, Un
}
private void showUnlockView(Vault vault) {
final UnlockController ctrl = showView("/fxml/unlock.fxml");
final UnlockController ctrl = unlockController.get();
final Parent root = ctrl.loadFxml();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didUnlock(UnlockController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
showUnlockedView(ctrl.getVault());
Platform.setImplicitExit(false);
}
private void showUnlockedView(Vault vault) {
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
final UnlockedController ctrl = unlockedController.get();
final Parent root = ctrl.loadFxml();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
ctrl.setVault(vault);
ctrl.setListener(this);
}
@Override
public void didLock(UnlockedController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
showUnlockView(ctrl.getVault());
if (getUnlockedDirectories().isEmpty()) {
if (getUnlockedVaults().isEmpty()) {
Platform.setImplicitExit(true);
}
}
private void showChangePasswordView(Vault vault) {
final ChangePasswordController ctrl = showView("/fxml/change_password.fxml");
final ChangePasswordController ctrl = changePasswordController.get();
final Parent root = ctrl.loadFxml();
contentPane.getChildren().clear();
contentPane.getChildren().add(root);
ctrl.setVault(vault);
ctrl.setListener(this);
}
@@ -324,45 +326,14 @@ public class MainController implements Initializable, InitializationListener, Un
showUnlockView(ctrl.getVault());
}
private void showMacWarningsWindow() {
if (macWarningsWindowVisible.getAndSet(true) == false) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
final Stage stage = new Stage();
stage.setTitle(rb.getString("macWarnings.windowTitle"));
stage.setScene(new Scene(root));
stage.sizeToScene();
stage.setResizable(false);
stage.setOnHidden(this::onHideMacWarningsWindow);
ActiveWindowStyleSupport.startObservingFocus(stage);
final MacWarningsController ctrl = loader.getController();
ctrl.setMacWarnings(this.aggregatedMacWarnings);
ctrl.setStage(stage);
stage.show();
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
}
private void onHideMacWarningsWindow(WindowEvent event) {
macWarningsWindowVisible.set(false);
aggregatedMacWarnings.clear();
}
/* Convenience */
public Collection<Vault> getDirectories() {
public Collection<Vault> getVaults() {
return vaultList.getItems();
}
public Collection<Vault> getUnlockedDirectories() {
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
public Collection<Vault> getUnlockedVaults() {
return getVaults().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
}
/* public Getter/Setter */

View File

@@ -15,43 +15,47 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.Comparator;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Text;
import javafx.util.StringConverter;
import javax.inject.Inject;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.CharUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WindowsDriveLetters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
public class UnlockController implements Initializable {
public class UnlockController extends AbstractFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private ResourceBundle rb;
private UnlockListener listener;
private Vault vault;
@@ -60,6 +64,12 @@ public class UnlockController implements Initializable {
@FXML
private TextField mountName;
@FXML
private ChoiceBox<Character> winDriveLetter;
@FXML
private Button advancedOptionsButton;
@FXML
private Button unlockButton;
@@ -73,23 +83,61 @@ public class UnlockController implements Initializable {
@FXML
private Hyperlink downloadsPageLink;
@FXML
private GridPane advancedOptions;
private final ExecutorService exec;
private final Application app;
private final WindowsDriveLetters driveLetters;
private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
@Inject
public UnlockController(Application app, ExecutorService exec) {
super();
public UnlockController(Application app, ExecutorService exec, WindowsDriveLetters driveLetters) {
this.app = app;
this.exec = exec;
this.driveLetters = driveLetters;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/unlock.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
mountName.textProperty().addListener(this::mountNameDidChange);
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
} else {
winDriveLetter.setVisible(false);
winDriveLetter.setManaged(false);
}
}
private void resetView() {
passwordField.clear();
unlockButton.setDisable(true);
advancedOptions.setVisible(false);
advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show"));
progressIndicator.setVisible(false);
if (SystemUtils.IS_OS_WINDOWS) {
winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
winDriveLetter.getItems().clear();
winDriveLetter.getItems().add(null);
winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
winDriveLetter.getItems().sort(new WinDriveLetterComparator());
winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
}
downloadsPageLink.setVisible(false);
messageText.setText(null);
}
// ****************************************
@@ -110,6 +158,91 @@ public class UnlockController implements Initializable {
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
}
// ****************************************
// Advanced options button
// ****************************************
@FXML
private void didClickAdvancedOptionsButton(ActionEvent event) {
advancedOptions.setVisible(!advancedOptions.isVisible());
if (advancedOptions.isVisible()) {
advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.hide"));
} else {
advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show"));
}
}
private void filterAlphanumericKeyEvents(KeyEvent t) {
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
return;
}
char c = CharUtils.toChar(t.getCharacter());
if (!(CharUtils.isAsciiAlphanumeric(c) || c == '_')) {
t.consume();
}
}
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
if (vault == null) {
return;
}
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
if (newValue.isEmpty()) {
mountName.setText(vault.getMountName());
} else {
vault.setMountName(newValue);
}
}
/**
* Converts 'C' to "C:" to translate between model and GUI.
*/
private class WinDriveLetterLabelConverter extends StringConverter<Character> {
@Override
public String toString(Character letter) {
if (letter == null) {
return resourceBundle.getString("unlock.choicebox.winDriveLetter.auto");
} else {
return Character.toString(letter) + ":";
}
}
@Override
public Character fromString(String string) {
if (resourceBundle.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
return null;
} else {
return CharUtils.toCharacterObject(string);
}
}
}
/**
* Natural sorting of ASCII letters, but <code>null</code> always on first, as this is "auto-assign".
*/
private static class WinDriveLetterComparator implements Comparator<Character> {
@Override
public int compare(Character c1, Character c2) {
if (c1 == null) {
return -1;
} else if (c2 == null) {
return 1;
} else {
return (char) c1 - (char) c2;
}
}
}
private void winDriveLetterDidChange(ObservableValue<? extends Character> property, Character oldValue, Character newValue) {
if (vault == null) {
return;
}
vault.setWinDriveLetter(newValue);
}
// ****************************************
// Unlock button
// ****************************************
@@ -125,38 +258,38 @@ public class UnlockController implements Initializable {
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!vault.startServer()) {
messageText.setText(rb.getString("unlock.messageLabel.startServerFailed"));
messageText.setText(resourceBundle.getString("unlock.messageLabel.startServerFailed"));
vault.getCryptor().destroy();
return;
}
// 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());
final Future<Boolean> futureMount = exec.submit(vault::mount);
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
} catch (DecryptFailedException | IOException ex) {
} catch (IOException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
messageText.setText(resourceBundle.getString("unlock.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
} catch (WrongPasswordException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(rb.getString("unlock.errorMessage.wrongPassword"));
messageText.setText(resourceBundle.getString("unlock.errorMessage.wrongPassword"));
Platform.runLater(passwordField::requestFocus);
} catch (UnsupportedKeyLengthException ex) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
messageText.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
} catch (UnsupportedVaultException e) {
setControlsDisabled(false);
progressIndicator.setVisible(false);
downloadsPageLink.setVisible(true);
if (e.isVaultOlderThanSoftware()) {
messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
} else if (e.isSoftwareOlderThanVault()) {
messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
}
} catch (DestroyFailedException e) {
setControlsDisabled(false);
@@ -171,6 +304,7 @@ public class UnlockController implements Initializable {
passwordField.setDisable(disable);
mountName.setDisable(disable);
unlockButton.setDisable(disable);
advancedOptionsButton.setDisable(disable);
}
private void unlockAndMountFinished(boolean mountSuccess) {
@@ -178,31 +312,19 @@ public class UnlockController implements Initializable {
setControlsDisabled(false);
if (vault.isUnlocked() && !mountSuccess) {
vault.stopServer();
vault.setUnlocked(false);
} else if (vault.isUnlocked() && mountSuccess) {
try {
vault.reveal();
} catch (CommandFailedException e) {
LOG.error("Failed to reveal mounted vault", e);
}
}
if (mountSuccess && listener != null) {
listener.didUnlock(this);
}
}
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();
}
}
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());
} else {
vault.setMountName(newValue);
}
}
/* Getter/Setter */
public Vault getVault() {
@@ -210,8 +332,27 @@ public class UnlockController implements Initializable {
}
public void setVault(Vault vault) {
this.resetView();
this.vault = vault;
this.mountName.setText(vault.getMountName());
if (SystemUtils.IS_OS_WINDOWS) {
chooseSelectedDriveLetter();
}
}
private void chooseSelectedDriveLetter() {
assert SystemUtils.IS_OS_WINDOWS;
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
vault.setWinDriveLetter(null);
}
final Character letter = vault.getWinDriveLetter();
if (letter == null) {
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
this.winDriveLetter.getSelectionModel().selectFirst();
} else {
this.winDriveLetter.getSelectionModel().select(letter);
}
}
public UnlockListener getListener() {

View File

@@ -11,25 +11,31 @@ package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.ResourceBundle;
import javax.inject.Inject;
import javax.inject.Provider;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.mount.CommandFailedException;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.Label;
import javafx.stage.Stage;
import javafx.util.Duration;
import org.cryptomator.crypto.CryptorIOSampling;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.mount.CommandFailedException;
public class UnlockedController implements Initializable {
public class UnlockedController extends AbstractFXMLViewController {
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
@@ -46,11 +52,38 @@ public class UnlockedController implements Initializable {
@FXML
private NumberAxis xAxis;
private ResourceBundle rb;
private final Stage macWarningsWindow = new Stage();
private final MacWarningsController macWarningsController;
@Inject
public UnlockedController(Provider<MacWarningsController> macWarningsControllerProvider) {
this.macWarningsController = macWarningsControllerProvider.get();
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/unlocked.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
macWarningsController.initStage(macWarningsWindow);
ActiveWindowStyleSupport.startObservingFocus(macWarningsWindow);
}
@FXML
private void didClickRevealVault(ActionEvent event) {
try {
vault.reveal();
} catch (CommandFailedException e) {
messageLabel.setText(resourceBundle.getString("unlocked.label.revealFailed"));
return;
}
}
@FXML
@@ -58,7 +91,7 @@ public class UnlockedController implements Initializable {
try {
vault.unmount();
} catch (CommandFailedException e) {
messageLabel.setText(rb.getString("unlocked.label.unmountFailed"));
messageLabel.setText(resourceBundle.getString("unlocked.label.unmountFailed"));
return;
}
vault.stopServer();
@@ -68,6 +101,22 @@ public class UnlockedController implements Initializable {
}
}
// ****************************************
// MAC Auth Warnings
// ****************************************
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
if (change.getList().size() > 0) {
Platform.runLater(() -> {
macWarningsWindow.show();
});
} else {
Platform.runLater(() -> {
macWarningsWindow.hide();
});
}
}
// ****************************************
// IO Graph
// ****************************************
@@ -87,6 +136,13 @@ public class UnlockedController implements Initializable {
ioAnimation.play();
}
private void stopIoSampling() {
if (ioAnimation != null) {
ioGraph.getData().clear();
ioAnimation.stop();
}
}
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
@@ -128,11 +184,23 @@ public class UnlockedController implements Initializable {
return vault;
}
public void setVault(Vault directory) {
this.vault = directory;
public void setVault(Vault vault) {
this.vault = vault;
macWarningsController.setVault(vault);
if (directory.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) directory.getCryptor());
// listen to MAC warnings as long as this vault is unlocked:
final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
vault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener);
vault.unlockedProperty().addListener((observable, oldValue, newValue) -> {
if (Boolean.FALSE.equals(newValue)) {
vault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener);
}
});
// sample crypto-throughput:
stopIoSampling();
if (vault.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) vault.getCryptor());
} else {
ioGraph.setVisible(false);
}

View File

@@ -16,17 +16,9 @@ import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ExecutorService;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Hyperlink;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethod;
@@ -34,42 +26,96 @@ import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.cookie.CookiePolicy;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
public class WelcomeController implements Initializable {
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
@Singleton
public class WelcomeController extends AbstractFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
@FXML
private ImageView botImageView;
@FXML
private CheckBox checkForUpdatesCheckbox;
@FXML
private Label checkForUpdatesStatus;
@FXML
private ProgressIndicator checkForUpdatesIndicator;
@FXML
private Hyperlink updateLink;
private final Application app;
private final Settings settings;
private final Comparator<String> semVerComparator;
private final ExecutorService executor;
private ResourceBundle rb;
@Inject
public WelcomeController(Application app, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
public WelcomeController(Application app, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
this.app = app;
this.settings = settings;
this.semVerComparator = semVerComparator;
this.executor = executor;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
this.botImageView.setImage(new Image(WelcomeController.class.getResource("/bot_welcome.png").toString()));
executor.execute(this::checkForUpdates);
protected URL getFxmlResourceUrl() {
return getClass().getResource("/fxml/welcome.fxml");
}
@Override
protected ResourceBundle getFxmlResourceBundle() {
return ResourceBundle.getBundle("localization");
}
@Override
public void initialize() {
botImageView.setImage(new Image(getClass().getResource("/bot_welcome.png").toString()));
checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled());
checkForUpdatesCheckbox.selectedProperty().addListener(this::checkForUpdatesChanged);
if (settings.isCheckForUpdatesEnabled()) {
executor.execute(this::checkForUpdates);
}
}
// ****************************************
// Check for updates
// ****************************************
private void checkForUpdatesChanged(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
assert newValue != null;
settings.setCheckForUpdatesEnabled(newValue);
if (newValue) {
executor.execute(this::checkForUpdates);
}
}
private void checkForUpdates() {
Platform.runLater(() -> {
checkForUpdatesCheckbox.setVisible(false);
checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.currentlyChecking"));
checkForUpdatesIndicator.setVisible(true);
});
final HttpClient client = new HttpClient();
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
@@ -85,6 +131,12 @@ public class WelcomeController implements Initializable {
}
} catch (IOException e) {
// no error handling required. Maybe next time the version check is successful.
} finally {
Platform.runLater(() -> {
checkForUpdatesCheckbox.setVisible(true);
checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.checkboxLabel"));
checkForUpdatesIndicator.setVisible(false);
});
}
}
@@ -103,7 +155,7 @@ public class WelcomeController implements Initializable {
final String currentVersion = WelcomeController.class.getPackage().getImplementationVersion();
LOG.debug("Current version: {}, lastest version: {}", currentVersion, latestVersion);
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
final String msg = String.format(rb.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
final String msg = String.format(resourceBundle.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
Platform.runLater(() -> {
this.updateLink.setText(msg);
this.updateLink.setVisible(true);

View File

@@ -16,18 +16,18 @@ import javafx.scene.control.PasswordField;
* Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap.
*/
public class SecPasswordField extends PasswordField {
private static final char SWIPE_CHAR = ' ';
/**
* {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[].
* The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars.
* <br/>
* Imagine the following example with <code>pass</code> being the password, <code>x</code> being the swipe char and <code>'</code> being the offset of the char array:
* <ol>
* <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
* <li>Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
* <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
* <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
* <li>Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
* <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
* </ol>
*/
public void swipe() {
@@ -37,8 +37,8 @@ public class SecPasswordField extends PasswordField {
this.getContent().insert(pwLength, new String(fillingChars), false);
this.getContent().delete(0, pwLength, true);
this.getContent().delete(0, pwLength, true);
// previous text has now been overwritten. still we need to update the text to trigger some property bindings:
this.setText("");
}
}

View File

@@ -6,15 +6,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import java.util.Set;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredClosable;
@@ -23,11 +22,19 @@ import org.cryptomator.ui.util.FXThreads;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.ui.util.mount.WebDavMounter.MountParam;
import org.cryptomator.webdav.WebDavServer;
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class Vault implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
@@ -43,9 +50,11 @@ public class Vault implements Serializable {
private final WebDavMounter mounter;
private final DeferredCloser closer;
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final ObservableSet<String> namesOfResourcesWithInvalidMac = FXThreads.observableSetOnMainThread(FXCollections.observableSet());
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
private String mountName;
private Character winDriveLetter;
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
@@ -77,11 +86,12 @@ public class Vault implements Serializable {
public synchronized boolean startServer() {
namesOfResourcesWithInvalidMac.clear();
whitelistedResourcesWithInvalidMac.clear();
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (o.isPresent() && o.get().isRunning()) {
return false;
}
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, mountName);
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, whitelistedResourcesWithInvalidMac, mountName);
if (servlet.start()) {
webDavServlet = closer.closeLater(servlet);
return true;
@@ -101,17 +111,24 @@ public class Vault implements Serializable {
} catch (DestroyFailedException e) {
LOG.error("Destruction of cryptor throw an exception.", e);
}
setUnlocked(false);
whitelistedResourcesWithInvalidMac.clear();
namesOfResourcesWithInvalidMac.clear();
}
public boolean mount() {
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
if (!o.isPresent() || !o.get().isRunning()) {
private Map<MountParam, Optional<String>> getMountParams() {
return ImmutableMap.of( //
MountParam.MOUNT_NAME, Optional.ofNullable(mountName), //
MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)) //
);
}
public Boolean mount() {
final ServletLifeCycleAdapter servlet = webDavServlet.get().orElse(null);
if (servlet == null || !servlet.isRunning()) {
return false;
}
try {
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), mountName));
webDavMount = closer.closeLater(mounter.mount(servlet.getServletUri(), getMountParams()));
return true;
} catch (CommandFailedException e) {
LOG.warn("mount failed", e);
@@ -119,6 +136,13 @@ public class Vault implements Serializable {
}
}
public void reveal() throws CommandFailedException {
final WebDavMount mnt = webDavMount.get().orElse(null);
if (mnt != null) {
mnt.reveal();
}
}
public void unmount() throws CommandFailedException {
final WebDavMount mnt = webDavMount.get().orElse(null);
if (mnt != null) {
@@ -156,12 +180,12 @@ public class Vault implements Serializable {
this.unlocked.set(unlocked);
}
public String getMountName() {
return mountName;
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
}
public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
public Set<String> getWhitelistedResourcesWithInvalidMac() {
return whitelistedResourcesWithInvalidMac;
}
/**
@@ -190,6 +214,10 @@ public class Vault implements Serializable {
return builder.toString();
}
public String getMountName() {
return mountName;
}
/**
* sets the mount name while normalizing it
*
@@ -204,6 +232,14 @@ public class Vault implements Serializable {
this.mountName = mountName;
}
public Character getWinDriveLetter() {
return winDriveLetter;
}
public void setWinDriveLetter(Character winDriveLetter) {
this.winDriveLetter = winDriveLetter;
}
/* hashcode/equals */
@Override

View File

@@ -2,14 +2,16 @@ package org.cryptomator.ui.model;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
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;
@Singleton
public class VaultFactory {
private final WebDavServer server;

View File

@@ -5,6 +5,10 @@ import java.nio.file.FileSystems;
import java.nio.file.Path;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
@@ -16,8 +20,8 @@ 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;
@Singleton
public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
private final VaultFactory vaultFactoy;
@@ -43,7 +47,11 @@ public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
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.writeStringField("mountName", value.getMountName());
final Character winDriveLetter = value.getWinDriveLetter();
if (winDriveLetter != null) {
jgen.writeStringField("winDriveLetter", Character.toString(winDriveLetter));
}
jgen.writeEndObject();
}
@@ -60,6 +68,9 @@ public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
if (node.has("mountName")) {
vault.setMountName(node.get("mountName").asText());
}
if (node.has("winDriveLetter")) {
vault.setWinDriveLetter(CharUtils.toCharacterObject(node.get("winDriveLetter").asText()));
}
return vault;
}

View File

@@ -16,13 +16,15 @@ import org.cryptomator.ui.model.Vault;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonPropertyOrder(value = {"directories"})
@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled"})
public class Settings implements Serializable {
private static final long serialVersionUID = 7609959894417878744L;
private List<Vault> directories;
private Boolean checkForUpdatesEnabled;
/**
* Package-private constructor; use {@link SettingsProvider}.
*/
@@ -43,4 +45,13 @@ public class Settings implements Serializable {
this.directories = directories;
}
public boolean isCheckForUpdatesEnabled() {
// not false meaning "null or true", so that true is the default value, if not setting exists yet.
return !Boolean.FALSE.equals(checkForUpdatesEnabled);
}
public void setCheckForUpdatesEnabled(boolean checkForUpdatesEnabled) {
this.checkForUpdatesEnabled = checkForUpdatesEnabled;
}
}

View File

@@ -11,6 +11,8 @@ import java.nio.file.StandardOpenOption;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.DeferredCloser;
@@ -18,8 +20,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Provider;
@Singleton
public class SettingsProvider implements Provider<Settings> {
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
@@ -50,7 +52,7 @@ public class SettingsProvider implements Provider<Settings> {
this.deferredCloser = deferredCloser;
this.objectMapper = objectMapper;
}
private Path getSettingsPath() throws IOException {
String settingsPathProperty = System.getProperty("settingsPath");
if (settingsPathProperty == null) {

View File

@@ -2,7 +2,6 @@ package org.cryptomator.ui.util;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WeakChangeListener;
import javafx.stage.Window;
public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
@@ -18,9 +17,8 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
}
/**
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root
* element, if the window is active. Otherwise {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined
* depending on the window's focus.<br/>
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root element, if the window is active. Otherwise
* {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined depending on the window's focus.<br/>
* <br/>
* Example:<br/>
* <code>
@@ -32,7 +30,7 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
* @return The observer
*/
public static ChangeListener<Boolean> startObservingFocus(final Window window) {
final ChangeListener<Boolean> observer = new WeakChangeListener<Boolean>(new ActiveWindowStyleSupport(window));
final ChangeListener<Boolean> observer = new ActiveWindowStyleSupport(window);
window.focusedProperty().addListener(observer);
return observer;
}

View File

@@ -20,6 +20,8 @@ import org.cryptomator.ui.controllers.MainController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
/**
* <p>
* Tries to bring open-close symmetry in contexts where the resource outlives
@@ -57,8 +59,10 @@ public class DeferredCloser implements AutoCloseable {
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
@VisibleForTesting
final Map<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
@VisibleForTesting
final AtomicLong counter = new AtomicLong();
public class ManagedResource<T> implements DeferredClosable<T> {
@@ -73,6 +77,7 @@ public class DeferredCloser implements AutoCloseable {
this.closer = closer;
}
@Override
public void close() {
final T oldObject = object.getAndSet(null);
if (oldObject != null) {
@@ -86,6 +91,7 @@ public class DeferredCloser implements AutoCloseable {
}
}
@Override
public Optional<T> get() throws IllegalStateException {
return Optional.ofNullable(object.get());
}
@@ -94,6 +100,7 @@ public class DeferredCloser implements AutoCloseable {
/**
* Closes all added objects which have not been closed before and releases references.
*/
@Override
public void close() {
for (Iterator<ManagedResource<?>> iterator = cleanups.values().iterator(); iterator.hasNext();) {
final ManagedResource<?> closableProvider = iterator.next();

View File

@@ -15,6 +15,7 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import javafx.application.Platform;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
/**
@@ -53,8 +54,7 @@ public final class FXThreads {
};
/**
* 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
* 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(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
@@ -74,8 +74,7 @@ public final class FXThreads {
}
/**
* 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
* 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(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
*
* <pre>
@@ -123,4 +122,8 @@ public final class FXThreads {
return new ObservableSetOnMainThread<E>(set);
}
public static <E> ObservableList<E> observableListOnMainThread(ObservableList<E> list) {
return new ObservableListOnMainThread<E>(list);
}
}

View File

@@ -0,0 +1,276 @@
package org.cryptomator.ui.util;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import javafx.application.Platform;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import com.google.common.collect.ImmutableList;
class ObservableListOnMainThread<E> implements ObservableList<E> {
private final ObservableList<E> list;
private final Collection<InvalidationListener> invalidationListeners;
private final Collection<ListChangeListener<? super E>> listChangeListeners;
public ObservableListOnMainThread(ObservableList<E> list) {
this.list = list;
this.invalidationListeners = new HashSet<>();
this.listChangeListeners = new HashSet<>();
this.list.addListener(this::invalidated);
this.list.addListener(this::onChanged);
}
@Override
public int size() {
return list.size();
}
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public boolean contains(Object o) {
return list.contains(o);
}
@Override
public Iterator<E> iterator() {
return list.iterator();
}
@Override
public Object[] toArray() {
return list.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return list.toArray(a);
}
@Override
public boolean add(E e) {
return list.add(e);
}
@Override
public boolean remove(Object o) {
return list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return list.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends E> c) {
return list.addAll(index, c);
}
@Override
public boolean removeAll(Collection<?> c) {
return list.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return list.retainAll(c);
}
@Override
public void clear() {
list.clear();
}
@Override
public E get(int index) {
return list.get(index);
}
@Override
public E set(int index, E element) {
return list.set(index, element);
}
@Override
public void add(int index, E element) {
list.add(index, element);
}
@Override
public E remove(int index) {
return list.remove(index);
}
@Override
public int indexOf(Object o) {
return list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return list.lastIndexOf(o);
}
@Override
public ListIterator<E> listIterator() {
return list.listIterator();
}
@Override
public ListIterator<E> listIterator(int index) {
return list.listIterator(index);
}
@Override
public List<E> subList(int fromIndex, int toIndex) {
return list.subList(fromIndex, toIndex);
}
@Override
public boolean addAll(@SuppressWarnings("unchecked") E... elements) {
return list.addAll(elements);
}
@Override
public boolean setAll(@SuppressWarnings("unchecked") E... elements) {
return list.addAll(elements);
}
@Override
public boolean setAll(Collection<? extends E> col) {
return list.setAll(col);
}
@Override
public boolean removeAll(@SuppressWarnings("unchecked") E... elements) {
return list.removeAll(elements);
}
@Override
public boolean retainAll(@SuppressWarnings("unchecked") E... elements) {
return list.retainAll(elements);
}
@Override
public void remove(int from, int to) {
list.remove(from, to);
}
private void invalidated(Observable observable) {
final Collection<InvalidationListener> listeners = ImmutableList.copyOf(invalidationListeners);
Platform.runLater(() -> {
for (InvalidationListener listener : listeners) {
listener.invalidated(this);
}
});
}
@Override
public void addListener(InvalidationListener listener) {
invalidationListeners.add(listener);
}
@Override
public void removeListener(InvalidationListener listener) {
invalidationListeners.remove(listener);
}
private void onChanged(Change<? extends E> change) {
final Change<? extends E> c = new ListChange(change);
final Collection<ListChangeListener<? super E>> listeners = ImmutableList.copyOf(listChangeListeners);
Platform.runLater(() -> {
for (ListChangeListener<? super E> listener : listeners) {
listener.onChanged(c);
}
});
}
@Override
public void addListener(ListChangeListener<? super E> listener) {
listChangeListeners.add(listener);
}
@Override
public void removeListener(ListChangeListener<? super E> listener) {
listChangeListeners.remove(listener);
}
private class ListChange extends ListChangeListener.Change<E> {
private final Change<? extends E> originalChange;
public ListChange(Change<? extends E> change) {
super(ObservableListOnMainThread.this);
this.originalChange = change;
}
@Override
public boolean wasAdded() {
return originalChange.wasAdded();
}
@Override
public boolean wasRemoved() {
return originalChange.wasRemoved();
}
@Override
public boolean next() {
return originalChange.next();
}
@Override
public void reset() {
originalChange.reset();
}
@Override
public int getFrom() {
return originalChange.getFrom();
}
@Override
public int getTo() {
return originalChange.getTo();
}
@Override
@SuppressWarnings("unchecked")
public List<E> getRemoved() {
return (List<E>) originalChange.getRemoved();
}
@Override
protected int[] getPermutation() {
if (originalChange.wasPermutated()) {
int[] permutations = new int[originalChange.getTo() - originalChange.getFrom()];
for (int i = 0; i < permutations.length; i++) {
permutations[i] = originalChange.getPermutation(i);
}
return permutations;
} else {
return new int[0];
}
}
}
}

View File

@@ -1,44 +0,0 @@
/*******************************************************************************
* 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:
* Sebastian Stenzel - initial implementation
******************************************************************************/
package org.cryptomator.ui.util;
import java.util.Collection;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
/**
* From the moment on, this aggregator is added as an observer to one or many {@link ObservableSet}s, change-events will be passed through
* to the given aggregation.
*/
public class ObservableSetAggregator<E> implements SetChangeListener<E> {
private final Collection<E> aggregation;
/**
* @param aggregation Set to which elements from observed subsets shall be added.
*/
public ObservableSetAggregator(final Collection<E> aggregation) {
this.aggregation = aggregation;
}
@Override
public void onChanged(Change<? extends E> change) {
if (change.getSet() == aggregation) {
// break cycle if aggregator observes aggregation
return;
}
if (change.wasAdded()) {
aggregation.add(change.getElementAdded());
} else if (change.wasRemoved()) {
aggregation.remove(change.getElementRemoved());
}
}
}

View File

@@ -11,6 +11,8 @@ import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.collections.SetChangeListener.Change;
import com.google.common.collect.ImmutableList;
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
private final ObservableSet<E> set;
@@ -91,8 +93,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
}
private void invalidated(Observable observable) {
final Collection<InvalidationListener> listeners = ImmutableList.copyOf(invalidationListeners);
Platform.runLater(() -> {
for (InvalidationListener listener : invalidationListeners) {
for (InvalidationListener listener : listeners) {
listener.invalidated(this);
}
});
@@ -110,8 +113,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
private void onChanged(Change<? extends E> change) {
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
final Collection<SetChangeListener<? super E>> listeners = ImmutableList.copyOf(setChangeListeners);
Platform.runLater(() -> {
for (SetChangeListener<? super E> listener : setChangeListeners) {
for (SetChangeListener<? super E> listener : listeners) {
listener.onChanged(c);
}
});

View File

@@ -9,6 +9,8 @@
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
/**
* A WebDavMounter acting as fallback if no other mounter works.
@@ -28,13 +30,18 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(URI uri, String name) {
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) {
displayMountInstructions();
return new AbstractWebDavMount() {
@Override
public void unmount() {
displayUnmountInstructions();
}
@Override
public void reveal() throws CommandFailedException {
displayRevealInstructions();
}
};
}
@@ -47,4 +54,8 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
// TODO display message to user pointing to cryptomator.org/mounting#unmount which describes what to do
}
private void displayRevealInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#reveal which describes what to do
}
}

View File

@@ -11,11 +11,20 @@
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@Singleton
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
@Inject
LinuxGvfsWebDavMounter() {}
@Override
public boolean shouldWork() {
@@ -38,48 +47,54 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
final Script testMountStillExistsScript = Script.fromLines(
"set -x",
"test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
final Script unmountScript = Script.fromLines(
"set -x",
"gvfs-mount -u \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
mountScript.execute();
try{
openMountWithWebdavUri("dav:"+uri.getRawSchemeSpecificPart()).execute();
}catch(CommandFailedException exception){
openMountWithWebdavUri("webdav:"+uri.getRawSchemeSpecificPart()).execute();
}
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
boolean mountStillExists;
try {
testMountStillExistsScript.execute();
mountStillExists = true;
} catch(CommandFailedException e) {
mountStillExists = false;
}
// only attempt unmount if user didn't unmount manually:
if (mountStillExists) {
unmountScript.execute();
}
}
};
return new LinuxGvfsWebDavMount(uri);
}
private static class LinuxGvfsWebDavMount extends AbstractWebDavMount {
private final URI webDavUri;
private final Script testMountStillExistsScript;
private final Script unmountScript;
private LinuxGvfsWebDavMount(URI webDavUri) {
this.webDavUri = webDavUri;
this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
}
@Override
public void unmount() throws CommandFailedException {
boolean mountStillExists;
try {
testMountStillExistsScript.execute();
mountStillExists = true;
} catch(CommandFailedException e) {
mountStillExists = false;
}
// only attempt unmount if user didn't unmount manually:
if (mountStillExists) {
unmountScript.execute();
}
}
private Script openMountWithWebdavUri(String webdavUri){
return Script.fromLines(
"set -x",
"xdg-open \"$DAV_URI\"")
.addEnv("DAV_URI", webdavUri);
@Override
public void reveal() throws CommandFailedException {
try {
openMountWithWebdavUri("dav:"+webDavUri.getRawSchemeSpecificPart()).execute();
} catch (CommandFailedException exception) {
openMountWithWebdavUri("webdav:"+webDavUri.getRawSchemeSpecificPart()).execute();
}
}
private Script openMountWithWebdavUri(String webdavUri){
return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri);
}
}
}

View File

@@ -12,12 +12,21 @@ package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.Script;
@Singleton
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
@Inject
MacOsXWebDavMounter() {}
@Override
public boolean shouldWork() {
@@ -30,30 +39,48 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final String mountName = mountParams.get(MountParam.MOUNT_NAME).orElseThrow(() -> {
return new IllegalArgumentException("Missing mount parameter MOUNT_NAME.");
});
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
final Script mountScript = Script.fromLines(
"mkdir \"$MOUNT_PATH\"",
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"",
"open \"$MOUNT_PATH\"")
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"")
.addEnv("DAV_AUTHORITY", uri.getRawAuthority())
.addEnv("DAV_PATH", uri.getRawPath())
.addEnv("MOUNT_PATH", path)
.addEnv("MOUNT_NAME", name);
final Script unmountScript = Script.fromLines(
"diskutil umount $MOUNT_PATH")
.addEnv("MOUNT_PATH", path);
.addEnv("MOUNT_NAME", mountName);
mountScript.execute();
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(path))) {
unmountScript.execute();
}
return new MacWebDavMount(path);
}
private static class MacWebDavMount extends AbstractWebDavMount {
private final String mountPath;
private final Script revealScript;
private final Script unmountScript;
private MacWebDavMount(String mountPath) {
this.mountPath = mountPath;
this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath);
this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath);
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(mountPath))) {
unmountScript.execute();
}
};
}
@Override
public void reveal() throws CommandFailedException {
revealScript.execute();
}
}
}

View File

@@ -0,0 +1,84 @@
package org.cryptomator.ui.util.mount;
import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import java.util.Collection;
import java.util.Iterator;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
class MountStrategies implements Collection<WebDavMounterStrategy> {
private final Collection<WebDavMounterStrategy> delegate;
@Inject
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) {
delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter));
}
public int size() {
return delegate.size();
}
public boolean isEmpty() {
return delegate.isEmpty();
}
public boolean contains(Object o) {
return delegate.contains(o);
}
public Iterator<WebDavMounterStrategy> iterator() {
return delegate.iterator();
}
public Object[] toArray() {
return delegate.toArray();
}
public <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
public boolean add(WebDavMounterStrategy e) {
return delegate.add(e);
}
public boolean remove(Object o) {
return delegate.remove(o);
}
public boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
public boolean addAll(Collection<? extends WebDavMounterStrategy> c) {
return delegate.addAll(c);
}
public boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
public boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
public void clear() {
delegate.clear();
}
public boolean equals(Object o) {
return delegate.equals(o);
}
public int hashCode() {
return delegate.hashCode();
}
}

View File

@@ -22,4 +22,11 @@ public interface WebDavMount extends AutoCloseable {
*/
void unmount() throws CommandFailedException;
/**
* Reveals the mounted drive in the operating systems default file browser.
*
* @throws CommandFailedException if the reveal operation fails
*/
void reveal() throws CommandFailedException;
}

View File

@@ -10,17 +10,22 @@
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
public interface WebDavMounter {
public static enum MountParam {MOUNT_NAME, WIN_DRIVE_LETTER}
/**
* Tries to mount a given 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.
* @param mountParams additional mount parameters, that might not get ignored by some OS-specific mounters.
* @return a {@link WebDavMount} representing the mounted share
* @throws CommandFailedException if the mount operation fails
* @throws IllegalArgumentException if mountParams is missing expected options
*/
WebDavMount mount(URI uri, String name) throws CommandFailedException;
WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException;
}

View File

@@ -9,25 +9,26 @@
******************************************************************************/
package org.cryptomator.ui.util.mount;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Provider;
@Singleton
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();
public WebDavMounterProvider(WebDavServer server, ExecutorService executorService, MountStrategies availableStrategies) {
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
executorService.execute(() -> {
this.choosenStrategy.warmUp(server.getPort());
});
@@ -38,14 +39,10 @@ public class WebDavMounterProvider implements Provider<WebDavMounter> {
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();
private WebDavMounterStrategy getStrategyWhichShouldWork(Collection<WebDavMounterStrategy> availableStrategies) {
WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter());
LOG.info("Using {}", strategy.getClass().getSimpleName());
return strategy;
}
}

View File

@@ -0,0 +1,41 @@
package org.cryptomator.ui.util.mount;
import static java.util.stream.Collectors.toSet;
import static java.util.stream.IntStream.rangeClosed;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Set;
import java.util.stream.StreamSupport;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import com.google.common.collect.Sets;
@Singleton
public final class WindowsDriveLetters {
private static final Set<Character> A_TO_Z = rangeClosed('A', 'Z').mapToObj(i -> (char) i).collect(toSet());
@Inject
public WindowsDriveLetters() {
}
public Set<Character> getOccupiedDriveLetters() {
if (!SystemUtils.IS_OS_WINDOWS) {
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
}
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
}
public Set<Character> getAvailableDriveLetters() {
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
}
}

View File

@@ -12,12 +12,16 @@ package org.cryptomator.ui.util.mount;
import static org.cryptomator.ui.util.command.Script.fromLines;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.util.command.CommandResult;
import org.cryptomator.ui.util.command.Script;
@@ -27,10 +31,18 @@ import org.cryptomator.ui.util.command.Script;
* <p>
* Tested on Windows 7 but should also work on Windows 8.
*/
@Singleton
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
private static final int MAX_MOUNT_ATTEMPTS = 5;
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*");
private static final int MAX_MOUNT_ATTEMPTS = 8;
private static final char AUTO_ASSIGN_DRIVE_LETTER = '*';
private final WindowsDriveLetters driveLetters;
@Inject
WindowsWebDavMounter(WindowsDriveLetters driveLetters) {
this.driveLetters = driveLetters;
}
@Override
public boolean shouldWork() {
@@ -39,62 +51,46 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
@Override
public void warmUp(int serverPort) {
// try {
// final Script mountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot\\bill-gates-mom-uses-goto /persistent:no");
// mountScript.addEnv("DAV_PORT", String.valueOf(serverPort));
// mountScript.execute(1, TimeUnit.SECONDS);
// } catch (CommandFailedException e) {
// // will most certainly throw an exception, because this is a fake WebDav path. But now windows has some DNS things cached :)
// }
// no-op
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
CommandResult mountResult;
try {
final Script mountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
mountResult = mountScript.execute(5, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
final Script mountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
mountResult = bypassProxyAndRetryMount(mountScript, proxyBypassScript);
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
final Character driveLetter = mountParams.get(MountParam.WIN_DRIVE_LETTER).map(CharUtils::toCharacterObject).orElse(AUTO_ASSIGN_DRIVE_LETTER);
if (driveLetters.getOccupiedDriveLetters().contains(driveLetter)) {
throw new CommandFailedException("Drive letter occupied.");
}
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script openExplorerScript = fromLines("start explorer.exe " + driveLetter);
openExplorerScript.execute();
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (isVolumeMounted(driveLetter)) {
unmountScript.execute();
}
}
};
}
private boolean isVolumeMounted(String driveLetter) {
for (Path path : FileSystems.getDefault().getRootDirectories()) {
if (path.toString().startsWith(driveLetter)) {
return true;
}
final String driveLetterStr = driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? CharUtils.toString(AUTO_ASSIGN_DRIVE_LETTER) : driveLetter + ":";
final Script localhostMountScript = fromLines("net use %DRIVE_LETTER% \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
localhostMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
localhostMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
CommandResult mountResult;
try {
mountResult = localhostMountScript.execute(5, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
final Script ipv6literaltMountScript = fromLines("net use %DRIVE_LETTER% \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
ipv6literaltMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
ipv6literaltMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript);
}
return false;
return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter);
}
private CommandResult bypassProxyAndRetryMount(Script mountScript, Script proxyBypassScript) throws CommandFailedException {
private CommandResult bypassProxyAndRetryMount(Script localhostMountScript, Script ipv6literalMountScript, Script proxyBypassScript) throws CommandFailedException {
CommandFailedException latestException = null;
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
try {
// wait a moment before next attempt
Thread.sleep(5000);
proxyBypassScript.execute();
return mountScript.execute(5, TimeUnit.SECONDS);
// alternate localhost and 0--1.ipv6literal.net
final Script mountScript = (i % 2 == 0) ? localhostMountScript : ipv6literalMountScript;
return mountScript.execute(3, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
latestException = ex;
} catch (InterruptedException ex) {
@@ -105,13 +101,42 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
throw latestException;
}
private String getDriveLetter(String result) throws CommandFailedException {
private Character getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {
return matcher.group(1);
return CharUtils.toCharacterObject(matcher.group(1));
} else {
throw new CommandFailedException("Failed to get a drive letter from net use output.");
}
}
private class WindowsWebDavMount extends AbstractWebDavMount {
private final Character driveLetter;
private final Script openExplorerScript;
private final Script unmountScript;
private WindowsWebDavMount(Character driveLetter) {
this.driveLetter = driveLetter;
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
this.unmountScript = fromLines("net use " + driveLetter + ": /delete").addEnv("DRIVE_LETTER", Character.toString(driveLetter));
}
@Override
public void unmount() throws CommandFailedException {
// only attempt unmount if user didn't unmount manually:
if (isVolumeMounted()) {
unmountScript.execute();
}
}
@Override
public void reveal() throws CommandFailedException {
openExplorerScript.execute();
}
private boolean isVolumeMounted() {
return driveLetters.getOccupiedDriveLetters().contains(driveLetter);
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@
<?import javafx.scene.text.Text?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>

View File

@@ -16,7 +16,7 @@
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Label?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.InitializeController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>

View File

@@ -15,7 +15,7 @@
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
<VBox styleClass="root" alignment="CENTER" prefHeight="225.0" prefWidth="525.0" spacing="12.0" fx:controller="org.cryptomator.ui.controllers.MacWarningsController" xmlns:fx="http://javafx.com/fxml">
<VBox styleClass="root" alignment="CENTER" prefHeight="225.0" prefWidth="525.0" spacing="12.0" xmlns:fx="http://javafx.com/fxml">
<padding><Insets top="12.0" right="12.0" bottom="12.0" left="12.0"/></padding>
@@ -24,7 +24,7 @@
<ListView fx:id="warningsList" VBox.vgrow="ALWAYS" focusTraversable="false" />
<HBox alignment="CENTER_RIGHT" spacing="12.0">
<children>
<Button text="%macWarnings.dismissButton" prefWidth="200.0" onAction="#didClickDismissButton" focusTraversable="false"/>
<Button fx:id="whitelistButton" text="%macWarnings.whitelistButton" prefWidth="200.0" onAction="#didClickWhitelistButton" focusTraversable="false"/>
<Button text="%macWarnings.moreInformationButton" defaultButton="true" prefWidth="200.0" onAction="#didClickMoreInformationButton" focusTraversable="false"/>
</children>
</HBox>

View File

@@ -18,12 +18,11 @@
<?import javafx.scene.control.MenuItem?>
<?import javafx.geometry.Insets?>
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.controllers.MainController" xmlns:fx="http://javafx.com/fxml">
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" 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="vaultListCellContextMenu">
<items>
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry" />
@@ -49,11 +48,7 @@
</ToolBar>
</children>
</VBox>
<Pane fx:id="contentPane">
<children>
<fx:reference source="welcomeView" />
</children>
</Pane>
<Pane fx:id="contentPane"/>
</children>
</HBox>

View File

@@ -21,8 +21,11 @@
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.text.Text?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.control.ChoiceBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
@@ -36,24 +39,56 @@
<!-- Row 0 -->
<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.mountName" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<HBox GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" spacing="12.0" alignment="CENTER_RIGHT">
<Button fx:id="advancedOptionsButton" text="%unlock.button.advancedOptions.show" prefWidth="150.0" onAction="#didClickAdvancedOptionsButton"/>
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" prefWidth="150.0" onAction="#didClickUnlockButton" disable="true"/>
</HBox>
<!-- Row 2 -->
<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-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 3 -->
<GridPane fx:id="advancedOptions" vgap="12.0" hgap="12.0" prefWidth="400.0" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" visible="false">
<padding>
<Insets top="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<!-- Row 3.0 -->
<Separator GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<HBox alignment="CENTER" prefWidth="400.0" GridPane.rowIndex="0" GridPane.columnIndex="0" GridPane.columnSpan="2">
<Label text="%unlock.label.advancedHeading" style="-fx-background-color: COLOR_BACKGROUND;">
<padding>
<Insets left="6.0" right="6.0"/>
</padding>
</Label>
</HBox>
<!-- Row 3.1 -->
<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 3.2 -->
<Label text="%unlock.label.winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<ChoiceBox fx:id="winDriveLetter" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
</GridPane>
<!-- Row 4 -->
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" >
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2">
<GridPane.margin>
<Insets top="24.0"/>
</GridPane.margin>
<children>
<Text fx:id="messageText" />
<Text fx:id="messageText" text="asd"/>
<Hyperlink fx:id="downloadsPageLink" text="%unlock.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" />
</children>
</TextFlow>
<!-- Row 5-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="5" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" />
</children>
</GridPane>

View File

@@ -16,8 +16,9 @@
<?import javafx.scene.layout.GridPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.HBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
@@ -36,7 +37,11 @@
<!-- Row 1 -->
<Label fx:id="messageLabel" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<Button text="%unlocked.button.lock" defaultButton="true" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickCloseVault" focusTraversable="false"/>
<HBox GridPane.rowIndex="1" GridPane.columnIndex="1" spacing="6.0" alignment="CENTER_RIGHT">
<Button text="%unlocked.button.reveal" prefWidth="100.0" onAction="#didClickRevealVault" focusTraversable="false"/>
<Button text="%unlocked.button.lock" prefWidth="100.0" onAction="#didClickCloseVault" focusTraversable="false"/>
</HBox>
</children>
</GridPane>

View File

@@ -17,12 +17,24 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ProgressIndicator?>
<AnchorPane xmlns:fx="http://javafx.com/fxml" fx:controller="org.cryptomator.ui.controllers.WelcomeController">
<AnchorPane xmlns:fx="http://javafx.com/fxml">
<children>
<Label AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="20.0" prefWidth="400.0" alignment="CENTER" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<Hyperlink AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="50.0" prefWidth="400.0" alignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" visible="false"/>
<VBox AnchorPane.leftAnchor="0.0" AnchorPane.topAnchor="20.0" prefWidth="400.0" alignment="CENTER" spacing="10.0">
<Label alignment="CENTER" style="-fx-font-size: 1.5em;" text="%welcome.welcomeLabel"/>
<HBox alignment="CENTER" spacing="5.0">
<CheckBox fx:id="checkForUpdatesCheckbox" />
<Label fx:id="checkForUpdatesStatus" text="%welcome.checkForUpdates.label.checkboxLabel"/>
<ProgressIndicator fx:id="checkForUpdatesIndicator" progress="-1" prefWidth="15.0" prefHeight="15.0" visible="false"/>
</HBox>
<Hyperlink alignment="CENTER" fx:id="updateLink" onAction="#didClickUpdateLink" visible="false" />
</VBox>
<ImageView fx:id="botImageView" AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="200.0" fitHeight="200.0" preserveRatio="true" smooth="false"/>

Some files were not shown because too many files have changed in this diff Show More