mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-19 03:01:27 +00:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095f60ec03 | ||
|
|
9ea9cb6eb2 | ||
|
|
301ba9cdb7 | ||
|
|
740c4c2ba9 | ||
|
|
18e7dcd91f | ||
|
|
95133152f9 | ||
|
|
4cd243e32a | ||
|
|
f454f48248 | ||
|
|
ad3801b223 | ||
|
|
3f946d1c82 | ||
|
|
ecb178d5b2 | ||
|
|
ed7dc60f5e | ||
|
|
6bbfacd794 | ||
|
|
5a06d01ef5 | ||
|
|
aac9ead633 | ||
|
|
cdcc1626ce | ||
|
|
738d2dfc34 | ||
|
|
9771c6d1e7 | ||
|
|
bc0a26b0ad | ||
|
|
7349ef754e | ||
|
|
e8e80f306b | ||
|
|
e1ce400bcd | ||
|
|
8c4d5a9614 | ||
|
|
93a87c86a4 | ||
|
|
685e347524 | ||
|
|
9d2d847727 | ||
|
|
a00086ff2d | ||
|
|
d76154c8d1 | ||
|
|
bc76ab285d | ||
|
|
0d3a5b4e70 | ||
|
|
48f544ef91 | ||
|
|
45cf87d089 | ||
|
|
d7186bb2dd |
@@ -1,7 +1,8 @@
|
|||||||
language: java
|
language: java
|
||||||
jdk:
|
jdk:
|
||||||
- oraclejdk8
|
- 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:
|
notifications:
|
||||||
webhooks:
|
webhooks:
|
||||||
urls:
|
urls:
|
||||||
@@ -11,9 +12,10 @@ notifications:
|
|||||||
on_start: false
|
on_start: false
|
||||||
deploy:
|
deploy:
|
||||||
provider: releases
|
provider: releases
|
||||||
|
prerelease: true
|
||||||
api_key:
|
api_key:
|
||||||
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
|
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
|
||||||
file: main/target/Cryptomator-$TRAVIS_TAG.jar
|
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
|
||||||
skip_cleanup: true
|
skip_cleanup: true
|
||||||
on:
|
on:
|
||||||
repo: cryptomator/cryptomator
|
repo: cryptomator/cryptomator
|
||||||
|
|||||||
@@ -12,16 +12,14 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>core</artifactId>
|
<artifactId>core</artifactId>
|
||||||
<name>Cryptomator WebDAV and I/O module</name>
|
<name>Cryptomator WebDAV and I/O module</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<jetty.version>9.2.10.v20150310</jetty.version>
|
<jetty.version>9.3.3.v20150827</jetty.version>
|
||||||
<jackrabbit.version>2.10.1</jackrabbit.version>
|
<jackrabbit.version>2.11.0</jackrabbit.version>
|
||||||
<commons.transaction.version>1.2</commons.transaction.version>
|
|
||||||
<jta.version>1.1</jta.version>
|
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
@@ -29,6 +27,11 @@
|
|||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>crypto-api</artifactId>
|
<artifactId>crypto-api</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.cryptomator</groupId>
|
||||||
|
<artifactId>crypto-aes</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Jetty (Servlet Container) -->
|
<!-- Jetty (Servlet Container) -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -41,6 +44,11 @@
|
|||||||
<artifactId>jetty-webapp</artifactId>
|
<artifactId>jetty-webapp</artifactId>
|
||||||
<version>${jetty.version}</version>
|
<version>${jetty.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>commons-httpclient</groupId>
|
||||||
|
<artifactId>commons-httpclient</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Jackrabbit -->
|
<!-- Jackrabbit -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -48,13 +56,13 @@
|
|||||||
<artifactId>jackrabbit-webdav</artifactId>
|
<artifactId>jackrabbit-webdav</artifactId>
|
||||||
<version>${jackrabbit.version}</version>
|
<version>${jackrabbit.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Guava -->
|
<!-- Guava -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.guava</groupId>
|
<groupId>com.google.guava</groupId>
|
||||||
<artifactId>guava</artifactId>
|
<artifactId>guava</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- I/O -->
|
<!-- I/O -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>commons-io</groupId>
|
<groupId>commons-io</groupId>
|
||||||
@@ -64,7 +72,7 @@
|
|||||||
<groupId>org.apache.commons</groupId>
|
<groupId>org.apache.commons</groupId>
|
||||||
<artifactId>commons-lang3</artifactId>
|
<artifactId>commons-lang3</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- JSON -->
|
<!-- JSON -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public final class WebDavServer {
|
|||||||
private static final int MAX_THREADS = 200;
|
private static final int MAX_THREADS = 200;
|
||||||
private static final int MIN_THREADS = 4;
|
private static final int MIN_THREADS = 4;
|
||||||
private static final int THREAD_IDLE_SECONDS = 20;
|
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 Server server;
|
||||||
private final ServerConnector localConnector;
|
private final ServerConnector localConnector;
|
||||||
private final ContextHandlerCollection servletCollection;
|
private final ContextHandlerCollection servletCollection;
|
||||||
@@ -50,11 +51,14 @@ public final class WebDavServer {
|
|||||||
server = new Server(tp);
|
server = new Server(tp);
|
||||||
localConnector = new ServerConnector(server);
|
localConnector = new ServerConnector(server);
|
||||||
localConnector.setHost(LOCALHOST);
|
localConnector.setHost(LOCALHOST);
|
||||||
|
localConnector.setIdleTimeout(CONNECTION_IDLE_MILLIS);
|
||||||
servletCollection = new ContextHandlerCollection();
|
servletCollection = new ContextHandlerCollection();
|
||||||
|
|
||||||
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
|
if (SystemUtils.IS_OS_WINDOWS) {
|
||||||
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
|
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
|
||||||
servletContext.addServlet(servlet, "/");
|
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
|
||||||
|
servletContext.addServlet(servlet, "/");
|
||||||
|
}
|
||||||
|
|
||||||
server.setConnectors(new Connector[] {localConnector});
|
server.setConnectors(new Connector[] {localConnector});
|
||||||
server.setHandler(servletCollection);
|
server.setHandler(servletCollection);
|
||||||
@@ -84,13 +88,11 @@ public final class WebDavServer {
|
|||||||
/**
|
/**
|
||||||
* @param workDir Path of encrypted folder.
|
* @param workDir Path of encrypted folder.
|
||||||
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
|
* @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
|
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
|
||||||
* authentication fails.
|
* @param name The name of the folder. Must be non-empty and only contain any of _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||||
* @param name The name of the folder. Must be non-empty and only contain any of
|
|
||||||
* _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
|
||||||
* @return servlet
|
* @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 {
|
try {
|
||||||
if (StringUtils.isEmpty(name)) {
|
if (StringUtils.isEmpty(name)) {
|
||||||
throw new IllegalArgumentException("name empty");
|
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 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 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, "/*");
|
servletContext.addServlet(servlet, "/*");
|
||||||
|
|
||||||
servletCollection.mapContexts();
|
servletCollection.mapContexts();
|
||||||
@@ -113,8 +115,8 @@ public final class WebDavServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> 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));
|
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
|
||||||
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
|
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class CleartextLocatorFactory implements DavLocatorFactory {
|
|||||||
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
|
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
|
||||||
final String fullPrefix = pathPrefix.endsWith("/") ? pathPrefix : pathPrefix + "/";
|
final String fullPrefix = pathPrefix.endsWith("/") ? pathPrefix : pathPrefix + "/";
|
||||||
final String href = fullPrefix.concat(encodedResourcePath);
|
final String href = fullPrefix.concat(encodedResourcePath);
|
||||||
assert !href.endsWith("/");
|
assert href.equals(fullPrefix) || !href.endsWith("/");
|
||||||
if (isCollection) {
|
if (isCollection) {
|
||||||
return href.concat("/");
|
return href.concat("/");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ import java.nio.file.FileAlreadyExistsException;
|
|||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
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.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.DavException;
|
||||||
import org.apache.jackrabbit.webdav.DavMethods;
|
import org.apache.jackrabbit.webdav.DavMethods;
|
||||||
import org.apache.jackrabbit.webdav.DavResource;
|
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.jackrabbit.webdav.lock.SimpleLockManager;
|
||||||
import org.apache.logging.log4j.util.Strings;
|
import org.apache.logging.log4j.util.Strings;
|
||||||
import org.cryptomator.crypto.Cryptor;
|
import org.cryptomator.crypto.Cryptor;
|
||||||
|
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
|
|
||||||
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
|
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 LockManager lockManager = new SimpleLockManager();
|
||||||
private final Cryptor cryptor;
|
private final Cryptor cryptor;
|
||||||
private final CryptoWarningHandler cryptoWarningHandler;
|
private final CryptoWarningHandler cryptoWarningHandler;
|
||||||
private final ExecutorService backgroundTaskExecutor;
|
|
||||||
private final Path dataRoot;
|
private final Path dataRoot;
|
||||||
private final FilenameTranslator filenameTranslator;
|
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);
|
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
|
||||||
this.cryptor = cryptor;
|
this.cryptor = cryptor;
|
||||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||||
this.backgroundTaskExecutor = backgroundTaskExecutor;
|
|
||||||
this.dataRoot = vaultRootPath.resolve("d");
|
this.dataRoot = vaultRootPath.resolve("d");
|
||||||
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
|
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
|
||||||
}
|
}
|
||||||
@@ -47,20 +53,36 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
|||||||
return createRootDirectory(locator, request.getDavSession());
|
return createRootDirectory(locator, request.getDavSession());
|
||||||
}
|
}
|
||||||
|
|
||||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
|
try {
|
||||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
|
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
|
||||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
|
||||||
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||||
return createDirectory(locator, request.getDavSession(), dirFilePath);
|
final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
|
||||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
// DIRECTORY
|
||||||
return createFilePart(locator, request.getDavSession(), request, filePath);
|
return createDirectory(locator, request.getDavSession(), dirFilePath);
|
||||||
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
|
||||||
return createFile(locator, request.getDavSession(), filePath);
|
// FILE RANGE
|
||||||
} else {
|
final Pair<String, String> requestRange = getRequestRange(rangeHeader);
|
||||||
// e.g. for MOVE operations:
|
response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
|
||||||
return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath);
|
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
|
@Override
|
||||||
@@ -69,16 +91,18 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
|||||||
return createRootDirectory(locator, session);
|
return createRootDirectory(locator, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
|
try {
|
||||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
|
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
|
||||||
if (Files.exists(dirFilePath)) {
|
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
|
||||||
return createDirectory(locator, session, dirFilePath);
|
if (Files.exists(dirFilePath)) {
|
||||||
} else if (Files.exists(filePath)) {
|
return createDirectory(locator, session, dirFilePath);
|
||||||
return createFile(locator, session, filePath);
|
} else if (Files.exists(filePath)) {
|
||||||
} else {
|
return createFile(locator, session, filePath);
|
||||||
// e.g. for MOVE operations:
|
}
|
||||||
return createNonExisting(locator, session, filePath, dirFilePath);
|
} catch (NonExistingParentException e) {
|
||||||
|
// return non-existing
|
||||||
}
|
}
|
||||||
|
return createNonExisting(locator, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
|
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.
|
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
|
||||||
* @throws IOException
|
|
||||||
*/
|
*/
|
||||||
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 String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||||
try {
|
try {
|
||||||
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||||
return parent.resolve(encryptedFilename);
|
return parent.resolve(encryptedFilename);
|
||||||
} catch (IOException e) {
|
} 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.
|
* @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 String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||||
try {
|
try {
|
||||||
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||||
return parent.resolve(encryptedFilename);
|
return parent.resolve(encryptedFilename);
|
||||||
} catch (IOException e) {
|
} 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.
|
* @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("/");
|
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
|
||||||
try {
|
try {
|
||||||
final Path result;
|
final Path result;
|
||||||
@@ -135,10 +226,13 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
|||||||
result = dataRoot.resolve(fixedRootDirectory);
|
result = dataRoot.resolve(fixedRootDirectory);
|
||||||
} else {
|
} else {
|
||||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
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 cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||||
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||||
final Path directoryFile = parent.resolve(encryptedFilename);
|
final Path directoryFile = parent.resolve(encryptedFilename);
|
||||||
|
if (!createNonExisting && !Files.exists(directoryFile)) {
|
||||||
|
throw new NonExistingParentException();
|
||||||
|
}
|
||||||
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
|
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
|
||||||
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||||
result = dataRoot.resolve(directory);
|
result = dataRoot.resolve(directory);
|
||||||
@@ -146,12 +240,12 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
|||||||
Files.createDirectories(result);
|
Files.createDirectories(result);
|
||||||
return result;
|
return result;
|
||||||
} catch (IOException e) {
|
} 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) {
|
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, Path filePath) {
|
||||||
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath);
|
return new EncryptedFilePart(this, locator, session, requestRange, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path 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);
|
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session, Path filePath, Path dirFilePath) {
|
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||||
return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath);
|
return new NonExistingNode(this, locator, session, lockManager, cryptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class NonExistingParentException extends Exception {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 4421121746624627094L;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,22 @@ import java.util.Collection;
|
|||||||
class CryptoWarningHandler {
|
class CryptoWarningHandler {
|
||||||
|
|
||||||
private final Collection<String> resourcesWithInvalidMac;
|
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.resourcesWithInvalidMac = resourcesWithInvalidMac;
|
||||||
|
this.whitelistedResources = whitelistedResources;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void macAuthFailed(String resourceName) {
|
public void macAuthFailed(String resourcePath) {
|
||||||
if (!resourcesWithInvalidMac.contains(resourceName)) {
|
// collection might be a list, but we don't want duplicates:
|
||||||
resourcesWithInvalidMac.add(resourceName);
|
if (!resourcesWithInvalidMac.contains(resourcePath)) {
|
||||||
|
resourcesWithInvalidMac.add(resourcePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean ignoreMac(String resourcePath) {
|
||||||
|
return whitelistedResources.contains(resourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import java.io.FileNotFoundException;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
@@ -156,7 +155,8 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
|||||||
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
|
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
|
||||||
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||||
final Path filePath = dirPath.resolve(ciphertextFilename);
|
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);
|
cryptor.encryptFile(inputContext.getInputStream(), c);
|
||||||
} catch (SecurityException e) {
|
} catch (SecurityException e) {
|
||||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||||
@@ -260,7 +260,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
|||||||
final Path srcPath = filePath;
|
final Path srcPath = filePath;
|
||||||
final Path dstPath;
|
final Path dstPath;
|
||||||
if (dest instanceof NonExistingNode) {
|
if (dest instanceof NonExistingNode) {
|
||||||
dstPath = ((NonExistingNode) dest).getDirFilePath();
|
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||||
} else {
|
} else {
|
||||||
dstPath = dest.filePath;
|
dstPath = dest.filePath;
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
|||||||
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
||||||
final Path dstDirFilePath;
|
final Path dstDirFilePath;
|
||||||
if (dest instanceof NonExistingNode) {
|
if (dest instanceof NonExistingNode) {
|
||||||
dstDirFilePath = ((NonExistingNode) dest).getDirFilePath();
|
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||||
} else {
|
} else {
|
||||||
dstDirFilePath = dest.filePath;
|
dstDirFilePath = dest.filePath;
|
||||||
}
|
}
|
||||||
@@ -289,7 +289,8 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
|||||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||||
}
|
}
|
||||||
final String dstDirId = UUID.randomUUID().toString();
|
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)));
|
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ package org.cryptomator.webdav.jackrabbit;
|
|||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
|
||||||
import java.nio.channels.OverlappingFileLockException;
|
import java.nio.channels.OverlappingFileLockException;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
|
||||||
import java.nio.file.AtomicMoveNotSupportedException;
|
import java.nio.file.AtomicMoveNotSupportedException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
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.DavPropertyName;
|
||||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||||
import org.cryptomator.crypto.Cryptor;
|
import org.cryptomator.crypto.Cryptor;
|
||||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
|
||||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
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);
|
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||||
|
|
||||||
protected final CryptoWarningHandler cryptoWarningHandler;
|
protected final CryptoWarningHandler cryptoWarningHandler;
|
||||||
|
protected final Long contentLength;
|
||||||
|
|
||||||
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
||||||
super(factory, locator, session, lockManager, cryptor, 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");
|
throw new IllegalArgumentException("filePath must not be null");
|
||||||
}
|
}
|
||||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||||
|
Long contentLength = null;
|
||||||
if (Files.isRegularFile(filePath)) {
|
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)) {
|
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||||
final Long contentLength = cryptor.decryptedContentLength(c);
|
contentLength = cryptor.decryptedContentLength(c);
|
||||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||||
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
|
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
|
||||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||||
@@ -61,14 +60,19 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
|||||||
} catch (OverlappingFileLockException e) {
|
} catch (OverlappingFileLockException e) {
|
||||||
// file header currently locked, report -1 for unknown size.
|
// file header currently locked, report -1 for unknown size.
|
||||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
|
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) {
|
} catch (MacAuthenticationFailedException e) {
|
||||||
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
|
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
|
||||||
// don't add content length DAV property
|
// 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
|
@Override
|
||||||
@@ -96,20 +100,17 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
|||||||
if (Files.isRegularFile(filePath)) {
|
if (Files.isRegularFile(filePath)) {
|
||||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
final Long contentLength = cryptor.decryptedContentLength(c);
|
||||||
if (contentLength != null) {
|
if (contentLength != null) {
|
||||||
outputContext.setContentLength(contentLength);
|
outputContext.setContentLength(contentLength);
|
||||||
}
|
}
|
||||||
if (outputContext.hasStream()) {
|
if (outputContext.hasStream()) {
|
||||||
cryptor.decryptFile(channel, outputContext.getOutputStream());
|
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||||
|
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
|
||||||
}
|
}
|
||||||
} catch (EOFException e) {
|
} catch (EOFException e) {
|
||||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
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 srcPath = filePath;
|
||||||
final Path dstPath;
|
final Path dstPath;
|
||||||
if (dest instanceof NonExistingNode) {
|
if (dest instanceof NonExistingNode) {
|
||||||
dstPath = ((NonExistingNode) dest).getFilePath();
|
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||||
} else {
|
} else {
|
||||||
dstPath = dest.filePath;
|
dstPath = dest.filePath;
|
||||||
}
|
}
|
||||||
@@ -136,7 +137,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
|||||||
final Path srcPath = filePath;
|
final Path srcPath = filePath;
|
||||||
final Path dstPath;
|
final Path dstPath;
|
||||||
if (dest instanceof NonExistingNode) {
|
if (dest instanceof NonExistingNode) {
|
||||||
dstPath = ((NonExistingNode) dest).getFilePath();
|
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||||
} else {
|
} else {
|
||||||
dstPath = dest.filePath;
|
dstPath = dest.filePath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,22 @@ package org.cryptomator.webdav.jackrabbit;
|
|||||||
|
|
||||||
import java.io.EOFException;
|
import java.io.EOFException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.channels.ClosedByInterruptException;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.StandardOpenOption;
|
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.ImmutablePair;
|
||||||
import org.apache.commons.lang3.tuple.MutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
|
||||||
import org.apache.jackrabbit.webdav.DavSession;
|
import org.apache.jackrabbit.webdav.DavSession;
|
||||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||||
import org.cryptomator.crypto.Cryptor;
|
import org.cryptomator.crypto.Cryptor;
|
||||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
|
||||||
import org.eclipse.jetty.http.HttpHeader;
|
import org.eclipse.jetty.http.HttpHeader;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
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.
|
* Delivers only the requested range of bytes from a file.
|
||||||
*
|
*
|
||||||
@@ -38,157 +26,60 @@ import com.google.common.cache.CacheBuilder;
|
|||||||
class EncryptedFilePart extends EncryptedFile {
|
class EncryptedFilePart extends EncryptedFile {
|
||||||
|
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
|
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();
|
|
||||||
|
|
||||||
/**
|
private final Pair<Long, Long> range;
|
||||||
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
|
|
||||||
*/
|
|
||||||
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
|
|
||||||
|
|
||||||
/**
|
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, LockManager lockManager, Cryptor cryptor,
|
||||||
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
|
CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
||||||
*/
|
|
||||||
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) {
|
|
||||||
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, 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) {
|
try {
|
||||||
if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
|
final Long lower = requestRange.getLeft().isEmpty() ? null : Long.valueOf(requestRange.getLeft());
|
||||||
final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
|
final Long upper = requestRange.getRight().isEmpty() ? null : Long.valueOf(requestRange.getRight());
|
||||||
cachedMacAuthenticationJobs.put(locator, macAuthJob);
|
if (lower == null) {
|
||||||
backgroundTaskExecutor.submit(macAuthJob);
|
range = new ImmutablePair<Long, Long>(contentLength - upper, contentLength - 1);
|
||||||
}
|
} else if (upper == null) {
|
||||||
}
|
range = new ImmutablePair<Long, Long>(lower, contentLength - 1);
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
left = range.getLeft();
|
range = new ImmutablePair<Long, Long>(lower, upper);
|
||||||
right = range.getRight();
|
|
||||||
}
|
|
||||||
if (result.getLeft() == null || left < result.getLeft()) {
|
|
||||||
result.setLeft(left);
|
|
||||||
}
|
|
||||||
if (result.getRight() == null || right > result.getRight()) {
|
|
||||||
result.setRight(right);
|
|
||||||
}
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
|
||||||
}
|
}
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void spool(OutputContext outputContext) throws IOException {
|
public void spool(OutputContext outputContext) throws IOException {
|
||||||
assert Files.isRegularFile(filePath);
|
assert Files.isRegularFile(filePath);
|
||||||
|
assert this.contentLength != null;
|
||||||
|
|
||||||
|
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
if (rangeLength <= 0) {
|
||||||
final Long fileSize = cryptor.decryptedContentLength(channel);
|
// unsatisfiable content range:
|
||||||
final Pair<Long, Long> range = getUnionRange(fileSize);
|
outputContext.setContentLength(0);
|
||||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
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.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()) {
|
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) {
|
} catch (EOFException e) {
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
|
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) {
|
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||||
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
|
return String.format("bytes %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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ interface FileConstants {
|
|||||||
/**
|
/**
|
||||||
* Number of bytes in the file header.
|
* Number of bytes in the file header.
|
||||||
*/
|
*/
|
||||||
long FILE_HEADER_LENGTH = 96;
|
long FILE_HEADER_LENGTH = 104;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allow range requests for files > 32MiB.
|
* Allow range requests for files > 32MiB.
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import java.io.IOException;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.channels.FileChannel;
|
import java.nio.channels.FileChannel;
|
||||||
import java.nio.channels.FileLock;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
@@ -130,13 +129,14 @@ class FilenameTranslator implements FileConstants {
|
|||||||
/* Locked I/O */
|
/* Locked I/O */
|
||||||
|
|
||||||
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
|
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));
|
c.write(ByteBuffer.wrap(bytes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] readAllBytesAtomically(Path path) throws IOException {
|
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());
|
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||||
c.read(buffer);
|
c.read(buffer);
|
||||||
return buffer.array();
|
return buffer.array();
|
||||||
|
|||||||
@@ -21,16 +21,12 @@ import org.apache.jackrabbit.webdav.io.OutputContext;
|
|||||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||||
import org.apache.jackrabbit.webdav.property.DavProperty;
|
import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||||
import org.cryptomator.crypto.Cryptor;
|
import org.cryptomator.crypto.Cryptor;
|
||||||
|
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
|
||||||
|
|
||||||
class NonExistingNode extends AbstractEncryptedNode {
|
class NonExistingNode extends AbstractEncryptedNode {
|
||||||
|
|
||||||
private final Path filePath;
|
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||||
private final Path dirFilePath;
|
|
||||||
|
|
||||||
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) {
|
|
||||||
super(factory, locator, session, lockManager, cryptor, null);
|
super(factory, locator, session, lockManager, cryptor, null);
|
||||||
this.filePath = filePath;
|
|
||||||
this.dirFilePath = dirFilePath;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,12 +79,26 @@ class NonExistingNode extends AbstractEncryptedNode {
|
|||||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -8,63 +8,50 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package org.cryptomator.webdav.jackrabbit;
|
package org.cryptomator.webdav.jackrabbit;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Collection;
|
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.ServletConfig;
|
||||||
import javax.servlet.ServletException;
|
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.DavLocatorFactory;
|
||||||
import org.apache.jackrabbit.webdav.DavResource;
|
import org.apache.jackrabbit.webdav.DavResource;
|
||||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||||
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
||||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||||
|
import org.apache.jackrabbit.webdav.WebdavResponse;
|
||||||
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
|
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
|
||||||
import org.cryptomator.crypto.Cryptor;
|
import org.cryptomator.crypto.Cryptor;
|
||||||
|
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class WebDavServlet extends AbstractWebdavServlet {
|
public class WebDavServlet extends AbstractWebdavServlet {
|
||||||
|
|
||||||
private static final long serialVersionUID = 7965170007048673022L;
|
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";
|
public static final String CFG_FS_ROOT = "cfg.fs.root";
|
||||||
private DavSessionProvider davSessionProvider;
|
private DavSessionProvider davSessionProvider;
|
||||||
private DavLocatorFactory davLocatorFactory;
|
private DavLocatorFactory davLocatorFactory;
|
||||||
private DavResourceFactory davResourceFactory;
|
private DavResourceFactory davResourceFactory;
|
||||||
private final Cryptor cryptor;
|
private final Cryptor cryptor;
|
||||||
private final CryptoWarningHandler cryptoWarningHandler;
|
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();
|
super();
|
||||||
this.cryptor = cryptor;
|
this.cryptor = cryptor;
|
||||||
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
|
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init(ServletConfig config) throws ServletException {
|
public void init(ServletConfig config) throws ServletException {
|
||||||
super.init(config);
|
super.init(config);
|
||||||
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
|
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
|
||||||
backgroundTaskExecutor = Executors.newCachedThreadPool();
|
|
||||||
davSessionProvider = new DavSessionProviderImpl();
|
davSessionProvider = new DavSessionProviderImpl();
|
||||||
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
|
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
|
||||||
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot);
|
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, 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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -102,4 +89,30 @@ public class WebDavServlet extends AbstractWebdavServlet {
|
|||||||
this.davResourceFactory = resourceFactory;
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
33
main/core/src/test/resources/log4j2.xml
Normal file
33
main/core/src/test/resources/log4j2.xml
Normal 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>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>crypto-aes</artifactId>
|
<artifactId>crypto-aes</artifactId>
|
||||||
<name>Cryptomator cryptographic module (AES)</name>
|
<name>Cryptomator cryptographic module (AES)</name>
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package org.cryptomator.crypto.aes256;
|
package org.cryptomator.crypto.aes256;
|
||||||
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.Channels;
|
||||||
|
import java.nio.channels.ReadableByteChannel;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.InvalidAlgorithmParameterException;
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
@@ -21,26 +22,24 @@ import java.security.MessageDigest;
|
|||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.CipherInputStream;
|
|
||||||
import javax.crypto.CipherOutputStream;
|
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
import javax.crypto.ShortBufferException;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import javax.security.auth.DestroyFailedException;
|
import javax.security.auth.DestroyFailedException;
|
||||||
import javax.security.auth.Destroyable;
|
import javax.security.auth.Destroyable;
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
import org.apache.commons.io.IOUtils;
|
||||||
import org.apache.commons.io.output.NullOutputStream;
|
|
||||||
import org.bouncycastle.crypto.generators.SCrypt;
|
import org.bouncycastle.crypto.generators.SCrypt;
|
||||||
import org.cryptomator.crypto.Cryptor;
|
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.DecryptFailedException;
|
||||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
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.UnsupportedVaultException;
|
||||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||||
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
|
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;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
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
|
* 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/.
|
* here: http://www.oracle.com/technetwork/java/javase/downloads/.
|
||||||
@@ -211,7 +213,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
|||||||
} catch (InvalidKeyException ex) {
|
} catch (InvalidKeyException ex) {
|
||||||
throw new IllegalArgumentException("Invalid key.", ex);
|
throw new IllegalArgumentException("Invalid key.", ex);
|
||||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException 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 {
|
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
||||||
// read header:
|
// read header:
|
||||||
encryptedFile.position(0);
|
encryptedFile.position(0);
|
||||||
final ByteBuffer headerBuf = ByteBuffer.allocate(64);
|
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||||
if (headerBytesRead != headerBuf.capacity()) {
|
if (headerBytesRead != headerBuf.capacity()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -319,20 +321,20 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
|||||||
headerBuf.position(0);
|
headerBuf.position(0);
|
||||||
headerBuf.get(iv);
|
headerBuf.get(iv);
|
||||||
|
|
||||||
// read content length:
|
// read sensitive header data:
|
||||||
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
|
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||||
headerBuf.position(16);
|
headerBuf.position(24);
|
||||||
headerBuf.get(encryptedContentLengthBytes);
|
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||||
|
|
||||||
// read stored header mac:
|
// read stored header mac:
|
||||||
final byte[] storedHeaderMac = new byte[32];
|
final byte[] storedHeaderMac = new byte[32];
|
||||||
headerBuf.position(32);
|
headerBuf.position(72);
|
||||||
headerBuf.get(storedHeaderMac);
|
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);
|
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||||
headerBuf.rewind();
|
headerBuf.rewind();
|
||||||
headerBuf.limit(32);
|
headerBuf.limit(72);
|
||||||
headerMac.update(headerBuf);
|
headerMac.update(headerBuf);
|
||||||
|
|
||||||
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
||||||
@@ -340,75 +342,37 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
|||||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
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 {
|
try {
|
||||||
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
|
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
|
||||||
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes);
|
return sizeCipher.doFinal(ciphertextBytes);
|
||||||
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
|
|
||||||
return fileSizeBuffer.getLong();
|
|
||||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||||
throw new IllegalStateException(e);
|
throw new IllegalStateException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] encryptContentLength(long contentLength, byte[] iv) {
|
private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
|
||||||
try {
|
try {
|
||||||
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
|
|
||||||
fileSizeBuffer.putLong(contentLength);
|
|
||||||
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
||||||
return sizeCipher.doFinal(fileSizeBuffer.array());
|
return sizeCipher.doFinal(plaintextBytes);
|
||||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||||
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
|
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
|
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||||
// read header:
|
// read header:
|
||||||
encryptedFile.position(0l);
|
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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// read header mac:
|
|
||||||
final byte[] storedHeaderMac = new byte[32];
|
|
||||||
headerBuf.position(32);
|
|
||||||
headerBuf.get(storedHeaderMac);
|
|
||||||
|
|
||||||
// read content mac:
|
|
||||||
final byte[] storedContentMac = new byte[32];
|
|
||||||
headerBuf.position(64);
|
|
||||||
headerBuf.get(storedContentMac);
|
|
||||||
|
|
||||||
// calculate mac over first 32 bytes of header:
|
|
||||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
|
||||||
headerBuf.position(0);
|
|
||||||
headerBuf.limit(32);
|
|
||||||
headerMac.update(headerBuf);
|
|
||||||
|
|
||||||
// calculate mac over content:
|
|
||||||
encryptedFile.position(96l);
|
|
||||||
final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
|
||||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
|
||||||
final InputStream macIn = new MacInputStream(in, contentMac);
|
|
||||||
IOUtils.copyLarge(macIn, new NullOutputStream());
|
|
||||||
|
|
||||||
// compare (in constant time):
|
|
||||||
final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
|
||||||
final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal());
|
|
||||||
return headerMacMatches && contentMacMatches;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
|
||||||
// read header:
|
|
||||||
encryptedFile.position(0l);
|
|
||||||
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
|
|
||||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||||
if (headerBytesRead != headerBuf.capacity()) {
|
if (headerBytesRead != headerBuf.capacity()) {
|
||||||
throw new IOException("Failed to read file header.");
|
throw new IOException("Failed to read file header.");
|
||||||
@@ -419,134 +383,348 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
|||||||
headerBuf.position(0);
|
headerBuf.position(0);
|
||||||
headerBuf.get(iv);
|
headerBuf.get(iv);
|
||||||
|
|
||||||
// read content length:
|
// read nonce:
|
||||||
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
|
final byte[] nonce = new byte[8];
|
||||||
headerBuf.position(16);
|
headerBuf.position(16);
|
||||||
headerBuf.get(encryptedContentLengthBytes);
|
headerBuf.get(nonce);
|
||||||
final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv);
|
|
||||||
|
// read sensitive header data:
|
||||||
|
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||||
|
headerBuf.position(24);
|
||||||
|
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||||
|
|
||||||
// read header mac:
|
// read header mac:
|
||||||
final byte[] headerMac = new byte[32];
|
final byte[] storedHeaderMac = new byte[32];
|
||||||
headerBuf.position(32);
|
headerBuf.position(72);
|
||||||
headerBuf.get(headerMac);
|
headerBuf.get(storedHeaderMac);
|
||||||
|
|
||||||
// read content mac:
|
// calculate mac over first 72 bytes of header:
|
||||||
final byte[] contentMac = new byte[32];
|
if (authenticate) {
|
||||||
headerBuf.position(64);
|
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||||
headerBuf.get(contentMac);
|
headerBuf.position(0);
|
||||||
|
headerBuf.limit(72);
|
||||||
// decrypt content
|
headerMac.update(headerBuf);
|
||||||
encryptedFile.position(96l);
|
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
||||||
final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey);
|
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
||||||
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.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
@Override
|
||||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||||
// read iv:
|
// read header:
|
||||||
encryptedFile.position(0l);
|
encryptedFile.position(0l);
|
||||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||||
final int numIvBytesRead = encryptedFile.read(countingIv);
|
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||||
|
if (headerBytesRead != headerBuf.capacity()) {
|
||||||
// check validity of header:
|
|
||||||
if (numIvBytesRead != AES_BLOCK_LENGTH) {
|
|
||||||
throw new IOException("Failed to read file header.");
|
throw new IOException("Failed to read file header.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// seek relevant position and update iv:
|
// read iv:
|
||||||
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
|
final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
||||||
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
|
headerBuf.position(0);
|
||||||
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
|
headerBuf.get(iv);
|
||||||
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
|
|
||||||
|
|
||||||
// fast forward stream:
|
// read nonce:
|
||||||
encryptedFile.position(96l + beginOfFirstRelevantBlock);
|
final byte[] nonce = new byte[8];
|
||||||
|
headerBuf.position(16);
|
||||||
|
headerBuf.get(nonce);
|
||||||
|
|
||||||
// generate cipher:
|
// read sensitive header data:
|
||||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||||
|
headerBuf.position(24);
|
||||||
|
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||||
|
|
||||||
// read content
|
// read header mac:
|
||||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
final byte[] storedHeaderMac = new byte[32];
|
||||||
final InputStream cipheredIn = new CipherInputStream(in, cipher);
|
headerBuf.position(72);
|
||||||
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
|
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
|
@Override
|
||||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||||
// truncate file
|
// truncate file
|
||||||
encryptedFile.truncate(0l);
|
encryptedFile.truncate(0l);
|
||||||
|
|
||||||
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
|
// choose a random header IV:
|
||||||
final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
|
final byte[] iv = randomData(AES_BLOCK_LENGTH);
|
||||||
ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
|
|
||||||
final byte[] iv = ivBuf.array();
|
|
||||||
|
|
||||||
// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content
|
// chosse 8 byte random nonce and 8 byte counter set to zero:
|
||||||
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
|
final byte[] nonce = randomData(8);
|
||||||
headerBuf.limit(96);
|
|
||||||
|
// 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);
|
encryptedFile.write(headerBuf);
|
||||||
|
|
||||||
// content encryption:
|
// prepare content encryption:
|
||||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
||||||
final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
|
||||||
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
|
return new EncryptWorker(lock, blockDone, currentBlock, inputQueue, 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.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// add random length padding to obfuscate file length:
|
@Override
|
||||||
final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
|
protected Cipher initCipher(long startBlockNum) {
|
||||||
final long minAdditionalBlocks = 4;
|
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||||
final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs)
|
nonceAndCounterBuf.put(nonce);
|
||||||
final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks
|
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
||||||
final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks);
|
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);
|
final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
|
||||||
for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) {
|
final LengthObfuscatingInputStream in = new LengthObfuscatingInputStream(plaintextFile, randomPadding);
|
||||||
blockSizeBufferedOut.write(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:
|
// 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.clear();
|
||||||
headerBuf.put(iv);
|
headerBuf.put(iv);
|
||||||
headerBuf.put(encryptContentLength(plaintextSize, iv));
|
headerBuf.put(nonce);
|
||||||
|
headerBuf.put(encryptHeaderData(sensitiveHeaderContentBuf.array(), iv));
|
||||||
headerBuf.flip();
|
headerBuf.flip();
|
||||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||||
headerMac.update(headerBuf);
|
headerMac.update(headerBuf);
|
||||||
headerBuf.limit(96);
|
headerBuf.limit(104);
|
||||||
headerBuf.put(headerMac.doFinal());
|
headerBuf.put(headerMac.doFinal());
|
||||||
headerBuf.put(contentMac.doFinal());
|
|
||||||
headerBuf.flip();
|
headerBuf.flip();
|
||||||
encryptedFile.position(0);
|
encryptedFile.position(0);
|
||||||
encryptedFile.write(headerBuf);
|
encryptedFile.write(headerBuf);
|
||||||
@@ -554,4 +732,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
|||||||
return plaintextSize;
|
return plaintextSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private byte[] longToByteArray(long lng) {
|
||||||
|
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ interface AesCryptographicConfiguration {
|
|||||||
*/
|
*/
|
||||||
int AES_BLOCK_LENGTH = 16;
|
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.
|
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
|||||||
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||||
public class KeyFile implements Serializable {
|
public class KeyFile implements Serializable {
|
||||||
|
|
||||||
static final Integer CURRENT_VERSION = 1;
|
static final Integer CURRENT_VERSION = 2;
|
||||||
private static final long serialVersionUID = 8578363158959619885L;
|
private static final long serialVersionUID = 8578363158959619885L;
|
||||||
|
|
||||||
private Integer version;
|
private Integer version;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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)
|
@Test(expected = DecryptFailedException.class)
|
||||||
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
|
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
|
||||||
// our test plaintext data:
|
// our test plaintext data:
|
||||||
@@ -112,7 +80,7 @@ public class Aes256CryptorTest {
|
|||||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||||
|
|
||||||
// encrypt:
|
// encrypt:
|
||||||
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
|
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
|
||||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||||
IOUtils.closeQuietly(plaintextIn);
|
IOUtils.closeQuietly(plaintextIn);
|
||||||
@@ -131,7 +99,7 @@ public class Aes256CryptorTest {
|
|||||||
// decrypt modified content (should fail with DecryptFailedException):
|
// decrypt modified content (should fail with DecryptFailedException):
|
||||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||||
cryptor.decryptFile(encryptedIn, plaintextOut);
|
cryptor.decryptFile(encryptedIn, plaintextOut, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -144,7 +112,7 @@ public class Aes256CryptorTest {
|
|||||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||||
|
|
||||||
// encrypt:
|
// encrypt:
|
||||||
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
|
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
|
||||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||||
IOUtils.closeQuietly(plaintextIn);
|
IOUtils.closeQuietly(plaintextIn);
|
||||||
@@ -159,7 +127,7 @@ public class Aes256CryptorTest {
|
|||||||
|
|
||||||
// decrypt:
|
// decrypt:
|
||||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
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(encryptedIn);
|
||||||
IOUtils.closeQuietly(plaintextOut);
|
IOUtils.closeQuietly(plaintextOut);
|
||||||
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
|
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
|
||||||
@@ -171,10 +139,10 @@ public class Aes256CryptorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||||
// our test plaintext data:
|
// 8MiB test plaintext data:
|
||||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
|
||||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||||
for (int i = 0; i < 65536; i++) {
|
for (int i = 0; i < 2097152; i++) {
|
||||||
bbIn.putInt(i);
|
bbIn.putInt(i);
|
||||||
}
|
}
|
||||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||||
@@ -183,7 +151,7 @@ public class Aes256CryptorTest {
|
|||||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||||
|
|
||||||
// encrypt:
|
// 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);
|
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||||
IOUtils.closeQuietly(plaintextIn);
|
IOUtils.closeQuietly(plaintextIn);
|
||||||
@@ -194,14 +162,14 @@ public class Aes256CryptorTest {
|
|||||||
// decrypt:
|
// decrypt:
|
||||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
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(encryptedIn);
|
||||||
IOUtils.closeQuietly(plaintextOut);
|
IOUtils.closeQuietly(plaintextOut);
|
||||||
Assert.assertTrue(numDecryptedBytes > 0);
|
Assert.assertTrue(numDecryptedBytes > 0);
|
||||||
|
|
||||||
// check decrypted data:
|
// check decrypted data:
|
||||||
final byte[] result = plaintextOut.toByteArray();
|
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);
|
Assert.assertArrayEquals(expected, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
main/crypto-aes/src/test/resources/log4j2.xml
Normal file
33
main/crypto-aes/src/test/resources/log4j2.xml
Normal 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>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>crypto-api</artifactId>
|
<artifactId>crypto-api</artifactId>
|
||||||
<name>Cryptomator cryptographic module API</name>
|
<name>Cryptomator cryptographic module API</name>
|
||||||
|
|||||||
@@ -53,18 +53,13 @@ public class AbstractCryptorDecorator implements Cryptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
|
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||||
return cryptor.isAuthentic(encryptedFile);
|
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||||
return cryptor.decryptFile(encryptedFile, plaintextFile);
|
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
|
||||||
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -75,16 +75,11 @@ public interface Cryptor extends Destroyable {
|
|||||||
*/
|
*/
|
||||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
|
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.
|
* @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
|
* @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)
|
* @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.
|
* @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
|
* @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.
|
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||||
|
|||||||
@@ -45,15 +45,15 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
|
|||||||
/* Cryptor */
|
/* Cryptor */
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||||
return cryptor.decryptFile(encryptedFile, countingInputStream);
|
return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||||
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
|
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.cryptomator.crypto.exceptions;
|
package org.cryptomator.crypto.exceptions;
|
||||||
|
|
||||||
public class DecryptFailedException extends StorageCryptingException {
|
public class DecryptFailedException extends CryptingException {
|
||||||
private static final long serialVersionUID = -3855673600374897828L;
|
private static final long serialVersionUID = -3855673600374897828L;
|
||||||
|
|
||||||
public DecryptFailedException(Throwable t) {
|
public DecryptFailedException(Throwable t) {
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
package org.cryptomator.crypto.exceptions;
|
package org.cryptomator.crypto.exceptions;
|
||||||
|
|
||||||
public class EncryptFailedException extends StorageCryptingException {
|
public class EncryptFailedException extends CryptingException {
|
||||||
private static final long serialVersionUID = -3855673600374897828L;
|
private static final long serialVersionUID = -3855673600374897828L;
|
||||||
|
|
||||||
|
public EncryptFailedException(Throwable t) {
|
||||||
|
super("Encryption failed.", t);
|
||||||
|
}
|
||||||
|
|
||||||
public EncryptFailedException(String msg) {
|
public EncryptFailedException(String msg) {
|
||||||
super(msg);
|
super(msg);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.cryptomator.crypto.exceptions;
|
package org.cryptomator.crypto.exceptions;
|
||||||
|
|
||||||
public class UnsupportedKeyLengthException extends StorageCryptingException {
|
public class UnsupportedKeyLengthException extends MasterkeyDecryptionException {
|
||||||
private static final long serialVersionUID = 8114147446419390179L;
|
private static final long serialVersionUID = 8114147446419390179L;
|
||||||
|
|
||||||
private final int requestedLength;
|
private final int requestedLength;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package org.cryptomator.crypto.exceptions;
|
package org.cryptomator.crypto.exceptions;
|
||||||
|
|
||||||
public class WrongPasswordException extends StorageCryptingException {
|
public class WrongPasswordException extends MasterkeyDecryptionException {
|
||||||
private static final long serialVersionUID = -602047799678568780L;
|
private static final long serialVersionUID = -602047799678568780L;
|
||||||
|
|
||||||
public WrongPasswordException() {
|
public WrongPasswordException() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>installer-debian</artifactId>
|
<artifactId>installer-debian</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
@@ -24,6 +24,15 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-libs</id>
|
||||||
|
<phase>prepare-package</phase>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
<version>1.7</version>
|
<version>1.7</version>
|
||||||
@@ -37,15 +46,34 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
<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}" />
|
<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: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:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||||
<fx:platform javafx="2.2+" j2se="8.0">
|
<fx:platform javafx="2.2+" j2se="8.0">
|
||||||
<fx:property name="logPath" value="~/.Cryptomator/cryptomator.log" />
|
<fx:property name="logPath" value="~/.Cryptomator/cryptomator.log" />
|
||||||
|
<fx:jvmarg value="-Xmx2048m"/>
|
||||||
</fx:platform>
|
</fx:platform>
|
||||||
<fx:resources>
|
<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:resources>
|
||||||
<fx:permissions elevated="false" />
|
<fx:permissions elevated="false" />
|
||||||
<fx:preferences install="true" />
|
<fx:preferences install="true" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>installer-osx</artifactId>
|
<artifactId>installer-osx</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
@@ -24,6 +24,15 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-libs</id>
|
||||||
|
<phase>prepare-package</phase>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
<version>1.7</version>
|
<version>1.7</version>
|
||||||
@@ -37,15 +46,34 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
<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}" />
|
<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: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:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||||
<fx:platform javafx="2.2+" j2se="8.0">
|
<fx:platform javafx="2.2+" j2se="8.0">
|
||||||
<fx:property name="logPath" value="~/Library/Logs/Cryptomator/cryptomator.log" />
|
<fx:property name="logPath" value="~/Library/Logs/Cryptomator/cryptomator.log" />
|
||||||
|
<fx:jvmarg value="-Xmx2048m"/>
|
||||||
</fx:platform>
|
</fx:platform>
|
||||||
<fx:resources>
|
<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:resources>
|
||||||
<fx:permissions elevated="false" />
|
<fx:permissions elevated="false" />
|
||||||
<fx:preferences install="true" />
|
<fx:preferences install="true" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>installer-win-portable</artifactId>
|
<artifactId>installer-win-portable</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
@@ -24,6 +24,15 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-libs</id>
|
||||||
|
<phase>prepare-package</phase>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
<version>1.7</version>
|
<version>1.7</version>
|
||||||
@@ -37,16 +46,34 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
<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}" />
|
<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: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:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||||
<fx:platform javafx="2.2+" j2se="8.0">
|
<fx:platform javafx="2.2+" j2se="8.0">
|
||||||
<fx:property name="settingsPath" value="./settings.json" />
|
<fx:property name="settingsPath" value="./settings.json" />
|
||||||
<fx:property name="logPath" value="cryptomator.log" />
|
<fx:property name="logPath" value="cryptomator.log" />
|
||||||
</fx:platform>
|
</fx:platform>
|
||||||
<fx:resources>
|
<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:resources>
|
||||||
<fx:permissions elevated="false" />
|
<fx:permissions elevated="false" />
|
||||||
<fx:preferences install="false" menu="false" shortcut="false" />
|
<fx:preferences install="false" menu="false" shortcut="false" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>installer-win</artifactId>
|
<artifactId>installer-win</artifactId>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
@@ -24,6 +24,15 @@
|
|||||||
|
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>copy-libs</id>
|
||||||
|
<phase>prepare-package</phase>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<artifactId>maven-antrun-plugin</artifactId>
|
<artifactId>maven-antrun-plugin</artifactId>
|
||||||
<version>1.7</version>
|
<version>1.7</version>
|
||||||
@@ -37,15 +46,33 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
<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}" />
|
<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: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:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||||
<fx:platform javafx="2.2+" j2se="8.0" >
|
<fx:platform javafx="2.2+" j2se="8.0" >
|
||||||
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
|
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
|
||||||
</fx:platform>
|
</fx:platform>
|
||||||
<fx:resources>
|
<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:resources>
|
||||||
<fx:permissions elevated="false" />
|
<fx:permissions elevated="false" />
|
||||||
<fx:preferences install="true" />
|
<fx:preferences install="true" />
|
||||||
|
|||||||
28
main/pom.xml
28
main/pom.xml
@@ -11,7 +11,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<name>Cryptomator</name>
|
<name>Cryptomator</name>
|
||||||
|
|
||||||
@@ -211,9 +211,35 @@
|
|||||||
<module>installer-win-portable</module>
|
<module>installer-win-portable</module>
|
||||||
</modules>
|
</modules>
|
||||||
</profile>
|
</profile>
|
||||||
|
<profile>
|
||||||
|
<id>uber-jar</id>
|
||||||
|
<modules>
|
||||||
|
<module>uber-jar</module>
|
||||||
|
</modules>
|
||||||
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|
||||||
<build>
|
<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>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
|||||||
57
main/uber-jar/pom.xml
Normal file
57
main/uber-jar/pom.xml
Normal 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>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.cryptomator</groupId>
|
<groupId>org.cryptomator</groupId>
|
||||||
<artifactId>main</artifactId>
|
<artifactId>main</artifactId>
|
||||||
<version>0.7.2</version>
|
<version>0.8.2</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>ui</artifactId>
|
<artifactId>ui</artifactId>
|
||||||
<name>Cryptomator GUI</name>
|
<name>Cryptomator GUI</name>
|
||||||
@@ -32,6 +32,12 @@
|
|||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Guava -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.guava</groupId>
|
||||||
|
<artifactId>guava</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- apache commons -->
|
<!-- apache commons -->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -53,35 +59,4 @@
|
|||||||
<artifactId>guice</artifactId>
|
<artifactId>guice</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</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>
|
</project>
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import javafx.scene.control.Button;
|
|||||||
import javafx.scene.control.Hyperlink;
|
import javafx.scene.control.Hyperlink;
|
||||||
import javafx.scene.text.Text;
|
import javafx.scene.text.Text;
|
||||||
|
|
||||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
|
||||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||||
@@ -109,7 +108,7 @@ public class ChangePasswordController implements Initializable {
|
|||||||
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
|
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
|
||||||
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
|
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
|
||||||
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
|
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
} catch (DecryptFailedException | IOException ex) {
|
} catch (IOException ex) {
|
||||||
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
|
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
|
||||||
LOG.error("Decryption failed for technical reasons.", ex);
|
LOG.error("Decryption failed for technical reasons.", ex);
|
||||||
newPasswordField.swipe();
|
newPasswordField.swipe();
|
||||||
|
|||||||
@@ -1,33 +1,83 @@
|
|||||||
package org.cryptomator.ui.controllers;
|
package org.cryptomator.ui.controllers;
|
||||||
|
|
||||||
|
import java.net.URL;
|
||||||
|
import java.util.ResourceBundle;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javafx.application.Application;
|
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.ListChangeListener.Change;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.WeakListChangeListener;
|
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.Initializable;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ListView;
|
import javafx.scene.control.ListView;
|
||||||
|
import javafx.scene.control.cell.CheckBoxListCell;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import javafx.util.StringConverter;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
public class MacWarningsController {
|
import org.cryptomator.ui.model.Vault;
|
||||||
|
|
||||||
|
public class MacWarningsController implements Initializable {
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ListView<String> warningsList;
|
private ListView<Warning> warningsList;
|
||||||
|
|
||||||
private Stage stage;
|
@FXML
|
||||||
|
private Button whitelistButton;
|
||||||
|
|
||||||
private final Application application;
|
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
|
@Inject
|
||||||
public MacWarningsController(Application application) {
|
public MacWarningsController(Application application) {
|
||||||
this.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
|
@FXML
|
||||||
private void didClickDismissButton(ActionEvent event) {
|
private void didClickWhitelistButton(ActionEvent event) {
|
||||||
stage.hide();
|
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
|
@FXML
|
||||||
@@ -35,24 +85,70 @@ public class MacWarningsController {
|
|||||||
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
|
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMacWarnings(ObservableList<String> macWarnings) {
|
private void unauthenticatedResourcesDidChange(Change<? extends String> change) {
|
||||||
this.warningsList.setItems(macWarnings);
|
while (change.next()) {
|
||||||
this.warningsList.getItems().addListener(new WeakListChangeListener<String>(this::warningsDidChange));
|
if (change.wasAdded()) {
|
||||||
}
|
warnings.addAll(change.getAddedSubList().stream().map(Warning::new).collect(Collectors.toList()));
|
||||||
|
} else if (change.wasRemoved()) {
|
||||||
// closes this window automatically, if all warnings disappeared (e.g. due to an unmount event)
|
change.getRemoved().forEach(str -> {
|
||||||
private void warningsDidChange(Change<? extends String> change) {
|
warnings.removeIf(w -> str.equals(w.name.get()));
|
||||||
if (change.getList().isEmpty()) {
|
});
|
||||||
stage.hide();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Stage getStage() {
|
private void warningsDidInvalidate(Observable observable) {
|
||||||
return stage;
|
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) {
|
public void setStage(Stage stage) {
|
||||||
this.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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,25 +13,21 @@ import java.io.IOException;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ListChangeListener;
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.SetChangeListener;
|
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
import javafx.geometry.Side;
|
import javafx.geometry.Side;
|
||||||
import javafx.scene.Parent;
|
import javafx.scene.Parent;
|
||||||
import javafx.scene.Scene;
|
|
||||||
import javafx.scene.control.ContextMenu;
|
import javafx.scene.control.ContextMenu;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.ListView;
|
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.Vault;
|
||||||
import org.cryptomator.ui.model.VaultFactory;
|
import org.cryptomator.ui.model.VaultFactory;
|
||||||
import org.cryptomator.ui.settings.Settings;
|
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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -85,9 +79,6 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
private final ControllerFactory controllerFactory;
|
private final ControllerFactory controllerFactory;
|
||||||
private final Settings settings;
|
private final Settings settings;
|
||||||
private final VaultFactory vaultFactoy;
|
private final VaultFactory vaultFactoy;
|
||||||
private final ObservableList<String> aggregatedMacWarnings;
|
|
||||||
private final SetChangeListener<String> macWarningsAggregator;
|
|
||||||
private final AtomicBoolean macWarningsWindowVisible;
|
|
||||||
|
|
||||||
private ResourceBundle rb;
|
private ResourceBundle rb;
|
||||||
|
|
||||||
@@ -97,9 +88,6 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
this.controllerFactory = controllerFactory;
|
this.controllerFactory = controllerFactory;
|
||||||
this.settings = settings;
|
this.settings = settings;
|
||||||
this.vaultFactoy = vaultFactoy;
|
this.vaultFactoy = vaultFactoy;
|
||||||
this.aggregatedMacWarnings = FXCollections.observableList(new ArrayList<>());
|
|
||||||
this.macWarningsAggregator = new ObservableSetAggregator<>(this.aggregatedMacWarnings);
|
|
||||||
this.macWarningsWindowVisible = new AtomicBoolean();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -110,8 +98,6 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
vaultList.setItems(items);
|
vaultList.setItems(items);
|
||||||
vaultList.setCellFactory(this::createDirecoryListCell);
|
vaultList.setCellFactory(this::createDirecoryListCell);
|
||||||
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
|
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
|
||||||
|
|
||||||
aggregatedMacWarnings.addListener(this::macWarningsDidChange);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -233,12 +219,6 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
showChangePasswordView(selectedVault);
|
showChangePasswordView(selectedVault);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
|
|
||||||
if (aggregatedMacWarnings.size() > 0) {
|
|
||||||
Platform.runLater(this::showMacWarningsWindow);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ****************************************
|
// ****************************************
|
||||||
// Subcontroller for right panel
|
// Subcontroller for right panel
|
||||||
// ****************************************
|
// ****************************************
|
||||||
@@ -293,7 +273,6 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void didUnlock(UnlockController ctrl) {
|
public void didUnlock(UnlockController ctrl) {
|
||||||
ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
|
|
||||||
showUnlockedView(ctrl.getVault());
|
showUnlockedView(ctrl.getVault());
|
||||||
Platform.setImplicitExit(false);
|
Platform.setImplicitExit(false);
|
||||||
}
|
}
|
||||||
@@ -306,9 +285,8 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void didLock(UnlockedController ctrl) {
|
public void didLock(UnlockedController ctrl) {
|
||||||
ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
|
|
||||||
showUnlockView(ctrl.getVault());
|
showUnlockView(ctrl.getVault());
|
||||||
if (getUnlockedDirectories().isEmpty()) {
|
if (getUnlockedVaults().isEmpty()) {
|
||||||
Platform.setImplicitExit(true);
|
Platform.setImplicitExit(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,45 +302,14 @@ public class MainController implements Initializable, InitializationListener, Un
|
|||||||
showUnlockView(ctrl.getVault());
|
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 */
|
/* Convenience */
|
||||||
|
|
||||||
public Collection<Vault> getDirectories() {
|
public Collection<Vault> getVaults() {
|
||||||
return vaultList.getItems();
|
return vaultList.getItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Collection<Vault> getUnlockedDirectories() {
|
public Collection<Vault> getUnlockedVaults() {
|
||||||
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
|
return getVaults().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
/* public Getter/Setter */
|
/* public Getter/Setter */
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ import javafx.scene.text.Text;
|
|||||||
import javax.security.auth.DestroyFailedException;
|
import javax.security.auth.DestroyFailedException;
|
||||||
|
|
||||||
import org.apache.commons.lang3.CharUtils;
|
import org.apache.commons.lang3.CharUtils;
|
||||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
|
||||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||||
@@ -134,7 +133,7 @@ public class UnlockController implements Initializable {
|
|||||||
vault.setUnlocked(true);
|
vault.setUnlocked(true);
|
||||||
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
|
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
|
||||||
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
|
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
|
||||||
} catch (DecryptFailedException | IOException ex) {
|
} catch (IOException ex) {
|
||||||
setControlsDisabled(false);
|
setControlsDisabled(false);
|
||||||
progressIndicator.setVisible(false);
|
progressIndicator.setVisible(false);
|
||||||
messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
|
messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
|
||||||
@@ -178,6 +177,7 @@ public class UnlockController implements Initializable {
|
|||||||
setControlsDisabled(false);
|
setControlsDisabled(false);
|
||||||
if (vault.isUnlocked() && !mountSuccess) {
|
if (vault.isUnlocked() && !mountSuccess) {
|
||||||
vault.stopServer();
|
vault.stopServer();
|
||||||
|
vault.setUnlocked(false);
|
||||||
}
|
}
|
||||||
if (mountSuccess && listener != null) {
|
if (mountSuccess && listener != null) {
|
||||||
listener.didUnlock(this);
|
listener.didUnlock(this);
|
||||||
|
|||||||
@@ -8,31 +8,45 @@
|
|||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package org.cryptomator.ui.controllers;
|
package org.cryptomator.ui.controllers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.ResourceBundle;
|
import java.util.ResourceBundle;
|
||||||
|
|
||||||
import javafx.animation.Animation;
|
import javafx.animation.Animation;
|
||||||
import javafx.animation.KeyFrame;
|
import javafx.animation.KeyFrame;
|
||||||
import javafx.animation.Timeline;
|
import javafx.animation.Timeline;
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.ListChangeListener;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.fxml.Initializable;
|
import javafx.fxml.Initializable;
|
||||||
|
import javafx.scene.Parent;
|
||||||
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.chart.LineChart;
|
import javafx.scene.chart.LineChart;
|
||||||
import javafx.scene.chart.NumberAxis;
|
import javafx.scene.chart.NumberAxis;
|
||||||
import javafx.scene.chart.XYChart.Data;
|
import javafx.scene.chart.XYChart.Data;
|
||||||
import javafx.scene.chart.XYChart.Series;
|
import javafx.scene.chart.XYChart.Series;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.stage.Stage;
|
||||||
import javafx.util.Duration;
|
import javafx.util.Duration;
|
||||||
|
|
||||||
import org.cryptomator.crypto.CryptorIOSampling;
|
import org.cryptomator.crypto.CryptorIOSampling;
|
||||||
|
import org.cryptomator.ui.MainModule.ControllerFactory;
|
||||||
import org.cryptomator.ui.model.Vault;
|
import org.cryptomator.ui.model.Vault;
|
||||||
|
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
|
||||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
public class UnlockedController implements Initializable {
|
public class UnlockedController implements Initializable {
|
||||||
|
|
||||||
private static final int IO_SAMPLING_STEPS = 100;
|
private static final int IO_SAMPLING_STEPS = 100;
|
||||||
private static final double IO_SAMPLING_INTERVAL = 0.25;
|
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 LockListener listener;
|
||||||
private Vault vault;
|
private Vault vault;
|
||||||
private Timeline ioAnimation;
|
private Timeline ioAnimation;
|
||||||
@@ -48,9 +62,30 @@ public class UnlockedController implements Initializable {
|
|||||||
|
|
||||||
private ResourceBundle rb;
|
private ResourceBundle rb;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public UnlockedController(ControllerFactory controllerFactory) {
|
||||||
|
this.controllerFactory = controllerFactory;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(URL url, ResourceBundle rb) {
|
public void initialize(URL url, ResourceBundle rb) {
|
||||||
this.rb = 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
|
@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
|
// IO Graph
|
||||||
// ****************************************
|
// ****************************************
|
||||||
@@ -128,11 +179,22 @@ public class UnlockedController implements Initializable {
|
|||||||
return vault;
|
return vault;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVault(Vault directory) {
|
public void setVault(Vault vault) {
|
||||||
this.vault = directory;
|
this.vault = vault;
|
||||||
|
macWarningCtrl.setVault(vault);
|
||||||
|
|
||||||
if (directory.getCryptor() instanceof CryptorIOSampling) {
|
// listen to MAC warnings as long as this vault is unlocked:
|
||||||
startIoSampling((CryptorIOSampling) directory.getCryptor());
|
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 {
|
} else {
|
||||||
ioGraph.setVisible(false);
|
ioGraph.setVisible(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import java.nio.file.Files;
|
|||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.text.Normalizer;
|
import java.text.Normalizer;
|
||||||
import java.text.Normalizer.Form;
|
import java.text.Normalizer.Form;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javafx.beans.property.ObjectProperty;
|
import javafx.beans.property.ObjectProperty;
|
||||||
import javafx.beans.property.SimpleObjectProperty;
|
import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableSet;
|
import javafx.collections.ObservableList;
|
||||||
|
|
||||||
import javax.security.auth.DestroyFailedException;
|
import javax.security.auth.DestroyFailedException;
|
||||||
|
|
||||||
@@ -43,7 +45,8 @@ public class Vault implements Serializable {
|
|||||||
private final WebDavMounter mounter;
|
private final WebDavMounter mounter;
|
||||||
private final DeferredCloser closer;
|
private final DeferredCloser closer;
|
||||||
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
|
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 String mountName;
|
||||||
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
|
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
|
||||||
@@ -77,11 +80,12 @@ public class Vault implements Serializable {
|
|||||||
|
|
||||||
public synchronized boolean startServer() {
|
public synchronized boolean startServer() {
|
||||||
namesOfResourcesWithInvalidMac.clear();
|
namesOfResourcesWithInvalidMac.clear();
|
||||||
|
whitelistedResourcesWithInvalidMac.clear();
|
||||||
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
|
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
|
||||||
if (o.isPresent() && o.get().isRunning()) {
|
if (o.isPresent() && o.get().isRunning()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, mountName);
|
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, whitelistedResourcesWithInvalidMac, mountName);
|
||||||
if (servlet.start()) {
|
if (servlet.start()) {
|
||||||
webDavServlet = closer.closeLater(servlet);
|
webDavServlet = closer.closeLater(servlet);
|
||||||
return true;
|
return true;
|
||||||
@@ -101,7 +105,7 @@ public class Vault implements Serializable {
|
|||||||
} catch (DestroyFailedException e) {
|
} catch (DestroyFailedException e) {
|
||||||
LOG.error("Destruction of cryptor throw an exception.", e);
|
LOG.error("Destruction of cryptor throw an exception.", e);
|
||||||
}
|
}
|
||||||
setUnlocked(false);
|
whitelistedResourcesWithInvalidMac.clear();
|
||||||
namesOfResourcesWithInvalidMac.clear();
|
namesOfResourcesWithInvalidMac.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,10 +164,14 @@ public class Vault implements Serializable {
|
|||||||
return mountName;
|
return mountName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
|
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
|
||||||
return namesOfResourcesWithInvalidMac;
|
return namesOfResourcesWithInvalidMac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> getWhitelistedResourcesWithInvalidMac() {
|
||||||
|
return whitelistedResourcesWithInvalidMac;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to form a similar string using the regular latin alphabet.
|
* Tries to form a similar string using the regular latin alphabet.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package org.cryptomator.ui.util;
|
|||||||
|
|
||||||
import javafx.beans.value.ChangeListener;
|
import javafx.beans.value.ChangeListener;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.beans.value.WeakChangeListener;
|
|
||||||
import javafx.stage.Window;
|
import javafx.stage.Window;
|
||||||
|
|
||||||
public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
|
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
|
* 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
|
||||||
* element, if the window is active. Otherwise {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined
|
* {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined depending on the window's focus.<br/>
|
||||||
* depending on the window's focus.<br/>
|
|
||||||
* <br/>
|
* <br/>
|
||||||
* Example:<br/>
|
* Example:<br/>
|
||||||
* <code>
|
* <code>
|
||||||
@@ -32,7 +30,7 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
|
|||||||
* @return The observer
|
* @return The observer
|
||||||
*/
|
*/
|
||||||
public static ChangeListener<Boolean> startObservingFocus(final Window window) {
|
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);
|
window.focusedProperty().addListener(observer);
|
||||||
return observer;
|
return observer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import java.util.concurrent.ExecutorService;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.ObservableSet;
|
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
|
* 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
|
||||||
* called. If you are interested in the exception, use
|
|
||||||
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
||||||
*
|
*
|
||||||
* <pre>
|
* <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
|
* 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
|
||||||
* called. If you are interested in the exception, use
|
|
||||||
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
||||||
*
|
*
|
||||||
* <pre>
|
* <pre>
|
||||||
@@ -123,4 +122,8 @@ public final class FXThreads {
|
|||||||
return new ObservableSetOnMainThread<E>(set);
|
return new ObservableSetOnMainThread<E>(set);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <E> ObservableList<E> observableListOnMainThread(ObservableList<E> list) {
|
||||||
|
return new ObservableListOnMainThread<E>(list);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,8 @@ import javafx.collections.ObservableSet;
|
|||||||
import javafx.collections.SetChangeListener;
|
import javafx.collections.SetChangeListener;
|
||||||
import javafx.collections.SetChangeListener.Change;
|
import javafx.collections.SetChangeListener.Change;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
||||||
|
|
||||||
private final ObservableSet<E> set;
|
private final ObservableSet<E> set;
|
||||||
@@ -91,8 +93,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void invalidated(Observable observable) {
|
private void invalidated(Observable observable) {
|
||||||
|
final Collection<InvalidationListener> listeners = ImmutableList.copyOf(invalidationListeners);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
for (InvalidationListener listener : invalidationListeners) {
|
for (InvalidationListener listener : listeners) {
|
||||||
listener.invalidated(this);
|
listener.invalidated(this);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -110,8 +113,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
|||||||
|
|
||||||
private void onChanged(Change<? extends E> change) {
|
private void onChanged(Change<? extends E> change) {
|
||||||
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
|
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
|
||||||
|
final Collection<SetChangeListener<? super E>> listeners = ImmutableList.copyOf(setChangeListeners);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
for (SetChangeListener<? super E> listener : setChangeListeners) {
|
for (SetChangeListener<? super E> listener : listeners) {
|
||||||
listener.onChanged(c);
|
listener.onChanged(c);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<ListView fx:id="warningsList" VBox.vgrow="ALWAYS" focusTraversable="false" />
|
<ListView fx:id="warningsList" VBox.vgrow="ALWAYS" focusTraversable="false" />
|
||||||
<HBox alignment="CENTER_RIGHT" spacing="12.0">
|
<HBox alignment="CENTER_RIGHT" spacing="12.0">
|
||||||
<children>
|
<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"/>
|
<Button text="%macWarnings.moreInformationButton" defaultButton="true" prefWidth="200.0" onAction="#didClickMoreInformationButton" focusTraversable="false"/>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|||||||
@@ -56,10 +56,10 @@ unlocked.label.unmountFailed=Ejecting drive failed.
|
|||||||
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
|
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)
|
||||||
|
|
||||||
# mac_warnings.fxml
|
# 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.message=Cryptomator detected potentially malicious corruptions in the following files:
|
||||||
macWarnings.moreInformationButton=Learn more
|
macWarnings.moreInformationButton=Learn more
|
||||||
macWarnings.dismissButton=I promise to be careful
|
macWarnings.whitelistButton=Decrypt selected anyway
|
||||||
|
|
||||||
# tray icon
|
# tray icon
|
||||||
tray.menu.open=Open
|
tray.menu.open=Open
|
||||||
|
|||||||
Reference in New Issue
Block a user