Compare commits

..

37 Commits
0.7.0 ... 0.8.2

Author SHA1 Message Date
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
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
68 changed files with 2327 additions and 856 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

@@ -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.8.2</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

@@ -40,6 +40,7 @@ public final class WebDavServer {
private static final int MAX_THREADS = 200;
private static final int MIN_THREADS = 4;
private static final int THREAD_IDLE_SECONDS = 20;
private static final int CONNECTION_IDLE_MILLIS = 100; // idle connection slow down random access on WebDAVFS for some reason. reconnect overhead can be tolerated
private final Server server;
private final ServerConnector localConnector;
private final ContextHandlerCollection servletCollection;
@@ -50,11 +51,14 @@ public final class WebDavServer {
server = new Server(tp);
localConnector = new ServerConnector(server);
localConnector.setHost(LOCALHOST);
localConnector.setIdleTimeout(CONNECTION_IDLE_MILLIS);
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 +88,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 +103,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 +115,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;
@@ -156,7 +155,8 @@ 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)) {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, 0L, FILE_HEADER_LENGTH, false)) {
cryptor.encryptFile(inputContext.getInputStream(), c);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
@@ -260,7 +260,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 +278,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 +289,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,17 @@ 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);
}
} 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 +120,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 +137,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,60 @@ 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, upper);
}
} 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 this.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) {
// unsatisfiable content range:
outputContext.setContentLength(0);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getRight(), range.getRight(), contentLength));
LOG.debug("Unsatisfiable content range: " + 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));
}
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);
}
} 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.warn("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.debug("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.debug("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.8.2</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,26 +22,24 @@ 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;
@@ -48,12 +47,15 @@ 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 +213,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,7 +310,7 @@ 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 ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = encryptedFile.read(headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
return null;
@@ -319,20 +321,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,75 +342,37 @@ 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 ByteBuffer headerBuf = ByteBuffer.allocate(104);
final int headerBytesRead = encryptedFile.read(headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
@@ -419,134 +383,348 @@ 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 = encryptedFile.read(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 = encryptedFile.read(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);
sensitiveHeaderContentBuf.position(Long.BYTES); // skip file size
sensitiveHeaderContentBuf.get(fileKeyBytes);
// 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 InputStream in = new SeekableByteChannelInputStream(encryptedFile);
byte[] buffer = new byte[CONTENT_MAC_BLOCK + 32];
int n = 0;
long blockNum = startBlock;
while ((n = IOUtils.read(in, buffer)) > 0 && bytesWritten < length) {
if (n < 32) {
throw new DecryptFailedException("Invalid file content, missing MAC.");
}
// check MAC of current block:
if (authenticate) {
contentMac.update(iv);
contentMac.update(longToByteArray(blockNum));
contentMac.update(buffer, 0, n - 32);
final byte[] calculatedMac = contentMac.doFinal();
final byte[] storedMac = new byte[32];
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
}
}
// decrypt block:
final byte[] plaintext = cipher.update(buffer, 0, n - 32);
final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
final long pending = length - bytesWritten;
final int available = plaintext.length - offset;
final int currentBatch = (int) Math.min(pending, available);
plaintextFile.write(plaintext, offset, currentBatch);
bytesWritten += currentBatch;
blockNum++;
}
return bytesWritten;
} 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 = channel.read(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 +732,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
return plaintextSize;
}
private byte[] longToByteArray(long lng) {
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
}
}

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,40 @@
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);
bytesWritten++;
}
}
@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);
bytesWritten += adjustedLen;
}
}
public long getBytesWritten() {
return bytesWritten;
}
}

View File

@@ -0,0 +1,129 @@
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 int available() throws IOException {
final int inputAvailable = in.available();
if (inputAvailable > 0) {
return inputAvailable;
} else {
// remaining padding
choosePaddingLengthOnce();
return paddingLength - paddingBytesRead;
}
}
@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);
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.8.2</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

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.8.2</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.8.2</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.8.2</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.8.2</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.8.2</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -211,9 +211,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.8.2</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.8.2</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>
@@ -53,35 +59,4 @@
<artifactId>guice</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>
<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

@@ -20,7 +20,6 @@ import javafx.scene.control.Button;
import javafx.scene.control.Hyperlink;
import javafx.scene.text.Text;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -109,7 +108,7 @@ 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) {
} catch (IOException ex) {
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
LOG.error("Decryption failed for technical reasons.", ex);
newPasswordField.swipe();

View File

@@ -1,33 +1,83 @@
package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
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.collections.WeakListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javax.inject.Inject;
public class MacWarningsController {
import org.cryptomator.ui.model.Vault;
public class MacWarningsController implements Initializable {
@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;
private ResourceBundle rb;
@Inject
public MacWarningsController(Application application) {
this.application = application;
}
@Override
public void initialize(URL location, ResourceBundle rb) {
this.rb = rb;
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;
}
}));
}
@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 +85,70 @@ 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();
}
private void windowVisibilityDidChange(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
if (Boolean.TRUE.equals(newValue)) {
stage.setTitle(String.format(rb.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 setStage(Stage stage) {
this.stage = stage;
stage.showingProperty().addListener(new WeakChangeListener<>(stageVisibilityChangeListener));
}
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,21 @@ 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 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;
@@ -51,8 +47,6 @@ 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;
@@ -85,9 +79,6 @@ public class MainController implements Initializable, InitializationListener, Un
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;
@@ -97,9 +88,6 @@ public class MainController implements Initializable, InitializationListener, Un
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();
}
@Override
@@ -110,8 +98,6 @@ public class MainController implements Initializable, InitializationListener, Un
vaultList.setItems(items);
vaultList.setCellFactory(this::createDirecoryListCell);
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
aggregatedMacWarnings.addListener(this::macWarningsDidChange);
}
@FXML
@@ -233,12 +219,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
// ****************************************
@@ -293,7 +273,6 @@ public class MainController implements Initializable, InitializationListener, Un
@Override
public void didUnlock(UnlockController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
showUnlockedView(ctrl.getVault());
Platform.setImplicitExit(false);
}
@@ -306,9 +285,8 @@ public class MainController implements Initializable, InitializationListener, Un
@Override
public void didLock(UnlockedController ctrl) {
ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
showUnlockView(ctrl.getVault());
if (getUnlockedDirectories().isEmpty()) {
if (getUnlockedVaults().isEmpty()) {
Platform.setImplicitExit(true);
}
}
@@ -324,45 +302,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

@@ -35,7 +35,6 @@ import javafx.scene.text.Text;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.CharUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
@@ -134,7 +133,7 @@ public class UnlockController implements Initializable {
vault.setUnlocked(true);
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"));
@@ -178,6 +177,7 @@ public class UnlockController implements Initializable {
setControlsDisabled(false);
if (vault.isUnlocked() && !mountSuccess) {
vault.stopServer();
vault.setUnlocked(false);
}
if (mountSuccess && listener != null) {
listener.didUnlock(this);

View File

@@ -8,31 +8,45 @@
******************************************************************************/
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
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.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
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.MainModule.ControllerFactory;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.mount.CommandFailedException;
import com.google.inject.Inject;
public class UnlockedController implements Initializable {
private static final int IO_SAMPLING_STEPS = 100;
private static final double IO_SAMPLING_INTERVAL = 0.25;
private final ControllerFactory controllerFactory;
private final Stage macWarningWindow = new Stage();
private MacWarningsController macWarningCtrl;
private LockListener listener;
private Vault vault;
private Timeline ioAnimation;
@@ -48,9 +62,30 @@ public class UnlockedController implements Initializable {
private ResourceBundle rb;
@Inject
public UnlockedController(ControllerFactory controllerFactory) {
this.controllerFactory = controllerFactory;
}
@Override
public void initialize(URL url, ResourceBundle rb) {
this.rb = rb;
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
loader.setControllerFactory(controllerFactory);
final Parent root = loader.load();
macWarningWindow.setScene(new Scene(root));
macWarningWindow.sizeToScene();
macWarningWindow.setResizable(false);
ActiveWindowStyleSupport.startObservingFocus(macWarningWindow);
macWarningCtrl = loader.getController();
macWarningCtrl.setStage(macWarningWindow);
} catch (IOException e) {
throw new IllegalStateException("Failed to load fxml file.", e);
}
}
@FXML
@@ -68,6 +103,22 @@ public class UnlockedController implements Initializable {
}
}
// ****************************************
// MAC Auth Warnings
// ****************************************
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
if (change.getList().size() > 0) {
Platform.runLater(() -> {
macWarningWindow.show();
});
} else {
Platform.runLater(() -> {
macWarningWindow.hide();
});
}
}
// ****************************************
// IO Graph
// ****************************************
@@ -128,11 +179,22 @@ public class UnlockedController implements Initializable {
return vault;
}
public void setVault(Vault directory) {
this.vault = directory;
public void setVault(Vault vault) {
this.vault = vault;
macWarningCtrl.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:
if (vault.getCryptor() instanceof CryptorIOSampling) {
startIoSampling((CryptorIOSampling) vault.getCryptor());
} else {
ioGraph.setVisible(false);
}

View File

@@ -6,12 +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.Optional;
import java.util.Set;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.collections.ObservableList;
import javax.security.auth.DestroyFailedException;
@@ -43,7 +45,8 @@ 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 DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
@@ -77,11 +80,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,7 +105,7 @@ public class Vault implements Serializable {
} catch (DestroyFailedException e) {
LOG.error("Destruction of cryptor throw an exception.", e);
}
setUnlocked(false);
whitelistedResourcesWithInvalidMac.clear();
namesOfResourcesWithInvalidMac.clear();
}
@@ -160,10 +164,14 @@ public class Vault implements Serializable {
return mountName;
}
public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
return namesOfResourcesWithInvalidMac;
}
public Set<String> getWhitelistedResourcesWithInvalidMac() {
return whitelistedResourcesWithInvalidMac;
}
/**
* Tries to form a similar string using the regular latin alphabet.
*

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

@@ -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

@@ -30,7 +30,7 @@ import org.cryptomator.ui.util.command.Script;
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 int MAX_MOUNT_ATTEMPTS = 8;
@Override
public boolean shouldWork() {
@@ -39,30 +39,26 @@ 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");
final Script mountScript = fromLines("net use * \\\\localhost@%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 localhostMountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script ipv6literaltMountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
ipv6literaltMountScript.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);
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript);
}
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script openExplorerScript = fromLines("start explorer.exe " + driveLetter);
openExplorerScript.execute();
@@ -77,7 +73,7 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
}
};
}
private boolean isVolumeMounted(String driveLetter) {
for (Path path : FileSystems.getDefault().getRootDirectories()) {
if (path.toString().startsWith(driveLetter)) {
@@ -86,15 +82,17 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
}
return false;
}
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) {

View File

@@ -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

@@ -56,10 +56,10 @@ unlocked.label.unmountFailed=Ejecting drive failed.
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
# mac_warnings.fxml
macWarnings.windowTitle=Danger - MAC authentication failed
macWarnings.windowTitle=Danger - Corrupted file in %s
macWarnings.message=Cryptomator detected potentially malicious corruptions in the following files:
macWarnings.moreInformationButton=Learn more
macWarnings.dismissButton=I promise to be careful
macWarnings.whitelistButton=Decrypt selected anyway
# tray icon
tray.menu.open=Open