mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-15 17:21:27 +00:00
Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e18a11886 | ||
|
|
386059a238 | ||
|
|
b4ab09b3aa | ||
|
|
289ac55ccd | ||
|
|
b5160cddb9 | ||
|
|
1a81b3a781 | ||
|
|
b6a5db5797 | ||
|
|
aaf98c4fb9 | ||
|
|
55d1ffe703 | ||
|
|
5fefa3c6d4 | ||
|
|
b404e52670 | ||
|
|
44475fa3f1 | ||
|
|
f430f3c579 | ||
|
|
3efa23987f | ||
|
|
1dce871354 | ||
|
|
d919c727cf | ||
|
|
b691e374eb | ||
|
|
ca88e05849 | ||
|
|
104c3b64f6 | ||
|
|
1bef4e786d | ||
|
|
c1f32105d8 | ||
|
|
09b4130c3e | ||
|
|
6d1e0fe609 | ||
|
|
e65c84ca1d | ||
|
|
095f60ec03 | ||
|
|
485df3aa71 | ||
|
|
6b073c1499 | ||
|
|
71983cc3a8 | ||
|
|
db2297d2f1 | ||
|
|
38ab167fa4 | ||
|
|
f87e8f55f1 | ||
|
|
a4e6365e0b | ||
|
|
06034fd95b | ||
|
|
571fee9524 | ||
|
|
af9deffa6d | ||
|
|
48b319ec99 | ||
|
|
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 | ||
|
|
85f3487cf0 | ||
|
|
4a754d6a6c | ||
|
|
abf9920caf | ||
|
|
dd2863da5b |
@@ -1,7 +1,8 @@
|
||||
language: java
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
script: mvn -fmain/pom.xml clean package
|
||||
before_install: "curl -L --cookie 'oraclelicense=accept-securebackup-cookie;' http://download.oracle.com/otn-pub/java/jce/8/jce_policy-8.zip -o /tmp/policy.zip && sudo unzip -j -o /tmp/policy.zip *.jar -d `jdk_switcher home oraclejdk8`/jre/lib/security && rm /tmp/policy.zip"
|
||||
script: mvn -fmain/pom.xml -Puber-jar clean package
|
||||
notifications:
|
||||
webhooks:
|
||||
urls:
|
||||
@@ -11,9 +12,10 @@ notifications:
|
||||
on_start: false
|
||||
deploy:
|
||||
provider: releases
|
||||
prerelease: true
|
||||
api_key:
|
||||
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
|
||||
file: main/ui/target/dist/Cryptomator.jar
|
||||
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
|
||||
97
NOTICE.md
97
NOTICE.md
@@ -4,53 +4,14 @@ Copyright (c) 2014, Sebastian Stenzel
|
||||
Cryptomator is licensed under the MIT license. The details can be found in the accompanying license file.
|
||||
|
||||
## Third party softwares
|
||||
|
||||
Cryptomator uses third party softwares that may be licensed under different licenses.
|
||||
|
||||
### AquaFX
|
||||
The ProgressIndicator in ui/src/main/resource/css/mac_theme.css contains code from the AquaFX project.
|
||||
|
||||
### Jackson
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
Copyright 2013 Claudine Zillmann (http://aquafx-project.com/)
|
||||
|
||||
**Licensing:** Jackson core and extension components may licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
**Credits:** A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
||||
|
||||
|
||||
### Jetty
|
||||
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
|
||||
|
||||
All rights reserved. This program and the accompanying materials
|
||||
are made available under the terms of the Eclipse Public License v1.0
|
||||
and Apache License v2.0 which accompanies this distribution.
|
||||
|
||||
The UnixCrypt.java code implements the one way cryptography used by
|
||||
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
|
||||
modified April 2001 by Iris Van den Broeke, Daniel Deville.
|
||||
Permission to use, copy, modify and distribute UnixCrypt
|
||||
for non-commercial or commercial purposes and without fee is
|
||||
granted provided that the copyright notice appears in all copies.
|
||||
|
||||
|
||||
### Jackrabbit WebDAV Library
|
||||
Copyright 2004-2014 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Based on source code originally developed by Day Software (http://www.day.com/).
|
||||
|
||||
### Apache Jakarta HttpClient
|
||||
Copyright 1999-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
### Apache Commons Collections
|
||||
Copyright 2001-2013 The Apache Software Foundation
|
||||
@@ -83,6 +44,17 @@ Copyright (c) 2013, ControlsFX
|
||||
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
### Dagger 2
|
||||
Copyright 2014 Google, Inc.
|
||||
Copyright 2012 Square, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0
|
||||
|
||||
### Apache Jakarta HttpClient
|
||||
Copyright 1999-2007 The Apache Software Foundation
|
||||
|
||||
This product includes software developed by The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
### Apache Log4j
|
||||
Copyright 1999-2012 Apache Software Foundation
|
||||
|
||||
@@ -90,7 +62,44 @@ This product includes software developed at The Apache Software Foundation (http
|
||||
|
||||
ResolverUtil.java Copyright 2005-2006 Tim Fennell
|
||||
|
||||
### Jackrabbit WebDAV Library
|
||||
Copyright 2004-2014 The Apache Software Foundation
|
||||
|
||||
This product includes software developed at The Apache Software Foundation (http://www.apache.org/).
|
||||
|
||||
Based on source code originally developed by Day Software (http://www.day.com/).
|
||||
|
||||
### Jackson
|
||||
Jackson is a high-performance, Free/Open Source JSON processing library.
|
||||
It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has
|
||||
been in development since 2007.
|
||||
It is currently developed by a community of developers, as well as supported
|
||||
commercially by FasterXML.com.
|
||||
|
||||
**Licensing:** Jackson core and extension components may licensed under different licenses.
|
||||
To find the details that apply to this artifact see the accompanying Apache 2.0 license file.
|
||||
For more information, including possible other licensing options, contact
|
||||
FasterXML.com (http://fasterxml.com).
|
||||
|
||||
**Credits:** A list of contributors may be found from CREDITS file, which is included
|
||||
in some artifacts (usually source distributions); but is always available
|
||||
from the source code management (SCM) system project uses.
|
||||
|
||||
### Jetty
|
||||
Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
|
||||
|
||||
All rights reserved. This program and the accompanying materials
|
||||
are made available under the terms of the Eclipse Public License v1.0
|
||||
and Apache License v2.0 which accompanies this distribution.
|
||||
|
||||
The UnixCrypt.java code implements the one way cryptography used by
|
||||
Unix systems for simple password protection. Copyright 1996 Aki Yoshida,
|
||||
modified April 2001 by Iris Van den Broeke, Daniel Deville.
|
||||
Permission to use, copy, modify and distribute UnixCrypt
|
||||
for non-commercial or commercial purposes and without fee is
|
||||
granted provided that the copyright notice appears in all copies.
|
||||
|
||||
### JUnit
|
||||
Copyright (c) 2000-2006, www.hamcrest.org
|
||||
|
||||
Licensed under the accompanying BSD license file.
|
||||
Licensed under the accompanying BSD license file.
|
||||
|
||||
16
README.md
16
README.md
@@ -13,24 +13,24 @@ If you want to take a look at the current beta version, go ahead and get your co
|
||||
- Totally transparent: Just work on the encrypted volume, as if it was an USB flash drive
|
||||
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory.
|
||||
- In fact it works with any directory. You can use it to encrypt as many folders as you like
|
||||
- AES encryption with 256 bit key length
|
||||
- AES encryption with 256-bit key length
|
||||
- Client-side. No accounts, no data shared with any online service
|
||||
- Filenames get encrypted too
|
||||
- No need to provide credentials for any 3rd party service
|
||||
- Open Source means: No backdoors. Control is better than trust
|
||||
- Use as many encrypted folders in your dropbox as you want. Each having individual passwords
|
||||
- No commerical interest, no government agency, no wasted taxpayers' money ;-)
|
||||
- Use as many encrypted folders in your Dropbox as you want. Each having individual passwords
|
||||
- No commercial interest, no government agency, no wasted taxpayers' money ;-)
|
||||
|
||||
### Privacy
|
||||
- 256 bit keys (unlimited strength policy bundled with native binaries - 128 bit elsewhere)
|
||||
- 256 bit keys (unlimited strength policy bundled with native binaries - 128-bit elsewhere)
|
||||
- Scrypt key derivation
|
||||
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
|
||||
- Cryptographically secure random numbers for salts, IVs and the master key of course
|
||||
- Sensitive data is swiped from the heap asap
|
||||
- Lightweight: [Complexity kills security](https://www.schneier.com/essays/archives/1999/11/a_plea_for_simplicit.html)
|
||||
|
||||
### Consistency
|
||||
- HMAC over file contents to recognize changed ciphertext before decryption
|
||||
- I/O operations are transactional and atomic, if the file systems supports it
|
||||
- I/O operations are transactional and atomic, if the file systems support it
|
||||
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no [SPOF](http://en.wikipedia.org/wiki/Single_point_of_failure)
|
||||
|
||||
## Building
|
||||
@@ -39,14 +39,14 @@ If you want to take a look at the current beta version, go ahead and get your co
|
||||
* Java 8
|
||||
* Maven 3
|
||||
* Optional: OS-dependent build tools for native packaging
|
||||
* Optional: JCE unlimited strength policy files (needed for 256 bit keys)
|
||||
* Optional: JCE unlimited strength policy files (needed for 256-bit keys)
|
||||
|
||||
#### Building on Debian-based OS
|
||||
```bash
|
||||
apt-get install oracle-java8-installer oracle-java8-unlimited-jce-policy fakeroot maven git
|
||||
git clone https://github.com/cryptomator/cryptomator.git
|
||||
cd cryptomator/main
|
||||
git checkout v0.6.0
|
||||
git checkout 0.7.1
|
||||
mvn clean install -Pdebian
|
||||
```
|
||||
|
||||
|
||||
@@ -12,16 +12,14 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>core</artifactId>
|
||||
<name>Cryptomator WebDAV and I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.2.10.v20150310</jetty.version>
|
||||
<jackrabbit.version>2.10.1</jackrabbit.version>
|
||||
<commons.transaction.version>1.2</commons.transaction.version>
|
||||
<jta.version>1.1</jta.version>
|
||||
<jetty.version>9.3.3.v20150827</jetty.version>
|
||||
<jackrabbit.version>2.11.0</jackrabbit.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -29,6 +27,11 @@
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jetty (Servlet Container) -->
|
||||
<dependency>
|
||||
@@ -41,6 +44,11 @@
|
||||
<artifactId>jetty-webapp</artifactId>
|
||||
<version>${jetty.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Jackrabbit -->
|
||||
<dependency>
|
||||
@@ -48,13 +56,13 @@
|
||||
<artifactId>jackrabbit-webdav</artifactId>
|
||||
<version>${jackrabbit.version}</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- I/O -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
@@ -64,7 +72,7 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
|
||||
@@ -52,9 +52,11 @@ public final class WebDavServer {
|
||||
localConnector.setHost(LOCALHOST);
|
||||
servletCollection = new ContextHandlerCollection();
|
||||
|
||||
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
|
||||
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
|
||||
servletContext.addServlet(servlet, "/");
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, "/", ServletContextHandler.NO_SESSIONS);
|
||||
final ServletHolder servlet = new ServletHolder(WindowsSucksServlet.class);
|
||||
servletContext.addServlet(servlet, "/");
|
||||
}
|
||||
|
||||
server.setConnectors(new Connector[] {localConnector});
|
||||
server.setHandler(servletCollection);
|
||||
@@ -84,13 +86,11 @@ public final class WebDavServer {
|
||||
/**
|
||||
* @param workDir Path of encrypted folder.
|
||||
* @param cryptor A fully initialized cryptor instance ready to en- or decrypt streams.
|
||||
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC
|
||||
* authentication fails.
|
||||
* @param name The name of the folder. Must be non-empty and only contain any of
|
||||
* _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||
* @param failingMacCollection A (observable, thread-safe) collection, to which the names of resources are written, whose MAC authentication fails.
|
||||
* @param name The name of the folder. Must be non-empty and only contain any of _ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789
|
||||
* @return servlet
|
||||
*/
|
||||
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final String name) {
|
||||
public ServletLifeCycleAdapter createServlet(final Path workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection, final String name) {
|
||||
try {
|
||||
if (StringUtils.isEmpty(name)) {
|
||||
throw new IllegalArgumentException("name empty");
|
||||
@@ -101,7 +101,7 @@ public final class WebDavServer {
|
||||
final URI uri = new URI(null, null, localConnector.getHost(), localConnector.getLocalPort(), "/" + UUID.randomUUID().toString() + "/" + name, null, null);
|
||||
|
||||
final ServletContextHandler servletContext = new ServletContextHandler(servletCollection, uri.getRawPath(), ServletContextHandler.SESSIONS);
|
||||
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection);
|
||||
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
|
||||
servletContext.addServlet(servlet, "/*");
|
||||
|
||||
servletCollection.mapContexts();
|
||||
@@ -113,8 +113,8 @@ public final class WebDavServer {
|
||||
}
|
||||
}
|
||||
|
||||
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection) {
|
||||
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection));
|
||||
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
|
||||
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor, failingMacCollection, whitelistedResourceCollection));
|
||||
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.exceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class IORuntimeException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -4713080133052143303L;
|
||||
|
||||
public IORuntimeException(IOException cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMessage() {
|
||||
return getCause().getMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocalizedMessage() {
|
||||
return getCause().getLocalizedMessage();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
@@ -19,6 +20,7 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
@@ -37,7 +39,6 @@ import org.apache.jackrabbit.webdav.property.DavPropertySet;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.PropEntry;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -196,7 +197,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
|
||||
final String parentResource = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(locator.getResourcePath()), "/");
|
||||
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
|
||||
try {
|
||||
return getFactory().createResource(parentLocator, session);
|
||||
@@ -212,7 +213,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
this.move((AbstractEncryptedNode) dest);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + this.getResourcePath() + " to " + dest.getResourcePath());
|
||||
throw new IORuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
|
||||
@@ -228,7 +229,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
this.copy((AbstractEncryptedNode) dest, shallow);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + this.getResourcePath() + " to " + dest.getResourcePath());
|
||||
throw new IORuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
|
||||
@@ -255,7 +256,11 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
@Override
|
||||
public ActiveLock[] getLocks() {
|
||||
final ActiveLock exclusiveWriteLock = getLock(Type.WRITE, Scope.EXCLUSIVE);
|
||||
return new ActiveLock[] {exclusiveWriteLock};
|
||||
if (exclusiveWriteLock != null) {
|
||||
return new ActiveLock[] {exclusiveWriteLock};
|
||||
} else {
|
||||
return new ActiveLock[0];
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -5,34 +5,36 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.util.EncodeUtil;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
|
||||
public class CleartextLocatorFactory implements DavLocatorFactory {
|
||||
|
||||
private final String pathPrefix;
|
||||
|
||||
public CleartextLocatorFactory(String pathPrefix) {
|
||||
this.pathPrefix = pathPrefix;
|
||||
this.pathPrefix = StringUtils.removeEnd(pathPrefix, "/");
|
||||
}
|
||||
|
||||
// resourcePath == repositoryPath. No encryption here.
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String href) {
|
||||
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
final String fullPrefix = StringUtils.removeEnd(prefix, "/");
|
||||
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
|
||||
|
||||
final String relativeCleartextPath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
|
||||
final String relativeCleartextPath = EncodeUtil.unescape(relativeHref);
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
return new CleartextLocator(relativeCleartextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
|
||||
assert resourcePath.startsWith("/");
|
||||
return new CleartextLocator(resourcePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
|
||||
assert path.startsWith("/");
|
||||
return new CleartextLocator(path);
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@ public class CleartextLocatorFactory implements DavLocatorFactory {
|
||||
private final String relativeCleartextPath;
|
||||
|
||||
private CleartextLocator(String relativeCleartextPath) {
|
||||
this.relativeCleartextPath = FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true);
|
||||
this.relativeCleartextPath = StringUtils.prependIfMissing(FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true), "/");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,20 +78,19 @@ public class CleartextLocatorFactory implements DavLocatorFactory {
|
||||
|
||||
@Override
|
||||
public String getHref(boolean isCollection) {
|
||||
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
|
||||
final String fullPrefix = pathPrefix.endsWith("/") ? pathPrefix : pathPrefix + "/";
|
||||
final String href = fullPrefix.concat(encodedResourcePath);
|
||||
assert !href.endsWith("/");
|
||||
if (isCollection) {
|
||||
return href.concat("/");
|
||||
final String encodedResourcePath = EncodeUtil.escapePath(relativeCleartextPath);
|
||||
if (isRootLocation()) {
|
||||
return pathPrefix + "/";
|
||||
} else if (isCollection) {
|
||||
return pathPrefix + encodedResourcePath + "/";
|
||||
} else {
|
||||
return href;
|
||||
return pathPrefix + encodedResourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRootLocation() {
|
||||
return Strings.isEmpty(relativeCleartextPath);
|
||||
return "/".equals(relativeCleartextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavMethods;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
@@ -19,24 +23,25 @@ import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
|
||||
|
||||
private static final String RANGE_BYTE_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
private final ExecutorService backgroundTaskExecutor;
|
||||
private final Path dataRoot;
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
|
||||
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) {
|
||||
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
|
||||
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
this.backgroundTaskExecutor = backgroundTaskExecutor;
|
||||
this.dataRoot = vaultRootPath.resolve("d");
|
||||
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
|
||||
}
|
||||
@@ -47,20 +52,36 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
||||
return createRootDirectory(locator, request.getDavSession());
|
||||
}
|
||||
|
||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
return createDirectory(locator, request.getDavSession(), dirFilePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), request, filePath);
|
||||
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
return createFile(locator, request.getDavSession(), filePath);
|
||||
} else {
|
||||
// e.g. for MOVE operations:
|
||||
return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath);
|
||||
try {
|
||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
final String ifRangeHeader = request.getHeader(HttpHeader.IF_RANGE.asString());
|
||||
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
// DIRECTORY
|
||||
return createDirectory(locator, request.getDavSession(), dirFilePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
|
||||
// FILE RANGE
|
||||
final Pair<String, String> requestRange = getRequestRange(rangeHeader);
|
||||
response.setStatus(DavServletResponse.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), requestRange, filePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && isRangeSatisfiable(rangeHeader) && !isIfRangePreconditionFulfilled(ifRangeHeader, filePath)) {
|
||||
// FULL FILE (if-range not fulfilled)
|
||||
return createFile(locator, request.getDavSession(), filePath);
|
||||
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null && !isRangeSatisfiable(rangeHeader)) {
|
||||
// FULL FILE (unsatisfiable range)
|
||||
response.setStatus(DavServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
|
||||
final EncryptedFile file = createFile(locator, request.getDavSession(), filePath);
|
||||
response.addHeader(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + file.getContentLength());
|
||||
return file;
|
||||
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
// FULL FILE (as requested)
|
||||
return createFile(locator, request.getDavSession(), filePath);
|
||||
}
|
||||
} catch (NonExistingParentException e) {
|
||||
// return non-existing
|
||||
}
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -69,16 +90,18 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
||||
return createRootDirectory(locator, session);
|
||||
}
|
||||
|
||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
|
||||
if (Files.exists(dirFilePath)) {
|
||||
return createDirectory(locator, session, dirFilePath);
|
||||
} else if (Files.exists(filePath)) {
|
||||
return createFile(locator, session, filePath);
|
||||
} else {
|
||||
// e.g. for MOVE operations:
|
||||
return createNonExisting(locator, session, filePath, dirFilePath);
|
||||
try {
|
||||
final Path filePath = getEncryptedFilePath(locator.getResourcePath(), false);
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath(), false);
|
||||
if (Files.exists(dirFilePath)) {
|
||||
return createDirectory(locator, session, dirFilePath);
|
||||
} else if (Files.exists(filePath)) {
|
||||
return createFile(locator, session, filePath);
|
||||
}
|
||||
} catch (NonExistingParentException e) {
|
||||
// return non-existing
|
||||
}
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
|
||||
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
|
||||
@@ -90,55 +113,128 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws IOException
|
||||
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
|
||||
*/
|
||||
private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
||||
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 encrypted path is missing
|
||||
*/
|
||||
Path getEncryptedFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
try {
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
return parent.resolve(encryptedFilename);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws IOException
|
||||
* @throws NonExistingParentException If one ancestor of the encrypted path is missing
|
||||
*/
|
||||
private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
||||
Path getEncryptedDirectoryFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
try {
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||
return parent.resolve(encryptedFilename);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param createNonExisting if <code>false</code>, a {@link NonExistingParentException} will be thrown for missing ancestors.
|
||||
* @return Absolute directory path for a given cleartext directory resourcePath.
|
||||
* @throws IOException
|
||||
* @throws NonExistingParentException if one ancestor directory is missing.
|
||||
*/
|
||||
private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException {
|
||||
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
|
||||
private Path getEncryptedDirectoryPath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert relativeCleartextPath.startsWith("/");
|
||||
assert "/".equals(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
|
||||
try {
|
||||
final Path result;
|
||||
if (Strings.isEmpty(relativeCleartextPath)) {
|
||||
if ("/".equals(relativeCleartextPath)) {
|
||||
// root level
|
||||
final String fixedRootDirectory = cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
|
||||
result = dataRoot.resolve(fixedRootDirectory);
|
||||
} else {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
||||
final String parentCleartextPath = StringUtils.prependIfMissing(FilenameUtils.getPathNoEndSeparator(relativeCleartextPath), "/");
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath, createNonExisting);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||
final Path directoryFile = parent.resolve(encryptedFilename);
|
||||
if (!createNonExisting && !Files.exists(directoryFile)) {
|
||||
throw new NonExistingParentException();
|
||||
}
|
||||
final String directoryId = filenameTranslator.getDirectoryId(directoryFile, true);
|
||||
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
result = dataRoot.resolve(directory);
|
||||
@@ -146,12 +242,12 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
||||
Files.createDirectories(result);
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) {
|
||||
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath);
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, Path filePath) {
|
||||
return new EncryptedFilePart(this, locator, session, requestRange, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||
}
|
||||
|
||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {
|
||||
@@ -178,8 +274,14 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
|
||||
}
|
||||
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session, Path filePath, Path dirFilePath) {
|
||||
return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath);
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||
return new NonExistingNode(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
static class NonExistingParentException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 4421121746624627094L;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,15 +5,22 @@ import java.util.Collection;
|
||||
class CryptoWarningHandler {
|
||||
|
||||
private final Collection<String> resourcesWithInvalidMac;
|
||||
private final Collection<String> whitelistedResources;
|
||||
|
||||
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac) {
|
||||
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
|
||||
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
|
||||
this.whitelistedResources = whitelistedResources;
|
||||
}
|
||||
|
||||
public void macAuthFailed(String resourceName) {
|
||||
if (!resourcesWithInvalidMac.contains(resourceName)) {
|
||||
resourcesWithInvalidMac.add(resourceName);
|
||||
public void macAuthFailed(String resourcePath) {
|
||||
// collection might be a list, but we don't want duplicates:
|
||||
if (!resourcesWithInvalidMac.contains(resourcePath)) {
|
||||
resourcesWithInvalidMac.add(resourcePath);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean ignoreMac(String resourcePath) {
|
||||
return whitelistedResources.contains(resourcePath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,20 +10,26 @@ package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.LinkedTransferQueue;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
@@ -37,16 +43,15 @@ import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.ActiveLock;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.ResourceType;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.CounterOverflowException;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DavRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -73,7 +78,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
try {
|
||||
directoryId = filenameTranslator.getDirectoryId(filePath, false);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
return directoryId;
|
||||
@@ -91,6 +96,11 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
}
|
||||
return directoryPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return Files.exists(filePath) && Files.exists(getDirectoryPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
@@ -156,39 +166,44 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
|
||||
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
final Path filePath = dirPath.resolve(ciphertextFilename);
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); final FileLock lock = c.lock(0L, FILE_HEADER_LENGTH, false)) {
|
||||
final Path tmpFilePath = Files.createTempFile(dirPath, null, null);
|
||||
// encrypt to tmp file:
|
||||
try (final FileChannel c = FileChannel.open(tmpFilePath, StandardOpenOption.WRITE, StandardOpenOption.DSYNC)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), c);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (CounterOverflowException e) {
|
||||
// lets indicate this to the client as a "file too big" error
|
||||
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
|
||||
} catch (EncryptFailedException e) {
|
||||
LOG.error("Encryption failed for unknown reasons.", e);
|
||||
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
// mv tmp to target file:
|
||||
try {
|
||||
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(tmpFilePath, filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
Files.setLastModifiedTime(filePath, FileTime.from(Instant.now()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
try {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER);
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavRuntimeException(new DavException(DavServletResponse.SC_NOT_FOUND));
|
||||
}
|
||||
|
||||
try (final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER)) {
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
|
||||
for (final Path childPath : directoryStream) {
|
||||
try {
|
||||
final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString());
|
||||
final String cleartextFilepath = FilenameUtils.concat(getResourcePath(), cleartextFilename);
|
||||
final String cleartextFilepath = locator.isRootLocation() ? '/' + cleartextFilename : locator.getResourcePath() + '/' + cleartextFilename;
|
||||
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
|
||||
final DavResource resource;
|
||||
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
|
||||
@@ -197,7 +212,9 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
|
||||
resource = factory.createChildFileResource(childLocator, session, childPath);
|
||||
}
|
||||
result.add(resource);
|
||||
if (resource.exists()) {
|
||||
result.add(resource);
|
||||
}
|
||||
} catch (DecryptFailedException e) {
|
||||
LOG.warn("Decryption of resource failed: " + childPath);
|
||||
continue;
|
||||
@@ -206,7 +223,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
return new DavResourceIteratorImpl(result);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new IORuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
} catch (DavException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new DavRuntimeException(e);
|
||||
@@ -227,30 +244,26 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
// https://tools.ietf.org/html/rfc4918#section-9.6
|
||||
// we must unlock anything we want to delete:
|
||||
for (ActiveLock lock : member.getLocks()) {
|
||||
member.unlock(lock.getToken());
|
||||
}
|
||||
// now we can delete the file or directory:
|
||||
try {
|
||||
final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
|
||||
final String ciphertextFilename;
|
||||
if (member instanceof EncryptedDir) {
|
||||
final EncryptedDir subDir = (EncryptedDir) member;
|
||||
// remove sub-members recursively before deleting own directory
|
||||
for (Iterator<DavResource> iterator = member.getMembers(); iterator.hasNext();) {
|
||||
DavResource m = iterator.next();
|
||||
member.removeMember(m);
|
||||
}
|
||||
final Path subDirPath = subDir.getDirectoryPath();
|
||||
if (subDirPath != null) {
|
||||
Files.deleteIfExists(subDirPath);
|
||||
}
|
||||
ciphertextFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
|
||||
deleteSubDirectory(subDir);
|
||||
} else {
|
||||
ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
final Path memberPath = dirPath.resolve(ciphertextFilename);
|
||||
Files.deleteIfExists(memberPath);
|
||||
}
|
||||
final Path memberPath = dirPath.resolve(ciphertextFilename);
|
||||
Files.deleteIfExists(memberPath);
|
||||
} catch (FileNotFoundException e) {
|
||||
// no-op
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,7 +273,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).getDirFilePath();
|
||||
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
@@ -278,7 +291,7 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
||||
final Path dstDirFilePath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstDirFilePath = ((NonExistingNode) dest).getDirFilePath();
|
||||
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||
} else {
|
||||
dstDirFilePath = dest.filePath;
|
||||
}
|
||||
@@ -289,7 +302,8 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
final String dstDirId = UUID.randomUUID().toString();
|
||||
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
|
||||
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
|
||||
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
@@ -328,5 +342,77 @@ class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given directory recursively by resolving subdirectories using their directory files.
|
||||
*/
|
||||
private void deleteSubDirectory(final EncryptedDir subDir) throws IOException {
|
||||
final Path subDirPath = subDir.getDirectoryPath();
|
||||
filenameTranslator.uncacheDirectoryId(subDir.filePath);
|
||||
Files.delete(subDir.filePath);
|
||||
final LinkedTransferQueue<Path> queue = new LinkedTransferQueue<>();
|
||||
queue.put(subDirPath);
|
||||
Path dir;
|
||||
while ((dir = queue.poll()) != null) {
|
||||
if (Files.exists(dir)) {
|
||||
Files.walkFileTree(dir, new RecursiveDirectoryDeletingVisitor(queue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all files it visits and enqueues subdirectories into a given {@link Queue} for deletion, too.
|
||||
*
|
||||
* If its parent directory is empty after deleting, it will get deleted, too.
|
||||
*/
|
||||
private class RecursiveDirectoryDeletingVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
private final Queue<Path> directories;
|
||||
|
||||
private RecursiveDirectoryDeletingVisitor(Queue<Path> directories) {
|
||||
this.directories = directories;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (file.toString().endsWith(DIR_EXT)) {
|
||||
final String directoryId = filenameTranslator.getDirectoryId(file, false);
|
||||
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
directories.add(directoryPath);
|
||||
filenameTranslator.uncacheDirectoryId(file);
|
||||
}
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
// first check, if we're the only remaining child:
|
||||
boolean hasSiblings = false;
|
||||
try (final DirectoryStream<Path> siblings = Files.newDirectoryStream(dir.getParent())) {
|
||||
for (Path sibling : siblings) {
|
||||
if (!dir.getFileName().equals(sibling.getFileName())) {
|
||||
hasSiblings = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// delete our current directory:
|
||||
Files.delete(dir);
|
||||
// if we have siblings, we still need our parent. Otherwise delete it, too:
|
||||
if (!hasSiblings) {
|
||||
Files.delete(dir.getParent());
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
|
||||
LOG.error("Failed to delete file " + file.toString(), exc);
|
||||
return FileVisitResult.TERMINATE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -10,10 +10,9 @@ package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -31,9 +30,7 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.slf4j.Logger;
|
||||
@@ -44,6 +41,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||
|
||||
protected final CryptoWarningHandler cryptoWarningHandler;
|
||||
protected final Long contentLength;
|
||||
|
||||
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
||||
super(factory, locator, session, lockManager, cryptor, filePath);
|
||||
@@ -51,9 +49,10 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
throw new IllegalArgumentException("filePath must not be null");
|
||||
}
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
Long contentLength = null;
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.tryLock(0L, FILE_HEADER_LENGTH, true)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(c);
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
contentLength = cryptor.decryptedContentLength(c);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
@@ -61,14 +60,19 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
} catch (OverlappingFileLockException e) {
|
||||
// file header currently locked, report -1 for unknown size.
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + filePath.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
|
||||
// don't add content length DAV property
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + filePath.toString(), e);
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
public Long getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,20 +100,19 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ); SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(c);
|
||||
if (contentLength != null) {
|
||||
outputContext.setContentLength(contentLength);
|
||||
}
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptFile(channel, outputContext.getOutputStream());
|
||||
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
|
||||
outputContext.getOutputStream().flush();
|
||||
}
|
||||
|
||||
} catch (EOFException e) {
|
||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + filePath.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,7 +122,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).getFilePath();
|
||||
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
@@ -136,7 +139,7 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).getFilePath();
|
||||
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
@@ -2,34 +2,22 @@ package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.ClosedByInterruptException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.MutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
||||
/**
|
||||
* Delivers only the requested range of bytes from a file.
|
||||
*
|
||||
@@ -38,157 +26,65 @@ import com.google.common.cache.CacheBuilder;
|
||||
class EncryptedFilePart extends EncryptedFile {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
|
||||
private static final String BYTE_UNIT_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
private static final Cache<DavResourceLocator, MacAuthenticationJob> cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
|
||||
|
||||
/**
|
||||
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
|
||||
private final Pair<Long, Long> range;
|
||||
|
||||
/**
|
||||
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
|
||||
|
||||
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
|
||||
|
||||
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
|
||||
ExecutorService backgroundTaskExecutor, Path filePath) {
|
||||
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, Pair<String, String> requestRange, LockManager lockManager, Cryptor cryptor,
|
||||
CryptoWarningHandler cryptoWarningHandler, Path filePath) {
|
||||
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (rangeHeader == null) {
|
||||
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
|
||||
}
|
||||
determineByteRanges(rangeHeader);
|
||||
|
||||
synchronized (cachedMacAuthenticationJobs) {
|
||||
if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
|
||||
final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
|
||||
cachedMacAuthenticationJobs.put(locator, macAuthJob);
|
||||
backgroundTaskExecutor.submit(macAuthJob);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void determineByteRanges(String rangeHeader) {
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length == 0) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
for (final String byteRange : byteRanges) {
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
|
||||
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
|
||||
if (lower > upper) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return One range, that spans all requested ranges.
|
||||
*/
|
||||
private Pair<Long, Long> getUnionRange(Long fileSize) {
|
||||
final long lastByte = fileSize - 1;
|
||||
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
|
||||
for (Pair<Long, Long> range : requestedContentRanges) {
|
||||
final long left;
|
||||
final long right;
|
||||
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
|
||||
left = lastByte - range.getRight();
|
||||
right = lastByte;
|
||||
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
|
||||
left = range.getLeft();
|
||||
right = lastByte;
|
||||
try {
|
||||
final Long lower = requestRange.getLeft().isEmpty() ? null : Long.valueOf(requestRange.getLeft());
|
||||
final Long upper = requestRange.getRight().isEmpty() ? null : Long.valueOf(requestRange.getRight());
|
||||
if (lower == null) {
|
||||
range = new ImmutablePair<Long, Long>(contentLength - upper, contentLength - 1);
|
||||
} else if (upper == null) {
|
||||
range = new ImmutablePair<Long, Long>(lower, contentLength - 1);
|
||||
} else {
|
||||
left = range.getLeft();
|
||||
right = range.getRight();
|
||||
}
|
||||
if (result.getLeft() == null || left < result.getLeft()) {
|
||||
result.setLeft(left);
|
||||
}
|
||||
if (result.getRight() == null || right > result.getRight()) {
|
||||
result.setRight(right);
|
||||
range = new ImmutablePair<Long, Long>(lower, Math.min(upper, contentLength - 1));
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
assert Files.isRegularFile(filePath);
|
||||
assert contentLength != null;
|
||||
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
||||
final Long fileSize = cryptor.decryptedContentLength(channel);
|
||||
final Pair<Long, Long> range = getUnionRange(fileSize);
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
if (rangeLength <= 0 || range.getLeft() > contentLength - 1) {
|
||||
// unsatisfiable content range:
|
||||
outputContext.setContentLength(0);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength);
|
||||
LOG.debug("Requested content range unsatisfiable: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
return;
|
||||
} else {
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
}
|
||||
|
||||
assert range.getLeft() > 0;
|
||||
assert range.getLeft() < contentLength;
|
||||
assert range.getRight() < contentLength;
|
||||
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
|
||||
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
|
||||
outputContext.getOutputStream().flush();
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + filePath.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
private class MacAuthenticationJob implements Runnable {
|
||||
|
||||
private final DavResourceLocator locator;
|
||||
|
||||
public MacAuthenticationJob(final DavResourceLocator locator) {
|
||||
if (locator == null) {
|
||||
throw new IllegalArgumentException("locator must not be null.");
|
||||
}
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
assert Files.isRegularFile(filePath);
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
||||
final boolean authentic = cryptor.isAuthentic(channel);
|
||||
if (!authentic) {
|
||||
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
|
||||
}
|
||||
} catch (ClosedByInterruptException ex) {
|
||||
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
|
||||
} catch (IOException e) {
|
||||
LOG.error("IOException during MAC verification of " + filePath.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return locator.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof MacAuthenticationJob) {
|
||||
final MacAuthenticationJob other = (MacAuthenticationJob) obj;
|
||||
return this.locator.equals(other.locator);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ interface FileConstants {
|
||||
/**
|
||||
* Number of bytes in the file header.
|
||||
*/
|
||||
long FILE_HEADER_LENGTH = 96;
|
||||
long FILE_HEADER_LENGTH = 104;
|
||||
|
||||
/**
|
||||
* Allow range requests for files > 32MiB.
|
||||
|
||||
@@ -5,7 +5,6 @@ import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
@@ -69,6 +68,14 @@ class FilenameTranslator implements FileConstants {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* to be called when a directory gets deleted, so the corresponding directory id is not longer cached.
|
||||
*/
|
||||
public void uncacheDirectoryId(Path directoryFile) throws IOException {
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
|
||||
directoryIdCache.remove(key);
|
||||
}
|
||||
|
||||
public Path getEncryptedDirectoryPath(String directoryId) {
|
||||
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
@@ -130,13 +137,14 @@ class FilenameTranslator implements FileConstants {
|
||||
/* Locked I/O */
|
||||
|
||||
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
|
||||
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
|
||||
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
|
||||
c.write(ByteBuffer.wrap(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] readAllBytesAtomically(Path path) throws IOException {
|
||||
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
|
||||
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||
c.read(buffer);
|
||||
return buffer.array();
|
||||
|
||||
@@ -21,16 +21,12 @@ import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
|
||||
|
||||
class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
private final Path filePath;
|
||||
private final Path dirFilePath;
|
||||
|
||||
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) {
|
||||
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor, null);
|
||||
this.filePath = filePath;
|
||||
this.dirFilePath = dirFilePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,12 +79,26 @@ class NonExistingNode extends AbstractEncryptedNode {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
public Path getFilePath() {
|
||||
return filePath;
|
||||
/**
|
||||
* @return lazily resolved file path, e.g. needed during MOVE operations.
|
||||
*/
|
||||
public Path materializeFilePath() {
|
||||
try {
|
||||
return factory.getEncryptedFilePath(locator.getResourcePath(), true);
|
||||
} catch (NonExistingParentException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Path getDirFilePath() {
|
||||
return dirFilePath;
|
||||
/**
|
||||
* @return lazily resolved directory file path, e.g. needed during MOVE operations.
|
||||
*/
|
||||
public Path materializeDirFilePath() {
|
||||
try {
|
||||
return factory.getEncryptedDirectoryFilePath(locator.getResourcePath(), true);
|
||||
} catch (NonExistingParentException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.NonReadableChannelException;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Instances of this class wrap a file lock, that is created upon construction and destroyed by {@link #close()}.
|
||||
*
|
||||
* If the construction fails (e.g. if the file system does not support locks) no exception will be thrown and no lock is created.
|
||||
*/
|
||||
class SilentlyFailingFileLock implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SilentlyFailingFileLock.class);
|
||||
|
||||
private final FileLock lock;
|
||||
|
||||
/**
|
||||
* Invokes #SilentlyFailingFileLock(FileChannel, long, long, boolean) with a position of 0 and a size of {@link Long#MAX_VALUE}.
|
||||
*/
|
||||
SilentlyFailingFileLock(FileChannel channel, boolean shared) {
|
||||
this(channel, 0L, Long.MAX_VALUE, shared);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NonReadableChannelException If shared is true this channel was not opened for reading
|
||||
* @throws NonWritableChannelException If shared is false but this channel was not opened for writing
|
||||
* @see FileChannel#lock(long, long, boolean)
|
||||
*/
|
||||
SilentlyFailingFileLock(FileChannel channel, long position, long size, boolean shared) {
|
||||
FileLock lock = null;
|
||||
try {
|
||||
lock = channel.tryLock(position, size, shared);
|
||||
} catch (IOException | OverlappingFileLockException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.trace("Unable to lock file.");
|
||||
}
|
||||
} finally {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (lock != null) {
|
||||
lock.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,63 +8,50 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||
import org.apache.jackrabbit.webdav.WebdavResponse;
|
||||
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class WebDavServlet extends AbstractWebdavServlet {
|
||||
|
||||
private static final long serialVersionUID = 7965170007048673022L;
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavServlet.class);
|
||||
public static final String CFG_FS_ROOT = "cfg.fs.root";
|
||||
private DavSessionProvider davSessionProvider;
|
||||
private DavLocatorFactory davLocatorFactory;
|
||||
private DavResourceFactory davResourceFactory;
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
private ExecutorService backgroundTaskExecutor;
|
||||
|
||||
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
|
||||
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
|
||||
super();
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection);
|
||||
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
super.init(config);
|
||||
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
|
||||
backgroundTaskExecutor = Executors.newCachedThreadPool();
|
||||
davSessionProvider = new DavSessionProviderImpl();
|
||||
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
|
||||
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
backgroundTaskExecutor.shutdown();
|
||||
try {
|
||||
final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS);
|
||||
if (!tasksFinished) {
|
||||
backgroundTaskExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
backgroundTaskExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
super.destroy();
|
||||
}
|
||||
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,4 +89,30 @@ public class WebDavServlet extends AbstractWebdavServlet {
|
||||
this.davResourceFactory = resourceFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
long t0 = System.nanoTime();
|
||||
super.doPut(request, response, resource);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
long t1 = System.nanoTime();
|
||||
LOG.trace("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
long t0 = System.nanoTime();
|
||||
try {
|
||||
super.doGet(request, response, resource);
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
|
||||
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
|
||||
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
if (LOG.isDebugEnabled()) {
|
||||
long t1 = System.nanoTime();
|
||||
LOG.trace("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<name>Cryptomator cryptographic module (AES)</name>
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.ReadableByteChannel;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
@@ -21,39 +22,38 @@ import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.output.NullOutputStream;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
|
||||
import org.cryptomator.crypto.exceptions.CounterOverflowException;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Aes256Cryptor.class);
|
||||
|
||||
/**
|
||||
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded
|
||||
* here: http://www.oracle.com/technetwork/java/javase/downloads/.
|
||||
@@ -211,7 +211,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException("Invalid key.", ex);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
|
||||
throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
|
||||
throw new IllegalStateException("Algorithm/Padding should exist.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,8 +308,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(64);
|
||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
return null;
|
||||
}
|
||||
@@ -319,20 +319,20 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
headerBuf.position(0);
|
||||
headerBuf.get(iv);
|
||||
|
||||
// read content length:
|
||||
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
|
||||
headerBuf.position(16);
|
||||
headerBuf.get(encryptedContentLengthBytes);
|
||||
// read sensitive header data:
|
||||
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||
headerBuf.position(24);
|
||||
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||
|
||||
// read stored header mac:
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
headerBuf.position(32);
|
||||
headerBuf.position(72);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// calculate mac over first 32 bytes of header:
|
||||
// calculate mac over first 72 bytes of header:
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.rewind();
|
||||
headerBuf.limit(32);
|
||||
headerBuf.limit(72);
|
||||
headerMac.update(headerBuf);
|
||||
|
||||
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
||||
@@ -340,76 +340,38 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
||||
}
|
||||
|
||||
return decryptContentLength(encryptedContentLengthBytes, iv);
|
||||
// decrypt sensitive header data:
|
||||
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
||||
final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
||||
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) {
|
||||
private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) {
|
||||
try {
|
||||
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
|
||||
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes);
|
||||
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
|
||||
return fileSizeBuffer.getLong();
|
||||
return sizeCipher.doFinal(ciphertextBytes);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] encryptContentLength(long contentLength, byte[] iv) {
|
||||
private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
|
||||
try {
|
||||
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
|
||||
fileSizeBuffer.putLong(contentLength);
|
||||
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
||||
return sizeCipher.doFinal(fileSizeBuffer.array());
|
||||
return sizeCipher.doFinal(plaintextBytes);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0l);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
|
||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// read header mac:
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
headerBuf.position(32);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// read content mac:
|
||||
final byte[] storedContentMac = new byte[32];
|
||||
headerBuf.position(64);
|
||||
headerBuf.get(storedContentMac);
|
||||
|
||||
// calculate mac over first 32 bytes of header:
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.position(0);
|
||||
headerBuf.limit(32);
|
||||
headerMac.update(headerBuf);
|
||||
|
||||
// calculate mac over content:
|
||||
encryptedFile.position(96l);
|
||||
final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream macIn = new MacInputStream(in, contentMac);
|
||||
IOUtils.copyLarge(macIn, new NullOutputStream());
|
||||
|
||||
// compare (in constant time):
|
||||
final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
||||
final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal());
|
||||
return headerMacMatches && contentMacMatches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0l);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
|
||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
@@ -419,134 +381,361 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
headerBuf.position(0);
|
||||
headerBuf.get(iv);
|
||||
|
||||
// read content length:
|
||||
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
|
||||
// read nonce:
|
||||
final byte[] nonce = new byte[8];
|
||||
headerBuf.position(16);
|
||||
headerBuf.get(encryptedContentLengthBytes);
|
||||
final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv);
|
||||
headerBuf.get(nonce);
|
||||
|
||||
// read sensitive header data:
|
||||
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||
headerBuf.position(24);
|
||||
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||
|
||||
// read header mac:
|
||||
final byte[] headerMac = new byte[32];
|
||||
headerBuf.position(32);
|
||||
headerBuf.get(headerMac);
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
headerBuf.position(72);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// read content mac:
|
||||
final byte[] contentMac = new byte[32];
|
||||
headerBuf.position(64);
|
||||
headerBuf.get(contentMac);
|
||||
|
||||
// decrypt content
|
||||
encryptedFile.position(96l);
|
||||
final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey);
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream macIn = new MacInputStream(in, calculatedContentMac);
|
||||
final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
|
||||
final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
|
||||
|
||||
// drain remaining bytes to /dev/null to complete MAC calculation:
|
||||
IOUtils.copyLarge(macIn, new NullOutputStream());
|
||||
|
||||
// compare (in constant time):
|
||||
final boolean macMatches = MessageDigest.isEqual(contentMac, calculatedContentMac.doFinal());
|
||||
if (!macMatches) {
|
||||
// This exception will be thrown AFTER we sent the decrypted content to the user.
|
||||
// This has two advantages:
|
||||
// - we don't need to read files twice
|
||||
// - we can still restore files suffering from non-malicious bit rotting
|
||||
// Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception.
|
||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
||||
// calculate mac over first 72 bytes of header:
|
||||
if (authenticate) {
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.position(0);
|
||||
headerBuf.limit(72);
|
||||
headerMac.update(headerBuf);
|
||||
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
||||
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
return bytesDecrypted;
|
||||
// decrypt sensitive header data:
|
||||
final byte[] fileKeyBytes = new byte[32];
|
||||
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
||||
final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
||||
sensitiveHeaderContentBuf.get(fileKeyBytes);
|
||||
|
||||
// prepare content decryption:
|
||||
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
||||
final LengthLimitingOutputStream paddingRemovingOutputStream = new LengthLimitingOutputStream(plaintextFile, fileSize);
|
||||
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
|
||||
return new DecryptWorker(lock, blockDone, currentBlock, inputQueue, authenticate, Channels.newChannel(paddingRemovingOutputStream)) {
|
||||
|
||||
@Override
|
||||
protected Cipher initCipher(long startBlockNum) {
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
||||
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
||||
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mac initMac() {
|
||||
return hmacSha256(hMacMasterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException {
|
||||
mac.update(iv);
|
||||
mac.update(longToByteArray(blockNum));
|
||||
mac.update(ciphertextBuf);
|
||||
final byte[] calculatedMac = mac.doFinal();
|
||||
final byte[] storedMac = new byte[mac.getMacLength()];
|
||||
macBuf.get(storedMac);
|
||||
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
||||
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException {
|
||||
assert plaintextBuf.remaining() >= cipher.getOutputSize(ciphertextBuf.remaining());
|
||||
try {
|
||||
cipher.update(ciphertextBuf, plaintextBuf);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new DecryptFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
// read as many blocks from file as possible, but wait if queue is full:
|
||||
encryptedFile.position(104l);
|
||||
final int maxNumBlocks = 64;
|
||||
int numBlocks = 1;
|
||||
int bytesRead = 0;
|
||||
long blockNumber = 0;
|
||||
do {
|
||||
if (numBlocks < maxNumBlocks) {
|
||||
numBlocks++;
|
||||
}
|
||||
final int inBufSize = numBlocks * (CONTENT_MAC_BLOCK + 32);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(inBufSize);
|
||||
bytesRead = readFromChannel(encryptedFile, buf);
|
||||
buf.flip();
|
||||
final int blocksRead = (int) Math.ceil(bytesRead / (double) (CONTENT_MAC_BLOCK + 32));
|
||||
final boolean consumedInTime = executor.offer(new BlocksData(buf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
|
||||
if (!consumedInTime) {
|
||||
break;
|
||||
}
|
||||
blockNumber += numBlocks;
|
||||
} while (bytesRead == numBlocks * (CONTENT_MAC_BLOCK + 32));
|
||||
|
||||
// wait for decryption workers to finish:
|
||||
try {
|
||||
executor.waitUntilDone();
|
||||
} catch (ExecutionException e) {
|
||||
final Throwable cause = e.getCause();
|
||||
if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
} else if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
} else {
|
||||
LOG.error("Unexpected exception", e);
|
||||
}
|
||||
} finally {
|
||||
destroyQuietly(fileKey);
|
||||
}
|
||||
|
||||
return paddingRemovingOutputStream.getBytesWritten();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
// read iv:
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0l);
|
||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numIvBytesRead = encryptedFile.read(countingIv);
|
||||
|
||||
// check validity of header:
|
||||
if (numIvBytesRead != AES_BLOCK_LENGTH) {
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
final int headerBytesRead = readFromChannel(encryptedFile, headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// seek relevant position and update iv:
|
||||
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
|
||||
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
|
||||
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
|
||||
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
|
||||
// read iv:
|
||||
final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
||||
headerBuf.position(0);
|
||||
headerBuf.get(iv);
|
||||
|
||||
// fast forward stream:
|
||||
encryptedFile.position(96l + beginOfFirstRelevantBlock);
|
||||
// read nonce:
|
||||
final byte[] nonce = new byte[8];
|
||||
headerBuf.position(16);
|
||||
headerBuf.get(nonce);
|
||||
|
||||
// generate cipher:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
// read sensitive header data:
|
||||
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||
headerBuf.position(24);
|
||||
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream cipheredIn = new CipherInputStream(in, cipher);
|
||||
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
|
||||
// read header mac:
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
headerBuf.position(72);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// calculate mac over first 72 bytes of header:
|
||||
if (authenticate) {
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.position(0);
|
||||
headerBuf.limit(72);
|
||||
headerMac.update(headerBuf);
|
||||
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
||||
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt sensitive header data:
|
||||
final byte[] fileKeyBytes = new byte[32];
|
||||
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
||||
final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
||||
sensitiveHeaderContentBuf.get(fileKeyBytes);
|
||||
|
||||
assert pos + length - 1 < fileSize;
|
||||
|
||||
// find first relevant block:
|
||||
final long startBlock = pos / CONTENT_MAC_BLOCK; // floor
|
||||
final long startByte = startBlock * (CONTENT_MAC_BLOCK + 32) + 104;
|
||||
final long offsetFromFirstBlock = pos - startBlock * CONTENT_MAC_BLOCK;
|
||||
|
||||
// append correct counter value to nonce:
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.putLong(startBlock * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
||||
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
||||
|
||||
// content decryption:
|
||||
encryptedFile.position(startByte);
|
||||
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
||||
final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
|
||||
final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
||||
|
||||
try {
|
||||
// reading ciphered input and MACs interleaved:
|
||||
long bytesWritten = 0;
|
||||
final ByteBuffer buf = ByteBuffer.allocate(CONTENT_MAC_BLOCK + 32);
|
||||
int n = 0;
|
||||
long blockNum = startBlock;
|
||||
while ((n = readFromChannel(encryptedFile, buf)) > 0 && bytesWritten < length) {
|
||||
if (n < 32) {
|
||||
throw new DecryptFailedException("Invalid file content, missing MAC.");
|
||||
}
|
||||
|
||||
buf.flip();
|
||||
final ByteBuffer ciphertextBuf = buf.asReadOnlyBuffer();
|
||||
ciphertextBuf.limit(n - 32);
|
||||
|
||||
// check MAC of current block:
|
||||
if (authenticate) {
|
||||
final byte[] storedMac = new byte[contentMac.getMacLength()];
|
||||
final ByteBuffer storedMacBuf = buf.asReadOnlyBuffer();
|
||||
storedMacBuf.position(n - 32);
|
||||
storedMacBuf.get(storedMac);
|
||||
contentMac.update(iv);
|
||||
contentMac.update(longToByteArray(blockNum));
|
||||
contentMac.update(ciphertextBuf);
|
||||
ciphertextBuf.rewind();
|
||||
final byte[] calculatedMac = contentMac.doFinal();
|
||||
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
||||
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt block:
|
||||
final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(ciphertextBuf.remaining()));
|
||||
cipher.update(ciphertextBuf, plaintextBuf);
|
||||
plaintextBuf.flip();
|
||||
final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
|
||||
final long pending = length - bytesWritten;
|
||||
final int available = plaintextBuf.remaining() - offset;
|
||||
final int currentBatch = (int) Math.min(pending, available);
|
||||
|
||||
plaintextFile.write(plaintextBuf.array(), offset, currentBatch);
|
||||
bytesWritten += currentBatch;
|
||||
blockNum++;
|
||||
buf.rewind();
|
||||
}
|
||||
return bytesWritten;
|
||||
} catch (ShortBufferException e) {
|
||||
throw new IllegalStateException("Output buffer size known to fit.", e);
|
||||
} finally {
|
||||
destroyQuietly(fileKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* header = {16 byte iv, 16 byte filesize, 32 byte headerMac, 32 byte contentMac}
|
||||
* header = {16 byte iv, 8 byte nonce, 48 byte sensitive header data (file size + file key + padding), 32 byte headerMac}
|
||||
*/
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
// truncate file
|
||||
encryptedFile.truncate(0l);
|
||||
|
||||
// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
|
||||
final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
|
||||
ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
|
||||
final byte[] iv = ivBuf.array();
|
||||
// choose a random header IV:
|
||||
final byte[] iv = randomData(AES_BLOCK_LENGTH);
|
||||
|
||||
// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
|
||||
headerBuf.limit(96);
|
||||
// chosse 8 byte random nonce and 8 byte counter set to zero:
|
||||
final byte[] nonce = randomData(8);
|
||||
|
||||
// choose a random content key:
|
||||
final byte[] fileKeyBytes = randomData(32);
|
||||
|
||||
// 104 byte header buffer (16 header IV, 8 content nonce, 48 sensitive header data, 32 headerMac), filled after writing the content
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
headerBuf.limit(104);
|
||||
encryptedFile.write(headerBuf);
|
||||
|
||||
// content encryption:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
||||
final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
||||
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
|
||||
final OutputStream macOut = new MacOutputStream(out, contentMac);
|
||||
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
|
||||
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
|
||||
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
|
||||
final Long plaintextSize;
|
||||
try {
|
||||
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
|
||||
} catch (CounterAwareInputLimitReachedException ex) {
|
||||
encryptedFile.truncate(0l);
|
||||
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
|
||||
}
|
||||
// prepare content encryption:
|
||||
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
||||
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
|
||||
return new EncryptWorker(lock, blockDone, currentBlock, inputQueue, encryptedFile) {
|
||||
|
||||
// add random length padding to obfuscate file length:
|
||||
final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
|
||||
final long minAdditionalBlocks = 4;
|
||||
final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs)
|
||||
final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks
|
||||
final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks);
|
||||
@Override
|
||||
protected Cipher initCipher(long startBlockNum) {
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
||||
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
||||
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.ENCRYPT_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mac initMac() {
|
||||
return hmacSha256(hMacMasterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf) {
|
||||
mac.update(iv);
|
||||
mac.update(longToByteArray(blockNum));
|
||||
mac.update(ciphertextBuf);
|
||||
return mac.doFinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException {
|
||||
try {
|
||||
assert ciphertextBuf.remaining() >= cipher.getOutputSize(plaintextBuf.remaining());
|
||||
cipher.update(plaintextBuf, ciphertextBuf);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new EncryptFailedException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// read as many blocks from file as possible, but wait if queue is full:
|
||||
final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
|
||||
for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) {
|
||||
blockSizeBufferedOut.write(randomPadding);
|
||||
final LengthObfuscatingInputStream in = new LengthObfuscatingInputStream(plaintextFile, randomPadding);
|
||||
final ReadableByteChannel channel = Channels.newChannel(in);
|
||||
int bytesRead = 0;
|
||||
long blockNumber = 0;
|
||||
final int maxNumBlocks = 64;
|
||||
int numBlocks = 0;
|
||||
do {
|
||||
if (numBlocks < maxNumBlocks) {
|
||||
numBlocks++;
|
||||
}
|
||||
final int inBufSize = numBlocks * CONTENT_MAC_BLOCK;
|
||||
final ByteBuffer inBuf = ByteBuffer.allocate(inBufSize);
|
||||
bytesRead = readFromChannel(channel, inBuf);
|
||||
inBuf.flip();
|
||||
final int blocksRead = (int) Math.ceil(bytesRead / (double) CONTENT_MAC_BLOCK);
|
||||
final boolean consumedInTime = executor.offer(new BlocksData(inBuf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
|
||||
if (!consumedInTime) {
|
||||
break;
|
||||
}
|
||||
blockNumber += numBlocks;
|
||||
} while (bytesRead == numBlocks * CONTENT_MAC_BLOCK);
|
||||
|
||||
// wait for encryption workers to finish:
|
||||
try {
|
||||
executor.waitUntilDone();
|
||||
} catch (ExecutionException e) {
|
||||
final Throwable cause = e.getCause();
|
||||
if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
} else if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
} else {
|
||||
LOG.error("Unexpected exception", e);
|
||||
}
|
||||
} finally {
|
||||
destroyQuietly(fileKey);
|
||||
}
|
||||
blockSizeBufferedOut.flush();
|
||||
|
||||
// create and write header:
|
||||
final long plaintextSize = in.getRealInputLength();
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.allocate(Long.BYTES + fileKeyBytes.length);
|
||||
sensitiveHeaderContentBuf.putLong(plaintextSize);
|
||||
sensitiveHeaderContentBuf.put(fileKeyBytes);
|
||||
headerBuf.clear();
|
||||
headerBuf.put(iv);
|
||||
headerBuf.put(encryptContentLength(plaintextSize, iv));
|
||||
headerBuf.put(nonce);
|
||||
headerBuf.put(encryptHeaderData(sensitiveHeaderContentBuf.array(), iv));
|
||||
headerBuf.flip();
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerMac.update(headerBuf);
|
||||
headerBuf.limit(96);
|
||||
headerBuf.limit(104);
|
||||
headerBuf.put(headerMac.doFinal());
|
||||
headerBuf.put(contentMac.doFinal());
|
||||
headerBuf.flip();
|
||||
encryptedFile.position(0);
|
||||
encryptedFile.write(headerBuf);
|
||||
@@ -554,4 +743,33 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
return plaintextSize;
|
||||
}
|
||||
|
||||
private byte[] longToByteArray(long lng) {
|
||||
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads bytes from a ReadableByteChannel.
|
||||
* <p>
|
||||
* This implementation guarantees that it will read as many bytes
|
||||
* as possible before giving up; this may not always be the case for
|
||||
* subclasses of {@link ReadableByteChannel}.
|
||||
*
|
||||
* @param input the byte channel to read
|
||||
* @param buffer byte buffer destination
|
||||
* @return the actual length read; may be less than requested if EOF was reached
|
||||
* @throws IOException if a read error occurs
|
||||
* @see
|
||||
* <a href="http://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/IOUtils.html">Apache Commons IOUtils 2.5</a>
|
||||
*/
|
||||
public static int readFromChannel(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException {
|
||||
final int length = buffer.remaining();
|
||||
while (buffer.remaining() > 0) {
|
||||
final int count = input.read(buffer);
|
||||
if (count == -1) { // EOF
|
||||
break;
|
||||
}
|
||||
}
|
||||
return length - buffer.remaining();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ interface AesCryptographicConfiguration {
|
||||
*/
|
||||
int AES_BLOCK_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Number of bytes, a content block over which a MAC is calculated consists of.
|
||||
*/
|
||||
int CONTENT_MAC_BLOCK = 32 * 1024;
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
|
||||
@@ -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"})
|
||||
public class KeyFile implements Serializable {
|
||||
|
||||
static final Integer CURRENT_VERSION = 1;
|
||||
static final Integer CURRENT_VERSION = 2;
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
|
||||
private Integer version;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class LengthLimitingOutputStream extends FilterOutputStream {
|
||||
|
||||
private final long limit;
|
||||
private volatile long bytesWritten;
|
||||
|
||||
public LengthLimitingOutputStream(OutputStream out, long limit) {
|
||||
super(out);
|
||||
this.limit = limit;
|
||||
this.bytesWritten = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
if (bytesWritten < limit) {
|
||||
out.write(b);
|
||||
increaseNumberOfWrittenBytes(1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
final long bytesAvailable = limit - bytesWritten;
|
||||
final int adjustedLen = (int) Math.min(len, bytesAvailable);
|
||||
if (adjustedLen > 0) {
|
||||
out.write(b, off, adjustedLen);
|
||||
increaseNumberOfWrittenBytes(adjustedLen);
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
private void increaseNumberOfWrittenBytes(int amount) throws IOException {
|
||||
bytesWritten += amount;
|
||||
if (bytesWritten >= limit) {
|
||||
out.flush();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
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 {
|
||||
if (paddingLength == -1) {
|
||||
// EOF not yet reached; delegate original stream to answer this rather complicated question:
|
||||
return in.available();
|
||||
} else {
|
||||
// EOF already reached, read from remaining padding:
|
||||
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)
|
||||
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
@@ -112,7 +80,7 @@ public class Aes256CryptorTest {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -131,7 +99,7 @@ public class Aes256CryptorTest {
|
||||
// decrypt modified content (should fail with DecryptFailedException):
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
cryptor.decryptFile(encryptedIn, plaintextOut);
|
||||
cryptor.decryptFile(encryptedIn, plaintextOut, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -144,7 +112,7 @@ public class Aes256CryptorTest {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096 + 32); // header + content + maximum possible size obfuscation padding + 32 bytes mac (per each 32k)
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -159,7 +127,7 @@ public class Aes256CryptorTest {
|
||||
|
||||
// decrypt:
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut);
|
||||
final Long numDecryptedBytes = cryptor.decryptFile(encryptedIn, plaintextOut, true);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
|
||||
@@ -171,10 +139,10 @@ public class Aes256CryptorTest {
|
||||
|
||||
@Test
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
||||
// 8MiB test plaintext data:
|
||||
final byte[] plaintextData = new byte[2097152 * Integer.BYTES];
|
||||
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
|
||||
for (int i = 0; i < 65536; i++) {
|
||||
for (int i = 0; i < 2097152; i++) {
|
||||
bbIn.putInt(i);
|
||||
}
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
@@ -183,7 +151,7 @@ public class Aes256CryptorTest {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (96 + plaintextData.length * 1.2));
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (104 + plaintextData.length * 1.2));
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -194,14 +162,14 @@ public class Aes256CryptorTest {
|
||||
// decrypt:
|
||||
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
|
||||
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
|
||||
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 260000 * Integer.BYTES, 4000 * Integer.BYTES, true);
|
||||
IOUtils.closeQuietly(encryptedIn);
|
||||
IOUtils.closeQuietly(plaintextOut);
|
||||
Assert.assertTrue(numDecryptedBytes > 0);
|
||||
|
||||
// check decrypted data:
|
||||
final byte[] result = plaintextOut.toByteArray();
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
|
||||
final byte[] expected = Arrays.copyOfRange(plaintextData, 260000 * Integer.BYTES, 264000 * Integer.BYTES);
|
||||
Assert.assertArrayEquals(expected, result);
|
||||
}
|
||||
|
||||
|
||||
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>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
<name>Cryptomator cryptographic module API</name>
|
||||
|
||||
@@ -53,18 +53,13 @@ public class AbstractCryptorDecorator implements Cryptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.isAuthentic(encryptedFile);
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptFile(encryptedFile, plaintextFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length);
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -75,16 +75,11 @@ public interface Cryptor extends Destroyable {
|
||||
*/
|
||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
|
||||
|
||||
/**
|
||||
* @return true, if the stored MAC matches the calculated one.
|
||||
*/
|
||||
boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||
* @throws DecryptFailedException If decryption failed
|
||||
*/
|
||||
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
|
||||
Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @param pos First byte (inclusive)
|
||||
@@ -92,7 +87,7 @@ public interface Cryptor extends Destroyable {
|
||||
* @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
|
||||
* @throws DecryptFailedException If decryption failed
|
||||
*/
|
||||
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
|
||||
Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
|
||||
|
||||
@@ -16,11 +16,11 @@ public interface CryptorIOSampling {
|
||||
/**
|
||||
* @return Number of encrypted bytes since the last reset.
|
||||
*/
|
||||
Long pollEncryptedBytes(boolean resetCounter);
|
||||
long pollEncryptedBytes(boolean resetCounter);
|
||||
|
||||
/**
|
||||
* @return Number of decrypted bytes since the last reset.
|
||||
*/
|
||||
Long pollDecryptedBytes(boolean resetCounter);
|
||||
long pollDecryptedBytes(boolean resetCounter);
|
||||
|
||||
}
|
||||
|
||||
@@ -4,20 +4,23 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
|
||||
/**
|
||||
* Decorates the Cryptor by decorating the In- and OutputStreams used during de-/encryption.
|
||||
*/
|
||||
public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling {
|
||||
|
||||
private final AtomicLong encryptedBytes;
|
||||
private final AtomicLong decryptedBytes;
|
||||
private final LongAdder encryptedBytes;
|
||||
private final LongAdder decryptedBytes;
|
||||
|
||||
private SamplingCryptorDecorator(Cryptor cryptor) {
|
||||
super(cryptor);
|
||||
encryptedBytes = new AtomicLong();
|
||||
decryptedBytes = new AtomicLong();
|
||||
encryptedBytes = new LongAdder();
|
||||
decryptedBytes = new LongAdder();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
@@ -25,35 +28,35 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollEncryptedBytes(boolean resetCounter) {
|
||||
public long pollEncryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return encryptedBytes.getAndSet(0);
|
||||
return encryptedBytes.sumThenReset();
|
||||
} else {
|
||||
return encryptedBytes.get();
|
||||
return encryptedBytes.sum();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollDecryptedBytes(boolean resetCounter) {
|
||||
public long pollDecryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return decryptedBytes.getAndSet(0);
|
||||
return decryptedBytes.sumThenReset();
|
||||
} else {
|
||||
return decryptedBytes.get();
|
||||
return decryptedBytes.sum();
|
||||
}
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptFile(encryptedFile, countingInputStream);
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptFile(encryptedFile, countingOutputStream, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -65,9 +68,9 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
|
||||
private class CountingInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final AtomicLong counter;
|
||||
private final LongAdder counter;
|
||||
|
||||
private CountingInputStream(AtomicLong counter, InputStream in) {
|
||||
private CountingInputStream(LongAdder counter, InputStream in) {
|
||||
this.in = in;
|
||||
this.counter = counter;
|
||||
}
|
||||
@@ -75,14 +78,14 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int count = in.read();
|
||||
counter.addAndGet(count);
|
||||
counter.add(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
int count = in.read(b, off, len);
|
||||
counter.addAndGet(count);
|
||||
counter.add(count);
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -91,22 +94,22 @@ public class SamplingCryptorDecorator extends AbstractCryptorDecorator implement
|
||||
private class CountingOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream out;
|
||||
private final AtomicLong counter;
|
||||
private final LongAdder counter;
|
||||
|
||||
private CountingOutputStream(AtomicLong counter, OutputStream out) {
|
||||
private CountingOutputStream(LongAdder counter, OutputStream out) {
|
||||
this.out = out;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
counter.incrementAndGet();
|
||||
counter.increment();
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
counter.addAndGet(len);
|
||||
counter.add(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class CounterOverflowException extends EncryptFailedException {
|
||||
private static final long serialVersionUID = 380066751064534731L;
|
||||
|
||||
public CounterOverflowException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
public class DecryptFailedException extends StorageCryptingException {
|
||||
public class DecryptFailedException extends CryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public DecryptFailedException(Throwable t) {
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class EncryptFailedException extends StorageCryptingException {
|
||||
public class EncryptFailedException extends CryptingException {
|
||||
private static final long serialVersionUID = -3855673600374897828L;
|
||||
|
||||
public EncryptFailedException(Throwable t) {
|
||||
super("Encryption failed.", t);
|
||||
}
|
||||
|
||||
public EncryptFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
public class UnsupportedKeyLengthException extends StorageCryptingException {
|
||||
public class UnsupportedKeyLengthException extends MasterkeyDecryptionException {
|
||||
private static final long serialVersionUID = 8114147446419390179L;
|
||||
|
||||
private final int requestedLength;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class WrongPasswordException extends StorageCryptingException {
|
||||
public class WrongPasswordException extends MasterkeyDecryptionException {
|
||||
private static final long serialVersionUID = -602047799678568780L;
|
||||
|
||||
public WrongPasswordException() {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
public class SeekableByteChannelInputStream extends InputStream {
|
||||
private final SeekableByteChannel channel;
|
||||
private volatile long markedPos = 0;
|
||||
|
||||
public SeekableByteChannelInputStream(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
final int read = channel.read(buffer);
|
||||
if (read == 1) {
|
||||
return buffer.get(0);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
|
||||
return channel.read(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
long available = channel.size() - channel.position();
|
||||
if (available > Integer.MAX_VALUE) {
|
||||
return Integer.MAX_VALUE;
|
||||
} else {
|
||||
return (int) available;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
final long pos = channel.position();
|
||||
final long max = channel.size();
|
||||
final long maxSkip = max - pos;
|
||||
final long actualSkip = Math.min(n, maxSkip);
|
||||
channel.position(channel.position() + actualSkip);
|
||||
return actualSkip;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.close();
|
||||
super.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void mark(int readlimit) {
|
||||
try {
|
||||
markedPos = channel.position();
|
||||
} catch (IOException e) {
|
||||
markedPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reset() throws IOException {
|
||||
channel.position(markedPos);
|
||||
}
|
||||
|
||||
public synchronized void resetTo(long position) throws IOException {
|
||||
channel.position(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.io;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
public class SeekableByteChannelOutputStream extends OutputStream {
|
||||
|
||||
private final SeekableByteChannel channel;
|
||||
|
||||
public SeekableByteChannelOutputStream(SeekableByteChannel channel) {
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
final byte actualByte = (byte) (b & 0x000000FF);
|
||||
final ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
buffer.put(actualByte);
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
final ByteBuffer buffer = ByteBuffer.wrap(b, off, len);
|
||||
channel.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
channel.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#truncate(long)
|
||||
*/
|
||||
public void truncate(long size) throws IOException {
|
||||
channel.truncate(size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#position()
|
||||
*/
|
||||
public long position() throws IOException {
|
||||
return channel.position();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see SeekableByteChannel#position(long)
|
||||
*/
|
||||
public void position(long newPosition) throws IOException {
|
||||
channel.position(newPosition);
|
||||
}
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-debian</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@@ -24,6 +24,15 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
@@ -37,15 +46,34 @@
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="deb" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0">
|
||||
<fx:property name="logPath" value="~/.Cryptomator/cryptomator.log" />
|
||||
<fx:jvmarg value="-Xmx2048m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-osx</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@@ -24,6 +24,15 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
@@ -37,15 +46,34 @@
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="dmg" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0">
|
||||
<fx:property name="logPath" value="~/Library/Logs/Cryptomator/cryptomator.log" />
|
||||
<fx:jvmarg value="-Xmx2048m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-win-portable</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@@ -24,6 +24,15 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
@@ -37,16 +46,34 @@
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0">
|
||||
<fx:property name="settingsPath" value="./settings.json" />
|
||||
<fx:property name="logPath" value="cryptomator.log" />
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="false" menu="false" shortcut="false" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-win</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
@@ -24,6 +24,15 @@
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<phase>prepare-package</phase>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
@@ -37,15 +46,33 @@
|
||||
<configuration>
|
||||
<target xmlns:fx="javafx:com.sun.javafx.tools.ant">
|
||||
<taskdef uri="javafx:com.sun.javafx.tools.ant" resource="com/sun/javafx/tools/ant/antlib.xml" classpath="${project.basedir}:${javafx.tools.ant.jar}" />
|
||||
|
||||
<!-- Define application to build -->
|
||||
<fx:application id="fxApp" name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
|
||||
<!-- Create main application jar -->
|
||||
<fx:jar destfile="${project.build.directory}/Cryptomator-${project.parent.version}.jar">
|
||||
<fx:application refid="fxApp" />
|
||||
<fx:fileset dir="${project.build.directory}" includes="libs/ui-${project.version}.jar"/>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar" />
|
||||
</fx:resources>
|
||||
<fx:manifest>
|
||||
<fx:attribute name="Implementation-Vendor" value="cryptomator.org" />
|
||||
<fx:attribute name="Implementation-Version" value="${project.version}" />
|
||||
</fx:manifest>
|
||||
</fx:jar>
|
||||
|
||||
<!-- Create native package -->
|
||||
<fx:deploy nativeBundles="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<fx:application refid="fxApp"/>
|
||||
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
|
||||
<fx:platform javafx="2.2+" j2se="8.0" >
|
||||
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="Cryptomator-${project.parent.version}.jar"/>
|
||||
<fx:fileset dir="${project.build.directory}" type="jar" includes="libs/*.jar" excludes="libs/ui-${project.version}.jar"/>
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
|
||||
45
main/pom.xml
45
main/pom.xml
@@ -11,7 +11,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
<version>${commons-codec.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
|
||||
<!-- org.apache.httpcomponents:httpclient is newer, but jackrabbit uses this version. We don't have a reason to upgrade -->
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<version>${commons-httpclient.version}</version>
|
||||
@@ -127,9 +127,15 @@
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<version>3.0</version>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<version>2.0.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
@@ -139,14 +145,13 @@
|
||||
<version>${jackson-databind.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JUnit -->
|
||||
<!-- JUnit / Mockito -->
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
@@ -211,9 +216,35 @@
|
||||
<module>installer-win-portable</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>uber-jar</id>
|
||||
<modules>
|
||||
<module>uber-jar</module>
|
||||
</modules>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-libs</id>
|
||||
<goals>
|
||||
<goal>copy-dependencies</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${project.build.directory}/libs</outputDirectory>
|
||||
<includeScope>runtime</includeScope>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
|
||||
57
main/uber-jar/pom.xml
Normal file
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.10.0</version>
|
||||
</parent>
|
||||
<artifactId>uber-jar</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Single über jar with all dependencies</name>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<finalName>Cryptomator-${project.parent.version}</finalName>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
|
||||
<Implementation-Version>${project.version}</Implementation-Version>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -12,7 +12,7 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.10.0</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
@@ -32,6 +32,12 @@
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- apache commons -->
|
||||
<dependency>
|
||||
@@ -49,39 +55,13 @@
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.dagger</groupId>
|
||||
<artifactId>dagger-compiler</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<outputDirectory>${project.parent.build.directory}</outputDirectory>
|
||||
<finalName>Cryptomator-${project.parent.version}</finalName>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
|
||||
<Implementation-Version>${project.version}</Implementation-Version>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.ui.controllers.MainController;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
|
||||
import dagger.Component;
|
||||
|
||||
@Singleton
|
||||
@Component(modules = CryptomatorModule.class)
|
||||
interface CryptomatorComponent {
|
||||
ExecutorService executorService();
|
||||
|
||||
DeferredCloser deferredCloser();
|
||||
|
||||
MainController mainController();
|
||||
}
|
||||
101
main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java
Normal file
101
main/ui/src/main/java/org/cryptomator/ui/CryptomatorModule.java
Normal file
@@ -0,0 +1,101 @@
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.SamplingCryptorDecorator;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.ui.model.VaultObjectMapperProvider;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.cryptomator.ui.settings.SettingsProvider;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
import org.cryptomator.ui.util.DeferredCloser.Closer;
|
||||
import org.cryptomator.ui.util.SemVerComparator;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
import javafx.application.Application;
|
||||
|
||||
@Module
|
||||
class CryptomatorModule {
|
||||
|
||||
private final Application application;
|
||||
private final DeferredCloser deferredCloser;
|
||||
|
||||
public CryptomatorModule(Application application) {
|
||||
this.application = application;
|
||||
this.deferredCloser = new DeferredCloser();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Application provideApplication() {
|
||||
return application;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
DeferredCloser provideDeferredCloser() {
|
||||
return deferredCloser;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("SemVer")
|
||||
Comparator<String> provideSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named("VaultJsonMapper")
|
||||
ObjectMapper provideVaultObjectMapper(VaultObjectMapperProvider vaultObjectMapperProvider) {
|
||||
return vaultObjectMapperProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
Settings provideSettings(SettingsProvider settingsProvider) {
|
||||
return settingsProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ExecutorService provideExecutorService() {
|
||||
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
WebDavMounter provideWebDavMounter(WebDavMounterProvider webDavMounterProvider) {
|
||||
return webDavMounterProvider.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
WebDavServer provideWebDavServer() {
|
||||
final WebDavServer webDavServer = new WebDavServer();
|
||||
webDavServer.start();
|
||||
return closeLater(webDavServer, WebDavServer::stop);
|
||||
}
|
||||
|
||||
@Provides
|
||||
Cryptor provideCryptor() {
|
||||
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
|
||||
}
|
||||
|
||||
private <T> T closeLater(T object, Closer<T> closer) {
|
||||
return deferredCloser.closeLater(object, closer).get().get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,15 +15,7 @@ import java.nio.file.Path;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.MainModule.ControllerFactory;
|
||||
import org.cryptomator.ui.controllers.MainController;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
|
||||
@@ -34,8 +26,10 @@ import org.cryptomator.ui.util.TrayIconUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
public class MainApplication extends Application {
|
||||
|
||||
@@ -44,28 +38,15 @@ public class MainApplication extends Application {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
|
||||
|
||||
private final ExecutorService executorService;
|
||||
private final ControllerFactory controllerFactory;
|
||||
private final DeferredCloser closer;
|
||||
private final MainController mainCtrl;
|
||||
|
||||
public MainApplication() {
|
||||
this(getInjector());
|
||||
}
|
||||
|
||||
private static Injector getInjector() {
|
||||
return Guice.createInjector(new MainModule());
|
||||
}
|
||||
|
||||
public MainApplication(Injector injector) {
|
||||
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class), injector.getInstance(MainApplicationReference.class));
|
||||
}
|
||||
|
||||
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer, MainApplicationReference appRef) {
|
||||
super();
|
||||
this.executorService = executorService;
|
||||
this.controllerFactory = controllerFactory;
|
||||
this.closer = closer;
|
||||
final CryptomatorComponent comp = DaggerCryptomatorComponent.builder().cryptomatorModule(new CryptomatorModule(this)).build();
|
||||
this.executorService = comp.executorService();
|
||||
this.closer = comp.deferredCloser();
|
||||
this.mainCtrl = comp.mainController();
|
||||
Cryptomator.addShutdownTask(closer::close);
|
||||
appRef.set(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -83,37 +64,32 @@ public class MainApplication extends Application {
|
||||
});
|
||||
|
||||
chooseNativeStylesheet();
|
||||
|
||||
mainCtrl.initStage(primaryStage);
|
||||
|
||||
final ResourceBundle rb = ResourceBundle.getBundle("localization");
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
|
||||
loader.setControllerFactory(controllerFactory);
|
||||
final Parent root = loader.load();
|
||||
final MainController ctrl = loader.getController();
|
||||
ctrl.setStage(primaryStage);
|
||||
final Scene scene = new Scene(root);
|
||||
primaryStage.setTitle(rb.getString("app.name"));
|
||||
primaryStage.setScene(scene);
|
||||
primaryStage.sizeToScene();
|
||||
primaryStage.setResizable(false);
|
||||
primaryStage.show();
|
||||
|
||||
ActiveWindowStyleSupport.startObservingFocus(primaryStage);
|
||||
TrayIconUtil.init(primaryStage, rb, () -> {
|
||||
quit();
|
||||
});
|
||||
|
||||
for (String arg : getParameters().getUnnamed()) {
|
||||
handleCommandLineArg(ctrl, arg);
|
||||
handleCommandLineArg(arg);
|
||||
}
|
||||
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
|
||||
Cryptomator.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(file.getAbsolutePath()));
|
||||
}
|
||||
|
||||
LocalInstance cryptomatorGuiInstance = closer.closeLater(SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService), LocalInstance::close).get().get();
|
||||
|
||||
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
|
||||
cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(arg));
|
||||
}
|
||||
|
||||
void handleCommandLineArg(final MainController ctrl, String arg) {
|
||||
private void handleCommandLineArg(String arg) {
|
||||
// only open files with our file extension:
|
||||
if (!arg.endsWith(Vault.VAULT_FILE_EXTENSION)) {
|
||||
LOG.warn("Invalid vault path %s", arg);
|
||||
@@ -134,8 +110,8 @@ public class MainApplication extends Application {
|
||||
|
||||
// add vault to ctrl:
|
||||
Platform.runLater(() -> {
|
||||
ctrl.addVault(vaultPath, true);
|
||||
ctrl.toFront();
|
||||
mainCtrl.addVault(vaultPath, true);
|
||||
mainCtrl.toFront();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,25 +138,4 @@ public class MainApplication extends Application {
|
||||
closer.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed to inject MainApplication. Problem: Application needs to be set asap after injector creation.
|
||||
*/
|
||||
static class MainApplicationReference {
|
||||
|
||||
private Application application;
|
||||
|
||||
private void set(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
public Application get() {
|
||||
if (application == null) {
|
||||
throw new IllegalStateException("not yet ready.");
|
||||
} else {
|
||||
return application;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 cryptomator.org
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Tillmann Gaida - initial implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.util.Callback;
|
||||
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.SamplingCryptorDecorator;
|
||||
import org.cryptomator.crypto.aes256.Aes256Cryptor;
|
||||
import org.cryptomator.ui.MainApplication.MainApplicationReference;
|
||||
import org.cryptomator.ui.model.VaultFactory;
|
||||
import org.cryptomator.ui.model.VaultObjectMapperProvider;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.cryptomator.ui.settings.SettingsProvider;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
import org.cryptomator.ui.util.DeferredCloser.Closer;
|
||||
import org.cryptomator.ui.util.SemVerComparator;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounterProvider;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Provider;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.name.Names;
|
||||
|
||||
public class MainModule extends AbstractModule {
|
||||
|
||||
private final DeferredCloser deferredCloser = new DeferredCloser();
|
||||
|
||||
public static interface ControllerFactory extends Callback<Class<?>, Object> {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(DeferredCloser.class).toInstance(deferredCloser);
|
||||
bind(ObjectMapper.class).annotatedWith(Names.named("VaultJsonMapper")).toProvider(VaultObjectMapperProvider.class);
|
||||
bind(Settings.class).toProvider(SettingsProvider.class);
|
||||
bind(WebDavMounter.class).toProvider(WebDavMounterProvider.class).asEagerSingleton();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ControllerFactory getControllerFactory(Injector injector) {
|
||||
return cls -> injector.getInstance(cls);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MainApplicationReference getApplicationBinding() {
|
||||
return new MainApplicationReference();
|
||||
}
|
||||
|
||||
@Provides
|
||||
Application getApplication(MainApplicationReference ref) {
|
||||
return ref.get();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Named("SemVer")
|
||||
@Singleton
|
||||
Comparator<String> getSemVerComparator() {
|
||||
return new SemVerComparator();
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
ExecutorService getExec() {
|
||||
return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
|
||||
}
|
||||
|
||||
@Provides
|
||||
Cryptor getCryptor() {
|
||||
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
VaultFactory getVaultFactory(WebDavServer server, Provider<Cryptor> cryptorProvider, WebDavMounter mounter, DeferredCloser closer) {
|
||||
return new VaultFactory(server, cryptorProvider, mounter, closer);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
WebDavServer getServer() {
|
||||
final WebDavServer webDavServer = new WebDavServer();
|
||||
webDavServer.start();
|
||||
return closeLater(webDavServer, WebDavServer::stop);
|
||||
}
|
||||
|
||||
<T> T closeLater(T object, Closer<T> closer) {
|
||||
return deferredCloser.closeLater(object, closer).get().get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package org.cryptomator.ui.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Controller presenting a single view.
|
||||
*/
|
||||
abstract class AbstractFXMLViewController implements Initializable {
|
||||
|
||||
private Parent fxmlRoot;
|
||||
|
||||
/**
|
||||
* URL from #initialize(URL, ResourceBundle)
|
||||
*/
|
||||
protected URL rootUrl;
|
||||
|
||||
/**
|
||||
* ResourceBundle from #initialize(URL, ResourceBundle)
|
||||
*/
|
||||
protected ResourceBundle resourceBundle;
|
||||
|
||||
/**
|
||||
* Gets the URL to the FXML file describing the view presented by this controller.<br/>
|
||||
*
|
||||
* A default implementation would look like this:<br/>
|
||||
* <code>
|
||||
* return getClass().getResource("/myView.fxml");
|
||||
* </code>
|
||||
*
|
||||
* @return FXML resource URL
|
||||
*/
|
||||
protected abstract URL getFxmlResourceUrl();
|
||||
|
||||
/**
|
||||
* @return Localization bundle for the FXML labels or <code>null</code>.
|
||||
*/
|
||||
protected abstract ResourceBundle getFxmlResourceBundle();
|
||||
|
||||
@Override
|
||||
public final void initialize(URL location, ResourceBundle resources) {
|
||||
this.rootUrl = location;
|
||||
this.resourceBundle = resources;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
protected void initialize() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a FXML loader used in {@link #loadFxml()}. This method can be overwritten for further loader customization.
|
||||
*
|
||||
* @return Configured loader ready to load.
|
||||
*/
|
||||
protected FXMLLoader createFxmlLoader() {
|
||||
final URL fxmlUrl = getFxmlResourceUrl();
|
||||
final ResourceBundle rb = getFxmlResourceBundle();
|
||||
final FXMLLoader loader = new FXMLLoader(fxmlUrl, rb);
|
||||
loader.setController(this);
|
||||
return loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the view presented by this controller from the FXML file return by {@link #getFxmlResourceUrl()}. This method can only be invoked once.
|
||||
*
|
||||
* @return Parent view element.
|
||||
*/
|
||||
protected final synchronized Parent loadFxml() {
|
||||
if (fxmlRoot == null) {
|
||||
final FXMLLoader loader = createFxmlLoader();
|
||||
try {
|
||||
fxmlRoot = loader.load();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Could not load FXML file from location: " + loader.getLocation(), e);
|
||||
}
|
||||
}
|
||||
return fxmlRoot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new scene with the root node from the FXML file and applies it to the given stage.
|
||||
*/
|
||||
public void initStage(Stage stage) {
|
||||
final Parent root = loadFxml();
|
||||
stage.setScene(new Scene(root));
|
||||
stage.sizeToScene();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Creates a new stage and calls {@link #initStage(Stage)}.
|
||||
*/
|
||||
public Stage createStage() {
|
||||
final Stage stage = new Stage();
|
||||
initStage(stage);
|
||||
return stage;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -10,17 +10,9 @@ import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.text.Text;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
@@ -29,13 +21,20 @@ import org.cryptomator.ui.model.Vault;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
public class ChangePasswordController implements Initializable {
|
||||
@Singleton
|
||||
public class ChangePasswordController extends AbstractFXMLViewController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class);
|
||||
|
||||
private ResourceBundle rb;
|
||||
private ChangePasswordListener listener;
|
||||
private Vault vault;
|
||||
|
||||
@@ -66,9 +65,17 @@ public class ChangePasswordController implements Initializable {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/change_password.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
oldPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
|
||||
newPasswordField.textProperty().addListener(this::passwordFieldsDidChange);
|
||||
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
|
||||
@@ -109,20 +116,20 @@ public class ChangePasswordController implements Initializable {
|
||||
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
|
||||
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
|
||||
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
} catch (DecryptFailedException | IOException ex) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
|
||||
} catch (IOException ex) {
|
||||
messageText.setText(resourceBundle.getString("changePassword.errorMessage.decryptionFailed"));
|
||||
LOG.error("Decryption failed for technical reasons.", ex);
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
return;
|
||||
} catch (WrongPasswordException e) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.wrongPassword"));
|
||||
messageText.setText(resourceBundle.getString("changePassword.errorMessage.wrongPassword"));
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
Platform.runLater(oldPasswordField::requestFocus);
|
||||
return;
|
||||
} catch (UnsupportedKeyLengthException ex) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
@@ -130,9 +137,9 @@ public class ChangePasswordController implements Initializable {
|
||||
} catch (UnsupportedVaultException e) {
|
||||
downloadsPageLink.setVisible(true);
|
||||
if (e.isVaultOlderThanSoftware()) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
} else if (e.isSoftwareOlderThanVault()) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
messageText.setText(resourceBundle.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
}
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
@@ -147,7 +154,7 @@ public class ChangePasswordController implements Initializable {
|
||||
final CharSequence newPassword = newPasswordField.getCharacters();
|
||||
try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) {
|
||||
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword);
|
||||
messageText.setText(rb.getString("changePassword.infoMessage.success"));
|
||||
messageText.setText(resourceBundle.getString("changePassword.infoMessage.success"));
|
||||
Platform.runLater(this::didChangePassword);
|
||||
// At this point the backup is still using the old password.
|
||||
// It will be changed as soon as the user unlocks the vault the next time.
|
||||
|
||||
@@ -19,23 +19,25 @@ import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class InitializeController implements Initializable {
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
|
||||
@Singleton
|
||||
public class InitializeController extends AbstractFXMLViewController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class);
|
||||
|
||||
private ResourceBundle localization;
|
||||
private Vault vault;
|
||||
private InitializationListener listener;
|
||||
|
||||
@@ -51,9 +53,22 @@ public class InitializeController implements Initializable {
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
|
||||
@Inject
|
||||
public InitializeController() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.localization = rb;
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/initialize.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
|
||||
retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange);
|
||||
}
|
||||
@@ -88,9 +103,9 @@ public class InitializeController implements Initializable {
|
||||
listener.didInitialize(this);
|
||||
}
|
||||
} catch (FileAlreadyExistsException ex) {
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
messageLabel.setText(resourceBundle.getString("initialize.messageLabel.alreadyInitialized"));
|
||||
} catch (InvalidPathException ex) {
|
||||
messageLabel.setText(localization.getString("initialize.messageLabel.invalidPath"));
|
||||
messageLabel.setText(resourceBundle.getString("initialize.messageLabel.invalidPath"));
|
||||
} catch (IOException ex) {
|
||||
LOG.error("I/O Exception", ex);
|
||||
} finally {
|
||||
|
||||
@@ -1,33 +1,97 @@
|
||||
package org.cryptomator.ui.controllers;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.collections.ListChangeListener.Change;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.WeakListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.stage.Stage;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class MacWarningsController {
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ListChangeListener.Change;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.cell.CheckBoxListCell;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
public class MacWarningsController extends AbstractFXMLViewController {
|
||||
|
||||
@FXML
|
||||
private ListView<String> warningsList;
|
||||
private ListView<Warning> warningsList;
|
||||
|
||||
private Stage stage;
|
||||
@FXML
|
||||
private Button whitelistButton;
|
||||
|
||||
private final Application application;
|
||||
private final ObservableList<Warning> warnings = FXCollections.observableArrayList();
|
||||
private final ListChangeListener<String> unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange;
|
||||
private final ChangeListener<Boolean> stageVisibilityChangeListener = this::windowVisibilityDidChange;
|
||||
private Stage stage;
|
||||
private Vault vault;
|
||||
|
||||
@Inject
|
||||
public MacWarningsController(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/mac_warnings.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
warnings.addListener(this::warningsDidInvalidate);
|
||||
warningsList.setItems(warnings);
|
||||
warningsList.setCellFactory(CheckBoxListCell.forListView(Warning::selectedProperty, new StringConverter<Warning>() {
|
||||
|
||||
@Override
|
||||
public String toString(Warning object) {
|
||||
return object.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Warning fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initStage(Stage stage) {
|
||||
super.initStage(stage);
|
||||
this.stage = stage;
|
||||
stage.showingProperty().addListener(new WeakChangeListener<>(stageVisibilityChangeListener));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void didClickDismissButton(ActionEvent event) {
|
||||
stage.hide();
|
||||
private void didClickWhitelistButton(ActionEvent event) {
|
||||
warnings.filtered(w -> w.isSelected()).stream().forEach(w -> {
|
||||
final String resourceToBeWhitelisted = w.getName();
|
||||
vault.getWhitelistedResourcesWithInvalidMac().add(resourceToBeWhitelisted);
|
||||
vault.getNamesOfResourcesWithInvalidMac().remove(resourceToBeWhitelisted);
|
||||
});
|
||||
warnings.removeIf(w -> w.isSelected());
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -35,24 +99,65 @@ public class MacWarningsController {
|
||||
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
|
||||
}
|
||||
|
||||
public void setMacWarnings(ObservableList<String> macWarnings) {
|
||||
this.warningsList.setItems(macWarnings);
|
||||
this.warningsList.getItems().addListener(new WeakListChangeListener<String>(this::warningsDidChange));
|
||||
}
|
||||
|
||||
// closes this window automatically, if all warnings disappeared (e.g. due to an unmount event)
|
||||
private void warningsDidChange(Change<? extends String> change) {
|
||||
if (change.getList().isEmpty()) {
|
||||
stage.hide();
|
||||
private void unauthenticatedResourcesDidChange(Change<? extends String> change) {
|
||||
while (change.next()) {
|
||||
if (change.wasAdded()) {
|
||||
warnings.addAll(change.getAddedSubList().stream().map(Warning::new).collect(Collectors.toList()));
|
||||
} else if (change.wasRemoved()) {
|
||||
change.getRemoved().forEach(str -> {
|
||||
warnings.removeIf(w -> str.equals(w.name.get()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Stage getStage() {
|
||||
return stage;
|
||||
private void warningsDidInvalidate(Observable observable) {
|
||||
disableWhitelistButtonIfNothingSelected();
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
this.stage = stage;
|
||||
private void windowVisibilityDidChange(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (Boolean.TRUE.equals(newValue)) {
|
||||
stage.setTitle(String.format(resourceBundle.getString("macWarnings.windowTitle"), vault.getName()));
|
||||
warnings.addAll(vault.getNamesOfResourcesWithInvalidMac().stream().map(Warning::new).collect(Collectors.toList()));
|
||||
vault.getNamesOfResourcesWithInvalidMac().addListener(this.unauthenticatedResourcesChangeListener);
|
||||
} else {
|
||||
vault.getNamesOfResourcesWithInvalidMac().clear();
|
||||
vault.getNamesOfResourcesWithInvalidMac().removeListener(this.unauthenticatedResourcesChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void disableWhitelistButtonIfNothingSelected() {
|
||||
whitelistButton.setDisable(warnings.filtered(w -> w.isSelected()).isEmpty());
|
||||
}
|
||||
|
||||
public void setVault(Vault vault) {
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
private class Warning {
|
||||
|
||||
private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(false);
|
||||
|
||||
public Warning(String name) {
|
||||
this.name.set(name);
|
||||
this.selectedProperty().addListener(change -> {
|
||||
disableWhitelistButtonIfNothingSelected();
|
||||
});
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,25 +13,35 @@ import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.ui.controllers.ChangePasswordController.ChangePasswordListener;
|
||||
import org.cryptomator.ui.controllers.InitializeController.InitializationListener;
|
||||
import org.cryptomator.ui.controllers.UnlockController.UnlockListener;
|
||||
import org.cryptomator.ui.controllers.UnlockedController.LockListener;
|
||||
import org.cryptomator.ui.controls.DirectoryListCell;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.model.VaultFactory;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import dagger.Lazy;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.SetChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.geometry.Side;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.ListView;
|
||||
@@ -42,23 +52,8 @@ import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.stage.WindowEvent;
|
||||
|
||||
import org.cryptomator.ui.MainModule.ControllerFactory;
|
||||
import org.cryptomator.ui.controllers.ChangePasswordController.ChangePasswordListener;
|
||||
import org.cryptomator.ui.controllers.InitializeController.InitializationListener;
|
||||
import org.cryptomator.ui.controllers.UnlockController.UnlockListener;
|
||||
import org.cryptomator.ui.controllers.UnlockedController.LockListener;
|
||||
import org.cryptomator.ui.controls.DirectoryListCell;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.model.VaultFactory;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
|
||||
import org.cryptomator.ui.util.ObservableSetAggregator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener, ChangePasswordListener {
|
||||
@Singleton
|
||||
public class MainController extends AbstractFXMLViewController implements InitializationListener, UnlockListener, LockListener, ChangePasswordListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
|
||||
|
||||
@@ -82,36 +77,50 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
@FXML
|
||||
private Pane contentPane;
|
||||
|
||||
private final ControllerFactory controllerFactory;
|
||||
private final Settings settings;
|
||||
private final VaultFactory vaultFactoy;
|
||||
private final ObservableList<String> aggregatedMacWarnings;
|
||||
private final SetChangeListener<String> macWarningsAggregator;
|
||||
private final AtomicBoolean macWarningsWindowVisible;
|
||||
|
||||
private ResourceBundle rb;
|
||||
private final Lazy<WelcomeController> welcomeController;
|
||||
private final Lazy<InitializeController> initializeController;
|
||||
private final Lazy<UnlockController> unlockController;
|
||||
private final Provider<UnlockedController> unlockedController;
|
||||
private final Lazy<ChangePasswordController> changePasswordController;
|
||||
|
||||
@Inject
|
||||
public MainController(ControllerFactory controllerFactory, Settings settings, VaultFactory vaultFactoy) {
|
||||
public MainController(Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController, Lazy<InitializeController> initializeController, Lazy<UnlockController> unlockController,
|
||||
Provider<UnlockedController> unlockedController, Lazy<ChangePasswordController> changePasswordController) {
|
||||
super();
|
||||
this.controllerFactory = controllerFactory;
|
||||
this.settings = settings;
|
||||
this.vaultFactoy = vaultFactoy;
|
||||
this.aggregatedMacWarnings = FXCollections.observableList(new ArrayList<>());
|
||||
this.macWarningsAggregator = new ObservableSetAggregator<>(this.aggregatedMacWarnings);
|
||||
this.macWarningsWindowVisible = new AtomicBoolean();
|
||||
this.welcomeController = welcomeController;
|
||||
this.initializeController = initializeController;
|
||||
this.unlockController = unlockController;
|
||||
this.unlockedController = unlockedController;
|
||||
this.changePasswordController = changePasswordController;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/main.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
final ObservableList<Vault> items = FXCollections.observableList(settings.getDirectories());
|
||||
vaultList.setItems(items);
|
||||
vaultList.setCellFactory(this::createDirecoryListCell);
|
||||
vaultList.getSelectionModel().getSelectedItems().addListener(this::selectedVaultDidChange);
|
||||
this.showWelcomeView();
|
||||
}
|
||||
|
||||
aggregatedMacWarnings.addListener(this::macWarningsDidChange);
|
||||
@Override
|
||||
public void initStage(Stage stage) {
|
||||
super.initStage(stage);
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -176,7 +185,8 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
* @param path non-null, writable, existing directory
|
||||
*/
|
||||
public void addVault(final Path path, boolean select) {
|
||||
if (path == null || !Files.isWritable(path)) {
|
||||
// TODO: Files.isWritable is broken on windows. Fix in Java 8u72, see https://bugs.openjdk.java.net/browse/JDK-8034057
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -205,14 +215,14 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
private void selectedVaultDidChange(ListChangeListener.Change<? extends Vault> change) {
|
||||
final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem();
|
||||
if (selectedVault == null) {
|
||||
stage.setTitle(rb.getString("app.name"));
|
||||
stage.setTitle(resourceBundle.getString("app.name"));
|
||||
showWelcomeView();
|
||||
} else if (!Files.isDirectory(selectedVault.getPath())) {
|
||||
Platform.runLater(() -> {
|
||||
vaultList.getItems().remove(selectedVault);
|
||||
vaultList.getSelectionModel().clearSelection();
|
||||
});
|
||||
stage.setTitle(rb.getString("app.name"));
|
||||
stage.setTitle(resourceBundle.getString("app.name"));
|
||||
showWelcomeView();
|
||||
} else {
|
||||
stage.setTitle(selectedVault.getName());
|
||||
@@ -233,12 +243,6 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
showChangePasswordView(selectedVault);
|
||||
}
|
||||
|
||||
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
|
||||
if (aggregatedMacWarnings.size() > 0) {
|
||||
Platform.runLater(this::showMacWarningsWindow);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Subcontroller for right panel
|
||||
// ****************************************
|
||||
@@ -257,25 +261,17 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T showView(String fxml) {
|
||||
try {
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
|
||||
loader.setControllerFactory(controllerFactory);
|
||||
final Parent root = loader.load();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
return loader.getController();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to load fxml file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void showWelcomeView() {
|
||||
this.showView("/fxml/welcome.fxml");
|
||||
final Parent root = welcomeController.get().loadFxml();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
}
|
||||
|
||||
private void showInitializeView(Vault vault) {
|
||||
final InitializeController ctrl = showView("/fxml/initialize.fxml");
|
||||
final InitializeController ctrl = initializeController.get();
|
||||
final Parent root = ctrl.loadFxml();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
ctrl.setVault(vault);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
@@ -286,35 +282,42 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
}
|
||||
|
||||
private void showUnlockView(Vault vault) {
|
||||
final UnlockController ctrl = showView("/fxml/unlock.fxml");
|
||||
final UnlockController ctrl = unlockController.get();
|
||||
final Parent root = ctrl.loadFxml();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
ctrl.setVault(vault);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didUnlock(UnlockController ctrl) {
|
||||
ctrl.getVault().getNamesOfResourcesWithInvalidMac().addListener(this.macWarningsAggregator);
|
||||
showUnlockedView(ctrl.getVault());
|
||||
Platform.setImplicitExit(false);
|
||||
}
|
||||
|
||||
private void showUnlockedView(Vault vault) {
|
||||
final UnlockedController ctrl = showView("/fxml/unlocked.fxml");
|
||||
final UnlockedController ctrl = unlockedController.get();
|
||||
final Parent root = ctrl.loadFxml();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
ctrl.setVault(vault);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void didLock(UnlockedController ctrl) {
|
||||
ctrl.getVault().getNamesOfResourcesWithInvalidMac().removeListener(this.macWarningsAggregator);
|
||||
showUnlockView(ctrl.getVault());
|
||||
if (getUnlockedDirectories().isEmpty()) {
|
||||
if (getUnlockedVaults().isEmpty()) {
|
||||
Platform.setImplicitExit(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void showChangePasswordView(Vault vault) {
|
||||
final ChangePasswordController ctrl = showView("/fxml/change_password.fxml");
|
||||
final ChangePasswordController ctrl = changePasswordController.get();
|
||||
final Parent root = ctrl.loadFxml();
|
||||
contentPane.getChildren().clear();
|
||||
contentPane.getChildren().add(root);
|
||||
ctrl.setVault(vault);
|
||||
ctrl.setListener(this);
|
||||
}
|
||||
@@ -324,45 +327,14 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
showUnlockView(ctrl.getVault());
|
||||
}
|
||||
|
||||
private void showMacWarningsWindow() {
|
||||
if (macWarningsWindowVisible.getAndSet(true) == false) {
|
||||
try {
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
|
||||
loader.setControllerFactory(controllerFactory);
|
||||
|
||||
final Parent root = loader.load();
|
||||
final Stage stage = new Stage();
|
||||
stage.setTitle(rb.getString("macWarnings.windowTitle"));
|
||||
stage.setScene(new Scene(root));
|
||||
stage.sizeToScene();
|
||||
stage.setResizable(false);
|
||||
stage.setOnHidden(this::onHideMacWarningsWindow);
|
||||
ActiveWindowStyleSupport.startObservingFocus(stage);
|
||||
|
||||
final MacWarningsController ctrl = loader.getController();
|
||||
ctrl.setMacWarnings(this.aggregatedMacWarnings);
|
||||
ctrl.setStage(stage);
|
||||
|
||||
stage.show();
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to load fxml file.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onHideMacWarningsWindow(WindowEvent event) {
|
||||
macWarningsWindowVisible.set(false);
|
||||
aggregatedMacWarnings.clear();
|
||||
}
|
||||
|
||||
/* Convenience */
|
||||
|
||||
public Collection<Vault> getDirectories() {
|
||||
public Collection<Vault> getVaults() {
|
||||
return vaultList.getItems();
|
||||
}
|
||||
|
||||
public Collection<Vault> getUnlockedDirectories() {
|
||||
return getDirectories().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
|
||||
public Collection<Vault> getUnlockedVaults() {
|
||||
return getVaults().stream().filter(d -> d.isUnlocked()).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/* public Getter/Setter */
|
||||
|
||||
@@ -15,43 +15,48 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Comparator;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ChoiceBox;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.util.FXThreads;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
import org.cryptomator.ui.util.mount.WindowsDriveLetters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
public class UnlockController implements Initializable {
|
||||
public class UnlockController extends AbstractFXMLViewController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
|
||||
|
||||
private ResourceBundle rb;
|
||||
private UnlockListener listener;
|
||||
private Vault vault;
|
||||
|
||||
@@ -60,6 +65,15 @@ public class UnlockController implements Initializable {
|
||||
|
||||
@FXML
|
||||
private TextField mountName;
|
||||
|
||||
@FXML
|
||||
private Label winDriveLetterLabel;
|
||||
|
||||
@FXML
|
||||
private ChoiceBox<Character> winDriveLetter;
|
||||
|
||||
@FXML
|
||||
private Button advancedOptionsButton;
|
||||
|
||||
@FXML
|
||||
private Button unlockButton;
|
||||
@@ -73,23 +87,63 @@ public class UnlockController implements Initializable {
|
||||
@FXML
|
||||
private Hyperlink downloadsPageLink;
|
||||
|
||||
@FXML
|
||||
private GridPane advancedOptions;
|
||||
|
||||
private final ExecutorService exec;
|
||||
private final Application app;
|
||||
private final WindowsDriveLetters driveLetters;
|
||||
private final ChangeListener<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
|
||||
|
||||
@Inject
|
||||
public UnlockController(Application app, ExecutorService exec) {
|
||||
super();
|
||||
public UnlockController(Application app, ExecutorService exec, WindowsDriveLetters driveLetters) {
|
||||
this.app = app;
|
||||
this.exec = exec;
|
||||
this.driveLetters = driveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/unlock.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
passwordField.textProperty().addListener(this::passwordFieldsDidChange);
|
||||
advancedOptions.managedProperty().bind(advancedOptions.visibleProperty());
|
||||
mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents);
|
||||
mountName.textProperty().addListener(this::mountNameDidChange);
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
winDriveLetter.setConverter(new WinDriveLetterLabelConverter());
|
||||
} else {
|
||||
winDriveLetterLabel.setVisible(false);
|
||||
winDriveLetterLabel.setManaged(false);
|
||||
winDriveLetter.setVisible(false);
|
||||
winDriveLetter.setManaged(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetView() {
|
||||
passwordField.clear();
|
||||
unlockButton.setDisable(true);
|
||||
advancedOptions.setVisible(false);
|
||||
advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show"));
|
||||
progressIndicator.setVisible(false);
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
winDriveLetter.valueProperty().removeListener(driveLetterChangeListener);
|
||||
winDriveLetter.getItems().clear();
|
||||
winDriveLetter.getItems().add(null);
|
||||
winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters());
|
||||
winDriveLetter.getItems().sort(new WinDriveLetterComparator());
|
||||
winDriveLetter.valueProperty().addListener(driveLetterChangeListener);
|
||||
}
|
||||
downloadsPageLink.setVisible(false);
|
||||
messageText.setText(null);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
@@ -110,6 +164,91 @@ public class UnlockController implements Initializable {
|
||||
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Advanced options button
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
private void didClickAdvancedOptionsButton(ActionEvent event) {
|
||||
advancedOptions.setVisible(!advancedOptions.isVisible());
|
||||
if (advancedOptions.isVisible()) {
|
||||
advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.hide"));
|
||||
} else {
|
||||
advancedOptionsButton.setText(resourceBundle.getString("unlock.button.advancedOptions.show"));
|
||||
}
|
||||
}
|
||||
|
||||
private void filterAlphanumericKeyEvents(KeyEvent t) {
|
||||
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
|
||||
return;
|
||||
}
|
||||
char c = CharUtils.toChar(t.getCharacter());
|
||||
if (!(CharUtils.isAsciiAlphanumeric(c) || c == '_')) {
|
||||
t.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
if (vault == null) {
|
||||
return;
|
||||
}
|
||||
// newValue is guaranteed to be a-z0-9_, see #filterAlphanumericKeyEvents
|
||||
if (newValue.isEmpty()) {
|
||||
mountName.setText(vault.getMountName());
|
||||
} else {
|
||||
vault.setMountName(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts 'C' to "C:" to translate between model and GUI.
|
||||
*/
|
||||
private class WinDriveLetterLabelConverter extends StringConverter<Character> {
|
||||
|
||||
@Override
|
||||
public String toString(Character letter) {
|
||||
if (letter == null) {
|
||||
return resourceBundle.getString("unlock.choicebox.winDriveLetter.auto");
|
||||
} else {
|
||||
return Character.toString(letter) + ":";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Character fromString(String string) {
|
||||
if (resourceBundle.getString("unlock.choicebox.winDriveLetter.auto").equals(string)) {
|
||||
return null;
|
||||
} else {
|
||||
return CharUtils.toCharacterObject(string);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Natural sorting of ASCII letters, but <code>null</code> always on first, as this is "auto-assign".
|
||||
*/
|
||||
private static class WinDriveLetterComparator implements Comparator<Character> {
|
||||
|
||||
@Override
|
||||
public int compare(Character c1, Character c2) {
|
||||
if (c1 == null) {
|
||||
return -1;
|
||||
} else if (c2 == null) {
|
||||
return 1;
|
||||
} else {
|
||||
return (char) c1 - (char) c2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void winDriveLetterDidChange(ObservableValue<? extends Character> property, Character oldValue, Character newValue) {
|
||||
if (vault == null) {
|
||||
return;
|
||||
}
|
||||
vault.setWinDriveLetter(newValue);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Unlock button
|
||||
// ****************************************
|
||||
@@ -125,38 +264,38 @@ public class UnlockController implements Initializable {
|
||||
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
|
||||
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
|
||||
if (!vault.startServer()) {
|
||||
messageText.setText(rb.getString("unlock.messageLabel.startServerFailed"));
|
||||
messageText.setText(resourceBundle.getString("unlock.messageLabel.startServerFailed"));
|
||||
vault.getCryptor().destroy();
|
||||
return;
|
||||
}
|
||||
// at this point we know for sure, that the masterkey can be decrypted, so lets make a backup:
|
||||
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
vault.setUnlocked(true);
|
||||
final Future<Boolean> futureMount = exec.submit(() -> vault.mount());
|
||||
final Future<Boolean> futureMount = exec.submit(vault::mount);
|
||||
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
|
||||
} catch (DecryptFailedException | IOException ex) {
|
||||
} catch (IOException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
|
||||
messageText.setText(resourceBundle.getString("unlock.errorMessage.decryptionFailed"));
|
||||
LOG.error("Decryption failed for technical reasons.", ex);
|
||||
} catch (WrongPasswordException e) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageText.setText(rb.getString("unlock.errorMessage.wrongPassword"));
|
||||
messageText.setText(resourceBundle.getString("unlock.errorMessage.wrongPassword"));
|
||||
Platform.runLater(passwordField::requestFocus);
|
||||
} catch (UnsupportedKeyLengthException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageText.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
|
||||
} catch (UnsupportedVaultException e) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
downloadsPageLink.setVisible(true);
|
||||
if (e.isVaultOlderThanSoftware()) {
|
||||
messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
} else if (e.isSoftwareOlderThanVault()) {
|
||||
messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
messageText.setText(resourceBundle.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
}
|
||||
} catch (DestroyFailedException e) {
|
||||
setControlsDisabled(false);
|
||||
@@ -171,6 +310,7 @@ public class UnlockController implements Initializable {
|
||||
passwordField.setDisable(disable);
|
||||
mountName.setDisable(disable);
|
||||
unlockButton.setDisable(disable);
|
||||
advancedOptionsButton.setDisable(disable);
|
||||
}
|
||||
|
||||
private void unlockAndMountFinished(boolean mountSuccess) {
|
||||
@@ -178,31 +318,19 @@ public class UnlockController implements Initializable {
|
||||
setControlsDisabled(false);
|
||||
if (vault.isUnlocked() && !mountSuccess) {
|
||||
vault.stopServer();
|
||||
vault.setUnlocked(false);
|
||||
} else if (vault.isUnlocked() && mountSuccess) {
|
||||
try {
|
||||
vault.reveal();
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.error("Failed to reveal mounted vault", e);
|
||||
}
|
||||
}
|
||||
if (mountSuccess && listener != null) {
|
||||
listener.didUnlock(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void filterAlphanumericKeyEvents(KeyEvent t) {
|
||||
if (t.getCharacter() == null || t.getCharacter().length() == 0) {
|
||||
return;
|
||||
}
|
||||
char c = t.getCharacter().charAt(0);
|
||||
if (!CharUtils.isAsciiAlphanumeric(c)) {
|
||||
t.consume();
|
||||
}
|
||||
}
|
||||
|
||||
private void mountNameDidChange(ObservableValue<? extends String> property, String oldValue, String newValue) {
|
||||
// newValue is guaranteed to be a-z0-9, see #filterAlphanumericKeyEvents
|
||||
if (newValue.isEmpty()) {
|
||||
mountName.setText(vault.getMountName());
|
||||
} else {
|
||||
vault.setMountName(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public Vault getVault() {
|
||||
@@ -210,8 +338,27 @@ public class UnlockController implements Initializable {
|
||||
}
|
||||
|
||||
public void setVault(Vault vault) {
|
||||
this.resetView();
|
||||
this.vault = vault;
|
||||
this.mountName.setText(vault.getMountName());
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
chooseSelectedDriveLetter();
|
||||
}
|
||||
}
|
||||
|
||||
private void chooseSelectedDriveLetter() {
|
||||
assert SystemUtils.IS_OS_WINDOWS;
|
||||
// if the vault prefers a drive letter, that is currently occupied, this is our last chance to reset this:
|
||||
if (driveLetters.getOccupiedDriveLetters().contains(vault.getWinDriveLetter())) {
|
||||
vault.setWinDriveLetter(null);
|
||||
}
|
||||
final Character letter = vault.getWinDriveLetter();
|
||||
if (letter == null) {
|
||||
// first option is known to be 'auto-assign' due to #WinDriveLetterComparator.
|
||||
this.winDriveLetter.getSelectionModel().selectFirst();
|
||||
} else {
|
||||
this.winDriveLetter.getSelectionModel().select(letter);
|
||||
}
|
||||
}
|
||||
|
||||
public UnlockListener getListener() {
|
||||
|
||||
@@ -11,25 +11,31 @@ package org.cryptomator.ui.controllers;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
|
||||
import org.cryptomator.crypto.CryptorIOSampling;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart.Data;
|
||||
import javafx.scene.chart.XYChart.Series;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import org.cryptomator.crypto.CryptorIOSampling;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
|
||||
public class UnlockedController implements Initializable {
|
||||
public class UnlockedController extends AbstractFXMLViewController {
|
||||
|
||||
private static final int IO_SAMPLING_STEPS = 100;
|
||||
private static final double IO_SAMPLING_INTERVAL = 0.25;
|
||||
@@ -46,11 +52,38 @@ public class UnlockedController implements Initializable {
|
||||
@FXML
|
||||
private NumberAxis xAxis;
|
||||
|
||||
private ResourceBundle rb;
|
||||
private final Stage macWarningsWindow = new Stage();
|
||||
private final MacWarningsController macWarningsController;
|
||||
|
||||
@Inject
|
||||
public UnlockedController(Provider<MacWarningsController> macWarningsControllerProvider) {
|
||||
this.macWarningsController = macWarningsControllerProvider.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/unlocked.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
macWarningsController.initStage(macWarningsWindow);
|
||||
ActiveWindowStyleSupport.startObservingFocus(macWarningsWindow);
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void didClickRevealVault(ActionEvent event) {
|
||||
try {
|
||||
vault.reveal();
|
||||
} catch (CommandFailedException e) {
|
||||
messageLabel.setText(resourceBundle.getString("unlocked.label.revealFailed"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
@@ -58,7 +91,7 @@ public class UnlockedController implements Initializable {
|
||||
try {
|
||||
vault.unmount();
|
||||
} catch (CommandFailedException e) {
|
||||
messageLabel.setText(rb.getString("unlocked.label.unmountFailed"));
|
||||
messageLabel.setText(resourceBundle.getString("unlocked.label.unmountFailed"));
|
||||
return;
|
||||
}
|
||||
vault.stopServer();
|
||||
@@ -68,6 +101,22 @@ public class UnlockedController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// MAC Auth Warnings
|
||||
// ****************************************
|
||||
|
||||
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
|
||||
if (change.getList().size() > 0) {
|
||||
Platform.runLater(() -> {
|
||||
macWarningsWindow.show();
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> {
|
||||
macWarningsWindow.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// IO Graph
|
||||
// ****************************************
|
||||
@@ -87,13 +136,24 @@ public class UnlockedController implements Initializable {
|
||||
ioAnimation.play();
|
||||
}
|
||||
|
||||
private void stopIoSampling() {
|
||||
if (ioAnimation != null) {
|
||||
ioGraph.getData().clear();
|
||||
ioAnimation.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private class IoSamplingAnimationHandler implements EventHandler<ActionEvent> {
|
||||
|
||||
private static final double BYTES_TO_MEGABYTES_FACTOR = 1.0 / IO_SAMPLING_INTERVAL / 1024.0 / 1024.0;
|
||||
private static final double SMOOTHING_FACTOR = 0.3;
|
||||
private static final long EFFECTIVELY_ZERO = 100000; // 100kb
|
||||
private final CryptorIOSampling sampler;
|
||||
private final Series<Number, Number> decryptedBytes;
|
||||
private final Series<Number, Number> encryptedBytes;
|
||||
private int step = 0;
|
||||
private long oldDecBytes = 0;
|
||||
private long oldEncBytes = 0;
|
||||
|
||||
public IoSamplingAnimationHandler(CryptorIOSampling sampler, Series<Number, Number> decryptedBytes, Series<Number, Number> encryptedBytes) {
|
||||
this.sampler = sampler;
|
||||
@@ -105,14 +165,20 @@ public class UnlockedController implements Initializable {
|
||||
public void handle(ActionEvent event) {
|
||||
step++;
|
||||
|
||||
final double decryptedMb = sampler.pollDecryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
|
||||
decryptedBytes.getData().add(new Data<Number, Number>(step, decryptedMb));
|
||||
final long decBytes = sampler.pollDecryptedBytes(true);
|
||||
final double smoothedDecBytes = oldDecBytes + SMOOTHING_FACTOR * (decBytes - oldDecBytes);
|
||||
final double smoothedDecMb = smoothedDecBytes * BYTES_TO_MEGABYTES_FACTOR;
|
||||
oldDecBytes = smoothedDecBytes > EFFECTIVELY_ZERO ? (long) smoothedDecBytes : 0l;
|
||||
decryptedBytes.getData().add(new Data<Number, Number>(step, smoothedDecMb));
|
||||
if (decryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
|
||||
decryptedBytes.getData().remove(0);
|
||||
}
|
||||
|
||||
final double encrypteddMb = sampler.pollEncryptedBytes(true) * BYTES_TO_MEGABYTES_FACTOR;
|
||||
encryptedBytes.getData().add(new Data<Number, Number>(step, encrypteddMb));
|
||||
final long encBytes = sampler.pollEncryptedBytes(true);
|
||||
final double smoothedEncBytes = oldEncBytes + SMOOTHING_FACTOR * (encBytes - oldEncBytes);
|
||||
final double smoothedEncMb = smoothedEncBytes * BYTES_TO_MEGABYTES_FACTOR;
|
||||
oldEncBytes = smoothedEncBytes > EFFECTIVELY_ZERO ? (long) smoothedEncBytes : 0l;
|
||||
encryptedBytes.getData().add(new Data<Number, Number>(step, smoothedEncMb));
|
||||
if (encryptedBytes.getData().size() > IO_SAMPLING_STEPS) {
|
||||
encryptedBytes.getData().remove(0);
|
||||
}
|
||||
@@ -128,11 +194,23 @@ public class UnlockedController implements Initializable {
|
||||
return vault;
|
||||
}
|
||||
|
||||
public void setVault(Vault directory) {
|
||||
this.vault = directory;
|
||||
public void setVault(Vault vault) {
|
||||
this.vault = vault;
|
||||
macWarningsController.setVault(vault);
|
||||
|
||||
if (directory.getCryptor() instanceof CryptorIOSampling) {
|
||||
startIoSampling((CryptorIOSampling) directory.getCryptor());
|
||||
// listen to MAC warnings as long as this vault is unlocked:
|
||||
final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
|
||||
vault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener);
|
||||
vault.unlockedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (Boolean.FALSE.equals(newValue)) {
|
||||
vault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener);
|
||||
}
|
||||
});
|
||||
|
||||
// sample crypto-throughput:
|
||||
stopIoSampling();
|
||||
if (vault.getCryptor() instanceof CryptorIOSampling) {
|
||||
startIoSampling((CryptorIOSampling) vault.getCryptor());
|
||||
} else {
|
||||
ioGraph.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -16,17 +16,9 @@ import java.util.Map;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.httpclient.HttpClient;
|
||||
import org.apache.commons.httpclient.HttpMethod;
|
||||
@@ -34,42 +26,96 @@ import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.commons.httpclient.cookie.CookiePolicy;
|
||||
import org.apache.commons.httpclient.methods.GetMethod;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.settings.Settings;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class WelcomeController implements Initializable {
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.CheckBox;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
|
||||
@Singleton
|
||||
public class WelcomeController extends AbstractFXMLViewController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
|
||||
|
||||
@FXML
|
||||
private ImageView botImageView;
|
||||
|
||||
@FXML
|
||||
private CheckBox checkForUpdatesCheckbox;
|
||||
|
||||
@FXML
|
||||
private Label checkForUpdatesStatus;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator checkForUpdatesIndicator;
|
||||
|
||||
@FXML
|
||||
private Hyperlink updateLink;
|
||||
|
||||
private final Application app;
|
||||
private final Settings settings;
|
||||
private final Comparator<String> semVerComparator;
|
||||
private final ExecutorService executor;
|
||||
private ResourceBundle rb;
|
||||
|
||||
@Inject
|
||||
public WelcomeController(Application app, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
|
||||
public WelcomeController(Application app, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.semVerComparator = semVerComparator;
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
this.botImageView.setImage(new Image(WelcomeController.class.getResource("/bot_welcome.png").toString()));
|
||||
executor.execute(this::checkForUpdates);
|
||||
protected URL getFxmlResourceUrl() {
|
||||
return getClass().getResource("/fxml/welcome.fxml");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ResourceBundle getFxmlResourceBundle() {
|
||||
return ResourceBundle.getBundle("localization");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
botImageView.setImage(new Image(getClass().getResource("/bot_welcome.png").toString()));
|
||||
checkForUpdatesCheckbox.setSelected(settings.isCheckForUpdatesEnabled());
|
||||
checkForUpdatesCheckbox.selectedProperty().addListener(this::checkForUpdatesChanged);
|
||||
if (settings.isCheckForUpdatesEnabled()) {
|
||||
executor.execute(this::checkForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Check for updates
|
||||
// ****************************************
|
||||
|
||||
private void checkForUpdatesChanged(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
assert newValue != null;
|
||||
settings.setCheckForUpdatesEnabled(newValue);
|
||||
if (newValue) {
|
||||
executor.execute(this::checkForUpdates);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkForUpdates() {
|
||||
Platform.runLater(() -> {
|
||||
checkForUpdatesCheckbox.setVisible(false);
|
||||
checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.currentlyChecking"));
|
||||
checkForUpdatesIndicator.setVisible(true);
|
||||
});
|
||||
final HttpClient client = new HttpClient();
|
||||
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
|
||||
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
|
||||
@@ -85,6 +131,12 @@ public class WelcomeController implements Initializable {
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// no error handling required. Maybe next time the version check is successful.
|
||||
} finally {
|
||||
Platform.runLater(() -> {
|
||||
checkForUpdatesCheckbox.setVisible(true);
|
||||
checkForUpdatesStatus.setText(resourceBundle.getString("welcome.checkForUpdates.label.checkboxLabel"));
|
||||
checkForUpdatesIndicator.setVisible(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +155,7 @@ public class WelcomeController implements Initializable {
|
||||
final String currentVersion = WelcomeController.class.getPackage().getImplementationVersion();
|
||||
LOG.debug("Current version: {}, lastest version: {}", currentVersion, latestVersion);
|
||||
if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) {
|
||||
final String msg = String.format(rb.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
|
||||
final String msg = String.format(resourceBundle.getString("welcome.newVersionMessage"), latestVersion, currentVersion);
|
||||
Platform.runLater(() -> {
|
||||
this.updateLink.setText(msg);
|
||||
this.updateLink.setVisible(true);
|
||||
|
||||
@@ -16,18 +16,18 @@ import javafx.scene.control.PasswordField;
|
||||
* Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap.
|
||||
*/
|
||||
public class SecPasswordField extends PasswordField {
|
||||
|
||||
|
||||
private static final char SWIPE_CHAR = ' ';
|
||||
|
||||
|
||||
/**
|
||||
* {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[].
|
||||
* The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars.
|
||||
* <br/>
|
||||
* Imagine the following example with <code>pass</code> being the password, <code>x</code> being the swipe char and <code>'</code> being the offset of the char array:
|
||||
* <ol>
|
||||
* <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
|
||||
* <li>Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
|
||||
* <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
|
||||
* <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
|
||||
* <li>Delete first 4 chars. Internal implementation will then copy the following chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
|
||||
* <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
|
||||
* </ol>
|
||||
*/
|
||||
public void swipe() {
|
||||
@@ -37,8 +37,8 @@ public class SecPasswordField extends PasswordField {
|
||||
this.getContent().insert(pwLength, new String(fillingChars), false);
|
||||
this.getContent().delete(0, pwLength, true);
|
||||
this.getContent().delete(0, pwLength, true);
|
||||
// previous text has now been overwritten. still we need to update the text to trigger some property bindings:
|
||||
this.setText("");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -6,15 +6,14 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableSet;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.ui.util.DeferredClosable;
|
||||
@@ -23,11 +22,19 @@ import org.cryptomator.ui.util.FXThreads;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
import org.cryptomator.ui.util.mount.WebDavMount;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter.MountParam;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
import org.cryptomator.webdav.WebDavServer.ServletLifeCycleAdapter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
public class Vault implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 3754487289683599469L;
|
||||
@@ -43,9 +50,11 @@ public class Vault implements Serializable {
|
||||
private final WebDavMounter mounter;
|
||||
private final DeferredCloser closer;
|
||||
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
|
||||
private final ObservableSet<String> namesOfResourcesWithInvalidMac = FXThreads.observableSetOnMainThread(FXCollections.observableSet());
|
||||
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
|
||||
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
|
||||
|
||||
private String mountName;
|
||||
private Character winDriveLetter;
|
||||
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
|
||||
private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
|
||||
|
||||
@@ -77,11 +86,12 @@ public class Vault implements Serializable {
|
||||
|
||||
public synchronized boolean startServer() {
|
||||
namesOfResourcesWithInvalidMac.clear();
|
||||
whitelistedResourcesWithInvalidMac.clear();
|
||||
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
|
||||
if (o.isPresent() && o.get().isRunning()) {
|
||||
return false;
|
||||
}
|
||||
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, mountName);
|
||||
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, whitelistedResourcesWithInvalidMac, mountName);
|
||||
if (servlet.start()) {
|
||||
webDavServlet = closer.closeLater(servlet);
|
||||
return true;
|
||||
@@ -101,17 +111,24 @@ public class Vault implements Serializable {
|
||||
} catch (DestroyFailedException e) {
|
||||
LOG.error("Destruction of cryptor throw an exception.", e);
|
||||
}
|
||||
setUnlocked(false);
|
||||
whitelistedResourcesWithInvalidMac.clear();
|
||||
namesOfResourcesWithInvalidMac.clear();
|
||||
}
|
||||
|
||||
public boolean mount() {
|
||||
Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
|
||||
if (!o.isPresent() || !o.get().isRunning()) {
|
||||
private Map<MountParam, Optional<String>> getMountParams() {
|
||||
return ImmutableMap.of( //
|
||||
MountParam.MOUNT_NAME, Optional.ofNullable(mountName), //
|
||||
MountParam.WIN_DRIVE_LETTER, Optional.ofNullable(CharUtils.toString(winDriveLetter)) //
|
||||
);
|
||||
}
|
||||
|
||||
public Boolean mount() {
|
||||
final ServletLifeCycleAdapter servlet = webDavServlet.get().orElse(null);
|
||||
if (servlet == null || !servlet.isRunning()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), mountName));
|
||||
webDavMount = closer.closeLater(mounter.mount(servlet.getServletUri(), getMountParams()));
|
||||
return true;
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("mount failed", e);
|
||||
@@ -119,6 +136,13 @@ public class Vault implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void reveal() throws CommandFailedException {
|
||||
final WebDavMount mnt = webDavMount.get().orElse(null);
|
||||
if (mnt != null) {
|
||||
mnt.reveal();
|
||||
}
|
||||
}
|
||||
|
||||
public void unmount() throws CommandFailedException {
|
||||
final WebDavMount mnt = webDavMount.get().orElse(null);
|
||||
if (mnt != null) {
|
||||
@@ -156,12 +180,12 @@ public class Vault implements Serializable {
|
||||
this.unlocked.set(unlocked);
|
||||
}
|
||||
|
||||
public String getMountName() {
|
||||
return mountName;
|
||||
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
|
||||
return namesOfResourcesWithInvalidMac;
|
||||
}
|
||||
|
||||
public ObservableSet<String> getNamesOfResourcesWithInvalidMac() {
|
||||
return namesOfResourcesWithInvalidMac;
|
||||
public Set<String> getWhitelistedResourcesWithInvalidMac() {
|
||||
return whitelistedResourcesWithInvalidMac;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,6 +214,10 @@ public class Vault implements Serializable {
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
public String getMountName() {
|
||||
return mountName;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets the mount name while normalizing it
|
||||
*
|
||||
@@ -204,6 +232,14 @@ public class Vault implements Serializable {
|
||||
this.mountName = mountName;
|
||||
}
|
||||
|
||||
public Character getWinDriveLetter() {
|
||||
return winDriveLetter;
|
||||
}
|
||||
|
||||
public void setWinDriveLetter(Character winDriveLetter) {
|
||||
this.winDriveLetter = winDriveLetter;
|
||||
}
|
||||
|
||||
/* hashcode/equals */
|
||||
|
||||
@Override
|
||||
|
||||
@@ -2,14 +2,16 @@ package org.cryptomator.ui.model;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
@Singleton
|
||||
public class VaultFactory {
|
||||
|
||||
private final WebDavServer server;
|
||||
|
||||
@@ -5,6 +5,10 @@ import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
@@ -16,8 +20,8 @@ import com.fasterxml.jackson.databind.JsonSerializer;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
@Singleton
|
||||
public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
|
||||
|
||||
private final VaultFactory vaultFactoy;
|
||||
@@ -43,7 +47,11 @@ public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
|
||||
public void serialize(Vault value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
|
||||
jgen.writeStartObject();
|
||||
jgen.writeStringField("path", value.getPath().toString());
|
||||
jgen.writeStringField("mountName", value.getMountName().toString());
|
||||
jgen.writeStringField("mountName", value.getMountName());
|
||||
final Character winDriveLetter = value.getWinDriveLetter();
|
||||
if (winDriveLetter != null) {
|
||||
jgen.writeStringField("winDriveLetter", Character.toString(winDriveLetter));
|
||||
}
|
||||
jgen.writeEndObject();
|
||||
}
|
||||
|
||||
@@ -60,6 +68,9 @@ public class VaultObjectMapperProvider implements Provider<ObjectMapper> {
|
||||
if (node.has("mountName")) {
|
||||
vault.setMountName(node.get("mountName").asText());
|
||||
}
|
||||
if (node.has("winDriveLetter")) {
|
||||
vault.setWinDriveLetter(CharUtils.toCharacterObject(node.get("winDriveLetter").asText()));
|
||||
}
|
||||
return vault;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,15 @@ import org.cryptomator.ui.model.Vault;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = {"directories"})
|
||||
@JsonPropertyOrder(value = {"directories", "checkForUpdatesEnabled"})
|
||||
public class Settings implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 7609959894417878744L;
|
||||
|
||||
private List<Vault> directories;
|
||||
|
||||
private Boolean checkForUpdatesEnabled;
|
||||
|
||||
/**
|
||||
* Package-private constructor; use {@link SettingsProvider}.
|
||||
*/
|
||||
@@ -43,4 +45,13 @@ public class Settings implements Serializable {
|
||||
this.directories = directories;
|
||||
}
|
||||
|
||||
public boolean isCheckForUpdatesEnabled() {
|
||||
// not false meaning "null or true", so that true is the default value, if not setting exists yet.
|
||||
return !Boolean.FALSE.equals(checkForUpdatesEnabled);
|
||||
}
|
||||
|
||||
public void setCheckForUpdatesEnabled(boolean checkForUpdatesEnabled) {
|
||||
this.checkForUpdatesEnabled = checkForUpdatesEnabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import java.nio.file.StandardOpenOption;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
@@ -18,8 +20,8 @@ import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.inject.Provider;
|
||||
|
||||
@Singleton
|
||||
public class SettingsProvider implements Provider<Settings> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
|
||||
@@ -50,7 +52,7 @@ public class SettingsProvider implements Provider<Settings> {
|
||||
this.deferredCloser = deferredCloser;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
|
||||
private Path getSettingsPath() throws IOException {
|
||||
String settingsPathProperty = System.getProperty("settingsPath");
|
||||
if (settingsPathProperty == null) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package org.cryptomator.ui.util;
|
||||
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.stage.Window;
|
||||
|
||||
public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
|
||||
@@ -18,9 +17,8 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root
|
||||
* element, if the window is active. Otherwise {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined
|
||||
* depending on the window's focus.<br/>
|
||||
* Creates and registers a listener on the given window, that will add the class {@value #ACTIVE_WINDOW_STYLE_CLASS} to the scenes root element, if the window is active. Otherwise
|
||||
* {@value #INACTIVE_WINDOW_STYLE_CLASS} will be added. Allows CSS rules to be defined depending on the window's focus.<br/>
|
||||
* <br/>
|
||||
* Example:<br/>
|
||||
* <code>
|
||||
@@ -32,7 +30,7 @@ public class ActiveWindowStyleSupport implements ChangeListener<Boolean> {
|
||||
* @return The observer
|
||||
*/
|
||||
public static ChangeListener<Boolean> startObservingFocus(final Window window) {
|
||||
final ChangeListener<Boolean> observer = new WeakChangeListener<Boolean>(new ActiveWindowStyleSupport(window));
|
||||
final ChangeListener<Boolean> observer = new ActiveWindowStyleSupport(window);
|
||||
window.focusedProperty().addListener(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ import org.cryptomator.ui.controllers.MainController;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Tries to bring open-close symmetry in contexts where the resource outlives
|
||||
@@ -57,8 +59,10 @@ public class DeferredCloser implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
|
||||
|
||||
@VisibleForTesting
|
||||
final Map<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
|
||||
|
||||
@VisibleForTesting
|
||||
final AtomicLong counter = new AtomicLong();
|
||||
|
||||
public class ManagedResource<T> implements DeferredClosable<T> {
|
||||
@@ -73,6 +77,7 @@ public class DeferredCloser implements AutoCloseable {
|
||||
this.closer = closer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
final T oldObject = object.getAndSet(null);
|
||||
if (oldObject != null) {
|
||||
@@ -86,6 +91,7 @@ public class DeferredCloser implements AutoCloseable {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<T> get() throws IllegalStateException {
|
||||
return Optional.ofNullable(object.get());
|
||||
}
|
||||
@@ -94,6 +100,7 @@ public class DeferredCloser implements AutoCloseable {
|
||||
/**
|
||||
* Closes all added objects which have not been closed before and releases references.
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
for (Iterator<ManagedResource<?>> iterator = cleanups.values().iterator(); iterator.hasNext();) {
|
||||
final ManagedResource<?> closableProvider = iterator.next();
|
||||
|
||||
@@ -15,6 +15,7 @@ import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.collections.ObservableSet;
|
||||
|
||||
/**
|
||||
@@ -53,8 +54,7 @@ public final class FXThreads {
|
||||
};
|
||||
|
||||
/**
|
||||
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
|
||||
* called. If you are interested in the exception, use
|
||||
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be called. If you are interested in the exception, use
|
||||
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
||||
*
|
||||
* <pre>
|
||||
@@ -74,8 +74,7 @@ public final class FXThreads {
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
|
||||
* called. If you are interested in the exception, use
|
||||
* Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be called. If you are interested in the exception, use
|
||||
* {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
|
||||
*
|
||||
* <pre>
|
||||
@@ -123,4 +122,8 @@ public final class FXThreads {
|
||||
return new ObservableSetOnMainThread<E>(set);
|
||||
}
|
||||
|
||||
public static <E> ObservableList<E> observableListOnMainThread(ObservableList<E> list) {
|
||||
return new ObservableListOnMainThread<E>(list);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.Change;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
||||
|
||||
private final ObservableSet<E> set;
|
||||
@@ -91,8 +93,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
||||
}
|
||||
|
||||
private void invalidated(Observable observable) {
|
||||
final Collection<InvalidationListener> listeners = ImmutableList.copyOf(invalidationListeners);
|
||||
Platform.runLater(() -> {
|
||||
for (InvalidationListener listener : invalidationListeners) {
|
||||
for (InvalidationListener listener : listeners) {
|
||||
listener.invalidated(this);
|
||||
}
|
||||
});
|
||||
@@ -110,8 +113,9 @@ class ObservableSetOnMainThread<E> implements ObservableSet<E> {
|
||||
|
||||
private void onChanged(Change<? extends E> change) {
|
||||
final Change<? extends E> c = new SetChange(this, change.getElementAdded(), change.getElementRemoved());
|
||||
final Collection<SetChangeListener<? super E>> listeners = ImmutableList.copyOf(setChangeListeners);
|
||||
Platform.runLater(() -> {
|
||||
for (SetChangeListener<? super E> listener : setChangeListeners) {
|
||||
for (SetChangeListener<? super E> listener : listeners) {
|
||||
listener.onChanged(c);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* A WebDavMounter acting as fallback if no other mounter works.
|
||||
@@ -28,13 +30,18 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMount mount(URI uri, String name) {
|
||||
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) {
|
||||
displayMountInstructions();
|
||||
return new AbstractWebDavMount() {
|
||||
@Override
|
||||
public void unmount() {
|
||||
displayUnmountInstructions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws CommandFailedException {
|
||||
displayRevealInstructions();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,4 +54,8 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
|
||||
// TODO display message to user pointing to cryptomator.org/mounting#unmount which describes what to do
|
||||
}
|
||||
|
||||
private void displayRevealInstructions() {
|
||||
// TODO display message to user pointing to cryptomator.org/mounting#reveal which describes what to do
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,11 +11,20 @@
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.command.Script;
|
||||
|
||||
@Singleton
|
||||
final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Inject
|
||||
LinuxGvfsWebDavMounter() {}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
@@ -38,48 +47,54 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
|
||||
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
||||
final Script mountScript = Script.fromLines(
|
||||
"set -x",
|
||||
"gvfs-mount \"dav:$DAV_SSP\"")
|
||||
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
|
||||
final Script testMountStillExistsScript = Script.fromLines(
|
||||
"set -x",
|
||||
"test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1")
|
||||
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
|
||||
final Script unmountScript = Script.fromLines(
|
||||
"set -x",
|
||||
"gvfs-mount -u \"dav:$DAV_SSP\"")
|
||||
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
|
||||
mountScript.execute();
|
||||
try{
|
||||
openMountWithWebdavUri("dav:"+uri.getRawSchemeSpecificPart()).execute();
|
||||
}catch(CommandFailedException exception){
|
||||
openMountWithWebdavUri("webdav:"+uri.getRawSchemeSpecificPart()).execute();
|
||||
}
|
||||
return new AbstractWebDavMount() {
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
boolean mountStillExists;
|
||||
try {
|
||||
testMountStillExistsScript.execute();
|
||||
mountStillExists = true;
|
||||
} catch(CommandFailedException e) {
|
||||
mountStillExists = false;
|
||||
}
|
||||
// only attempt unmount if user didn't unmount manually:
|
||||
if (mountStillExists) {
|
||||
unmountScript.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
return new LinuxGvfsWebDavMount(uri);
|
||||
}
|
||||
|
||||
private static class LinuxGvfsWebDavMount extends AbstractWebDavMount {
|
||||
private final URI webDavUri;
|
||||
private final Script testMountStillExistsScript;
|
||||
private final Script unmountScript;
|
||||
|
||||
private LinuxGvfsWebDavMount(URI webDavUri) {
|
||||
this.webDavUri = webDavUri;
|
||||
this.testMountStillExistsScript = Script.fromLines("set -x", "test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
|
||||
this.unmountScript = Script.fromLines("set -x", "gvfs-mount -u \"dav:$DAV_SSP\"").addEnv("DAV_SSP", webDavUri.getRawSchemeSpecificPart());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
boolean mountStillExists;
|
||||
try {
|
||||
testMountStillExistsScript.execute();
|
||||
mountStillExists = true;
|
||||
} catch(CommandFailedException e) {
|
||||
mountStillExists = false;
|
||||
}
|
||||
// only attempt unmount if user didn't unmount manually:
|
||||
if (mountStillExists) {
|
||||
unmountScript.execute();
|
||||
}
|
||||
}
|
||||
|
||||
private Script openMountWithWebdavUri(String webdavUri){
|
||||
return Script.fromLines(
|
||||
"set -x",
|
||||
"xdg-open \"$DAV_URI\"")
|
||||
.addEnv("DAV_URI", webdavUri);
|
||||
@Override
|
||||
public void reveal() throws CommandFailedException {
|
||||
try {
|
||||
openMountWithWebdavUri("dav:"+webDavUri.getRawSchemeSpecificPart()).execute();
|
||||
} catch (CommandFailedException exception) {
|
||||
openMountWithWebdavUri("webdav:"+webDavUri.getRawSchemeSpecificPart()).execute();
|
||||
}
|
||||
}
|
||||
|
||||
private Script openMountWithWebdavUri(String webdavUri){
|
||||
return Script.fromLines("set -x", "xdg-open \"$DAV_URI\"").addEnv("DAV_URI", webdavUri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,12 +12,21 @@ package org.cryptomator.ui.util.mount;
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.command.Script;
|
||||
|
||||
@Singleton
|
||||
final class MacOsXWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Inject
|
||||
MacOsXWebDavMounter() {}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
@@ -30,30 +39,48 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
|
||||
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
||||
final String mountName = mountParams.get(MountParam.MOUNT_NAME).orElseThrow(() -> {
|
||||
return new IllegalArgumentException("Missing mount parameter MOUNT_NAME.");
|
||||
});
|
||||
|
||||
// we don't use the uri to derive a path, as it *could* be longer than 255 chars.
|
||||
final String path = "/Volumes/Cryptomator_" + UUID.randomUUID().toString();
|
||||
final Script mountScript = Script.fromLines(
|
||||
"mkdir \"$MOUNT_PATH\"",
|
||||
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"",
|
||||
"open \"$MOUNT_PATH\"")
|
||||
"mount_webdav -S -v $MOUNT_NAME \"$DAV_AUTHORITY$DAV_PATH\" \"$MOUNT_PATH\"")
|
||||
.addEnv("DAV_AUTHORITY", uri.getRawAuthority())
|
||||
.addEnv("DAV_PATH", uri.getRawPath())
|
||||
.addEnv("MOUNT_PATH", path)
|
||||
.addEnv("MOUNT_NAME", name);
|
||||
final Script unmountScript = Script.fromLines(
|
||||
"diskutil umount $MOUNT_PATH")
|
||||
.addEnv("MOUNT_PATH", path);
|
||||
.addEnv("MOUNT_NAME", mountName);
|
||||
mountScript.execute();
|
||||
return new AbstractWebDavMount() {
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
// only attempt unmount if user didn't unmount manually:
|
||||
if (Files.exists(FileSystems.getDefault().getPath(path))) {
|
||||
unmountScript.execute();
|
||||
}
|
||||
return new MacWebDavMount(path);
|
||||
}
|
||||
|
||||
private static class MacWebDavMount extends AbstractWebDavMount {
|
||||
private final String mountPath;
|
||||
private final Script revealScript;
|
||||
private final Script unmountScript;
|
||||
|
||||
private MacWebDavMount(String mountPath) {
|
||||
this.mountPath = mountPath;
|
||||
this.revealScript = Script.fromLines("open \"$MOUNT_PATH\"").addEnv("MOUNT_PATH", mountPath);
|
||||
this.unmountScript = Script.fromLines("diskutil umount $MOUNT_PATH").addEnv("MOUNT_PATH", mountPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
// only attempt unmount if user didn't unmount manually:
|
||||
if (Files.exists(FileSystems.getDefault().getPath(mountPath))) {
|
||||
unmountScript.execute();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws CommandFailedException {
|
||||
revealScript.execute();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.unmodifiableList;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
@Singleton
|
||||
class MountStrategies implements Collection<WebDavMounterStrategy> {
|
||||
|
||||
private final Collection<WebDavMounterStrategy> delegate;
|
||||
|
||||
@Inject
|
||||
MountStrategies(LinuxGvfsWebDavMounter linuxMounter, MacOsXWebDavMounter osxMounter, WindowsWebDavMounter winMounter) {
|
||||
delegate = unmodifiableList(asList(linuxMounter, osxMounter, winMounter));
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return delegate.size();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return delegate.isEmpty();
|
||||
}
|
||||
|
||||
public boolean contains(Object o) {
|
||||
return delegate.contains(o);
|
||||
}
|
||||
|
||||
public Iterator<WebDavMounterStrategy> iterator() {
|
||||
return delegate.iterator();
|
||||
}
|
||||
|
||||
public Object[] toArray() {
|
||||
return delegate.toArray();
|
||||
}
|
||||
|
||||
public <T> T[] toArray(T[] a) {
|
||||
return delegate.toArray(a);
|
||||
}
|
||||
|
||||
public boolean add(WebDavMounterStrategy e) {
|
||||
return delegate.add(e);
|
||||
}
|
||||
|
||||
public boolean remove(Object o) {
|
||||
return delegate.remove(o);
|
||||
}
|
||||
|
||||
public boolean containsAll(Collection<?> c) {
|
||||
return delegate.containsAll(c);
|
||||
}
|
||||
|
||||
public boolean addAll(Collection<? extends WebDavMounterStrategy> c) {
|
||||
return delegate.addAll(c);
|
||||
}
|
||||
|
||||
public boolean removeAll(Collection<?> c) {
|
||||
return delegate.removeAll(c);
|
||||
}
|
||||
|
||||
public boolean retainAll(Collection<?> c) {
|
||||
return delegate.retainAll(c);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
delegate.clear();
|
||||
}
|
||||
|
||||
public boolean equals(Object o) {
|
||||
return delegate.equals(o);
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return delegate.hashCode();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -22,4 +22,11 @@ public interface WebDavMount extends AutoCloseable {
|
||||
*/
|
||||
void unmount() throws CommandFailedException;
|
||||
|
||||
/**
|
||||
* Reveals the mounted drive in the operating systems default file browser.
|
||||
*
|
||||
* @throws CommandFailedException if the reveal operation fails
|
||||
*/
|
||||
void reveal() throws CommandFailedException;
|
||||
|
||||
}
|
||||
|
||||
@@ -10,17 +10,22 @@
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface WebDavMounter {
|
||||
|
||||
public static enum MountParam {MOUNT_NAME, WIN_DRIVE_LETTER}
|
||||
|
||||
/**
|
||||
* Tries to mount a given webdav share.
|
||||
*
|
||||
* @param uri URI of the webdav share
|
||||
* @param name the name under which the folder is to be mounted. This might be ignored.
|
||||
* @param mountParams additional mount parameters, that might not get ignored by some OS-specific mounters.
|
||||
* @return a {@link WebDavMount} representing the mounted share
|
||||
* @throws CommandFailedException if the mount operation fails
|
||||
* @throws IllegalArgumentException if mountParams is missing expected options
|
||||
*/
|
||||
WebDavMount mount(URI uri, String name) throws CommandFailedException;
|
||||
WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException;
|
||||
|
||||
}
|
||||
|
||||
@@ -9,25 +9,26 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Provider;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.cryptomator.webdav.WebDavServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.google.inject.Provider;
|
||||
|
||||
@Singleton
|
||||
public class WebDavMounterProvider implements Provider<WebDavMounter> {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WebDavMounterProvider.class);
|
||||
private static final WebDavMounterStrategy[] STRATEGIES = {new WindowsWebDavMounter(), new MacOsXWebDavMounter(), new LinuxGvfsWebDavMounter()};
|
||||
private final WebDavMounterStrategy choosenStrategy;
|
||||
|
||||
@Inject
|
||||
public WebDavMounterProvider(WebDavServer server, ExecutorService executorService) {
|
||||
this.choosenStrategy = getStrategyWhichShouldWork();
|
||||
public WebDavMounterProvider(WebDavServer server, ExecutorService executorService, MountStrategies availableStrategies) {
|
||||
this.choosenStrategy = getStrategyWhichShouldWork(availableStrategies);
|
||||
executorService.execute(() -> {
|
||||
this.choosenStrategy.warmUp(server.getPort());
|
||||
});
|
||||
@@ -38,14 +39,10 @@ public class WebDavMounterProvider implements Provider<WebDavMounter> {
|
||||
return this.choosenStrategy;
|
||||
}
|
||||
|
||||
private static WebDavMounterStrategy getStrategyWhichShouldWork() {
|
||||
for (WebDavMounterStrategy strategy : STRATEGIES) {
|
||||
if (strategy.shouldWork()) {
|
||||
LOG.info("Using {}", strategy.getClass().getSimpleName());
|
||||
return strategy;
|
||||
}
|
||||
}
|
||||
return new FallbackWebDavMounter();
|
||||
private WebDavMounterStrategy getStrategyWhichShouldWork(Collection<WebDavMounterStrategy> availableStrategies) {
|
||||
WebDavMounterStrategy strategy = availableStrategies.stream().filter(WebDavMounterStrategy::shouldWork).findFirst().orElse(new FallbackWebDavMounter());
|
||||
LOG.info("Using {}", strategy.getClass().getSimpleName());
|
||||
return strategy;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.cryptomator.ui.util.mount;
|
||||
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
import static java.util.stream.IntStream.rangeClosed;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
|
||||
@Singleton
|
||||
public final class WindowsDriveLetters {
|
||||
|
||||
private static final Set<Character> A_TO_Z = rangeClosed('A', 'Z').mapToObj(i -> (char) i).collect(toSet());
|
||||
|
||||
@Inject
|
||||
public WindowsDriveLetters() {
|
||||
}
|
||||
|
||||
public Set<Character> getOccupiedDriveLetters() {
|
||||
if (!SystemUtils.IS_OS_WINDOWS) {
|
||||
throw new UnsupportedOperationException("This method is only defined for Windows file systems");
|
||||
}
|
||||
Iterable<Path> rootDirs = FileSystems.getDefault().getRootDirectories();
|
||||
return StreamSupport.stream(rootDirs.spliterator(), false).map(Path::toString).map(CharUtils::toChar).map(Character::toUpperCase).collect(toSet());
|
||||
}
|
||||
|
||||
public Set<Character> getAvailableDriveLetters() {
|
||||
return Sets.difference(A_TO_Z, getOccupiedDriveLetters());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,12 +12,16 @@ package org.cryptomator.ui.util.mount;
|
||||
import static org.cryptomator.ui.util.command.Script.fromLines;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.ui.util.command.CommandResult;
|
||||
import org.cryptomator.ui.util.command.Script;
|
||||
@@ -27,10 +31,18 @@ import org.cryptomator.ui.util.command.Script;
|
||||
* <p>
|
||||
* Tested on Windows 7 but should also work on Windows 8.
|
||||
*/
|
||||
@Singleton
|
||||
final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
|
||||
private static final int MAX_MOUNT_ATTEMPTS = 5;
|
||||
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]):\\s*");
|
||||
private static final int MAX_MOUNT_ATTEMPTS = 8;
|
||||
private static final char AUTO_ASSIGN_DRIVE_LETTER = '*';
|
||||
private final WindowsDriveLetters driveLetters;
|
||||
|
||||
@Inject
|
||||
WindowsWebDavMounter(WindowsDriveLetters driveLetters) {
|
||||
this.driveLetters = driveLetters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldWork() {
|
||||
@@ -39,62 +51,46 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
|
||||
@Override
|
||||
public void warmUp(int serverPort) {
|
||||
// try {
|
||||
// final Script mountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot\\bill-gates-mom-uses-goto /persistent:no");
|
||||
// mountScript.addEnv("DAV_PORT", String.valueOf(serverPort));
|
||||
// mountScript.execute(1, TimeUnit.SECONDS);
|
||||
// } catch (CommandFailedException e) {
|
||||
// // will most certainly throw an exception, because this is a fake WebDav path. But now windows has some DNS things cached :)
|
||||
// }
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
|
||||
CommandResult mountResult;
|
||||
try {
|
||||
final Script mountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
|
||||
mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
||||
mountResult = mountScript.execute(5, TimeUnit.SECONDS);
|
||||
} catch (CommandFailedException ex) {
|
||||
final Script mountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
|
||||
mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
||||
final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
|
||||
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
|
||||
mountResult = bypassProxyAndRetryMount(mountScript, proxyBypassScript);
|
||||
public WebDavMount mount(URI uri, Map<MountParam, Optional<String>> mountParams) throws CommandFailedException {
|
||||
final Character driveLetter = mountParams.get(MountParam.WIN_DRIVE_LETTER).map(CharUtils::toCharacterObject).orElse(AUTO_ASSIGN_DRIVE_LETTER);
|
||||
if (driveLetters.getOccupiedDriveLetters().contains(driveLetter)) {
|
||||
throw new CommandFailedException("Drive letter occupied.");
|
||||
}
|
||||
|
||||
final String driveLetter = getDriveLetter(mountResult.getStdOut());
|
||||
final Script openExplorerScript = fromLines("start explorer.exe " + driveLetter);
|
||||
openExplorerScript.execute();
|
||||
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
|
||||
return new AbstractWebDavMount() {
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
// only attempt unmount if user didn't unmount manually:
|
||||
if (isVolumeMounted(driveLetter)) {
|
||||
unmountScript.execute();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private boolean isVolumeMounted(String driveLetter) {
|
||||
for (Path path : FileSystems.getDefault().getRootDirectories()) {
|
||||
if (path.toString().startsWith(driveLetter)) {
|
||||
return true;
|
||||
}
|
||||
final String driveLetterStr = driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? CharUtils.toString(AUTO_ASSIGN_DRIVE_LETTER) : driveLetter + ":";
|
||||
final Script localhostMountScript = fromLines("net use %DRIVE_LETTER% \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
|
||||
localhostMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
|
||||
localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
|
||||
localhostMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
||||
CommandResult mountResult;
|
||||
try {
|
||||
mountResult = localhostMountScript.execute(5, TimeUnit.SECONDS);
|
||||
} catch (CommandFailedException ex) {
|
||||
final Script ipv6literaltMountScript = fromLines("net use %DRIVE_LETTER% \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
|
||||
ipv6literaltMountScript.addEnv("DRIVE_LETTER", driveLetterStr);
|
||||
ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
|
||||
ipv6literaltMountScript.addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
|
||||
final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
|
||||
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
|
||||
mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript);
|
||||
}
|
||||
return false;
|
||||
return new WindowsWebDavMount(driveLetter.charValue() == AUTO_ASSIGN_DRIVE_LETTER ? getDriveLetter(mountResult.getStdOut()) : driveLetter);
|
||||
}
|
||||
|
||||
private CommandResult bypassProxyAndRetryMount(Script mountScript, Script proxyBypassScript) throws CommandFailedException {
|
||||
private CommandResult bypassProxyAndRetryMount(Script localhostMountScript, Script ipv6literalMountScript, Script proxyBypassScript) throws CommandFailedException {
|
||||
CommandFailedException latestException = null;
|
||||
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
|
||||
try {
|
||||
// wait a moment before next attempt
|
||||
Thread.sleep(5000);
|
||||
proxyBypassScript.execute();
|
||||
return mountScript.execute(5, TimeUnit.SECONDS);
|
||||
// alternate localhost and 0--1.ipv6literal.net
|
||||
final Script mountScript = (i % 2 == 0) ? localhostMountScript : ipv6literalMountScript;
|
||||
return mountScript.execute(3, TimeUnit.SECONDS);
|
||||
} catch (CommandFailedException ex) {
|
||||
latestException = ex;
|
||||
} catch (InterruptedException ex) {
|
||||
@@ -105,13 +101,42 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
|
||||
throw latestException;
|
||||
}
|
||||
|
||||
private String getDriveLetter(String result) throws CommandFailedException {
|
||||
private Character getDriveLetter(String result) throws CommandFailedException {
|
||||
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
|
||||
if (matcher.find()) {
|
||||
return matcher.group(1);
|
||||
return CharUtils.toCharacterObject(matcher.group(1));
|
||||
} else {
|
||||
throw new CommandFailedException("Failed to get a drive letter from net use output.");
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowsWebDavMount extends AbstractWebDavMount {
|
||||
private final Character driveLetter;
|
||||
private final Script openExplorerScript;
|
||||
private final Script unmountScript;
|
||||
|
||||
private WindowsWebDavMount(Character driveLetter) {
|
||||
this.driveLetter = driveLetter;
|
||||
this.openExplorerScript = fromLines("start explorer.exe " + driveLetter + ":");
|
||||
this.unmountScript = fromLines("net use " + driveLetter + ": /delete").addEnv("DRIVE_LETTER", Character.toString(driveLetter));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unmount() throws CommandFailedException {
|
||||
// only attempt unmount if user didn't unmount manually:
|
||||
if (isVolumeMounted()) {
|
||||
unmountScript.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reveal() throws CommandFailedException {
|
||||
openExplorerScript.execute();
|
||||
}
|
||||
|
||||
private boolean isVolumeMounted() {
|
||||
return driveLetters.getOccupiedDriveLetters().contains(driveLetter);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@
|
||||
<?import javafx.scene.text.Text?>
|
||||
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" xmlns:fx="http://javafx.com/fxml">
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
|
||||
<padding>
|
||||
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
|
||||
</padding>
|
||||
@@ -34,25 +34,25 @@
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label text="%changePassword.label.oldPassword" GridPane.rowIndex="0" GridPane.columnIndex="0" />
|
||||
<SecPasswordField fx:id="oldPasswordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
|
||||
<Label text="%changePassword.label.oldPassword" GridPane.rowIndex="0" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="oldPasswordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label text="%changePassword.label.newPassword" GridPane.rowIndex="1" GridPane.columnIndex="0" />
|
||||
<SecPasswordField fx:id="newPasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
|
||||
<Label text="%changePassword.label.newPassword" GridPane.rowIndex="1" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="newPasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Label text="%changePassword.label.retypePassword" GridPane.rowIndex="2" GridPane.columnIndex="0" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
|
||||
<Label text="%changePassword.label.retypePassword" GridPane.rowIndex="2" GridPane.columnIndex="0" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Button fx:id="changePasswordButton" text="%changePassword.button.change" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
|
||||
<Button fx:id="changePasswordButton" text="%changePassword.button.change" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true" cacheShape="true" cache="true"/>
|
||||
|
||||
<!-- Row 4 -->
|
||||
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2">
|
||||
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true">
|
||||
<children>
|
||||
<Text fx:id="messageText" />
|
||||
<Hyperlink fx:id="downloadsPageLink" text="%changePassword.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" />
|
||||
<Text fx:id="messageText" cache="true" />
|
||||
<Hyperlink fx:id="downloadsPageLink" text="%changePassword.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" cacheShape="true" cache="true" />
|
||||
</children>
|
||||
</TextFlow>
|
||||
</children>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<?import javafx.scene.layout.ColumnConstraints?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.InitializeController" xmlns:fx="http://javafx.com/fxml">
|
||||
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
|
||||
<padding>
|
||||
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
|
||||
</padding>
|
||||
@@ -28,18 +28,18 @@
|
||||
|
||||
<children>
|
||||
<!-- Row 0 -->
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.password" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="0" GridPane.columnIndex="1" />
|
||||
<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.password" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="passwordField" GridPane.rowIndex="0" GridPane.columnIndex="1" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 1 -->
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.retypePassword" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" />
|
||||
<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.retypePassword" cacheShape="true" cache="true" />
|
||||
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 2 -->
|
||||
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" />
|
||||
<Button fx:id="okButton" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" text="%initialize.button.ok" prefWidth="150.0" onAction="#initializeVault" focusTraversable="false" disable="true" cacheShape="true" cache="true" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" />
|
||||
<Label fx:id="messageLabel" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" />
|
||||
</children>
|
||||
</GridPane>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
|
||||
<VBox styleClass="root" alignment="CENTER" prefHeight="225.0" prefWidth="525.0" spacing="12.0" fx:controller="org.cryptomator.ui.controllers.MacWarningsController" xmlns:fx="http://javafx.com/fxml">
|
||||
<VBox styleClass="root" alignment="CENTER" prefHeight="225.0" prefWidth="525.0" spacing="12.0" xmlns:fx="http://javafx.com/fxml">
|
||||
|
||||
<padding><Insets top="12.0" right="12.0" bottom="12.0" left="12.0"/></padding>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
<ListView fx:id="warningsList" VBox.vgrow="ALWAYS" focusTraversable="false" />
|
||||
<HBox alignment="CENTER_RIGHT" spacing="12.0">
|
||||
<children>
|
||||
<Button text="%macWarnings.dismissButton" prefWidth="200.0" onAction="#didClickDismissButton" focusTraversable="false"/>
|
||||
<Button fx:id="whitelistButton" text="%macWarnings.whitelistButton" prefWidth="200.0" onAction="#didClickWhitelistButton" focusTraversable="false"/>
|
||||
<Button text="%macWarnings.moreInformationButton" defaultButton="true" prefWidth="200.0" onAction="#didClickMoreInformationButton" focusTraversable="false"/>
|
||||
</children>
|
||||
</HBox>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user