Compare commits
101 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
d43396bcfb | ||
|
|
b6383f49b1 | ||
|
|
c5b241a68a | ||
|
|
00a39c80cb | ||
|
|
8d8fe74d3a | ||
|
|
e767436f5d | ||
|
|
03cdf1fdc9 | ||
|
|
49646aae41 | ||
|
|
f3aa636b8b | ||
|
|
c73f18e3b8 | ||
|
|
5f40ce50e7 | ||
|
|
744f9db958 | ||
|
|
111ee99ae1 | ||
|
|
7d81ff3b43 | ||
|
|
00a2c6c5ae | ||
|
|
587c45ee63 | ||
|
|
3d3cb7bb86 | ||
|
|
0e3513e86d | ||
|
|
8845efb983 | ||
|
|
88f81d2682 | ||
|
|
58d500baaf | ||
|
|
103ea9047f | ||
|
|
f4b07b9807 | ||
|
|
6a3b4d486d | ||
|
|
13bcde318b | ||
|
|
242486c0b1 | ||
|
|
ea9c8eee83 | ||
|
|
0d969432c2 | ||
|
|
be369b480b | ||
|
|
4cf872f916 | ||
|
|
3d3c36b66f | ||
|
|
54c2afe3d1 | ||
|
|
3c71878b6b | ||
|
|
f36a61df1c | ||
|
|
1642aa4688 | ||
|
|
6f9b16a7dc | ||
|
|
66ed9126de | ||
|
|
a07efc5209 | ||
|
|
bbeeb79812 | ||
|
|
4d08e9d72b | ||
|
|
040f260bf0 | ||
|
|
cdf9c28a38 | ||
|
|
a6972f62f2 | ||
|
|
1db32470b1 | ||
|
|
ed022412fe | ||
|
|
a2356b62c7 | ||
|
|
9aa6117fb0 | ||
|
|
b9b85a58ac | ||
|
|
9024465d6c | ||
|
|
f22142a876 | ||
|
|
652c4cbafb | ||
|
|
188a13b202 | ||
|
|
75c21b4c9b | ||
|
|
c7ecd612c9 | ||
|
|
3f8f0b1fa7 | ||
|
|
2b4b359adb | ||
|
|
0562a909f9 | ||
|
|
c10d80de18 | ||
|
|
05abea0508 | ||
|
|
d19ffc327b | ||
|
|
a042c14fb9 | ||
|
|
a4be81267e | ||
|
|
c1dd902a10 | ||
|
|
0994e7bb39 | ||
|
|
1f3b91f187 | ||
|
|
e883a04577 |
23
.travis.yml
@@ -1,11 +1,22 @@
|
||||
language: java
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
script: mvn -fmain/pom.xml clean package
|
||||
- oraclejdk8
|
||||
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:
|
||||
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
|
||||
on_success: change # options: [always|never|change] default: always
|
||||
on_failure: always # options: [always|never|change] default: always
|
||||
on_start: false # default: false
|
||||
- https://webhooks.gitter.im/e/7d429ab35361726e26f2
|
||||
on_success: change
|
||||
on_failure: always
|
||||
on_start: false
|
||||
deploy:
|
||||
provider: releases
|
||||
prerelease: true
|
||||
api_key:
|
||||
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
|
||||
file: main/uber-jar/target/Cryptomator-$TRAVIS_TAG.jar
|
||||
skip_cleanup: true
|
||||
on:
|
||||
repo: cryptomator/cryptomator
|
||||
tags: true
|
||||
|
||||
23
README.md
@@ -1,15 +1,17 @@
|
||||
Cryptomator
|
||||
====================
|
||||
|
||||
[](https://gitter.im/totalvoidness/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://travis-ci.org/cryptomator/cryptomator)
|
||||
[](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://flattr.com/submit/auto?user_id=totalvoidness&url=https%3A%2F%2Fgithub.com%2Ftotalvoidness%2Fcryptomator&title=Cryptomator&language=en_GB&tags=github&category=software)
|
||||
|
||||
Multiplatform transparent client-side encryption of your files in the cloud.
|
||||
|
||||
If you want to take a look at the current beta version, go ahead and get your copy of cryptomator on [Cryptomator.org](https://cryptomator.org) or clone and build Cryptomator using Maven (instructions below).
|
||||
|
||||
## Features
|
||||
- Totally transparent: Just work on the encrypted volume, as if it was an USB drive
|
||||
- Works with Dropbox, OneDrive (Skydrive), Google Drive and any other cloud storage, that syncs with a local directory
|
||||
- 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
|
||||
- Client-side. No accounts, no data shared with any online service
|
||||
@@ -17,18 +19,19 @@ If you want to take a look at the current beta version, go ahead and get your co
|
||||
- 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 ;-)
|
||||
|
||||
### Privacy
|
||||
- 256 bit keys (unlimited strength policy bundled with native binaries - 128 bit elsewhere)
|
||||
- Scrypt key derivation
|
||||
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
|
||||
- Sensitive data is swiped from the heap asap
|
||||
- Lightweight: Complexity kills security
|
||||
- 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
|
||||
- Each file contains all information needed for decryption (except for the key of course). No common metadata means no SPOF
|
||||
- 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
|
||||
|
||||
@@ -36,19 +39,17 @@ 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 (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/totalvoidness/cryptomator.git
|
||||
git clone https://github.com/cryptomator/cryptomator.git
|
||||
cd cryptomator/main
|
||||
git checkout v0.4.0
|
||||
mvn clean install
|
||||
git checkout 0.7.1
|
||||
mvn clean install -Pdebian
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT X Consortium license. See the LICENSE file for more info.
|
||||
|
||||
[](https://travis-ci.org/totalvoidness/cryptomator)
|
||||
|
||||
@@ -12,16 +12,14 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.5.2</version>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>core</artifactId>
|
||||
<name>Cryptomator WebDAV and I/O module</name>
|
||||
|
||||
<properties>
|
||||
<jetty.version>9.2.5.v20141112</jetty.version>
|
||||
<jackrabbit.version>2.9.0</jackrabbit.version>
|
||||
<commons.transaction.version>1.2</commons.transaction.version>
|
||||
<jta.version>1.1</jta.version>
|
||||
<jetty.version>9.3.1.v20150714</jetty.version>
|
||||
<jackrabbit.version>2.10.1</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>
|
||||
@@ -49,6 +57,12 @@
|
||||
<version>${jackrabbit.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- I/O -->
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
@@ -58,9 +72,11 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -11,6 +11,7 @@ package org.cryptomator.webdav;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collection;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
@@ -83,11 +84,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 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, 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");
|
||||
@@ -98,7 +99,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);
|
||||
final ServletHolder servlet = getWebDavServletHolder(workDir.toString(), cryptor, failingMacCollection, whitelistedResourceCollection);
|
||||
servletContext.addServlet(servlet, "/*");
|
||||
|
||||
servletCollection.mapContexts();
|
||||
@@ -110,8 +111,8 @@ public final class WebDavServer {
|
||||
}
|
||||
}
|
||||
|
||||
private ServletHolder getWebDavServletHolder(final String workDir, final Cryptor cryptor) {
|
||||
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
|
||||
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;
|
||||
}
|
||||
@@ -123,7 +124,7 @@ public final class WebDavServer {
|
||||
/**
|
||||
* Exposes implementation-specific methods to other modules.
|
||||
*/
|
||||
public class ServletLifeCycleAdapter {
|
||||
public class ServletLifeCycleAdapter implements AutoCloseable {
|
||||
|
||||
private final LifeCycle lifecycle;
|
||||
private final URI servletUri;
|
||||
@@ -161,6 +162,11 @@ public final class WebDavServer {
|
||||
return servletUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ public class IORuntimeException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -4713080133052143303L;
|
||||
|
||||
public IORuntimeException(IOException ioException) {
|
||||
super(ioException);
|
||||
public IORuntimeException(IOException cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -6,22 +6,21 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
@@ -35,6 +34,7 @@ import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
|
||||
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;
|
||||
@@ -45,22 +45,34 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
|
||||
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
|
||||
private static final String[] DAV_CREATIONDATE_PROPNAMES = {DavPropertyName.CREATIONDATE.getName(), "Win32CreationTime"};
|
||||
private static final String[] DAV_MODIFIEDDATE_PROPNAMES = {DavPropertyName.GETLASTMODIFIED.getName(), "Win32LastModifiedTime"};
|
||||
|
||||
protected final DavResourceFactory factory;
|
||||
protected final CryptoResourceFactory factory;
|
||||
protected final DavResourceLocator locator;
|
||||
protected final DavSession session;
|
||||
protected final LockManager lockManager;
|
||||
protected final Cryptor cryptor;
|
||||
protected final Path filePath;
|
||||
protected final DavPropertySet properties;
|
||||
|
||||
protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) {
|
||||
this.factory = factory;
|
||||
this.locator = locator;
|
||||
this.session = session;
|
||||
this.lockManager = lockManager;
|
||||
this.cryptor = cryptor;
|
||||
this.filePath = filePath;
|
||||
this.properties = new DavPropertySet();
|
||||
this.determineProperties();
|
||||
if (filePath != null && Files.exists(filePath)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + filePath.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -75,8 +87,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
return Files.exists(path);
|
||||
return Files.exists(filePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,16 +118,13 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
return Files.getLastModifiedTime(path).toMillis();
|
||||
return Files.getLastModifiedTime(filePath).toMillis();
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void determineProperties();
|
||||
|
||||
@Override
|
||||
public DavPropertyName[] getPropertyNames() {
|
||||
return getProperties().getPropertyNames();
|
||||
@@ -136,25 +144,27 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
public void setProperty(DavProperty<?> property) throws DavException {
|
||||
getProperties().add(property);
|
||||
|
||||
LOG.info("Set property {}", property.getName());
|
||||
|
||||
try {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) {
|
||||
final String createDateStr = (String) property.getValue();
|
||||
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(null, null, createTime);
|
||||
LOG.info("Updating Creation Date: {}", createTime.toString());
|
||||
} else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) {
|
||||
final String lastModifiedTimeStr = (String) property.getValue();
|
||||
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
|
||||
LOG.trace("Set property {}", property.getName());
|
||||
|
||||
final String namespacelessPropertyName = property.getName().getName();
|
||||
if (Files.exists(filePath)) {
|
||||
try {
|
||||
if (Arrays.asList(DAV_CREATIONDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
|
||||
final String createDateStr = (String) property.getValue();
|
||||
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(null, null, createTime);
|
||||
LOG.debug("Updating Creation Date: {}", createTime.toString());
|
||||
} else if (Arrays.asList(DAV_MODIFIEDDATE_PROPNAMES).contains(namespacelessPropertyName) && property.getValue() instanceof String) {
|
||||
final String lastModifiedTimeStr = (String) property.getValue();
|
||||
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
|
||||
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.debug("Updating Last Modified Date: {}", lastModifiedTime.toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +196,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
|
||||
final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
|
||||
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
|
||||
try {
|
||||
return getFactory().createResource(parentLocator, session);
|
||||
@@ -196,49 +206,37 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(DavResource dest) throws DavException {
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// move:
|
||||
public final void move(DavResource dest) throws DavException {
|
||||
if (dest instanceof AbstractEncryptedNode) {
|
||||
try {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
|
||||
this.move((AbstractEncryptedNode) dest);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + this.getResourcePath() + " to " + dest.getResourcePath());
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error moving file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void move(AbstractEncryptedNode dest) throws DavException, IOException;
|
||||
|
||||
@Override
|
||||
public void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
final Path src = ResourcePathUtils.getPhysicalPath(this);
|
||||
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
|
||||
try {
|
||||
// check for conflicts:
|
||||
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
|
||||
throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString());
|
||||
}
|
||||
|
||||
// copy:
|
||||
public final void copy(DavResource dest, boolean shallow) throws DavException {
|
||||
if (dest instanceof AbstractEncryptedNode) {
|
||||
try {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
this.copy((AbstractEncryptedNode) dest, shallow);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + this.getResourcePath() + " to " + dest.getResourcePath());
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error copying file from " + src.toString() + " to " + dst.toString());
|
||||
throw new IORuntimeException(e);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + dest.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException;
|
||||
|
||||
@Override
|
||||
public boolean isLockable(Type type, Scope scope) {
|
||||
return true;
|
||||
@@ -281,7 +279,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceFactory getFactory() {
|
||||
public CryptoResourceFactory getFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
|
||||
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
|
||||
super(normalMap, reverseMap, inverseBidiMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
|
||||
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
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;
|
||||
}
|
||||
|
||||
// resourcePath == repositoryPath. No encryption here.
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String href) {
|
||||
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
|
||||
|
||||
final String relativeCleartextPath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
|
||||
return new CleartextLocator(relativeCleartextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
|
||||
return new CleartextLocator(resourcePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
|
||||
return new CleartextLocator(path);
|
||||
}
|
||||
|
||||
private class CleartextLocator implements DavResourceLocator {
|
||||
|
||||
private final String relativeCleartextPath;
|
||||
|
||||
private CleartextLocator(String relativeCleartextPath) {
|
||||
this.relativeCleartextPath = FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return pathPrefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return relativeCleartextPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspacePath() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspaceName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(DavResourceLocator locator) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(String workspaceName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@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.equals(fullPrefix) || !href.endsWith("/");
|
||||
if (isCollection) {
|
||||
return href.concat("/");
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRootLocation() {
|
||||
return Strings.isEmpty(relativeCleartextPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavLocatorFactory getFactory() {
|
||||
return CleartextLocatorFactory.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepositoryPath() {
|
||||
return relativeCleartextPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Locator: " + relativeCleartextPath + " (Prefix: " + pathPrefix + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return relativeCleartextPath.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof CleartextLocator) {
|
||||
final CleartextLocator other = (CleartextLocator) obj;
|
||||
return relativeCleartextPath == null && other.relativeCleartextPath == null || relativeCleartextPath.equals(other.relativeCleartextPath);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
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;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
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.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
|
||||
|
||||
private static final String RANGE_BYTE_PREFIX = "bytes=";
|
||||
private static final char RANGE_SET_SEP = ',';
|
||||
private static final char RANGE_SEP = '-';
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
private final Path dataRoot;
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
|
||||
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
|
||||
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
this.dataRoot = vaultRootPath.resolve("d");
|
||||
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
if (locator.isRootLocation()) {
|
||||
return createRootDirectory(locator, request.getDavSession());
|
||||
}
|
||||
|
||||
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
|
||||
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
if (locator.isRootLocation()) {
|
||||
return createRootDirectory(locator, session);
|
||||
}
|
||||
|
||||
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 {
|
||||
return createDirectory(locator, session, existingDirectoryFile);
|
||||
}
|
||||
|
||||
DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
|
||||
return createFile(locator, session, existingFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if a partial response should be generated according to an If-Range precondition.
|
||||
*/
|
||||
private boolean isIfRangePreconditionFulfilled(String ifRangeHeader, Path filePath) throws DavException {
|
||||
if (ifRangeHeader == null) {
|
||||
// no header set -> fulfilled implicitly
|
||||
return true;
|
||||
} else {
|
||||
try {
|
||||
final FileTime expectedTime = FileTimeUtils.fromRfc1123String(ifRangeHeader);
|
||||
final FileTime actualTime = Files.getLastModifiedTime(filePath);
|
||||
return expectedTime.compareTo(actualTime) == 0;
|
||||
} catch (DateTimeParseException e) {
|
||||
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "Unsupported If-Range header: " + ifRangeHeader);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return <code>true</code> if and only if exactly one byte range has been requested.
|
||||
*/
|
||||
private boolean isRangeSatisfiable(String rangeHeader) {
|
||||
assert rangeHeader != null;
|
||||
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
|
||||
return false;
|
||||
}
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length != 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the given range header field, if it is supported. Only headers containing a single byte range are supported.<br/>
|
||||
* <code>
|
||||
* bytes=100-200<br/>
|
||||
* bytes=-500<br/>
|
||||
* bytes=1000-
|
||||
* </code>
|
||||
*
|
||||
* @return Tuple of left and right range.
|
||||
* @throws DavException HTTP statuscode 400 for malformed requests.
|
||||
* @throws IllegalArgumentException If the given rangeHeader is not satisfiable. Check with {@link #isRangeSatisfiable(String)} before.
|
||||
*/
|
||||
private Pair<String, String> getRequestRange(String rangeHeader) throws DavException {
|
||||
assert rangeHeader != null;
|
||||
if (!rangeHeader.startsWith(RANGE_BYTE_PREFIX)) {
|
||||
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
|
||||
}
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, RANGE_BYTE_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length != 1) {
|
||||
throw new IllegalArgumentException("Unsatisfiable range. Should have generated 416 resonse.");
|
||||
}
|
||||
final String byteRange = byteRanges[0];
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new DavException(DavServletResponse.SC_BAD_REQUEST, "malformed range header: " + rangeHeader);
|
||||
}
|
||||
return new ImmutablePair<>(bytePos[0], bytePos[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws NonExistingParentException If one ancestor of the enrypted path is missing
|
||||
*/
|
||||
Path getEncryptedFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = 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 IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws NonExistingParentException If one ancestor of the enrypted path is missing
|
||||
*/
|
||||
Path getEncryptedDirectoryFilePath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
final String parentCleartextPath = 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 IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param createNonExisting if <code>false</code>, a {@link NonExistingParentException} will be thrown for missing ancestors.
|
||||
* @return Absolute directory path for a given cleartext directory resourcePath.
|
||||
* @throws NonExistingParentException if one ancestor directory is missing.
|
||||
*/
|
||||
private Path getEncryptedDirectoryPath(String relativeCleartextPath, boolean createNonExisting) throws NonExistingParentException {
|
||||
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
|
||||
try {
|
||||
final Path result;
|
||||
if (Strings.isEmpty(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 = 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);
|
||||
}
|
||||
Files.createDirectories(result);
|
||||
return result;
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
|
||||
}
|
||||
|
||||
private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
final Path rootFile = dataRoot.resolve(ROOT_FILE);
|
||||
final Path rootDir = filenameTranslator.getEncryptedDirectoryPath("");
|
||||
try {
|
||||
// make sure, root dir always exists.
|
||||
// create dir first (because it fails silently, if alreay existing)
|
||||
Files.createDirectories(rootDir);
|
||||
Files.createFile(rootFile);
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
// no-op
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE));
|
||||
}
|
||||
|
||||
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) {
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
class CryptoWarningHandler {
|
||||
|
||||
private final Collection<String> resourcesWithInvalidMac;
|
||||
private final Collection<String> whitelistedResources;
|
||||
|
||||
public CryptoWarningHandler(Collection<String> resourcesWithInvalidMac, Collection<String> whitelistedResources) {
|
||||
this.resourcesWithInvalidMac = resourcesWithInvalidMac;
|
||||
this.whitelistedResources = whitelistedResources;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,242 +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.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.util.EncodeUtil;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.SensitiveDataSwipeListener;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
|
||||
|
||||
class DavLocatorFactoryImpl implements DavLocatorFactory, SensitiveDataSwipeListener, CryptorIOSupport {
|
||||
|
||||
private static final int MAX_CACHED_PATHS = 10000;
|
||||
private final Path fsRoot;
|
||||
private final Cryptor cryptor;
|
||||
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
|
||||
|
||||
DavLocatorFactoryImpl(String fsRoot, Cryptor cryptor) {
|
||||
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
|
||||
this.cryptor = cryptor;
|
||||
cryptor.addSensitiveDataSwipeListener(this);
|
||||
}
|
||||
|
||||
/* DavLocatorFactory */
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String href) {
|
||||
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
|
||||
|
||||
final String resourcePath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
|
||||
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DecryptFailedRuntimeException, which should a checked exception, but Jackrabbit doesn't allow that.
|
||||
*/
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
|
||||
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
|
||||
|
||||
try {
|
||||
final String resourcePath = (isResourcePath) ? path : getResourcePath(path);
|
||||
return new DavResourceLocatorImpl(fullPrefix, resourcePath);
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new DecryptFailedRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
|
||||
try {
|
||||
return createResourceLocator(prefix, workspacePath, resourcePath, true);
|
||||
} catch (DecryptFailedRuntimeException e) {
|
||||
throw new IllegalStateException("Tried to decrypt resourcePath. Only repositoryPaths can be encrypted.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/* Encryption/Decryption */
|
||||
|
||||
/**
|
||||
* @return Encrypted absolute paths on the file system.
|
||||
*/
|
||||
private String getRepositoryPath(String resourcePath) {
|
||||
String encryptedPath = pathCache.get(resourcePath);
|
||||
if (encryptedPath == null) {
|
||||
encryptedPath = encryptRepositoryPath(resourcePath);
|
||||
pathCache.put(resourcePath, encryptedPath);
|
||||
}
|
||||
return encryptedPath;
|
||||
}
|
||||
|
||||
private String encryptRepositoryPath(String resourcePath) {
|
||||
if (resourcePath == null) {
|
||||
return fsRoot.toString();
|
||||
}
|
||||
final String encryptedRepoPath = cryptor.encryptPath(resourcePath, FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return fsRoot.resolve(encryptedRepoPath).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Decrypted path for use in URIs.
|
||||
*/
|
||||
private String getResourcePath(String repositoryPath) throws DecryptFailedException {
|
||||
String decryptedPath = pathCache.getKey(repositoryPath);
|
||||
if (decryptedPath == null) {
|
||||
decryptedPath = decryptResourcePath(repositoryPath);
|
||||
pathCache.put(decryptedPath, repositoryPath);
|
||||
}
|
||||
return decryptedPath;
|
||||
}
|
||||
|
||||
private String decryptResourcePath(String repositoryPath) throws DecryptFailedException {
|
||||
final Path absRepoPath = FileSystems.getDefault().getPath(repositoryPath);
|
||||
if (fsRoot.equals(absRepoPath)) {
|
||||
return null;
|
||||
} else {
|
||||
final Path relativeRepositoryPath = fsRoot.relativize(absRepoPath);
|
||||
final String resourcePath = cryptor.decryptPath(relativeRepositoryPath.toString(), FileSystems.getDefault().getSeparator().charAt(0), '/', this);
|
||||
return resourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
/* CryptorIOSupport */
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) throws IOException {
|
||||
final Path metaDataFile = fsRoot.resolve(encryptedPath);
|
||||
if (!Files.isReadable(metaDataFile)) {
|
||||
return null;
|
||||
} else {
|
||||
return Files.readAllBytes(metaDataFile);
|
||||
}
|
||||
}
|
||||
|
||||
/* SensitiveDataSwipeListener */
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
pathCache.clear();
|
||||
}
|
||||
|
||||
/* Locator */
|
||||
|
||||
private class DavResourceLocatorImpl implements DavResourceLocator {
|
||||
|
||||
private final String prefix;
|
||||
private final String resourcePath;
|
||||
|
||||
private DavResourceLocatorImpl(String prefix, String resourcePath) {
|
||||
this.prefix = prefix;
|
||||
this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspacePath() {
|
||||
return isRootLocation() ? null : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspaceName() {
|
||||
return getPrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(DavResourceLocator locator) {
|
||||
return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(String workspaceName) {
|
||||
return getWorkspaceName().equals(workspaceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHref(boolean isCollection) {
|
||||
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
|
||||
final String href = getPrefix().concat(encodedResourcePath);
|
||||
if (isCollection && !href.endsWith("/")) {
|
||||
return href.concat("/");
|
||||
} else if (!isCollection && href.endsWith("/")) {
|
||||
return href.substring(0, href.length() - 1);
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRootLocation() {
|
||||
return getResourcePath() == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavLocatorFactory getFactory() {
|
||||
return DavLocatorFactoryImpl.this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRepositoryPath() {
|
||||
return DavLocatorFactoryImpl.this.getRepositoryPath(getResourcePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final HashCodeBuilder builder = new HashCodeBuilder();
|
||||
builder.append(prefix);
|
||||
builder.append(resourcePath);
|
||||
return builder.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof DavResourceLocatorImpl) {
|
||||
final DavResourceLocatorImpl other = (DavResourceLocatorImpl) obj;
|
||||
final EqualsBuilder builder = new EqualsBuilder();
|
||||
builder.append(this.prefix, other.prefix);
|
||||
builder.append(this.resourcePath, other.resourcePath);
|
||||
return builder.isEquals();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,88 +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.jackrabbit;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.commons.httpclient.HttpStatus;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavMethods;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletRequest;
|
||||
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.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode;
|
||||
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
class DavResourceFactoryImpl implements DavResourceFactory {
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
|
||||
DavResourceFactoryImpl(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
|
||||
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), request);
|
||||
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
return createFile(locator, request.getDavSession());
|
||||
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
return createDirectory(locator, request.getDavSession());
|
||||
} else {
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(locator);
|
||||
|
||||
if (path != null && Files.isRegularFile(path)) {
|
||||
return createFile(locator, session);
|
||||
} else if (path != null && Files.isDirectory(path)) {
|
||||
return createDirectory(locator, session);
|
||||
} else {
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
}
|
||||
|
||||
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
|
||||
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
|
||||
}
|
||||
|
||||
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedFile(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session) {
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||
return new NonExistingNode(this, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*******************************************************************************
|
||||
* 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.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.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;
|
||||
|
||||
class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
private String directoryId;
|
||||
private Path directoryPath;
|
||||
|
||||
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) {
|
||||
super(factory, locator, session, lockManager, cryptor, filePath);
|
||||
this.filenameTranslator = filenameTranslator;
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Path or <code>null</code>, if directory does not yet exist.
|
||||
*/
|
||||
protected synchronized String getDirectoryId() {
|
||||
if (directoryId == null) {
|
||||
try {
|
||||
directoryId = filenameTranslator.getDirectoryId(filePath, false);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
return directoryId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Path or <code>null</code>, if directory does not yet exist.
|
||||
*/
|
||||
private synchronized Path getDirectoryPath() {
|
||||
if (directoryPath == null) {
|
||||
final String dirId = getDirectoryId();
|
||||
if (dirId != null) {
|
||||
directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
}
|
||||
}
|
||||
return directoryPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
try {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
return -1;
|
||||
} else {
|
||||
return Files.getLastModifiedTime(dirPath).toMillis();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
if (resource instanceof AbstractEncryptedNode) {
|
||||
addMember((AbstractEncryptedNode) resource, inputContext);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + resource.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void addMember(AbstractEncryptedNode childResource, InputContext inputContext) throws DavException {
|
||||
if (childResource.isCollection()) {
|
||||
this.addMemberDir(childResource.getLocator(), inputContext);
|
||||
} else {
|
||||
this.addMemberFile(childResource.getLocator(), inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
|
||||
final String ciphertextDirName = filenameTranslator.getEncryptedDirFileName(cleartextDirName);
|
||||
final Path dirFilePath = dirPath.resolve(ciphertextDirName);
|
||||
final String directoryId = filenameTranslator.getDirectoryId(dirFilePath, true);
|
||||
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
Files.createDirectories(directoryPath);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
try {
|
||||
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 SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, 0L, FILE_HEADER_LENGTH, false)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), c);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} 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());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(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 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 DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
|
||||
final DavResource resource;
|
||||
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
|
||||
resource = factory.createChildDirectoryResource(childLocator, session, childPath);
|
||||
} else {
|
||||
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
|
||||
resource = factory.createChildFileResource(childLocator, session, childPath);
|
||||
}
|
||||
result.add(resource);
|
||||
} catch (DecryptFailedException e) {
|
||||
LOG.warn("Decryption of resource failed: " + childPath);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return new DavResourceIteratorImpl(result);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (DavException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new DavRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
if (member instanceof AbstractEncryptedNode) {
|
||||
removeMember((AbstractEncryptedNode) member);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported resource type: " + member.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
private void removeMember(AbstractEncryptedNode member) throws DavException {
|
||||
final Path dirPath = getDirectoryPath();
|
||||
if (dirPath == null) {
|
||||
throw new DavException(DavServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
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);
|
||||
} else {
|
||||
ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
}
|
||||
final Path memberPath = dirPath.resolve(ciphertextFilename);
|
||||
Files.deleteIfExists(memberPath);
|
||||
} catch (FileNotFoundException e) {
|
||||
// no-op
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
|
||||
// when moving a directory we only need to move the file (actual dir is ID-dependent and won't change)
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
// move:
|
||||
Files.createDirectories(dstPath.getParent());
|
||||
try {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
||||
final Path dstDirFilePath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstDirFilePath = ((NonExistingNode) dest).materializeDirFilePath();
|
||||
} else {
|
||||
dstDirFilePath = dest.filePath;
|
||||
}
|
||||
|
||||
// copy dirFile:
|
||||
final String srcDirId = getDirectoryId();
|
||||
if (srcDirId == null) {
|
||||
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);
|
||||
SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, false)) {
|
||||
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
|
||||
// copy actual dir:
|
||||
if (!shallow) {
|
||||
copyDirectoryContents(srcDirId, dstDirId);
|
||||
} else {
|
||||
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
|
||||
Files.createDirectories(dstDirPath);
|
||||
}
|
||||
}
|
||||
|
||||
private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException {
|
||||
final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId);
|
||||
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
|
||||
Files.createDirectories(dstDirPath);
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER);
|
||||
for (final Path srcChildPath : directoryStream) {
|
||||
final String childName = srcChildPath.getFileName().toString();
|
||||
final Path dstChildPath = dstDirPath.resolve(childName);
|
||||
if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) {
|
||||
try {
|
||||
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
} else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) {
|
||||
final String srcSubdirId = filenameTranslator.getDirectoryId(srcChildPath, false);
|
||||
final String dstSubdirId = filenameTranslator.getDirectoryId(dstChildPath, true);
|
||||
copyDirectoryContents(srcSubdirId, dstSubdirId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*******************************************************************************
|
||||
* 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.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.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.MacAuthenticationFailedException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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);
|
||||
if (filePath == null) {
|
||||
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); 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()));
|
||||
}
|
||||
} catch (OverlappingFileLockException e) {
|
||||
// file header currently locked, report -1 for unknown size.
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
|
||||
// don't add content length DAV property
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + filePath.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
this.contentLength = contentLength;
|
||||
}
|
||||
|
||||
public Long getContentLength() {
|
||||
return contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not add member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Can not list members of file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not remove member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
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()) {
|
||||
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||
cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
|
||||
final Path srcPath = filePath;
|
||||
final Path dstPath;
|
||||
if (dest instanceof NonExistingNode) {
|
||||
dstPath = ((NonExistingNode) dest).materializeFilePath();
|
||||
} else {
|
||||
dstPath = dest.filePath;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||
} catch (AtomicMoveNotSupportedException e) {
|
||||
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.eclipse.jetty.http.HttpHeader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Delivers only the requested range of bytes from a file.
|
||||
*
|
||||
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
|
||||
*/
|
||||
class EncryptedFilePart extends EncryptedFile {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
|
||||
|
||||
private final Pair<Long, Long> range;
|
||||
|
||||
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);
|
||||
|
||||
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 {
|
||||
range = new ImmutablePair<Long, Long>(lower, upper);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("Invalid byte range: " + requestRange, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
assert Files.isRegularFile(filePath);
|
||||
assert this.contentLength != null;
|
||||
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
|
||||
if (rangeLength <= 0) {
|
||||
// unsatisfiable content range:
|
||||
outputContext.setContentLength(0);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getRight(), range.getRight(), contentLength));
|
||||
LOG.debug("Unsatisfiable content range: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
return;
|
||||
} else {
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength));
|
||||
}
|
||||
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) {
|
||||
if (outputContext.hasStream()) {
|
||||
final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath());
|
||||
cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.trace("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("bytes %d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/*******************************************************************************
|
||||
* 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.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
interface FileConstants {
|
||||
|
||||
/**
|
||||
* Number of bytes in the file header.
|
||||
*/
|
||||
long FILE_HEADER_LENGTH = 104;
|
||||
|
||||
/**
|
||||
* Allow range requests for files > 32MiB.
|
||||
*/
|
||||
long RANGE_REQUEST_LOWER_LIMIT = 32 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
|
||||
* Parent folder path uses up to 58 chars (sha256 -> 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.<br/>
|
||||
* 128 chars would be enought for up to 80 plaintext chars. Also we need up to 9 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
|
||||
*/
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 137;
|
||||
|
||||
/**
|
||||
* Dummy file, on which file attributes can be stored for the root directory.
|
||||
*/
|
||||
String ROOT_FILE = "root";
|
||||
|
||||
/**
|
||||
* For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String DIR_EXT = ".dir";
|
||||
|
||||
/**
|
||||
* For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_DIR_EXT = ".lng.dir";
|
||||
|
||||
/**
|
||||
* For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String FILE_EXT = ".file";
|
||||
|
||||
/**
|
||||
* For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_FILE_EXT = ".lng.file";
|
||||
|
||||
/**
|
||||
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
|
||||
*/
|
||||
int LONG_NAME_PREFIX_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* Matches valid encrypted filenames (both normal and long filenames - see {@link #ENCRYPTED_FILENAME_LENGTH_LIMIT}).
|
||||
*/
|
||||
PathMatcher ENCRYPTED_FILE_MATCHER = new PathMatcher() {
|
||||
|
||||
private final Pattern BASIC_NAME_PATTERN = Pattern.compile("^[a-z2-7]+=*$", Pattern.CASE_INSENSITIVE);
|
||||
private final Pattern LONG_NAME_PATTERN = Pattern.compile("^[a-z2-7]{8}[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
@Override
|
||||
public boolean matches(Path path) {
|
||||
final String filename = path.getFileName().toString();
|
||||
if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT);
|
||||
return LONG_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT);
|
||||
return BASIC_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT);
|
||||
return LONG_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT);
|
||||
return BASIC_NAME_PATTERN.matcher(basename).matches();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}.
|
||||
*/
|
||||
Filter<Path> DIRECTORY_CONTENT_FILTER = new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Instant;
|
||||
@@ -0,0 +1,226 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
class FilenameTranslator implements FileConstants {
|
||||
|
||||
private static final int MAX_CACHED_DIRECTORY_IDS = 5000;
|
||||
private static final int MAX_CACHED_METADATA_FILES = 1000;
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final Path dataRoot;
|
||||
private final Path metadataRoot;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final Map<Pair<Path, FileTime>, String> directoryIdCache = new LRUMap<>(MAX_CACHED_DIRECTORY_IDS); // <directoryFile, directoryId>
|
||||
private final Map<Pair<Path, FileTime>, LongFilenameMetadata> metadataCache = new LRUMap<>(MAX_CACHED_METADATA_FILES); // <metadataFile, metadata>
|
||||
|
||||
public FilenameTranslator(Cryptor cryptor, Path vaultRoot) {
|
||||
this.cryptor = cryptor;
|
||||
this.dataRoot = vaultRoot.resolve("d");
|
||||
this.metadataRoot = vaultRoot.resolve("m");
|
||||
}
|
||||
|
||||
/* file and directory name en/decryption */
|
||||
|
||||
public String getDirectoryId(Path directoryFile, boolean createIfNonexisting) throws IOException {
|
||||
try {
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
|
||||
String directoryId = directoryIdCache.get(key);
|
||||
if (directoryId == null) {
|
||||
directoryId = new String(readAllBytesAtomically(directoryFile), StandardCharsets.UTF_8);
|
||||
directoryIdCache.put(key, directoryId);
|
||||
}
|
||||
return directoryId;
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
if (createIfNonexisting) {
|
||||
final String directoryId = UUID.randomUUID().toString();
|
||||
writeAllBytesAtomically(directoryFile, directoryId.getBytes(StandardCharsets.UTF_8));
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(directoryFile, Files.getLastModifiedTime(directoryFile));
|
||||
directoryIdCache.put(key, directoryId);
|
||||
return directoryId;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Path getEncryptedDirectoryPath(String directoryId) {
|
||||
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
return dataRoot.resolve(encrypted);
|
||||
}
|
||||
|
||||
public String getEncryptedFilename(String cleartextFilename) throws IOException {
|
||||
return getEncryptedFilename(cleartextFilename, FILE_EXT, LONG_FILE_EXT);
|
||||
}
|
||||
|
||||
public String getEncryptedDirFileName(String cleartextDirName) throws IOException {
|
||||
return getEncryptedFilename(cleartextDirName, DIR_EXT, LONG_DIR_EXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in {@link FileConstants#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* For filenames longer than this limit we use a metadata file containing the full encrypted paths. For the actual filename a unique alternative is created by concatenating the metadata filename
|
||||
* and a unique id.
|
||||
*/
|
||||
private String getEncryptedFilename(String cleartextFilename, String basicExt, String longExt) throws IOException {
|
||||
final String ivAndCiphertext = cryptor.encryptFilename(cleartextFilename);
|
||||
if (ivAndCiphertext.length() + basicExt.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
final String longFilename = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + longExt;
|
||||
this.writeMetadata(metadataGroup, metadata);
|
||||
return longFilename;
|
||||
} else {
|
||||
return ivAndCiphertext + basicExt;
|
||||
}
|
||||
}
|
||||
|
||||
public String getCleartextFilename(String encryptedFilename) throws DecryptFailedException, IOException {
|
||||
final String ciphertext;
|
||||
if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_FILE_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, FILE_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, FILE_EXT);
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_DIR_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, DIR_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, DIR_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + encryptedFilename);
|
||||
}
|
||||
return cryptor.decryptFilename(ciphertext);
|
||||
}
|
||||
|
||||
/* 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 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 SilentlyFailingFileLock lock = new SilentlyFailingFileLock(c, true)) {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||
c.read(buffer);
|
||||
return buffer.array();
|
||||
}
|
||||
}
|
||||
|
||||
/* Long name metadata files */
|
||||
|
||||
private void writeMetadata(String metadataGroup, LongFilenameMetadata metadata) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
Files.createDirectories(metadataDir);
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
|
||||
// evict previously cached entries:
|
||||
try {
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
|
||||
metadataCache.remove(key);
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
// didn't exist yet? then we don't need to do anything anyway.
|
||||
}
|
||||
|
||||
// write:
|
||||
final byte[] metadataContent = objectMapper.writeValueAsBytes(metadata);
|
||||
writeAllBytesAtomically(metadataFile, metadataContent);
|
||||
|
||||
// add to cache:
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
|
||||
metadataCache.put(key, metadata);
|
||||
}
|
||||
|
||||
private LongFilenameMetadata readMetadata(String metadataGroup) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
try {
|
||||
// use cached metadata, if possible:
|
||||
final Pair<Path, FileTime> key = ImmutablePair.of(metadataFile, Files.getLastModifiedTime(metadataFile));
|
||||
LongFilenameMetadata metadata = metadataCache.get(key);
|
||||
// else read from filesystem:
|
||||
if (metadata == null) {
|
||||
final byte[] metadataContent = readAllBytesAtomically(metadataFile);
|
||||
metadata = objectMapper.readValue(metadataContent, LongFilenameMetadata.class);
|
||||
metadataCache.put(key, metadata);
|
||||
}
|
||||
return metadata;
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
// not yet existing:
|
||||
return new LongFilenameMetadata();
|
||||
}
|
||||
}
|
||||
|
||||
private static class LongFilenameMetadata implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 6214509403824421320L;
|
||||
|
||||
@JsonDeserialize(as = DualHashBidiMap.class)
|
||||
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
|
||||
return encryptedFilenames.get(uuid);
|
||||
}
|
||||
|
||||
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
|
||||
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
|
||||
if (uuid == null) {
|
||||
uuid = UUID.randomUUID();
|
||||
encryptedFilenames.put(uuid, encryptedFilename);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
// used by jackson
|
||||
@SuppressWarnings("unused")
|
||||
public BidiMap<UUID, String> getEncryptedFilenames() {
|
||||
return encryptedFilenames;
|
||||
}
|
||||
|
||||
// used by jackson
|
||||
@SuppressWarnings("unused")
|
||||
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
|
||||
this.encryptedFilenames = encryptedFilenames;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
@@ -0,0 +1,104 @@
|
||||
/*******************************************************************************
|
||||
* 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.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.LockManager;
|
||||
import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.jackrabbit.CryptoResourceFactory.NonExistingParentException;
|
||||
|
||||
class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode destination) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setProperty(DavProperty<?> property) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return lazily resolved directory file path, e.g. needed during MOVE operations.
|
||||
*/
|
||||
public Path materializeDirFilePath() {
|
||||
try {
|
||||
return factory.getEncryptedDirectoryFilePath(locator.getResourcePath(), true);
|
||||
} catch (NonExistingParentException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.NonReadableChannelException;
|
||||
import java.nio.channels.NonWritableChannelException;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Instances of this class wrap a file lock, that is created upon construction and destroyed by {@link #close()}.
|
||||
*
|
||||
* If the construction fails (e.g. if the file system does not support locks) no exception will be thrown and no lock is created.
|
||||
*/
|
||||
class SilentlyFailingFileLock implements AutoCloseable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SilentlyFailingFileLock.class);
|
||||
|
||||
private final FileLock lock;
|
||||
|
||||
/**
|
||||
* Invokes #SilentlyFailingFileLock(FileChannel, long, long, boolean) with a position of 0 and a size of {@link Long#MAX_VALUE}.
|
||||
*/
|
||||
SilentlyFailingFileLock(FileChannel channel, boolean shared) {
|
||||
this(channel, 0L, Long.MAX_VALUE, shared);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NonReadableChannelException If shared is true this channel was not opened for reading
|
||||
* @throws NonWritableChannelException If shared is false but this channel was not opened for writing
|
||||
* @see FileChannel#lock(long, long, boolean)
|
||||
*/
|
||||
SilentlyFailingFileLock(FileChannel channel, long position, long size, boolean shared) {
|
||||
FileLock lock = null;
|
||||
try {
|
||||
lock = channel.tryLock(position, size, shared);
|
||||
} catch (IOException | OverlappingFileLockException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.warn("Unable to lock file.");
|
||||
}
|
||||
} finally {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (lock != null) {
|
||||
lock.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,41 +8,50 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
|
||||
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;
|
||||
|
||||
public WebDavServlet(final Cryptor cryptor) {
|
||||
public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection, final Collection<String> whitelistedResourceCollection) {
|
||||
super();
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = new CryptoWarningHandler(failingMacCollection, whitelistedResourceCollection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(ServletConfig config) throws ServletException {
|
||||
super.init(config);
|
||||
|
||||
davSessionProvider = new DavSessionProviderImpl();
|
||||
|
||||
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
|
||||
this.davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
|
||||
|
||||
this.davResourceFactory = new DavResourceFactoryImpl(cryptor);
|
||||
davSessionProvider = new DavSessionProviderImpl();
|
||||
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
|
||||
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -80,4 +89,30 @@ public class WebDavServlet extends AbstractWebdavServlet {
|
||||
this.davResourceFactory = resourceFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPut(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
long t0 = System.nanoTime();
|
||||
super.doPut(request, response, resource);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
long t1 = System.nanoTime();
|
||||
LOG.debug("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
long t0 = System.nanoTime();
|
||||
try {
|
||||
super.doGet(request, response, resource);
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("File integrity violation for " + resource.getLocator().getResourcePath());
|
||||
cryptoWarningHandler.macAuthFailed(resource.getLocator().getResourcePath());
|
||||
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
|
||||
}
|
||||
if (LOG.isDebugEnabled()) {
|
||||
long t1 = System.nanoTime();
|
||||
LOG.debug("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,183 +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.jackrabbit.resources;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
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.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.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.webdav.exceptions.DavRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
|
||||
public EncryptedDir(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
if (resource.isCollection()) {
|
||||
this.addMemberDir(resource, inputContext);
|
||||
} else {
|
||||
this.addMemberFile(resource, inputContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
try {
|
||||
Files.createDirectories(childPath);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create subdirectory.", e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
|
||||
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), channel);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
final Path dir = ResourcePathUtils.getPhysicalPath(this);
|
||||
try {
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
|
||||
for (final Path childPath : directoryStream) {
|
||||
try {
|
||||
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
|
||||
final DavResource resource = factory.createResource(childLocator, session);
|
||||
result.add(resource);
|
||||
} catch (DecryptFailedRuntimeException e) {
|
||||
LOG.warn("Decryption of resource failed: " + childPath);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return new DavResourceIteratorImpl(result);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (DavException e) {
|
||||
LOG.error("Exception during getMembers.", e);
|
||||
throw new DavRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
|
||||
try {
|
||||
if (Files.exists(memberPath)) {
|
||||
Files.walkFileTree(memberPath, new DeletingFileVisitor());
|
||||
}
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
if (Files.exists(path)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
// don't add any further properties
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all files and folders, it visits.
|
||||
*/
|
||||
private static class DeletingFileVisitor extends SimpleFileVisitor<Path> {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) throws IOException {
|
||||
if (attributes.isRegularFile()) {
|
||||
Files.delete(file);
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
|
||||
Files.delete(dir);
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +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.jackrabbit.resources;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.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;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EncryptedFile extends AbstractEncryptedNode {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||
|
||||
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not add member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Can not list members of file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Can not remove member to file.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
if (contentLength != null) {
|
||||
outputContext.setContentLength(contentLength);
|
||||
}
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptFile(channel, outputContext.getOutputStream());
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
LOG.warn("Unexpected end of stream (possibly client hung up).");
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("MAC authentication failed, file content {} might be compromised.", getLocator().getResourcePath());
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + path.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + path.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit.resources;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
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.DavResourceFactory;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Delivers only the requested range of bytes from a file.
|
||||
*
|
||||
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
|
||||
*/
|
||||
public 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 = '-';
|
||||
|
||||
/**
|
||||
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
|
||||
*/
|
||||
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
|
||||
|
||||
/**
|
||||
* 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(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (rangeHeader == null) {
|
||||
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
|
||||
}
|
||||
determineByteRanges(rangeHeader);
|
||||
}
|
||||
|
||||
private void determineByteRanges(String rangeHeader) {
|
||||
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
|
||||
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
|
||||
if (byteRanges.length == 0) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
for (final String byteRange : byteRanges) {
|
||||
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
|
||||
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
|
||||
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
|
||||
if (lower > upper) {
|
||||
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
|
||||
}
|
||||
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return One range, that spans all requested ranges.
|
||||
*/
|
||||
private Pair<Long, Long> getUnionRange(Long fileSize) {
|
||||
final long lastByte = fileSize - 1;
|
||||
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
|
||||
for (Pair<Long, Long> range : requestedContentRanges) {
|
||||
final long left;
|
||||
final long right;
|
||||
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
|
||||
left = lastByte - range.getRight();
|
||||
right = lastByte;
|
||||
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
|
||||
left = range.getLeft();
|
||||
right = lastByte;
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
final Path path = ResourcePathUtils.getPhysicalPath(this);
|
||||
if (Files.exists(path)) {
|
||||
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
|
||||
final Long fileSize = cryptor.decryptedContentLength(channel);
|
||||
final Pair<Long, Long> range = getUnionRange(fileSize);
|
||||
final Long rangeLength = range.getRight() - range.getLeft() + 1;
|
||||
outputContext.setContentLength(rangeLength);
|
||||
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
|
||||
if (outputContext.hasStream()) {
|
||||
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
|
||||
}
|
||||
} catch (DecryptFailedException e) {
|
||||
throw new IOException("Error decrypting file " + path.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
|
||||
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +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.jackrabbit.resources;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
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.LockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
|
||||
public class NonExistingNode extends AbstractEncryptedNode {
|
||||
|
||||
public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.jackrabbit.resources;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
|
||||
public final class ResourcePathUtils {
|
||||
|
||||
private ResourcePathUtils() {
|
||||
throw new IllegalStateException("not instantiable");
|
||||
}
|
||||
|
||||
public static Path getPhysicalPath(DavResource resource) {
|
||||
return getPhysicalPath(resource.getLocator());
|
||||
}
|
||||
|
||||
public static Path getPhysicalPath(DavResourceLocator locator) {
|
||||
return FileSystems.getDefault().getPath(locator.getRepositoryPath());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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.5.2</version>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-aes</artifactId>
|
||||
<name>Cryptomator cryptographic module (AES)</name>
|
||||
|
||||
@@ -8,59 +8,57 @@
|
||||
******************************************************************************/
|
||||
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.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
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.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
import org.cryptomator.crypto.AbstractCryptor;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
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.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicConfiguration, FileNamingConventions {
|
||||
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/.
|
||||
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded
|
||||
* here: http://www.oracle.com/technetwork/java/javase/downloads/.
|
||||
*/
|
||||
private static final int AES_KEY_LENGTH_IN_BITS;
|
||||
|
||||
@@ -77,8 +75,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or
|
||||
* {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with {@link #swipeSensitiveData()}.
|
||||
* The decrypted master key. Its lifecycle starts with the construction of an Aes256Cryptor instance or {@link #decryptMasterKey(InputStream, CharSequence)}. Its lifecycle ends with
|
||||
* {@link #swipeSensitiveData()}.
|
||||
*/
|
||||
private SecretKey primaryMasterKey;
|
||||
|
||||
@@ -132,6 +130,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
|
||||
// save encrypted masterkey:
|
||||
final KeyFile keyfile = new KeyFile();
|
||||
keyfile.setVersion(KeyFile.CURRENT_VERSION);
|
||||
keyfile.setScryptSalt(kekSalt);
|
||||
keyfile.setScryptCostParam(SCRYPT_COST_PARAM);
|
||||
keyfile.setScryptBlockSize(SCRYPT_BLOCK_SIZE);
|
||||
@@ -148,17 +147,21 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
|
||||
*
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
|
||||
* password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
|
||||
* this case Java JCE needs to be installed.
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed.
|
||||
* @throws UnsupportedVaultException If the masterkey file is too old or too modern.
|
||||
*/
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
|
||||
try {
|
||||
// load encrypted masterkey:
|
||||
final KeyFile keyfile = objectMapper.readValue(in, KeyFile.class);
|
||||
|
||||
// check version
|
||||
if (keyfile.getVersion() != KeyFile.CURRENT_VERSION) {
|
||||
throw new UnsupportedVaultException(keyfile.getVersion(), KeyFile.CURRENT_VERSION);
|
||||
}
|
||||
|
||||
// check, whether the key length is supported:
|
||||
final int maxKeyLen = Cipher.getMaxAllowedKeyLength(AES_KEY_ALGORITHM);
|
||||
if (keyfile.getKeyLength() > maxKeyLen) {
|
||||
@@ -166,7 +169,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
|
||||
// derive key:
|
||||
final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(), keyfile.getScryptBlockSize(), AES_KEY_LENGTH_IN_BITS);
|
||||
final SecretKey kek = scrypt(password, keyfile.getScryptSalt(), keyfile.getScryptCostParam(), keyfile.getScryptBlockSize(), keyfile.getKeyLength());
|
||||
|
||||
// decrypt and check password by catching AEAD exception
|
||||
final Cipher decCipher = aesKeyWrapCipher(kek, Cipher.UNWRAP_MODE);
|
||||
@@ -184,7 +187,12 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveDataInternal() {
|
||||
public boolean isDestroyed() {
|
||||
return primaryMasterKey.isDestroyed() && hMacMasterKey.isDestroyed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
destroyQuietly(primaryMasterKey);
|
||||
destroyQuietly(hMacMasterKey);
|
||||
}
|
||||
@@ -205,7 +213,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,17 +229,16 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
}
|
||||
|
||||
private Cipher aesEcbCipher(SecretKey key, int cipherMode) {
|
||||
private Cipher aesCbcCipher(SecretKey key, byte[] iv, int cipherMode) {
|
||||
try {
|
||||
final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER);
|
||||
cipher.init(cipherMode, key);
|
||||
final Cipher cipher = Cipher.getInstance(AES_CBC_CIPHER);
|
||||
cipher.init(cipherMode, key, new IvParameterSpec(iv));
|
||||
return cipher;
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException("Invalid key.", ex);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
|
||||
throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidAlgorithmParameterException ex) {
|
||||
throw new AssertionError("Every implementation of the Java platform is required to support AES/CBC/PKCS5Padding, which accepts an IV", ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Mac hmacSha256(SecretKey key) {
|
||||
@@ -246,6 +253,14 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
}
|
||||
|
||||
private MessageDigest sha256() {
|
||||
try {
|
||||
return MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("Every implementation of the Java platform is required to support Sha-256");
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] randomData(int length) {
|
||||
final byte[] result = new byte[length];
|
||||
securePrng.nextBytes(result);
|
||||
@@ -269,296 +284,456 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
try {
|
||||
final String[] cleartextPathComps = StringUtils.split(cleartextPath, cleartextPathSep);
|
||||
final List<String> encryptedPathComps = new ArrayList<>(cleartextPathComps.length);
|
||||
for (final String cleartext : cleartextPathComps) {
|
||||
final String encrypted = encryptPathComponent(cleartext, primaryMasterKey, hMacMasterKey, ioSupport);
|
||||
encryptedPathComps.add(encrypted);
|
||||
}
|
||||
return StringUtils.join(encryptedPathComps, encryptedPathSep);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new IllegalStateException("Unable to encrypt path: " + cleartextPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
|
||||
* Encryption will blow up the filename length due to aes block sizes and base32 encoding. The result may be too long for some old file
|
||||
* systems.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in
|
||||
* {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No
|
||||
* cryptographically secure hash is needed here. We just want an uniform distribution for better load balancing. All encrypted filenames
|
||||
* with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
|
||||
* alternative names are stored.<br/>
|
||||
* <br/>
|
||||
* These alternative names consist of the checksum, a unique id and a special file extension defined in
|
||||
* {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
|
||||
*/
|
||||
private String encryptPathComponent(final String cleartext, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException {
|
||||
final byte[] cleartextBytes = cleartext.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// encrypt:
|
||||
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes);
|
||||
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
|
||||
|
||||
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String groupPrefix = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
|
||||
final String alternativeFileName = groupPrefix + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
|
||||
this.storeMetadata(ioSupport, metadataFilename, metadata);
|
||||
return alternativeFileName;
|
||||
} else {
|
||||
return ivAndCiphertext + BASIC_FILE_EXT;
|
||||
}
|
||||
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
|
||||
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
|
||||
final byte[] hashed = sha256().digest(encryptedBytes);
|
||||
final String encryptedThenHashedPath = ENCRYPTED_FILENAME_CODEC.encodeAsString(hashed);
|
||||
return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
|
||||
try {
|
||||
final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
|
||||
final List<String> cleartextPathComps = new ArrayList<>(encryptedPathComps.length);
|
||||
for (final String encrypted : encryptedPathComps) {
|
||||
final String cleartext = decryptPathComponent(encrypted, primaryMasterKey, hMacMasterKey, ioSupport);
|
||||
cleartextPathComps.add(new String(cleartext));
|
||||
}
|
||||
return StringUtils.join(cleartextPathComps, cleartextPathSep);
|
||||
} catch (InvalidKeyException | IOException e) {
|
||||
throw new IllegalStateException("Unable to decrypt path: " + encryptedPath, e);
|
||||
}
|
||||
public String encryptFilename(String cleartextName) {
|
||||
final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
|
||||
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
|
||||
return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
|
||||
*/
|
||||
private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
|
||||
final String ciphertext;
|
||||
if (encrypted.endsWith(LONG_NAME_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEnd(encrypted, LONG_NAME_FILE_EXT);
|
||||
final String groupPrefix = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final String metadataFilename = groupPrefix + METADATA_FILE_EXT;
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataFilename);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (encrypted.endsWith(BASIC_FILE_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + encrypted);
|
||||
}
|
||||
|
||||
// decrypt:
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
|
||||
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes);
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertextName);
|
||||
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(primaryMasterKey, hMacMasterKey, encryptedBytes);
|
||||
return new String(cleartextBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LongFilenameMetadata getMetadata(CryptorIOSupport ioSupport, String metadataFile) throws IOException {
|
||||
final byte[] fileContent = ioSupport.readPathSpecificMetadata(metadataFile);
|
||||
if (fileContent == null) {
|
||||
return new LongFilenameMetadata();
|
||||
} else {
|
||||
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
|
||||
ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
|
||||
// skip 128bit IV + 256 bit MAC:
|
||||
encryptedFile.position(48);
|
||||
|
||||
// read encrypted value:
|
||||
final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer);
|
||||
|
||||
// return "unknown" value, if EOF
|
||||
if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) {
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// decrypt size:
|
||||
// read iv:
|
||||
final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
||||
headerBuf.position(0);
|
||||
headerBuf.get(iv);
|
||||
|
||||
// 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(72);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// calculate mac over first 72 bytes of header:
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.rewind();
|
||||
headerBuf.limit(72);
|
||||
headerMac.update(headerBuf);
|
||||
|
||||
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
|
||||
if (!macMatches) {
|
||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
||||
}
|
||||
|
||||
// decrypt sensitive header data:
|
||||
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
||||
final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
||||
|
||||
return fileSize;
|
||||
}
|
||||
|
||||
private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) {
|
||||
try {
|
||||
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
|
||||
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
|
||||
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
|
||||
return fileSizeBuffer.getLong();
|
||||
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
|
||||
return sizeCipher.doFinal(ciphertextBytes);
|
||||
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void encryptedContentLength(SeekableByteChannel encryptedFile, Long contentLength) throws IOException {
|
||||
final ByteBuffer encryptedFileSizeBuffer;
|
||||
|
||||
// encrypt content length in ECB mode (content length is less than one block):
|
||||
private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
|
||||
try {
|
||||
final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
|
||||
fileSizeBuffer.putLong(contentLength);
|
||||
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
|
||||
final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
|
||||
encryptedFileSizeBuffer = ByteBuffer.wrap(encryptedFileSize);
|
||||
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
|
||||
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);
|
||||
}
|
||||
|
||||
// skip 128bit IV + 256 bit MAC:
|
||||
encryptedFile.position(48);
|
||||
|
||||
// write result:
|
||||
encryptedFile.write(encryptedFileSizeBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
// read iv:
|
||||
encryptedFile.position(0);
|
||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numIvBytesRead = encryptedFile.read(countingIv);
|
||||
|
||||
// init mac:
|
||||
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
|
||||
|
||||
// read stored mac:
|
||||
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
|
||||
final int numMacBytesRead = encryptedFile.read(storedMac);
|
||||
|
||||
// read file size:
|
||||
final Long fileSize = decryptedContentLength(encryptedFile);
|
||||
|
||||
// check validity of header:
|
||||
if (numIvBytesRead != AES_BLOCK_LENGTH || numMacBytesRead != calculatedMac.getMacLength() || fileSize == null) {
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0l);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// go to begin of content:
|
||||
encryptedFile.position(64);
|
||||
// read iv:
|
||||
final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
||||
headerBuf.position(0);
|
||||
headerBuf.get(iv);
|
||||
|
||||
// generate cipher:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
// read nonce:
|
||||
final byte[] nonce = new byte[8];
|
||||
headerBuf.position(16);
|
||||
headerBuf.get(nonce);
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream macIn = new MacInputStream(in, calculatedMac);
|
||||
final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
|
||||
final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
|
||||
// read sensitive header data:
|
||||
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||
headerBuf.position(24);
|
||||
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||
|
||||
// drain remaining bytes to /dev/null to complete MAC calculation:
|
||||
IOUtils.copyLarge(macIn, new NullOutputStream());
|
||||
// read header mac:
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
headerBuf.position(72);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// compare (in constant time):
|
||||
final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
|
||||
if (!macMatches) {
|
||||
// This exception will be thrown AFTER we sent the decrypted content to the user.
|
||||
// This has two advantages:
|
||||
// - we don't need to read files twice
|
||||
// - we can still restore files suffering from non-malicious bit rotting
|
||||
// Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception.
|
||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
||||
// calculate mac over first 72 bytes of header:
|
||||
if (authenticate) {
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.position(0);
|
||||
headerBuf.limit(72);
|
||||
headerMac.update(headerBuf);
|
||||
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
||||
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
return bytesDecrypted;
|
||||
// decrypt sensitive header data:
|
||||
final byte[] fileKeyBytes = new byte[32];
|
||||
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
||||
final Long fileSize = sensitiveHeaderContentBuf.getLong();
|
||||
sensitiveHeaderContentBuf.get(fileKeyBytes);
|
||||
|
||||
// prepare content decryption:
|
||||
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
||||
final LengthLimitingOutputStream paddingRemovingOutputStream = new LengthLimitingOutputStream(plaintextFile, fileSize);
|
||||
final CryptoWorkerExecutor executor = new CryptoWorkerExecutor(Runtime.getRuntime().availableProcessors(), (lock, blockDone, currentBlock, inputQueue) -> {
|
||||
return new DecryptWorker(lock, blockDone, currentBlock, inputQueue, authenticate, Channels.newChannel(paddingRemovingOutputStream)) {
|
||||
|
||||
@Override
|
||||
protected Cipher initCipher(long startBlockNum) {
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.putLong(startBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
||||
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
||||
return aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Mac initMac() {
|
||||
return hmacSha256(hMacMasterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void checkMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf, ByteBuffer macBuf) throws MacAuthenticationFailedException {
|
||||
mac.update(iv);
|
||||
mac.update(longToByteArray(blockNum));
|
||||
mac.update(ciphertextBuf);
|
||||
final byte[] calculatedMac = mac.doFinal();
|
||||
final byte[] storedMac = new byte[mac.getMacLength()];
|
||||
macBuf.get(storedMac);
|
||||
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
||||
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void decrypt(Cipher cipher, ByteBuffer ciphertextBuf, ByteBuffer plaintextBuf) throws DecryptFailedException {
|
||||
assert plaintextBuf.remaining() >= cipher.getOutputSize(ciphertextBuf.remaining());
|
||||
try {
|
||||
cipher.update(ciphertextBuf, plaintextBuf);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new DecryptFailedException(e);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
});
|
||||
|
||||
// read as many blocks from file as possible, but wait if queue is full:
|
||||
encryptedFile.position(104l);
|
||||
final int maxNumBlocks = 64;
|
||||
int numBlocks = 1;
|
||||
int bytesRead = 0;
|
||||
long blockNumber = 0;
|
||||
do {
|
||||
if (numBlocks < maxNumBlocks) {
|
||||
numBlocks++;
|
||||
}
|
||||
final int inBufSize = numBlocks * (CONTENT_MAC_BLOCK + 32);
|
||||
final ByteBuffer buf = ByteBuffer.allocate(inBufSize);
|
||||
bytesRead = encryptedFile.read(buf);
|
||||
buf.flip();
|
||||
final int blocksRead = (int) Math.ceil(bytesRead / (double) (CONTENT_MAC_BLOCK + 32));
|
||||
final boolean consumedInTime = executor.offer(new BlocksData(buf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
|
||||
if (!consumedInTime) {
|
||||
break;
|
||||
}
|
||||
blockNumber += numBlocks;
|
||||
} while (bytesRead == numBlocks * (CONTENT_MAC_BLOCK + 32));
|
||||
|
||||
// wait for decryption workers to finish:
|
||||
try {
|
||||
executor.waitUntilDone();
|
||||
} catch (ExecutionException e) {
|
||||
final Throwable cause = e.getCause();
|
||||
if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
} else if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
} else {
|
||||
LOG.error("Unexpected exception", e);
|
||||
}
|
||||
} finally {
|
||||
destroyQuietly(fileKey);
|
||||
}
|
||||
|
||||
return paddingRemovingOutputStream.getBytesWritten();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
|
||||
// read iv:
|
||||
encryptedFile.position(0);
|
||||
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
final int numIvBytesRead = encryptedFile.read(countingIv);
|
||||
|
||||
// check validity of header:
|
||||
if (numIvBytesRead != AES_BLOCK_LENGTH) {
|
||||
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
// read header:
|
||||
encryptedFile.position(0l);
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(104);
|
||||
final int headerBytesRead = encryptedFile.read(headerBuf);
|
||||
if (headerBytesRead != headerBuf.capacity()) {
|
||||
throw new IOException("Failed to read file header.");
|
||||
}
|
||||
|
||||
// seek relevant position and update iv:
|
||||
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
|
||||
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
|
||||
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
|
||||
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock);
|
||||
// read iv:
|
||||
final byte[] iv = new byte[AES_BLOCK_LENGTH];
|
||||
headerBuf.position(0);
|
||||
headerBuf.get(iv);
|
||||
|
||||
// fast forward stream:
|
||||
encryptedFile.position(64 + beginOfFirstRelevantBlock);
|
||||
// read nonce:
|
||||
final byte[] nonce = new byte[8];
|
||||
headerBuf.position(16);
|
||||
headerBuf.get(nonce);
|
||||
|
||||
// generate cipher:
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
|
||||
// read sensitive header data:
|
||||
final byte[] encryptedSensitiveHeaderContentBytes = new byte[48];
|
||||
headerBuf.position(24);
|
||||
headerBuf.get(encryptedSensitiveHeaderContentBytes);
|
||||
|
||||
// read content
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
final InputStream cipheredIn = new CipherInputStream(in, cipher);
|
||||
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
|
||||
// read header mac:
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
headerBuf.position(72);
|
||||
headerBuf.get(storedHeaderMac);
|
||||
|
||||
// calculate mac over first 72 bytes of header:
|
||||
if (authenticate) {
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerBuf.position(0);
|
||||
headerBuf.limit(72);
|
||||
headerMac.update(headerBuf);
|
||||
if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
|
||||
throw new MacAuthenticationFailedException("Header MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt sensitive header data:
|
||||
final byte[] fileKeyBytes = new byte[32];
|
||||
final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv);
|
||||
final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes);
|
||||
sensitiveHeaderContentBuf.position(Long.BYTES); // skip file size
|
||||
sensitiveHeaderContentBuf.get(fileKeyBytes);
|
||||
|
||||
// find first relevant block:
|
||||
final long startBlock = pos / CONTENT_MAC_BLOCK; // floor
|
||||
final long startByte = startBlock * (CONTENT_MAC_BLOCK + 32) + 104;
|
||||
final long offsetFromFirstBlock = pos - startBlock * CONTENT_MAC_BLOCK;
|
||||
|
||||
// append correct counter value to nonce:
|
||||
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH);
|
||||
nonceAndCounterBuf.put(nonce);
|
||||
nonceAndCounterBuf.putLong(startBlock * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH);
|
||||
final byte[] nonceAndCounter = nonceAndCounterBuf.array();
|
||||
|
||||
// content decryption:
|
||||
encryptedFile.position(startByte);
|
||||
final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM);
|
||||
final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE);
|
||||
final Mac contentMac = this.hmacSha256(hMacMasterKey);
|
||||
|
||||
try {
|
||||
// reading ciphered input and MACs interleaved:
|
||||
long bytesWritten = 0;
|
||||
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
|
||||
byte[] buffer = new byte[CONTENT_MAC_BLOCK + 32];
|
||||
int n = 0;
|
||||
long blockNum = startBlock;
|
||||
while ((n = IOUtils.read(in, buffer)) > 0 && bytesWritten < length) {
|
||||
if (n < 32) {
|
||||
throw new DecryptFailedException("Invalid file content, missing MAC.");
|
||||
}
|
||||
|
||||
// check MAC of current block:
|
||||
if (authenticate) {
|
||||
contentMac.update(iv);
|
||||
contentMac.update(longToByteArray(blockNum));
|
||||
contentMac.update(buffer, 0, n - 32);
|
||||
final byte[] calculatedMac = contentMac.doFinal();
|
||||
final byte[] storedMac = new byte[32];
|
||||
System.arraycopy(buffer, n - 32, storedMac, 0, 32);
|
||||
if (!MessageDigest.isEqual(calculatedMac, storedMac)) {
|
||||
throw new MacAuthenticationFailedException("Content MAC authentication failed.");
|
||||
}
|
||||
}
|
||||
|
||||
// decrypt block:
|
||||
final byte[] plaintext = cipher.update(buffer, 0, n - 32);
|
||||
final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0;
|
||||
final long pending = length - bytesWritten;
|
||||
final int available = plaintext.length - offset;
|
||||
final int currentBatch = (int) Math.min(pending, available);
|
||||
|
||||
plaintextFile.write(plaintext, offset, currentBatch);
|
||||
bytesWritten += currentBatch;
|
||||
blockNum++;
|
||||
}
|
||||
return bytesWritten;
|
||||
} finally {
|
||||
destroyQuietly(fileKey);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* header = {16 byte iv, 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 {
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
// truncate file
|
||||
encryptedFile.truncate(0);
|
||||
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 countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
|
||||
countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
|
||||
encryptedFile.write(countingIv);
|
||||
// choose a random header IV:
|
||||
final byte[] iv = randomData(AES_BLOCK_LENGTH);
|
||||
|
||||
// init crypto stuff:
|
||||
final Mac mac = this.hmacSha256(hMacMasterKey);
|
||||
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
|
||||
// chosse 8 byte random nonce and 8 byte counter set to zero:
|
||||
final byte[] nonce = randomData(8);
|
||||
|
||||
// init mac buffer and skip 32 bytes
|
||||
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
|
||||
encryptedFile.write(macBuffer);
|
||||
// choose a random content key:
|
||||
final byte[] fileKeyBytes = randomData(32);
|
||||
|
||||
// encrypt and write "zero length" as a placeholder, which will be read by concurrent requests, as long as encryption didn't finish:
|
||||
encryptedContentLength(encryptedFile, 0l);
|
||||
// 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);
|
||||
|
||||
// write content:
|
||||
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
|
||||
final OutputStream macOut = new MacOutputStream(out, mac);
|
||||
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
|
||||
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
|
||||
final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut);
|
||||
// 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) {
|
||||
|
||||
// ensure total byte count is a multiple of the block size, in CTR mode:
|
||||
final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH);
|
||||
blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]);
|
||||
@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);
|
||||
}
|
||||
|
||||
// append a few blocks of fake data:
|
||||
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
|
||||
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
|
||||
final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
|
||||
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
|
||||
blockSizeBufferedOut.write(emptyBytes);
|
||||
@Override
|
||||
protected Mac initMac() {
|
||||
return hmacSha256(hMacMasterKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected byte[] calcMac(Mac mac, long blockNum, ByteBuffer ciphertextBuf) {
|
||||
mac.update(iv);
|
||||
mac.update(longToByteArray(blockNum));
|
||||
mac.update(ciphertextBuf);
|
||||
return mac.doFinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void encrypt(Cipher cipher, ByteBuffer plaintextBuf, ByteBuffer ciphertextBuf) throws EncryptFailedException {
|
||||
try {
|
||||
assert ciphertextBuf.remaining() >= cipher.getOutputSize(plaintextBuf.remaining());
|
||||
cipher.update(plaintextBuf, ciphertextBuf);
|
||||
} catch (ShortBufferException e) {
|
||||
throw new EncryptFailedException(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// read as many blocks from file as possible, but wait if queue is full:
|
||||
final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
|
||||
final LengthObfuscatingInputStream in = new LengthObfuscatingInputStream(plaintextFile, randomPadding);
|
||||
final ReadableByteChannel channel = Channels.newChannel(in);
|
||||
int bytesRead = 0;
|
||||
long blockNumber = 0;
|
||||
final int maxNumBlocks = 64;
|
||||
int numBlocks = 0;
|
||||
do {
|
||||
if (numBlocks < maxNumBlocks) {
|
||||
numBlocks++;
|
||||
}
|
||||
final int inBufSize = numBlocks * CONTENT_MAC_BLOCK;
|
||||
final ByteBuffer inBuf = ByteBuffer.allocate(inBufSize);
|
||||
bytesRead = channel.read(inBuf);
|
||||
inBuf.flip();
|
||||
final int blocksRead = (int) Math.ceil(bytesRead / (double) CONTENT_MAC_BLOCK);
|
||||
final boolean consumedInTime = executor.offer(new BlocksData(inBuf.asReadOnlyBuffer(), blockNumber, blocksRead), 1, TimeUnit.SECONDS);
|
||||
if (!consumedInTime) {
|
||||
break;
|
||||
}
|
||||
blockNumber += numBlocks;
|
||||
} while (bytesRead == numBlocks * CONTENT_MAC_BLOCK);
|
||||
|
||||
// wait for encryption workers to finish:
|
||||
try {
|
||||
executor.waitUntilDone();
|
||||
} catch (ExecutionException e) {
|
||||
final Throwable cause = e.getCause();
|
||||
if (cause instanceof IOException) {
|
||||
throw (IOException) cause;
|
||||
} else if (cause instanceof RuntimeException) {
|
||||
throw (RuntimeException) cause;
|
||||
} else {
|
||||
LOG.error("Unexpected exception", e);
|
||||
}
|
||||
} finally {
|
||||
destroyQuietly(fileKey);
|
||||
}
|
||||
blockSizeBufferedOut.flush();
|
||||
|
||||
// write MAC of total ciphertext:
|
||||
macBuffer.clear();
|
||||
macBuffer.put(mac.doFinal());
|
||||
macBuffer.flip();
|
||||
encryptedFile.position(16); // right behind the IV
|
||||
encryptedFile.write(macBuffer); // 256 bit MAC
|
||||
|
||||
// encrypt and write plaintextSize:
|
||||
encryptedContentLength(encryptedFile, plaintextSize);
|
||||
// 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(nonce);
|
||||
headerBuf.put(encryptHeaderData(sensitiveHeaderContentBuf.array(), iv));
|
||||
headerBuf.flip();
|
||||
final Mac headerMac = this.hmacSha256(hMacMasterKey);
|
||||
headerMac.update(headerBuf);
|
||||
headerBuf.limit(104);
|
||||
headerBuf.put(headerMac.doFinal());
|
||||
headerBuf.flip();
|
||||
encryptedFile.position(0);
|
||||
encryptedFile.write(headerBuf);
|
||||
|
||||
return plaintextSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_GLOB_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
private byte[] longToByteArray(long lng) {
|
||||
return ByteBuffer.allocate(Long.SIZE / Byte.SIZE).putLong(lng).array();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
interface AesCryptographicConfiguration {
|
||||
|
||||
/**
|
||||
@@ -26,14 +29,14 @@ interface AesCryptographicConfiguration {
|
||||
int SCRYPT_BLOCK_SIZE = 8;
|
||||
|
||||
/**
|
||||
* Number of bytes of the master key. Should be the maximum possible AES key length to provide best security.
|
||||
* Preferred number of bytes of the master key.
|
||||
*/
|
||||
int PREF_MASTER_KEY_LENGTH_IN_BITS = 256;
|
||||
|
||||
/**
|
||||
* Number of bytes used as seed for the PRNG.
|
||||
*/
|
||||
int PRNG_SEED_LENGTH = 32;
|
||||
int PRNG_SEED_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Algorithm used for random number generation.
|
||||
@@ -60,19 +63,18 @@ interface AesCryptographicConfiguration {
|
||||
String AES_KEYWRAP_CIPHER = "AESWrap";
|
||||
|
||||
/**
|
||||
* Cipher specs for file name and file content encryption. Using CTR-mode for random access.<br/>
|
||||
* <strong>Important</strong>: As JCE doesn't support a padding, input must be a multiple of the block size.
|
||||
* Cipher specs for file content encryption. Using CTR-mode for random access.<br/>
|
||||
*
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher
|
||||
*/
|
||||
String AES_CTR_CIPHER = "AES/CTR/NoPadding";
|
||||
|
||||
/**
|
||||
* Cipher specs for single block encryption (like file size).
|
||||
* Cipher specs for file header encryption (fixed-length block cipher).<br/>
|
||||
*
|
||||
* @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
|
||||
*/
|
||||
String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
|
||||
String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding";
|
||||
|
||||
/**
|
||||
* AES block size is 128 bit or 16 bytes.
|
||||
@@ -80,10 +82,13 @@ interface AesCryptographicConfiguration {
|
||||
int AES_BLOCK_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* Number of non-zero bytes in the IV used for file name encryption. Less means shorter encrypted filenames, more means higher entropy.
|
||||
* Maximum length is {@value #AES_BLOCK_LENGTH}. Even the shortest base32 (see {@link FileNamingConventions#ENCRYPTED_FILENAME_CODEC})
|
||||
* encoded byte array will need 8 chars. The maximum number of bytes that fit in 8 base32 chars is 5. Thus 5 is the ideal length.
|
||||
* Number of bytes, a content block over which a MAC is calculated consists of.
|
||||
*/
|
||||
int FILE_NAME_IV_LENGTH = 5;
|
||||
int CONTENT_MAC_BLOCK = 32 * 1024;
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ final class AesSivCipherUtil {
|
||||
private static final byte[] BYTES_ZERO = new byte[16];
|
||||
private static final byte DOUBLING_CONST = (byte) 0x87;
|
||||
|
||||
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException {
|
||||
static byte[] sivEncrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) {
|
||||
final byte[] aesKeyBytes = aesKey.getEncoded();
|
||||
final byte[] macKeyBytes = macKey.getEncoded();
|
||||
if (aesKeyBytes == null || macKeyBytes == null) {
|
||||
@@ -41,6 +41,8 @@ final class AesSivCipherUtil {
|
||||
}
|
||||
try {
|
||||
return sivEncrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
} finally {
|
||||
Arrays.fill(aesKeyBytes, (byte) 0);
|
||||
Arrays.fill(macKeyBytes, (byte) 0);
|
||||
@@ -78,7 +80,7 @@ final class AesSivCipherUtil {
|
||||
return ArrayUtils.addAll(iv, ciphertext);
|
||||
}
|
||||
|
||||
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws InvalidKeyException, DecryptFailedException {
|
||||
static byte[] sivDecrypt(SecretKey aesKey, SecretKey macKey, byte[] plaintext, byte[]... additionalData) throws DecryptFailedException {
|
||||
final byte[] aesKeyBytes = aesKey.getEncoded();
|
||||
final byte[] macKeyBytes = macKey.getEncoded();
|
||||
if (aesKeyBytes == null || macKeyBytes == null) {
|
||||
@@ -86,6 +88,8 @@ final class AesSivCipherUtil {
|
||||
}
|
||||
try {
|
||||
return sivDecrypt(aesKeyBytes, macKeyBytes, plaintext, additionalData);
|
||||
} catch (InvalidKeyException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
} finally {
|
||||
Arrays.fill(aesKeyBytes, (byte) 0);
|
||||
Arrays.fill(macKeyBytes, (byte) 0);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -1,61 +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.aes256;
|
||||
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.PathMatcher;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
interface FileNamingConventions {
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
/**
|
||||
* Maximum length possible on file systems with a filename limit of 255 chars.<br/>
|
||||
* Also we would need a few chars for our file extension, so lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
|
||||
*/
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 250;
|
||||
|
||||
/**
|
||||
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String BASIC_FILE_EXT = ".aes";
|
||||
|
||||
/**
|
||||
* Prefix in front of the actual encrypted file name used as IV.
|
||||
*/
|
||||
String IV_PREFIX_SEPARATOR = "_";
|
||||
|
||||
/**
|
||||
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_NAME_FILE_EXT = ".lng.aes";
|
||||
|
||||
/**
|
||||
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
|
||||
*/
|
||||
int LONG_NAME_PREFIX_LENGTH = 8;
|
||||
|
||||
/**
|
||||
* For metadata files for a certain group of files. The cryptor may decide what files to assign to the same group; hopefully using some
|
||||
* kind of uniform distribution for better load balancing.
|
||||
*/
|
||||
String METADATA_FILE_EXT = ".meta";
|
||||
|
||||
/**
|
||||
* Matches both, {@value #BASIC_FILE_EXT} and {@value #LONG_NAME_FILE_EXT} files.
|
||||
*/
|
||||
PathMatcher ENCRYPTED_FILE_GLOB_MATCHER = FileSystems.getDefault().getPathMatcher("glob:**/*{" + BASIC_FILE_EXT + "," + LONG_NAME_FILE_EXT + "}");
|
||||
|
||||
}
|
||||
@@ -4,10 +4,13 @@ import java.io.Serializable;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
@JsonPropertyOrder(value = {"scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
@JsonPropertyOrder(value = {"version", "scryptSalt", "scryptCostParam", "scryptBlockSize", "keyLength", "primaryMasterKey", "hMacMasterKey"})
|
||||
public class KeyFile implements Serializable {
|
||||
|
||||
static final Integer CURRENT_VERSION = 2;
|
||||
private static final long serialVersionUID = 8578363158959619885L;
|
||||
|
||||
private Integer version;
|
||||
private byte[] scryptSalt;
|
||||
private int scryptCostParam;
|
||||
private int scryptBlockSize;
|
||||
@@ -15,6 +18,14 @@ public class KeyFile implements Serializable {
|
||||
private byte[] primaryMasterKey;
|
||||
private byte[] hMacMasterKey;
|
||||
|
||||
public Integer getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public void setVersion(Integer version) {
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public byte[] getScryptSalt() {
|
||||
return scryptSalt;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
public class LengthLimitingOutputStream extends FilterOutputStream {
|
||||
|
||||
private final long limit;
|
||||
private volatile long bytesWritten;
|
||||
|
||||
public LengthLimitingOutputStream(OutputStream out, long limit) {
|
||||
super(out);
|
||||
this.limit = limit;
|
||||
this.bytesWritten = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
if (bytesWritten < limit) {
|
||||
out.write(b);
|
||||
bytesWritten++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
final long bytesAvailable = limit - bytesWritten;
|
||||
final int adjustedLen = (int) Math.min(len, bytesAvailable);
|
||||
if (adjustedLen > 0) {
|
||||
out.write(b, off, adjustedLen);
|
||||
bytesWritten += adjustedLen;
|
||||
}
|
||||
}
|
||||
|
||||
public long getBytesWritten() {
|
||||
return bytesWritten;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import java.io.FilterInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
/**
|
||||
* Not thread-safe!
|
||||
*/
|
||||
public class LengthObfuscatingInputStream extends FilterInputStream {
|
||||
|
||||
private final byte[] padding;
|
||||
private int paddingLength = -1;
|
||||
private long inputBytesRead = 0;
|
||||
private int paddingBytesRead = 0;
|
||||
|
||||
LengthObfuscatingInputStream(InputStream in, byte[] padding) {
|
||||
super(in);
|
||||
this.padding = padding;
|
||||
}
|
||||
|
||||
long getRealInputLength() {
|
||||
return inputBytesRead;
|
||||
}
|
||||
|
||||
private void choosePaddingLengthOnce() {
|
||||
if (paddingLength == -1) {
|
||||
long upperBound = Math.min(Math.max(inputBytesRead / 10, 4096), 16 * 1024 * 1024); // 10% of original bytes (at least 4KiB), but not more than 16MiBs
|
||||
paddingLength = (int) (Math.random() * upperBound);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
final int b = in.read();
|
||||
if (b != -1) {
|
||||
// stream available:
|
||||
inputBytesRead++;
|
||||
return b;
|
||||
} else {
|
||||
choosePaddingLengthOnce();
|
||||
return readFromPadding();
|
||||
}
|
||||
}
|
||||
|
||||
private int readFromPadding() {
|
||||
if (paddingLength == -1) {
|
||||
throw new IllegalStateException("No padding length chosen yet.");
|
||||
}
|
||||
|
||||
if (paddingBytesRead < paddingLength) {
|
||||
// padding available:
|
||||
return padding[paddingBytesRead++ % padding.length];
|
||||
} else {
|
||||
// end of stream AND padding
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
final int bytesRead = IOUtils.read(in, b, off, len); // 0 on EOF
|
||||
inputBytesRead += bytesRead;
|
||||
|
||||
if (bytesRead == len) {
|
||||
return bytesRead;
|
||||
} else if (bytesRead < len) {
|
||||
choosePaddingLengthOnce();
|
||||
final int additionalBytesNeeded = len - bytesRead;
|
||||
final int additionalBytesRead = readFromPadding(b, off + bytesRead, additionalBytesNeeded);
|
||||
return (bytesRead == 0 && additionalBytesRead == 0) ? -1 : bytesRead + additionalBytesRead;
|
||||
} else {
|
||||
// bytesRead > len:
|
||||
throw new IllegalStateException("Read more bytes than requested.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bytes read from padding (0, if fully read)
|
||||
*/
|
||||
private int readFromPadding(byte[] b, int off, int len) {
|
||||
if (len < 0) {
|
||||
throw new IllegalArgumentException("Length must not be negative");
|
||||
}
|
||||
if (paddingLength == -1) {
|
||||
throw new IllegalStateException("No padding length chosen yet.");
|
||||
}
|
||||
|
||||
final int remainingPadding = paddingLength - paddingBytesRead;
|
||||
if (remainingPadding > len) {
|
||||
// padding available:
|
||||
for (int i = 0; i < len; i++) {
|
||||
b[off + i] = padding[paddingBytesRead++ % padding.length];
|
||||
}
|
||||
return len;
|
||||
} else {
|
||||
// partly available:
|
||||
for (int i = 0; i < remainingPadding; i++) {
|
||||
b[off + i] = padding[paddingBytesRead++ % padding.length];
|
||||
}
|
||||
return remainingPadding;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
throw new IOException("Skip not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
final int inputAvailable = in.available();
|
||||
if (inputAvailable > 0) {
|
||||
return inputAvailable;
|
||||
} else {
|
||||
// remaining padding
|
||||
choosePaddingLengthOnce();
|
||||
return paddingLength - paddingBytesRead;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,49 +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.aes256;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
|
||||
|
||||
class LongFilenameMetadata implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 6214509403824421320L;
|
||||
|
||||
@JsonDeserialize(as = DualHashBidiMap.class)
|
||||
private BidiMap<UUID, String> encryptedFilenames = new DualHashBidiMap<>();
|
||||
|
||||
/* Getter/Setter */
|
||||
|
||||
public synchronized String getEncryptedFilenameForUUID(final UUID uuid) {
|
||||
return encryptedFilenames.get(uuid);
|
||||
}
|
||||
|
||||
public synchronized UUID getOrCreateUuidForEncryptedFilename(String encryptedFilename) {
|
||||
UUID uuid = encryptedFilenames.getKey(encryptedFilename);
|
||||
if (uuid == null) {
|
||||
uuid = UUID.randomUUID();
|
||||
encryptedFilenames.put(uuid, encryptedFilename);
|
||||
}
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public BidiMap<UUID, String> getEncryptedFilenames() {
|
||||
return encryptedFilenames;
|
||||
}
|
||||
|
||||
public void setEncryptedFilenames(BidiMap<UUID, String> encryptedFilenames) {
|
||||
this.encryptedFilenames = encryptedFilenames;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,41 +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();
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,13 +15,14 @@ import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.CryptorIOSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -29,12 +30,12 @@ import org.junit.Test;
|
||||
public class Aes256CryptorTest {
|
||||
|
||||
@Test
|
||||
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException {
|
||||
public void testCorrectPassword() throws IOException, WrongPasswordException, DecryptFailedException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
cryptor.destroy();
|
||||
|
||||
final Aes256Cryptor decryptor = new Aes256Cryptor();
|
||||
final InputStream in = new ByteArrayInputStream(out.toByteArray());
|
||||
@@ -45,12 +46,12 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException, UnsupportedVaultException {
|
||||
final String pw = "asd";
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
cryptor.encryptMasterKey(out, pw);
|
||||
cryptor.swipeSensitiveData();
|
||||
cryptor.destroy();
|
||||
IOUtils.closeQuietly(out);
|
||||
|
||||
// all these passwords are expected to fail.
|
||||
@@ -70,7 +71,7 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test(expected = DecryptFailedException.class)
|
||||
public void testIntegrityAuthentication() throws IOException, DecryptFailedException {
|
||||
public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
@@ -79,7 +80,7 @@ public class Aes256CryptorTest {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -98,11 +99,11 @@ 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
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = "Hello World".getBytes();
|
||||
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
|
||||
@@ -111,7 +112,7 @@ public class Aes256CryptorTest {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate(104 + plaintextData.length + 4096);
|
||||
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
|
||||
cryptor.encryptFile(plaintextIn, encryptedOut);
|
||||
IOUtils.closeQuietly(plaintextIn);
|
||||
@@ -126,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());
|
||||
@@ -137,11 +138,11 @@ public class Aes256CryptorTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
|
||||
// our test plaintext data:
|
||||
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
|
||||
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException {
|
||||
// 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);
|
||||
@@ -150,7 +151,7 @@ public class Aes256CryptorTest {
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// encrypt:
|
||||
final ByteBuffer encryptedData = ByteBuffer.allocate((int) (64 + 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);
|
||||
@@ -161,62 +162,43 @@ 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);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
|
||||
final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// short path components
|
||||
// directory paths
|
||||
final String originalPath1 = "foo/bar/baz";
|
||||
final String encryptedPath1a = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
final String encryptedPath1b = cryptor.encryptPath(originalPath1, '/', '/', ioSupportMock);
|
||||
final String encryptedPath1a = cryptor.encryptDirectoryPath(originalPath1, "/");
|
||||
final String encryptedPath1b = cryptor.encryptDirectoryPath(originalPath1, "/");
|
||||
Assert.assertEquals(encryptedPath1a, encryptedPath1b);
|
||||
final String decryptedPath1 = cryptor.decryptPath(encryptedPath1a, '/', '/', ioSupportMock);
|
||||
Assert.assertEquals(originalPath1, decryptedPath1);
|
||||
|
||||
// long path components
|
||||
// long file names
|
||||
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
|
||||
final String originalPath2 = "foo/" + str50chars + str50chars + str50chars + str50chars + str50chars + "/baz";
|
||||
final String encryptedPath2a = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
final String encryptedPath2b = cryptor.encryptPath(originalPath2, '/', '/', ioSupportMock);
|
||||
final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt";
|
||||
final String encryptedPath2a = cryptor.encryptFilename(originalPath2);
|
||||
final String encryptedPath2b = cryptor.encryptFilename(originalPath2);
|
||||
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
|
||||
final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
|
||||
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a);
|
||||
Assert.assertEquals(originalPath2, decryptedPath2);
|
||||
|
||||
// block size length path components
|
||||
// block size length file names
|
||||
final String originalPath3 = "aaaabbbbccccdddd";
|
||||
final String encryptedPath3a = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
|
||||
final String encryptedPath3b = cryptor.encryptPath(originalPath3, '/', '/', ioSupportMock);
|
||||
final String encryptedPath3a = cryptor.encryptFilename(originalPath3);
|
||||
final String encryptedPath3b = cryptor.encryptFilename(originalPath3);
|
||||
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
|
||||
final String decryptedPath3 = cryptor.decryptPath(encryptedPath3a, '/', '/', ioSupportMock);
|
||||
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a);
|
||||
Assert.assertEquals(originalPath3, decryptedPath3);
|
||||
}
|
||||
|
||||
private static class CryptoIOSupportMock implements CryptorIOSupport {
|
||||
|
||||
private final Map<String, byte[]> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
|
||||
map.put(encryptedPath, encryptedMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readPathSpecificMetadata(String encryptedPath) {
|
||||
return map.get(encryptedPath);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.5.2</version>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>crypto-api</artifactId>
|
||||
<name>Cryptomator cryptographic module API</name>
|
||||
@@ -27,5 +27,9 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -1,38 +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;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public abstract class AbstractCryptor implements Cryptor {
|
||||
|
||||
private final Set<SensitiveDataSwipeListener> swipeListeners = new HashSet<>();
|
||||
|
||||
@Override
|
||||
public final void swipeSensitiveData() {
|
||||
this.swipeSensitiveDataInternal();
|
||||
for (final SensitiveDataSwipeListener sensitiveDataSwipeListener : swipeListeners) {
|
||||
sensitiveDataSwipeListener.swipeSensitiveData();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void swipeSensitiveDataInternal();
|
||||
|
||||
@Override
|
||||
public final void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
this.swipeListeners.add(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
this.swipeListeners.remove(listener);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
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;
|
||||
|
||||
public class AbstractCryptorDecorator implements Cryptor {
|
||||
|
||||
protected final Cryptor cryptor;
|
||||
|
||||
public AbstractCryptorDecorator(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
cryptor.encryptMasterKey(out, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException {
|
||||
cryptor.decryptMasterKey(in, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
|
||||
return cryptor.encryptDirectoryPath(cleartextDirectoryId, nativePathSep);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName) {
|
||||
return cryptor.encryptFilename(cleartextName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
return cryptor.decryptFilename(ciphertextName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
||||
return cryptor.decryptedContentLength(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptFile(encryptedFile, plaintextFile, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
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
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
return cryptor.encryptFile(plaintextFile, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
cryptor.destroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDestroyed() {
|
||||
return cryptor.isDestroyed();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,17 +12,20 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Provides access to cryptographic functions. All methods are threadsafe.
|
||||
*/
|
||||
public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
public interface Cryptor extends Destroyable {
|
||||
|
||||
/**
|
||||
* Encrypts the current masterKey with the given password and writes the result to the given output stream.
|
||||
@@ -33,53 +36,50 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
* Reads the encrypted masterkey from the given input stream and decrypts it with the given password.
|
||||
*
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong
|
||||
* password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In
|
||||
* this case Java JCE needs to be installed.
|
||||
* @throws WrongPasswordException If the provided password was wrong. Note: Sometimes the algorithm itself fails due to a wrong password. In this case a DecryptFailedException will be thrown.
|
||||
* @throws UnsupportedKeyLengthException If the masterkey has been encrypted with a higher key length than supported by the system. In this case Java JCE needs to be installed.
|
||||
* @throws UnsupportedVaultException If the masterkey file is too old or too modern.
|
||||
*/
|
||||
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
|
||||
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException, UnsupportedVaultException;
|
||||
|
||||
/**
|
||||
* Encrypts each plaintext path component for its own.
|
||||
* Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories.
|
||||
*
|
||||
* @param cleartextPath A relative path (UTF-8 encoded)
|
||||
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if cleartextPath is a sole
|
||||
* file name without any path separators.
|
||||
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if cleartextPath is a sole file name
|
||||
* without any path separators.
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Encrypted path components concatenated by the given encryptedPathSep. Must not start with encryptedPathSep, unless the
|
||||
* encrypted path is explicitly absolute.
|
||||
* @param cleartextDirectoryId A unique directory id
|
||||
* @param nativePathSep Path separator like "/" used on local file system. Must not be null, even if cleartextPath is a sole file name without any path separators.
|
||||
* @return Encrypted path.
|
||||
*/
|
||||
String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
|
||||
String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep);
|
||||
|
||||
/**
|
||||
* Decrypts each encrypted path component for its own.
|
||||
* Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir.
|
||||
*
|
||||
* @param encryptedPath A relative path (UTF-8 encoded)
|
||||
* @param encryptedPathSep Path separator char like '/' used on local file system. Must not be null, even if encryptedPath is a sole
|
||||
* file name without any path separators.
|
||||
* @param cleartextPathSep Path separator char like '/' used in webdav URIs. Must not be null, even if encryptedPath is a sole file name
|
||||
* without any path separators.
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Decrypted path components concatenated by the given cleartextPathSep. Must not start with cleartextPathSep, unless the
|
||||
* cleartext path is explicitly absolute.
|
||||
* @param cleartextName A plaintext filename without any preceeding directory paths.
|
||||
* @return Encrypted filename.
|
||||
*/
|
||||
String encryptFilename(String cleartextName);
|
||||
|
||||
/**
|
||||
* Decrypts the name of a file.
|
||||
*
|
||||
* @param ciphertextName A ciphertext filename without any preceeding directory paths.
|
||||
* @return Decrypted filename.
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
*/
|
||||
String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException;
|
||||
String decryptFilename(String ciphertextName) throws DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
* @return Content length of the decrypted file or <code>null</code> if unknown.
|
||||
* @throws MacAuthenticationFailedException If the MAC auth failed.
|
||||
*/
|
||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException;
|
||||
Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
|
||||
|
||||
/**
|
||||
* @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)
|
||||
@@ -87,21 +87,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
|
||||
* @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.
|
||||
*/
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException;
|
||||
|
||||
/**
|
||||
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting
|
||||
* metadata file of the {@link Cryptor}.
|
||||
*/
|
||||
Filter<Path> getPayloadFilesFilter();
|
||||
|
||||
void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
|
||||
void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
|
||||
|
||||
}
|
||||
|
||||
@@ -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.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Methods that may be called by the Cryptor when accessing a path.
|
||||
*/
|
||||
public interface CryptorIOSupport {
|
||||
|
||||
/**
|
||||
* Persists encryptedMetadata to the given encryptedPath.
|
||||
*
|
||||
* @param encryptedPath A relative path
|
||||
* @throws IOException
|
||||
*/
|
||||
void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Previously written encryptedMetadata stored at the given encryptedPath or <code>null</code> if no such file exists.
|
||||
*/
|
||||
byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
import org.apache.commons.collections4.bidimap.AbstractDualBidiMap;
|
||||
import org.apache.commons.collections4.map.LRUMap;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
public class PathCachingCryptorDecorator extends AbstractCryptorDecorator {
|
||||
|
||||
private static final int MAX_CACHED_PATHS = 5000;
|
||||
private static final int MAX_CACHED_NAMES = 5000;
|
||||
|
||||
private final Map<String, String> pathCache = new LRUMap<>(MAX_CACHED_PATHS); // <cleartextDirectoryId, ciphertextPath>
|
||||
private final BidiMap<String, String> nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); // <cleartextName, ciphertextName>
|
||||
|
||||
private PathCachingCryptorDecorator(Cryptor cryptor) {
|
||||
super(cryptor);
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new PathCachingCryptorDecorator(cryptor);
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
|
||||
return pathCache.computeIfAbsent(cleartextDirectoryId, id -> cryptor.encryptDirectoryPath(id, nativePathSep));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName) {
|
||||
return nameCache.computeIfAbsent(cleartextName, name -> cryptor.encryptFilename(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
String cleartextName = nameCache.getKey(ciphertextName);
|
||||
if (cleartextName == null) {
|
||||
cleartextName = cryptor.decryptFilename(ciphertextName);
|
||||
nameCache.put(cleartextName, ciphertextName);
|
||||
}
|
||||
return cleartextName;
|
||||
}
|
||||
|
||||
private static class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
|
||||
|
||||
BidiLRUMap(int maxSize) {
|
||||
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
|
||||
}
|
||||
|
||||
protected BidiLRUMap(final Map<K, V> normalMap, final Map<V, K> reverseMap, final BidiMap<V, K> inverseBidiMap) {
|
||||
super(normalMap, reverseMap, inverseBidiMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BidiMap<V, K> createBidiMap(Map<V, K> normalMap, Map<K, V> reverseMap, BidiMap<K, V> inverseMap) {
|
||||
return new BidiLRUMap<V, K>(normalMap, reverseMap, inverseMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
|
||||
public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling {
|
||||
|
||||
private final AtomicLong encryptedBytes;
|
||||
private final AtomicLong decryptedBytes;
|
||||
|
||||
private SamplingCryptorDecorator(Cryptor cryptor) {
|
||||
super(cryptor);
|
||||
encryptedBytes = new AtomicLong();
|
||||
decryptedBytes = new AtomicLong();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new SamplingCryptorDecorator(cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollEncryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return encryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return encryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollDecryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return decryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return decryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
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, boolean authenticate) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingOutputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptRange(encryptedFile, countingOutputStream, pos, length, authenticate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
||||
private class CountingInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingInputStream(AtomicLong counter, InputStream in) {
|
||||
this.in = in;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int count = in.read();
|
||||
counter.addAndGet(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);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CountingOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream out;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingOutputStream(AtomicLong counter, OutputStream out) {
|
||||
this.out = out;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
counter.incrementAndGet();
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
counter.addAndGet(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
|
||||
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final AtomicLong encryptedBytes;
|
||||
private final AtomicLong decryptedBytes;
|
||||
|
||||
private SamplingDecorator(Cryptor cryptor) {
|
||||
this.cryptor = cryptor;
|
||||
encryptedBytes = new AtomicLong();
|
||||
decryptedBytes = new AtomicLong();
|
||||
}
|
||||
|
||||
public static Cryptor decorate(Cryptor cryptor) {
|
||||
return new SamplingDecorator(cryptor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void swipeSensitiveData() {
|
||||
cryptor.swipeSensitiveData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollEncryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return encryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return encryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long pollDecryptedBytes(boolean resetCounter) {
|
||||
if (resetCounter) {
|
||||
return decryptedBytes.getAndSet(0);
|
||||
} else {
|
||||
return decryptedBytes.get();
|
||||
}
|
||||
}
|
||||
|
||||
/* Cryptor */
|
||||
|
||||
@Override
|
||||
public void encryptMasterKey(OutputStream out, CharSequence password) throws IOException {
|
||||
cryptor.encryptMasterKey(out, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException {
|
||||
cryptor.decryptMasterKey(in, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
|
||||
encryptedBytes.addAndGet(StringUtils.length(cleartextPath));
|
||||
return cryptor.encryptPath(cleartextPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
|
||||
decryptedBytes.addAndGet(StringUtils.length(encryptedPath));
|
||||
return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
|
||||
return cryptor.decryptedContentLength(encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
|
||||
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
|
||||
return cryptor.decryptFile(encryptedFile, countingInputStream);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException {
|
||||
final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile);
|
||||
return cryptor.encryptFile(countingInputStream, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return cryptor.getPayloadFilesFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.addSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener) {
|
||||
cryptor.removeSensitiveDataSwipeListener(listener);
|
||||
}
|
||||
|
||||
private class CountingInputStream extends InputStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingInputStream(AtomicLong counter, InputStream in) {
|
||||
this.in = in;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
int count = in.read();
|
||||
counter.addAndGet(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);
|
||||
return count;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class CountingOutputStream extends OutputStream {
|
||||
|
||||
private final OutputStream out;
|
||||
private final AtomicLong counter;
|
||||
|
||||
private CountingOutputStream(AtomicLong counter, OutputStream out) {
|
||||
this.out = out;
|
||||
this.counter = counter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
counter.incrementAndGet();
|
||||
out.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
counter.addAndGet(len);
|
||||
out.write(b, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,19 +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;
|
||||
|
||||
public interface SensitiveDataSwipeListener {
|
||||
|
||||
/**
|
||||
* Removes sensitive data from memory. Depending on the data (e.g. for passwords) it might be necessary to overwrite the memory before
|
||||
* freeing the object.
|
||||
*/
|
||||
void swipeSensitiveData();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.cryptomator.crypto.exceptions;
|
||||
|
||||
public class UnsupportedVaultException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5147549533387945622L;
|
||||
|
||||
private final Integer detectedVersion;
|
||||
private final Integer supportedVersion;
|
||||
|
||||
public UnsupportedVaultException(Integer detectedVersion, Integer supportedVersion) {
|
||||
super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
|
||||
this.detectedVersion = detectedVersion;
|
||||
this.supportedVersion = supportedVersion;
|
||||
}
|
||||
|
||||
public Integer getDetectedVersion() {
|
||||
return detectedVersion;
|
||||
}
|
||||
|
||||
public Integer getSupportedVersion() {
|
||||
return supportedVersion;
|
||||
}
|
||||
|
||||
public boolean isVaultOlderThanSoftware() {
|
||||
return detectedVersion == null || detectedVersion < supportedVersion;
|
||||
}
|
||||
|
||||
public boolean isSoftwareOlderThanVault() {
|
||||
return detectedVersion > supportedVersion;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 250 KiB |
16
main/installer-debian/package/linux/control
Normal file
@@ -0,0 +1,16 @@
|
||||
Package: APPLICATION_PACKAGE
|
||||
Version: APPLICATION_VERSION
|
||||
Section: contrib/utils
|
||||
Maintainer: Sebastian Stenzel <sebastian.stenzel@gmail.com>
|
||||
Homepage: https://cryptomator.org
|
||||
Vcs-Git: https://github.com/totalvoidness/cryptomator.git
|
||||
Vcs-Browser: https://github.com/totalvoidness/cryptomator
|
||||
Priority: optional
|
||||
Architecture: APPLICATION_ARCH
|
||||
Provides: APPLICATION_PACKAGE
|
||||
Installed-Size: APPLICATION_INSTALLED_SIZE
|
||||
Depends: gvfs-bin, gvfs-backends, gvfs-fuse, xdg-utils
|
||||
Description: Multi-platform client-side encryption of your cloud files.
|
||||
Cryptomator provides free client-side AES encryption for your cloud files.
|
||||
Create encrypted vaults, which get mounted as virtual volumes. Whatever
|
||||
you save on one of these volumes will end up encrypted inside your vault.
|
||||
23
main/installer-debian/package/linux/copyright
Normal file
@@ -0,0 +1,23 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: cryptomator
|
||||
Source: <https://github.com/totalvoidness/cryptomator>
|
||||
|
||||
Copyright: 2015 Sebastian Stenzel <sebastian.stenzel@gmail.com> and contributors.
|
||||
License: MIT
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
.
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
88
main/installer-debian/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-debian</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Debian installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<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>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<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 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="${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" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" ?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.7.4</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
@@ -24,8 +24,7 @@
|
||||
<string>DEPLOY_BUNDLE_SHORT_VERSION</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See http://developer.apple.com/library/mac/#releasenotes/General/SubmittingToMacAppStore/_index.html
|
||||
for list of AppStore categories -->
|
||||
<!-- See http://developer.apple.com/library/mac/#releasenotes/General/SubmittingToMacAppStore/_index.html for list of AppStore categories -->
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>DEPLOY_BUNDLE_CATEGORY</string>
|
||||
<key>CFBundleVersion</key>
|
||||
@@ -47,30 +46,57 @@
|
||||
DEPLOY_JVM_OPTIONS
|
||||
</array>
|
||||
<key>JVMUserOptions</key>
|
||||
<dict>
|
||||
<dict>
|
||||
DEPLOY_JVM_USER_OPTIONS
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<!-- hide from dock -->
|
||||
<key>LSUIElement</key>
|
||||
<string>1</string>
|
||||
<!-- register .cryptomator bundle extension -->
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<true/>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>Cryptomator.icns</string>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>cryptomator</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>org.cryptomator.folder</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>cryptomator</string>
|
||||
</array>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>Cryptomator.icns</string>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Cryptomator Vault</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>org.cryptomator.folder</string>
|
||||
</array>
|
||||
<key>LSTypeIsPackage</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>com.apple.package</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Cryptomator Vault</string>
|
||||
<key>UTTypeIconFile</key>
|
||||
<string>Cryptomator.icns</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>org.cryptomator.folder</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>cryptomator</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
88
main/installer-osx/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-osx</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Mac OS X installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<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>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<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 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="${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" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
After Width: | Height: | Size: 9.6 KiB |
|
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 361 KiB |
74
main/installer-win-portable/package/windows/Cryptomator.iss
Normal file
@@ -0,0 +1,74 @@
|
||||
;This file will be executed next to the application bundle image
|
||||
;I.e. current directory will contain folder APPLICATION_NAME with application files
|
||||
[Setup]
|
||||
AppId={{PRODUCT_APP_IDENTIFIER}}
|
||||
AppName=APPLICATION_NAME
|
||||
AppVersion=APPLICATION_VERSION
|
||||
AppVerName=APPLICATION_NAME APPLICATION_VERSION
|
||||
AppPublisher=APPLICATION_VENDOR
|
||||
AppComments=APPLICATION_COMMENTS
|
||||
AppCopyright=APPLICATION_COPYRIGHT
|
||||
AppPublisherURL=https://cryptomator.org/
|
||||
;AppSupportURL=http://java.com/
|
||||
;AppUpdatesURL=http://java.com/
|
||||
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
|
||||
DisableStartupPrompt=Yes
|
||||
DisableDirPage=No
|
||||
DisableProgramGroupPage=Yes
|
||||
DisableReadyPage=Yes
|
||||
DisableFinishedPage=No
|
||||
DisableWelcomePage=Yes
|
||||
DefaultGroupName=APPLICATION_GROUP
|
||||
;Optional License
|
||||
LicenseFile=APPLICATION_LICENSE_FILE
|
||||
;WinXP or above
|
||||
MinVersion=0,5.1
|
||||
OutputBaseFilename=INSTALLER_FILE_NAME
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
PrivilegesRequired=admin
|
||||
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
|
||||
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
|
||||
UninstallDisplayName=APPLICATION_NAME
|
||||
WizardImageStretch=No
|
||||
WizardSmallImageFile=Cryptomator-setup-icon.bmp
|
||||
WizardImageBackColor=$ffffff
|
||||
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Files]
|
||||
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
|
||||
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
|
||||
|
||||
[Code]
|
||||
function returnTrue(): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function returnFalse(): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
// Possible future improvements:
|
||||
// if version less or same => just launch app
|
||||
// if upgrade => check if same app is running and wait for it to exit
|
||||
// Add pack200/unpack200 support?
|
||||
Result := True;
|
||||
end;
|
||||
89
main/installer-win-portable/pom.xml
Normal file
@@ -0,0 +1,89 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-win-portable</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator (Portable) Windows installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<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>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<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 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:jvmarg value="-Xmx1024m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<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" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
BIN
main/installer-win/package/windows/Cryptomator-setup-icon.bmp
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
main/installer-win/package/windows/Cryptomator.ico
Normal file
|
After Width: | Height: | Size: 361 KiB |
80
main/installer-win/package/windows/Cryptomator.iss
Normal file
@@ -0,0 +1,80 @@
|
||||
;This file will be executed next to the application bundle image
|
||||
;I.e. current directory will contain folder APPLICATION_NAME with application files
|
||||
[Setup]
|
||||
AppId={{PRODUCT_APP_IDENTIFIER}}
|
||||
AppName=APPLICATION_NAME
|
||||
AppVersion=APPLICATION_VERSION
|
||||
AppVerName=APPLICATION_NAME APPLICATION_VERSION
|
||||
AppPublisher=APPLICATION_VENDOR
|
||||
AppComments=APPLICATION_COMMENTS
|
||||
AppCopyright=APPLICATION_COPYRIGHT
|
||||
AppPublisherURL=https://cryptomator.org/
|
||||
;AppSupportURL=http://java.com/
|
||||
;AppUpdatesURL=http://java.com/
|
||||
DefaultDirName=APPLICATION_INSTALL_ROOT\APPLICATION_NAME
|
||||
DisableStartupPrompt=Yes
|
||||
DisableDirPage=No
|
||||
DisableProgramGroupPage=Yes
|
||||
DisableReadyPage=Yes
|
||||
DisableFinishedPage=No
|
||||
DisableWelcomePage=Yes
|
||||
DefaultGroupName=APPLICATION_GROUP
|
||||
;Optional License
|
||||
LicenseFile=APPLICATION_LICENSE_FILE
|
||||
;WinXP or above
|
||||
MinVersion=0,5.1
|
||||
OutputBaseFilename=INSTALLER_FILE_NAME
|
||||
Compression=lzma
|
||||
SolidCompression=yes
|
||||
PrivilegesRequired=admin
|
||||
SetupIconFile=APPLICATION_NAME\APPLICATION_NAME.ico
|
||||
UninstallDisplayIcon={app}\APPLICATION_NAME.ico
|
||||
UninstallDisplayName=APPLICATION_NAME
|
||||
WizardImageStretch=No
|
||||
WizardSmallImageFile=Cryptomator-setup-icon.bmp
|
||||
WizardImageBackColor=$ffffff
|
||||
ArchitecturesInstallIn64BitMode=ARCHITECTURE_BIT_MODE
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Registry]
|
||||
;Root: HKCU; Subkey: "Software\Microsoft\Windows\CurrentVersion\Internet Settings"; ValueType: dword; ValueName: "AutoDetect"; ValueData: "0"
|
||||
Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Services\WebClient\Parameters"; ValueType: dword; ValueName: "FileSizeLimitInBytes"; ValueData: "$ffffffff"
|
||||
|
||||
[Files]
|
||||
Source: "APPLICATION_NAME\APPLICATION_NAME.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||
Source: "APPLICATION_NAME\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
|
||||
[Icons]
|
||||
Name: "{group}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_MENU_SHORTCUT()
|
||||
Name: "{commondesktop}\APPLICATION_NAME"; Filename: "{app}\APPLICATION_NAME.exe"; IconFilename: "{app}\APPLICATION_NAME.ico"; Check: APPLICATION_DESKTOP_SHORTCUT()
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Description: "{cm:LaunchProgram,APPLICATION_NAME}"; Flags: nowait postinstall skipifsilent; Check: APPLICATION_NOT_SERVICE()
|
||||
Filename: "{app}\RUN_FILENAME.exe"; Parameters: "-install -svcName ""APPLICATION_NAME"" -svcDesc ""APPLICATION_DESCRIPTION"" -mainExe ""APPLICATION_LAUNCHER_FILENAME"" START_ON_INSTALL RUN_AT_STARTUP"; Check: APPLICATION_SERVICE()
|
||||
Filename: "net"; Parameters: "stop webclient"; Description: "Stopping WebClient..."; Flags: waituntilterminated runhidden
|
||||
Filename: "net"; Parameters: "start webclient"; Description: "Restarting WebClient..."; Flags: waituntilterminated runhidden
|
||||
|
||||
[UninstallRun]
|
||||
Filename: "{app}\RUN_FILENAME.exe "; Parameters: "-uninstall -svcName APPLICATION_NAME STOP_ON_UNINSTALL"; Check: APPLICATION_SERVICE()
|
||||
|
||||
[Code]
|
||||
function returnTrue(): Boolean;
|
||||
begin
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
function returnFalse(): Boolean;
|
||||
begin
|
||||
Result := False;
|
||||
end;
|
||||
|
||||
function InitializeSetup(): Boolean;
|
||||
begin
|
||||
// Possible future improvements:
|
||||
// if version less or same => just launch app
|
||||
// if upgrade => check if same app is running and wait for it to exit
|
||||
// Add pack200/unpack200 support?
|
||||
Result := True;
|
||||
end;
|
||||
88
main/installer-win/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>installer-win</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator Windows installer</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<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>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<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 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:jvmarg value="-Xmx1024m"/>
|
||||
</fx:platform>
|
||||
<fx:resources>
|
||||
<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" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
80
main/pom.xml
@@ -1,10 +1,17 @@
|
||||
<?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 -->
|
||||
<!--
|
||||
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>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.5.2</version>
|
||||
<version>0.8.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Cryptomator</name>
|
||||
|
||||
@@ -32,6 +39,7 @@
|
||||
<commons-collections.version>4.0</commons-collections.version>
|
||||
<commons-lang3.version>3.3.2</commons-lang3.version>
|
||||
<commons-codec.version>1.10</commons-codec.version>
|
||||
<commons-httpclient.version>3.1</commons-httpclient.version>
|
||||
<jackson-databind.version>2.4.4</jackson-databind.version>
|
||||
<mockito.version>1.10.19</mockito.version>
|
||||
</properties>
|
||||
@@ -103,7 +111,20 @@
|
||||
<artifactId>commons-codec</artifactId>
|
||||
<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 -->
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
<version>${commons-httpclient.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Guava -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>18.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
@@ -165,7 +186,60 @@
|
||||
<module>ui</module>
|
||||
</modules>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>debian</id>
|
||||
<modules>
|
||||
<module>installer-debian</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>osx</id>
|
||||
<modules>
|
||||
<module>installer-osx</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>win</id>
|
||||
<modules>
|
||||
<module>installer-win</module>
|
||||
</modules>
|
||||
</profile>
|
||||
<profile>
|
||||
<id>win-portable</id>
|
||||
<modules>
|
||||
<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
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
Copyright (c) 2014 Sebastian Stenzel
|
||||
This file is licensed under the terms of the MIT license.
|
||||
See the LICENSE.txt file for more info.
|
||||
|
||||
Contributors:
|
||||
Sebastian Stenzel - initial API and implementation
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.8.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,17 +12,11 @@
|
||||
<parent>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>main</artifactId>
|
||||
<version>0.5.2</version>
|
||||
<version>0.8.0</version>
|
||||
</parent>
|
||||
<artifactId>ui</artifactId>
|
||||
<name>Cryptomator GUI</name>
|
||||
|
||||
<properties>
|
||||
<javafx.application.name>Cryptomator</javafx.application.name>
|
||||
<exec.mainClass>org.cryptomator.ui.Cryptomator</exec.mainClass>
|
||||
<javafx.tools.ant.jar>${java.home}/../lib/ant-javafx.jar</javafx.tools.ant.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
@@ -38,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>
|
||||
@@ -48,71 +48,15 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-httpclient</groupId>
|
||||
<artifactId>commons-httpclient</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DI -->
|
||||
<dependency>
|
||||
<groupId>com.google.inject</groupId>
|
||||
<artifactId>guice</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>make-assembly</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<finalName>${javafx.application.name}</finalName>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<archive>
|
||||
<manifestEntries>
|
||||
<Main-Class>${exec.mainClass}</Main-Class>
|
||||
</manifestEntries>
|
||||
</archive>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<version>1.7</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-deployment-bundle</id>
|
||||
<phase>install</phase>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
<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}" />
|
||||
|
||||
<fx:deploy nativeBundles="all" outdir="${project.build.directory}/dist" outfile="${project.build.finalName}" verbose="false">
|
||||
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
|
||||
<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:resources>
|
||||
<fx:fileset dir="${project.build.directory}" includes="${javafx.application.name}.jar" />
|
||||
</fx:resources>
|
||||
<fx:permissions elevated="false" />
|
||||
<fx:preferences install="true" />
|
||||
</fx:deploy>
|
||||
</target>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -43,7 +43,6 @@ public class MainApplication extends Application {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
|
||||
|
||||
private final CleanShutdownPerformer cleanShutdownPerformer = new CleanShutdownPerformer();
|
||||
private final ExecutorService executorService;
|
||||
private final ControllerFactory controllerFactory;
|
||||
private final DeferredCloser closer;
|
||||
@@ -53,22 +52,20 @@ public class MainApplication extends Application {
|
||||
}
|
||||
|
||||
private static Injector getInjector() {
|
||||
try {
|
||||
return Guice.createInjector(new MainModule());
|
||||
} catch (Exception e) {
|
||||
throw e;
|
||||
}
|
||||
return Guice.createInjector(new MainModule());
|
||||
}
|
||||
|
||||
public MainApplication(Injector injector) {
|
||||
this(injector.getInstance(ExecutorService.class), injector.getInstance(ControllerFactory.class), injector.getInstance(DeferredCloser.class));
|
||||
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) {
|
||||
public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer, MainApplicationReference appRef) {
|
||||
super();
|
||||
this.executorService = executorService;
|
||||
this.controllerFactory = controllerFactory;
|
||||
this.closer = closer;
|
||||
Cryptomator.addShutdownTask(closer::close);
|
||||
appRef.set(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -85,8 +82,6 @@ public class MainApplication extends Application {
|
||||
}
|
||||
});
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(cleanShutdownPerformer);
|
||||
|
||||
chooseNativeStylesheet();
|
||||
final ResourceBundle rb = ResourceBundle.getBundle("localization");
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
|
||||
@@ -165,18 +160,27 @@ public class MainApplication extends Application {
|
||||
@Override
|
||||
public void stop() {
|
||||
closer.close();
|
||||
try {
|
||||
Runtime.getRuntime().removeShutdownHook(cleanShutdownPerformer);
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private class CleanShutdownPerformer extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,22 +8,27 @@
|
||||
******************************************************************************/
|
||||
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.SamplingDecorator;
|
||||
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;
|
||||
@@ -57,6 +62,24 @@ public class MainModule extends AbstractModule {
|
||||
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() {
|
||||
@@ -65,7 +88,7 @@ public class MainModule extends AbstractModule {
|
||||
|
||||
@Provides
|
||||
Cryptor getCryptor() {
|
||||
return SamplingDecorator.decorate(new Aes256Cryptor());
|
||||
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -10,16 +10,18 @@ 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.Label;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
@@ -49,11 +51,17 @@ public class ChangePasswordController implements Initializable {
|
||||
private Button changePasswordButton;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
private Text messageText;
|
||||
|
||||
@FXML
|
||||
private Hyperlink downloadsPageLink;
|
||||
|
||||
private final Application app;
|
||||
|
||||
@Inject
|
||||
public ChangePasswordController() {
|
||||
public ChangePasswordController(Application app) {
|
||||
super();
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -76,12 +84,22 @@ public class ChangePasswordController implements Initializable {
|
||||
changePasswordButton.setDisable(oldPasswordIsEmpty || newPasswordIsEmpty || !passwordsAreEqual);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Downloads link
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
public void didClickDownloadsLink(ActionEvent event) {
|
||||
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Change password button
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
private void didClickChangePasswordButton(ActionEvent event) {
|
||||
downloadsPageLink.setVisible(false);
|
||||
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
|
||||
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
|
||||
|
||||
@@ -90,24 +108,34 @@ 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) {
|
||||
messageLabel.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
|
||||
} catch (IOException ex) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
|
||||
LOG.error("Decryption failed for technical reasons.", ex);
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
return;
|
||||
} catch (WrongPasswordException e) {
|
||||
messageLabel.setText(rb.getString("changePassword.errorMessage.wrongPassword"));
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.wrongPassword"));
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
Platform.runLater(oldPasswordField::requestFocus);
|
||||
return;
|
||||
} catch (UnsupportedKeyLengthException ex) {
|
||||
messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
return;
|
||||
} catch (UnsupportedVaultException e) {
|
||||
downloadsPageLink.setVisible(true);
|
||||
if (e.isVaultOlderThanSoftware()) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware") + " ");
|
||||
} else if (e.isSoftwareOlderThanVault()) {
|
||||
messageText.setText(rb.getString("changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
}
|
||||
newPasswordField.swipe();
|
||||
retypePasswordField.swipe();
|
||||
return;
|
||||
} finally {
|
||||
oldPasswordField.swipe();
|
||||
}
|
||||
@@ -118,7 +146,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);
|
||||
messageLabel.setText(rb.getString("changePassword.infoMessage.success"));
|
||||
messageText.setText(rb.getString("changePassword.infoMessage.success"));
|
||||
Platform.runLater(this::didChangePassword);
|
||||
// At this point the backup is still using the old password.
|
||||
// It will be changed as soon as the user unlocks the vault the next time.
|
||||
|
||||
@@ -12,6 +12,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
@@ -78,6 +79,11 @@ public class InitializeController implements Initializable {
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) {
|
||||
vault.getCryptor().encryptMasterKey(masterKeyOutputStream, password);
|
||||
final String dataRootDir = vault.getCryptor().encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
|
||||
final Path dataRootPath = vault.getPath().resolve("d").resolve(dataRootDir);
|
||||
final Path metadataPath = vault.getPath().resolve("m");
|
||||
Files.createDirectories(dataRootPath);
|
||||
Files.createDirectories(metadataPath);
|
||||
if (listener != null) {
|
||||
listener.didInitialize(this);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
package org.cryptomator.ui.controllers;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.beans.Observable;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.collections.ListChangeListener.Change;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.cell.CheckBoxListCell;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
|
||||
public class MacWarningsController implements Initializable {
|
||||
|
||||
@FXML
|
||||
private ListView<Warning> warningsList;
|
||||
|
||||
@FXML
|
||||
private Button whitelistButton;
|
||||
|
||||
private final Application application;
|
||||
private final ObservableList<Warning> warnings = FXCollections.observableArrayList();
|
||||
private final ListChangeListener<String> unauthenticatedResourcesChangeListener = this::unauthenticatedResourcesDidChange;
|
||||
private final ChangeListener<Boolean> stageVisibilityChangeListener = this::windowVisibilityDidChange;
|
||||
private Stage stage;
|
||||
private Vault vault;
|
||||
private ResourceBundle rb;
|
||||
|
||||
@Inject
|
||||
public MacWarningsController(Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
warnings.addListener(this::warningsDidInvalidate);
|
||||
warningsList.setItems(warnings);
|
||||
warningsList.setCellFactory(CheckBoxListCell.forListView(Warning::selectedProperty, new StringConverter<Warning>() {
|
||||
|
||||
@Override
|
||||
public String toString(Warning object) {
|
||||
return object.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Warning fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void 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
|
||||
private void didClickMoreInformationButton(ActionEvent event) {
|
||||
application.getHostServices().showDocument("https://cryptomator.org/help.html#macWarning");
|
||||
}
|
||||
|
||||
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()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void warningsDidInvalidate(Observable observable) {
|
||||
disableWhitelistButtonIfNothingSelected();
|
||||
}
|
||||
|
||||
private void windowVisibilityDidChange(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
if (Boolean.TRUE.equals(newValue)) {
|
||||
stage.setTitle(String.format(rb.getString("macWarnings.windowTitle"), vault.getName()));
|
||||
warnings.addAll(vault.getNamesOfResourcesWithInvalidMac().stream().map(Warning::new).collect(Collectors.toList()));
|
||||
vault.getNamesOfResourcesWithInvalidMac().addListener(this.unauthenticatedResourcesChangeListener);
|
||||
} else {
|
||||
vault.getNamesOfResourcesWithInvalidMac().clear();
|
||||
vault.getNamesOfResourcesWithInvalidMac().removeListener(this.unauthenticatedResourcesChangeListener);
|
||||
}
|
||||
}
|
||||
|
||||
private void disableWhitelistButtonIfNothingSelected() {
|
||||
whitelistButton.setDisable(warnings.filtered(w -> w.isSelected()).isEmpty());
|
||||
}
|
||||
|
||||
public void setStage(Stage stage) {
|
||||
this.stage = stage;
|
||||
stage.showingProperty().addListener(new WeakChangeListener<>(stageVisibilityChangeListener));
|
||||
}
|
||||
|
||||
public void setVault(Vault vault) {
|
||||
this.vault = vault;
|
||||
}
|
||||
|
||||
private class Warning {
|
||||
|
||||
private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();
|
||||
private final BooleanProperty selected = new SimpleBooleanProperty(false);
|
||||
|
||||
public Warning(String name) {
|
||||
this.name.set(name);
|
||||
this.selectedProperty().addListener(change -> {
|
||||
disableWhitelistButtonIfNothingSelected();
|
||||
});
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name.get();
|
||||
}
|
||||
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -124,18 +124,21 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
final FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator vault", "*" + Vault.VAULT_FILE_EXTENSION));
|
||||
final File file = fileChooser.showSaveDialog(stage);
|
||||
if (file == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (file != null) {
|
||||
final Path vaultDir;
|
||||
// enforce .cryptomator file extension:
|
||||
if (!file.getName().endsWith(Vault.VAULT_FILE_EXTENSION)) {
|
||||
final Path correctedPath = file.toPath().resolveSibling(file.getName() + Vault.VAULT_FILE_EXTENSION);
|
||||
vaultDir = Files.createDirectory(correctedPath);
|
||||
} else {
|
||||
vaultDir = Files.createDirectory(file.toPath());
|
||||
}
|
||||
addVault(vaultDir, true);
|
||||
final Path vaultDir;
|
||||
// enforce .cryptomator file extension:
|
||||
if (!file.getName().endsWith(Vault.VAULT_FILE_EXTENSION)) {
|
||||
vaultDir = file.toPath().resolveSibling(file.getName() + Vault.VAULT_FILE_EXTENSION);
|
||||
} else {
|
||||
vaultDir = file.toPath();
|
||||
}
|
||||
if (!Files.exists(vaultDir)) {
|
||||
Files.createDirectory(vaultDir);
|
||||
}
|
||||
addVault(vaultDir, true);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Unable to create vault", e);
|
||||
}
|
||||
@@ -181,7 +184,7 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
|
||||
private ListCell<Vault> createDirecoryListCell(ListView<Vault> param) {
|
||||
final DirectoryListCell cell = new DirectoryListCell();
|
||||
cell.setContextMenu(vaultListCellContextMenu);
|
||||
cell.setVaultContextMenu(vaultListCellContextMenu);
|
||||
return cell;
|
||||
}
|
||||
|
||||
@@ -283,7 +286,7 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
@Override
|
||||
public void didLock(UnlockedController ctrl) {
|
||||
showUnlockView(ctrl.getVault());
|
||||
if (getUnlockedDirectories().isEmpty()) {
|
||||
if (getUnlockedVaults().isEmpty()) {
|
||||
Platform.setImplicitExit(true);
|
||||
}
|
||||
}
|
||||
@@ -301,12 +304,12 @@ public class MainController implements Initializable, InitializationListener, Un
|
||||
|
||||
/* 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 */
|
||||
|
||||
@@ -19,20 +19,24 @@ 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.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Hyperlink;
|
||||
import javafx.scene.control.ProgressIndicator;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.text.Text;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.lang3.CharUtils;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
|
||||
import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.ui.controls.SecPasswordField;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
@@ -63,13 +67,18 @@ public class UnlockController implements Initializable {
|
||||
private ProgressIndicator progressIndicator;
|
||||
|
||||
@FXML
|
||||
private Label messageLabel;
|
||||
private Text messageText;
|
||||
|
||||
@FXML
|
||||
private Hyperlink downloadsPageLink;
|
||||
|
||||
private final ExecutorService exec;
|
||||
private final Application app;
|
||||
|
||||
@Inject
|
||||
public UnlockController(ExecutorService exec) {
|
||||
public UnlockController(Application app, ExecutorService exec) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.exec = exec;
|
||||
}
|
||||
|
||||
@@ -91,6 +100,15 @@ public class UnlockController implements Initializable {
|
||||
unlockButton.setDisable(passwordIsEmpty);
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Downloads link
|
||||
// ****************************************
|
||||
|
||||
@FXML
|
||||
public void didClickDownloadsLink(ActionEvent event) {
|
||||
app.getHostServices().showDocument("https://cryptomator.org/downloads/");
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// Unlock button
|
||||
// ****************************************
|
||||
@@ -99,39 +117,50 @@ public class UnlockController implements Initializable {
|
||||
private void didClickUnlockButton(ActionEvent event) {
|
||||
setControlsDisabled(true);
|
||||
progressIndicator.setVisible(true);
|
||||
downloadsPageLink.setVisible(false);
|
||||
final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE);
|
||||
final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE);
|
||||
final CharSequence password = passwordField.getCharacters();
|
||||
try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) {
|
||||
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
|
||||
if (!vault.startServer()) {
|
||||
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
|
||||
vault.getCryptor().swipeSensitiveData();
|
||||
messageText.setText(rb.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());
|
||||
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::didUnlockAndMount);
|
||||
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, (result) -> {
|
||||
setControlsDisabled(false);
|
||||
});
|
||||
} catch (DecryptFailedException | IOException ex) {
|
||||
FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::unlockAndMountFinished);
|
||||
} catch (IOException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
|
||||
messageText.setText(rb.getString("unlock.errorMessage.decryptionFailed"));
|
||||
LOG.error("Decryption failed for technical reasons.", ex);
|
||||
} catch (WrongPasswordException e) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(rb.getString("unlock.errorMessage.wrongPassword"));
|
||||
messageText.setText(rb.getString("unlock.errorMessage.wrongPassword"));
|
||||
Platform.runLater(passwordField::requestFocus);
|
||||
} catch (UnsupportedKeyLengthException ex) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
|
||||
messageText.setText(rb.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") + " ");
|
||||
} else if (e.isSoftwareOlderThanVault()) {
|
||||
messageText.setText(rb.getString("unlock.errorMessage.unsupportedVersion.softwareOlderThanVault") + " ");
|
||||
}
|
||||
} catch (DestroyFailedException e) {
|
||||
setControlsDisabled(false);
|
||||
progressIndicator.setVisible(false);
|
||||
LOG.error("Destruction of cryptor threw an exception.", e);
|
||||
} finally {
|
||||
passwordField.swipe();
|
||||
}
|
||||
@@ -143,9 +172,14 @@ public class UnlockController implements Initializable {
|
||||
unlockButton.setDisable(disable);
|
||||
}
|
||||
|
||||
private void didUnlockAndMount(boolean mountSuccess) {
|
||||
private void unlockAndMountFinished(boolean mountSuccess) {
|
||||
progressIndicator.setVisible(false);
|
||||
if (listener != null) {
|
||||
setControlsDisabled(false);
|
||||
if (vault.isUnlocked() && !mountSuccess) {
|
||||
vault.stopServer();
|
||||
vault.setUnlocked(false);
|
||||
}
|
||||
if (mountSuccess && listener != null) {
|
||||
listener.didUnlock(this);
|
||||
}
|
||||
}
|
||||
@@ -164,6 +198,8 @@ public class UnlockController implements Initializable {
|
||||
// newValue is guaranteed to be a-z0-9, see #filterAlphanumericKeyEvents
|
||||
if (newValue.isEmpty()) {
|
||||
mountName.setText(vault.getMountName());
|
||||
} else {
|
||||
vault.setMountName(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,25 +8,35 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
import javafx.animation.Animation;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.chart.LineChart;
|
||||
import javafx.scene.chart.NumberAxis;
|
||||
import javafx.scene.chart.XYChart.Data;
|
||||
import javafx.scene.chart.XYChart.Series;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
|
||||
import org.cryptomator.crypto.CryptorIOSampling;
|
||||
import org.cryptomator.ui.MainModule.ControllerFactory;
|
||||
import org.cryptomator.ui.model.Vault;
|
||||
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
||||
@@ -34,6 +44,9 @@ public class UnlockedController implements Initializable {
|
||||
|
||||
private static final int IO_SAMPLING_STEPS = 100;
|
||||
private static final double IO_SAMPLING_INTERVAL = 0.25;
|
||||
private final ControllerFactory controllerFactory;
|
||||
private final Stage macWarningWindow = new Stage();
|
||||
private MacWarningsController macWarningCtrl;
|
||||
private LockListener listener;
|
||||
private Vault vault;
|
||||
private Timeline ioAnimation;
|
||||
@@ -47,18 +60,42 @@ public class UnlockedController implements Initializable {
|
||||
@FXML
|
||||
private NumberAxis xAxis;
|
||||
|
||||
private ResourceBundle rb;
|
||||
|
||||
@Inject
|
||||
public UnlockedController() {
|
||||
super();
|
||||
public UnlockedController(ControllerFactory controllerFactory) {
|
||||
this.controllerFactory = controllerFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(URL url, ResourceBundle rb) {
|
||||
this.rb = rb;
|
||||
|
||||
try {
|
||||
final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/mac_warnings.fxml"), rb);
|
||||
loader.setControllerFactory(controllerFactory);
|
||||
|
||||
final Parent root = loader.load();
|
||||
macWarningWindow.setScene(new Scene(root));
|
||||
macWarningWindow.sizeToScene();
|
||||
macWarningWindow.setResizable(false);
|
||||
ActiveWindowStyleSupport.startObservingFocus(macWarningWindow);
|
||||
|
||||
macWarningCtrl = loader.getController();
|
||||
macWarningCtrl.setStage(macWarningWindow);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("Failed to load fxml file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
private void didClickCloseVault(ActionEvent event) {
|
||||
vault.unmount();
|
||||
try {
|
||||
vault.unmount();
|
||||
} catch (CommandFailedException e) {
|
||||
messageLabel.setText(rb.getString("unlocked.label.unmountFailed"));
|
||||
return;
|
||||
}
|
||||
vault.stopServer();
|
||||
vault.setUnlocked(false);
|
||||
if (listener != null) {
|
||||
@@ -66,6 +103,22 @@ public class UnlockedController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// MAC Auth Warnings
|
||||
// ****************************************
|
||||
|
||||
private void macWarningsDidChange(ListChangeListener.Change<? extends String> change) {
|
||||
if (change.getList().size() > 0) {
|
||||
Platform.runLater(() -> {
|
||||
macWarningWindow.show();
|
||||
});
|
||||
} else {
|
||||
Platform.runLater(() -> {
|
||||
macWarningWindow.hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ****************************************
|
||||
// IO Graph
|
||||
// ****************************************
|
||||
@@ -126,11 +179,22 @@ public class UnlockedController implements Initializable {
|
||||
return vault;
|
||||
}
|
||||
|
||||
public void setVault(Vault directory) {
|
||||
this.vault = directory;
|
||||
public void setVault(Vault vault) {
|
||||
this.vault = vault;
|
||||
macWarningCtrl.setVault(vault);
|
||||
|
||||
if (directory.getCryptor() instanceof CryptorIOSampling) {
|
||||
startIoSampling((CryptorIOSampling) directory.getCryptor());
|
||||
// listen to MAC warnings as long as this vault is unlocked:
|
||||
final ListChangeListener<String> macWarningsListener = this::macWarningsDidChange;
|
||||
vault.getNamesOfResourcesWithInvalidMac().addListener(macWarningsListener);
|
||||
vault.unlockedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if (Boolean.FALSE.equals(newValue)) {
|
||||
vault.getNamesOfResourcesWithInvalidMac().removeListener(macWarningsListener);
|
||||
}
|
||||
});
|
||||
|
||||
// sample crypto-throughput:
|
||||
if (vault.getCryptor() instanceof CryptorIOSampling) {
|
||||
startIoSampling((CryptorIOSampling) vault.getCryptor());
|
||||
} else {
|
||||
ioGraph.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.controllers;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
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 org.apache.commons.httpclient.HttpClient;
|
||||
import org.apache.commons.httpclient.HttpMethod;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class WelcomeController implements Initializable {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(WelcomeController.class);
|
||||
|
||||
@FXML
|
||||
private ImageView botImageView;
|
||||
|
||||
@FXML
|
||||
private Hyperlink updateLink;
|
||||
|
||||
private final Application app;
|
||||
private final Comparator<String> semVerComparator;
|
||||
private final ExecutorService executor;
|
||||
private ResourceBundle rb;
|
||||
|
||||
@Inject
|
||||
public WelcomeController(Application app, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
|
||||
this.app = app;
|
||||
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);
|
||||
}
|
||||
|
||||
private void checkForUpdates() {
|
||||
final HttpClient client = new HttpClient();
|
||||
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
|
||||
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
|
||||
client.getParams().setConnectionManagerTimeout(5000);
|
||||
try {
|
||||
client.executeMethod(method);
|
||||
if (method.getStatusCode() == HttpStatus.SC_OK) {
|
||||
final byte[] responseData = method.getResponseBody();
|
||||
final ObjectMapper mapper = new ObjectMapper();
|
||||
final Map<String, String> map = mapper.readValue(responseData, new TypeReference<HashMap<String, String>>() {
|
||||
});
|
||||
this.compareVersions(map);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// no error handling required. Maybe next time the version check is successful.
|
||||
}
|
||||
}
|
||||
|
||||
private void compareVersions(final Map<String, String> latestVersions) {
|
||||
final String latestVersion;
|
||||
if (SystemUtils.IS_OS_MAC_OSX) {
|
||||
latestVersion = latestVersions.get("mac");
|
||||
} else if (SystemUtils.IS_OS_WINDOWS) {
|
||||
latestVersion = latestVersions.get("win");
|
||||
} else if (SystemUtils.IS_OS_LINUX) {
|
||||
latestVersion = latestVersions.get("linux");
|
||||
} else {
|
||||
// no version check possible on unsupported OS
|
||||
return;
|
||||
}
|
||||
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);
|
||||
Platform.runLater(() -> {
|
||||
this.updateLink.setText(msg);
|
||||
this.updateLink.setVisible(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@FXML
|
||||
public void didClickUpdateLink(ActionEvent event) {
|
||||
app.getHostServices().showDocument("https://cryptomator.org/#download");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package org.cryptomator.ui.controls;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.scene.paint.Paint;
|
||||
@@ -21,6 +22,7 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
|
||||
private static final Color GREEN_STROKE = Color.rgb(48, 183, 64);
|
||||
|
||||
private final Circle statusIndicator = new Circle(4.5);
|
||||
private ContextMenu vaultContextMenu;
|
||||
|
||||
public DirectoryListCell() {
|
||||
setGraphic(statusIndicator);
|
||||
@@ -38,6 +40,7 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
|
||||
if (item == null) {
|
||||
setText(null);
|
||||
setTooltip(null);
|
||||
setContextMenu(null);
|
||||
statusIndicator.setVisible(false);
|
||||
} else {
|
||||
setText(item.getName());
|
||||
@@ -45,12 +48,14 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
|
||||
statusIndicator.setVisible(true);
|
||||
item.unlockedProperty().addListener(this);
|
||||
updateStatusIndicator();
|
||||
updateContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
|
||||
updateStatusIndicator();
|
||||
updateContextMenu();
|
||||
}
|
||||
|
||||
private void updateStatusIndicator() {
|
||||
@@ -60,4 +65,16 @@ public class DirectoryListCell extends DraggableListCell<Vault> implements Chang
|
||||
statusIndicator.setStroke(strokeColor);
|
||||
}
|
||||
|
||||
private void updateContextMenu() {
|
||||
if (getItem().isUnlocked()) {
|
||||
this.setContextMenu(null);
|
||||
} else {
|
||||
this.setContextMenu(vaultContextMenu);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVaultContextMenu(ContextMenu contextMenu) {
|
||||
this.vaultContextMenu = contextMenu;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package org.cryptomator.ui.logging;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Serializable;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.apache.logging.log4j.core.Filter;
|
||||
import org.apache.logging.log4j.core.Layout;
|
||||
import org.apache.logging.log4j.core.appender.AbstractOutputStreamAppender;
|
||||
import org.apache.logging.log4j.core.appender.FileManager;
|
||||
import org.apache.logging.log4j.core.config.plugins.Plugin;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginElement;
|
||||
import org.apache.logging.log4j.core.config.plugins.PluginFactory;
|
||||
import org.apache.logging.log4j.core.layout.PatternLayout;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
|
||||
/**
|
||||
* A preconfigured FileAppender only relying on a configurable system property, e.g. <code>-DlogPath=/var/log/cryptomator.log</code>.<br/>
|
||||
* Other than the normal {@link org.apache.logging.log4j.core.appender.FileAppender} paths can be resolved relative to the users home directory.
|
||||
*/
|
||||
@Plugin(name = "ConfigurableFile", category = "Core", elementType = "appender", printObject = true)
|
||||
public class ConfigurableFileAppender extends AbstractOutputStreamAppender<FileManager> {
|
||||
|
||||
private static final long serialVersionUID = -6548221568069606389L;
|
||||
private static final int DEFAULT_BUFFER_SIZE = 8192;
|
||||
private static final String DEFAULT_FILE_NAME = "cryptomator.log";
|
||||
private static final Pattern DRIVE_LETTER_WITH_PRECEEDING_SLASH = Pattern.compile("^/[A-Z]:", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
protected ConfigurableFileAppender(String name, Layout<? extends Serializable> layout, Filter filter, FileManager manager) {
|
||||
super(name, layout, filter, true, true, manager);
|
||||
LOGGER.info("Logging to " + manager.getFileName());
|
||||
}
|
||||
|
||||
@PluginFactory
|
||||
public static ConfigurableFileAppender createAppender(@PluginAttribute("name") final String name, @PluginAttribute("pathPropertyName") final String pathPropertyName,
|
||||
@PluginElement("Layout") Layout<? extends Serializable> layout) {
|
||||
|
||||
if (name == null) {
|
||||
LOGGER.error("No name provided for HomeDirectoryAwareFileAppender");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pathPropertyName == null) {
|
||||
LOGGER.error("No pathPropertyName provided for HomeDirectoryAwareFileAppender with name " + name);
|
||||
return null;
|
||||
}
|
||||
|
||||
String fileName = System.getProperty(pathPropertyName);
|
||||
if (Strings.isEmpty(fileName)) {
|
||||
fileName = DEFAULT_FILE_NAME;
|
||||
}
|
||||
|
||||
final Path filePath;
|
||||
if (fileName.startsWith("~/")) {
|
||||
// home-dir-relative Path:
|
||||
final Path userHome = FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
|
||||
filePath = userHome.resolve(fileName.substring(2));
|
||||
} else if (fileName.startsWith("/")) {
|
||||
// absolute Path:
|
||||
filePath = FileSystems.getDefault().getPath(fileName);
|
||||
} else if (SystemUtils.IS_OS_WINDOWS && fileName.startsWith("%appdata%/")) {
|
||||
final String appdata = System.getenv("APPDATA");
|
||||
final Path appdataPath = appdata != null ? FileSystems.getDefault().getPath(appdata) : FileSystems.getDefault().getPath(SystemUtils.USER_HOME);
|
||||
filePath = appdataPath.resolve(fileName.substring(10));
|
||||
} else {
|
||||
// relative Path:
|
||||
try {
|
||||
String jarFileLocation = ConfigurableFileAppender.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
|
||||
if (SystemUtils.IS_OS_WINDOWS && DRIVE_LETTER_WITH_PRECEEDING_SLASH.matcher(jarFileLocation).find()) {
|
||||
// on windows we need to remove a preceeding slash from "/C:/foo/bar":
|
||||
jarFileLocation = jarFileLocation.substring(1);
|
||||
}
|
||||
final Path workingDir = FileSystems.getDefault().getPath(jarFileLocation).getParent();
|
||||
filePath = workingDir.resolve(fileName);
|
||||
} catch (URISyntaxException e) {
|
||||
LOGGER.error("Unable to resolve working directory ", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (layout == null) {
|
||||
layout = PatternLayout.createDefaultLayout();
|
||||
}
|
||||
|
||||
if (!Files.exists(filePath.getParent())) {
|
||||
try {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Could not create parent directories for log file located at " + filePath.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
final FileManager manager = FileManager.getFileManager(filePath.toString(), false, false, true, null, layout, DEFAULT_BUFFER_SIZE);
|
||||
return new ConfigurableFileAppender(name, layout, null, manager);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,15 +6,22 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.text.Normalizer;
|
||||
import java.text.Normalizer.Form;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.ui.util.DeferredClosable;
|
||||
import org.cryptomator.ui.util.DeferredCloser;
|
||||
import org.cryptomator.ui.util.FXThreads;
|
||||
import org.cryptomator.ui.util.mount.CommandFailedException;
|
||||
import org.cryptomator.ui.util.mount.WebDavMount;
|
||||
import org.cryptomator.ui.util.mount.WebDavMounter;
|
||||
@@ -38,6 +45,8 @@ public class Vault implements Serializable {
|
||||
private final WebDavMounter mounter;
|
||||
private final DeferredCloser closer;
|
||||
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
|
||||
private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
|
||||
private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
|
||||
|
||||
private String mountName;
|
||||
private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
|
||||
@@ -70,22 +79,34 @@ 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, getMountName());
|
||||
ServletLifeCycleAdapter servlet = server.createServlet(path, cryptor, namesOfResourcesWithInvalidMac, whitelistedResourcesWithInvalidMac, mountName);
|
||||
if (servlet.start()) {
|
||||
webDavServlet = closer.closeLater(servlet, ServletLifeCycleAdapter::stop);
|
||||
webDavServlet = closer.closeLater(servlet);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void stopServer() {
|
||||
unmount();
|
||||
try {
|
||||
unmount();
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("Unmounting failed. Locking anyway...", e);
|
||||
}
|
||||
webDavServlet.close();
|
||||
cryptor.swipeSensitiveData();
|
||||
try {
|
||||
cryptor.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
LOG.error("Destruction of cryptor throw an exception.", e);
|
||||
}
|
||||
whitelistedResourcesWithInvalidMac.clear();
|
||||
namesOfResourcesWithInvalidMac.clear();
|
||||
}
|
||||
|
||||
public boolean mount() {
|
||||
@@ -94,7 +115,7 @@ public class Vault implements Serializable {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), getMountName()), WebDavMount::unmount);
|
||||
webDavMount = closer.closeLater(mounter.mount(o.get().getServletUri(), mountName));
|
||||
return true;
|
||||
} catch (CommandFailedException e) {
|
||||
LOG.warn("mount failed", e);
|
||||
@@ -102,8 +123,12 @@ public class Vault implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void unmount() {
|
||||
webDavMount.close();
|
||||
public void unmount() throws CommandFailedException {
|
||||
final WebDavMount mnt = webDavMount.get().orElse(null);
|
||||
if (mnt != null) {
|
||||
mnt.unmount();
|
||||
}
|
||||
webDavMount = DeferredClosable.empty();
|
||||
}
|
||||
|
||||
/* Getter/Setter */
|
||||
@@ -139,6 +164,14 @@ public class Vault implements Serializable {
|
||||
return mountName;
|
||||
}
|
||||
|
||||
public ObservableList<String> getNamesOfResourcesWithInvalidMac() {
|
||||
return namesOfResourcesWithInvalidMac;
|
||||
}
|
||||
|
||||
public Set<String> getWhitelistedResourcesWithInvalidMac() {
|
||||
return whitelistedResourcesWithInvalidMac;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to form a similar string using the regular latin alphabet.
|
||||
*
|
||||
|
||||
@@ -50,14 +50,22 @@ 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) {
|
||||
return SETTINGS_DIR.resolve(SETTINGS_FILE);
|
||||
} else {
|
||||
return FileSystems.getDefault().getPath(settingsPathProperty);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Settings get() {
|
||||
Settings settings = null;
|
||||
try {
|
||||
Files.createDirectories(SETTINGS_DIR);
|
||||
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
|
||||
final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
|
||||
final Path settingsPath = getSettingsPath();
|
||||
final InputStream in = Files.newInputStream(settingsPath, StandardOpenOption.READ);
|
||||
settings = objectMapper.readValue(in, Settings.class);
|
||||
settings.getDirectories().removeIf(v -> !v.isValidVaultDirectory());
|
||||
} catch (IOException e) {
|
||||
@@ -73,9 +81,9 @@ public class SettingsProvider implements Provider<Settings> {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.createDirectories(SETTINGS_DIR);
|
||||
final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
|
||||
final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
|
||||
final Path settingsPath = getSettingsPath();
|
||||
Files.createDirectories(settingsPath.getParent());
|
||||
final OutputStream out = Files.newOutputStream(settingsPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
|
||||
objectMapper.writeValue(out, settings);
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to save settings.", e);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.ui.util;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -91,11 +92,13 @@ public class DeferredCloser implements AutoCloseable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes all added objects which have not been closed before.
|
||||
* Closes all added objects which have not been closed before and releases references.
|
||||
*/
|
||||
public void close() {
|
||||
for (ManagedResource<?> closableProvider : cleanups.values()) {
|
||||
for (Iterator<ManagedResource<?>> iterator = cleanups.values().iterator(); iterator.hasNext();) {
|
||||
final ManagedResource<?> closableProvider = iterator.next();
|
||||
closableProvider.close();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||