Compare commits

..

52 Commits
0.6.0 ... 0.7.2

Author SHA1 Message Date
Sebastian Stenzel
85f3487cf0 fixes #64 by attempting to mount the more common "localhost", which is more likely to be excluded from proxy autoconfiguration scripts. 2015-06-05 15:17:33 +02:00
Sebastian Stenzel
4a754d6a6c Beginning development of 0.8.0 2015-05-31 00:04:26 +02:00
Sebastian Stenzel
abf9920caf its getting late... 2015-05-30 22:29:59 +02:00
Sebastian Stenzel
dd2863da5b 0.7.1 (fixed debian build)
updated travis script (requires git release tags to be equal to maven version, so starting with this tag we drop the preceeding "v")
2015-05-30 22:22:50 +02:00
Sebastian Stenzel
d43396bcfb updated version 2015-05-30 21:31:26 +02:00
Sebastian Stenzel
b6383f49b1 logging to %appdata% on windows 2015-05-30 20:55:29 +02:00
Sebastian Stenzel
c5b241a68a cleanup 2015-05-30 20:40:08 +02:00
Sebastian Stenzel
00a39c80cb Merge branch 'windows-unc-path-mounter' 2015-05-30 20:39:23 +02:00
Sebastian Stenzel
8d8fe74d3a restored ability to open vaults with 128 bit keylength 2015-05-30 20:13:11 +02:00
Sebastian Stenzel
e767436f5d updated jackrabbit (fixing security issue, see https://issues.apache.org/jira/browse/JCR-3883) 2015-05-29 23:39:36 +02:00
Sebastian Stenzel
03cdf1fdc9 added metadata caching 2015-05-29 11:18:23 +02:00
Sebastian Stenzel
49646aae41 improved directory name caching (>95% hitrate now) 2015-05-29 10:47:50 +02:00
Sebastian Stenzel
f3aa636b8b windows mount/unmount improvements 2015-05-28 17:34:56 +02:00
Sebastian Stenzel
c73f18e3b8 using ipv6-literal instead of localhost and bypassing proxy for localhost (wtf anyway) again... 2015-05-25 17:32:55 +02:00
Sebastian Stenzel
5f40ce50e7 fixes #41 2015-05-25 16:43:41 +02:00
Sebastian Stenzel
744f9db958 fixes #52 2015-05-25 16:22:52 +02:00
Sebastian Stenzel
111ee99ae1 - fixed invalid path for windows logfiles
- yet another attempt to improve (i don't even dare to say fix) #41
2015-05-25 14:37:12 +02:00
Sebastian Stenzel
7d81ff3b43 Merge pull request #59 from MuscleRumble/master
Replaced tray icon with monochrome version
2015-05-24 23:19:00 +02:00
Tobias Hagemann
00a2c6c5ae Replaced tray icon with monochrome version 2015-05-24 23:16:54 +02:00
Sebastian Stenzel
587c45ee63 added a default logging location, if logPath property is not set. 2015-05-24 22:30:12 +02:00
Sebastian Stenzel
3d3cb7bb86 Writing logfiles now. 2015-05-24 21:51:37 +02:00
Sebastian Stenzel
0e3513e86d - locking file header during creation,
- suggesting range request for files > 32MiB only
2015-05-22 22:26:39 +02:00
Sebastian Stenzel
8845efb983 fixed infinite number of authentication jobs resulting in heavy cpu load 2015-05-22 22:04:32 +02:00
Sebastian Stenzel
88f81d2682 Merge branch 'webdav-directory-moving' 2015-05-21 18:50:56 +02:00
Sebastian Stenzel
58d500baaf Merge pull request #58 from flyingarg/master
fixes #57
Thank you very much, @flyingarg
2015-05-18 17:27:36 +02:00
Mohit Raju
103ea9047f updated method and paramternames to openMountWithWebdavUri 2015-05-18 16:13:25 +03:00
Mohit Raju
f4b07b9807 restructure openFMWithWebdavSchema 2015-05-18 12:40:19 +03:00
Mohit Raju
6a3b4d486d added contributor name 2015-05-18 11:50:35 +03:00
Mohit Raju
13bcde318b removing debug logs 2015-05-18 10:10:07 +03:00
Mohit Raju
242486c0b1 Allowing webdav schema name fallback 2015-05-17 16:57:22 +03:00
Sebastian Stenzel
ea9c8eee83 yet another refactoring session (functionality restored now) 2015-05-15 23:17:24 +02:00
Sebastian Stenzel
0d969432c2 some more flat hierarchy fixes 2015-05-15 18:13:34 +02:00
Sebastian Stenzel
be369b480b some more destruction... 2015-05-14 21:48:02 +02:00
Sebastian Stenzel
4cf872f916 directory moving 2015-05-14 07:37:56 +02:00
Sebastian Stenzel
3d3c36b66f Update README.md 2015-05-12 22:19:36 +02:00
Sebastian Stenzel
54c2afe3d1 os-specific installer modules 2015-05-11 00:37:31 +02:00
Sebastian Stenzel
3c71878b6b First attempt of adding a portable version for windows users. (Issue #48) 2015-05-10 17:23:57 +02:00
Sebastian Stenzel
f36a61df1c Merge pull request #54 from cryptomator/flatDirectoryStructure
Flat directory structure
2015-05-10 14:54:49 +02:00
Sebastian Stenzel
1642aa4688 fixes #49 2015-05-10 14:13:07 +02:00
Sebastian Stenzel
6f9b16a7dc fixes #53 2015-05-10 14:00:00 +02:00
Sebastian Stenzel
66ed9126de version check during masterkey decryption -> added option to go to download page of different version 2015-05-10 12:39:28 +02:00
Sebastian Stenzel
a07efc5209 Proper error handling for outdated vault formats 2015-05-05 17:29:51 +02:00
Sebastian Stenzel
bbeeb79812 reduced max file name size, locking metadata files before read/write. 2015-05-05 06:50:16 +02:00
Sebastian Stenzel
4d08e9d72b cleanup 2015-05-04 22:02:47 +02:00
Sebastian Stenzel
040f260bf0 authenticated file header 2015-05-04 21:31:41 +02:00
Sebastian Stenzel
cdf9c28a38 refactored directory structure, so windows (and OneDrive) can handle vaults better 2015-04-28 18:19:05 +02:00
Sebastian Stenzel
a6972f62f2 Merge pull request #51 from MuscleRumble/master
Fixed .cryptomator bundle extension registration in OS X
2015-04-17 15:01:34 +02:00
Tobias Hagemann
1db32470b1 Fixed .cryptomator bundle extension registration in OS X 2015-04-17 10:22:10 +02:00
Sebastian Stenzel
ed022412fe fixed travis build for untagged versions 2015-04-08 21:42:06 +02:00
Sebastian Stenzel
a2356b62c7 Updated travis configuration and paths to new GitHub repo 2015-04-08 21:32:57 +02:00
Sebastian Stenzel
9aa6117fb0 Fixes #47
References #41 (increased wait time before retrying)
2015-03-16 15:03:03 +01:00
Sebastian Stenzel
b9b85a58ac Increased Version to 0.7.0-SNAPSHOT 2015-03-14 22:10:51 +01:00
75 changed files with 2307 additions and 1377 deletions

View File

@@ -1,11 +1,20 @@
language: java
jdk:
- oraclejdk8
- oraclejdk8
script: mvn -fmain/pom.xml 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
api_key:
secure: ZjE1j93v3qbPIe2YbmhS319aCbMdLQw0HuymmluTurxXsZtn9D4t2+eTr99vBVxGRuB5lzzGezPR5zjk5W7iHF7xhwrawXrFzr2rPJWzWFt0aM+Ry2njU1ROTGGXGTbv4anWeBlgMxLEInTAy/9ytOGNJlec83yc0THpOY2wxnk=
file: main/target/Cryptomator-$TRAVIS_TAG.jar
skip_cleanup: true
on:
repo: cryptomator/cryptomator
tags: true

View File

@@ -1,8 +1,9 @@
Cryptomator
====================
[![Build Status](https://travis-ci.org/totalvoidness/cryptomator.svg?branch=master)](https://travis-ci.org/totalvoidness/cryptomator)
[![Join the chat at https://gitter.im/totalvoidness/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/totalvoidness/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Build Status](https://travis-ci.org/cryptomator/cryptomator.svg?branch=master)](https://travis-ci.org/cryptomator/cryptomator)
[![Join the chat at https://gitter.im/totalvoidness/cryptomator](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cryptomator/cryptomator?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Flattr Cryptomator](https://api.flattr.com/button/flattr-badge-large.png)](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.
@@ -43,13 +44,12 @@ If you want to take a look at the current beta version, go ahead and get your co
#### 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.5.1
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.

View File

@@ -12,14 +12,14 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.6.0</version>
<version>0.7.2</version>
</parent>
<artifactId>core</artifactId>
<name>Cryptomator WebDAV and I/O module</name>
<properties>
<jetty.version>9.2.10.v20150310</jetty.version>
<jackrabbit.version>2.9.1</jackrabbit.version>
<jackrabbit.version>2.10.1</jackrabbit.version>
<commons.transaction.version>1.2</commons.transaction.version>
<jta.version>1.1</jta.version>
</properties>
@@ -64,9 +64,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>

View File

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

View File

@@ -9,19 +9,18 @@
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;
}

View File

@@ -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);
}
}

View File

@@ -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.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;
}
}
}
}

View File

@@ -0,0 +1,185 @@
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.util.concurrent.ExecutorService;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.FilenameUtils;
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.eclipse.jetty.http.HttpHeader;
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final ExecutorService backgroundTaskExecutor;
private final Path dataRoot;
private final FilenameTranslator filenameTranslator;
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) {
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.backgroundTaskExecutor = backgroundTaskExecutor;
this.dataRoot = vaultRootPath.resolve("d");
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
if (locator.isRootLocation()) {
return createRootDirectory(locator, request.getDavSession());
}
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession(), dirFilePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request, filePath);
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession(), filePath);
} else {
// e.g. for MOVE operations:
return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath);
}
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (locator.isRootLocation()) {
return createRootDirectory(locator, session);
}
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
if (Files.exists(dirFilePath)) {
return createDirectory(locator, session, dirFilePath);
} else if (Files.exists(filePath)) {
return createFile(locator, session, filePath);
} else {
// e.g. for MOVE operations:
return createNonExisting(locator, session, filePath, dirFilePath);
}
}
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 Absolute file path for a given cleartext file resourcePath.
* @throws IOException
*/
private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws IOException
*/
private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
/**
* @return Absolute directory path for a given cleartext directory resourcePath.
* @throws IOException
*/
private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException {
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 = createEncryptedDirectoryPath(parentCleartextPath);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
final String encryptedFilename = filenameTranslator.getEncryptedDirFileName(cleartextFilename);
final Path directoryFile = parent.resolve(encryptedFilename);
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 DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath);
}
private EncryptedFile 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, Path filePath, Path dirFilePath) {
return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath);
}
}

View File

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

View File

@@ -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 java.util.concurrent.ExecutorService;
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.eclipse.jetty.http.HttpHeader;
class DavResourceFactoryImpl implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final ExecutorService backgroundTaskExecutor;
DavResourceFactoryImpl(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) {
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.backgroundTaskExecutor = backgroundTaskExecutor;
}
@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, cryptoWarningHandler, backgroundTaskExecutor);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler);
}
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);
}
}

View File

@@ -8,22 +8,28 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.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.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
@@ -37,19 +43,53 @@ 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.DecryptFailedRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedDir extends AbstractEncryptedNode {
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(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
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
@@ -58,59 +98,107 @@ class EncryptedDir extends AbstractEncryptedNode {
}
@Override
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
if (resource.isCollection()) {
this.addMemberDir(resource, inputContext);
} else {
this.addMemberFile(resource, inputContext);
public long getModificationTime() {
try {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
return -1;
} else {
return Files.getLastModifiedTime(dirPath).toMillis();
}
} catch (IOException e) {
return -1;
}
}
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
@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 {
Files.createDirectories(childPath);
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) {
LOG.error("Failed to create subdirectory.", e);
throw new IORuntimeException(e);
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, 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);
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 FileLock lock = c.lock(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);
} 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());
}
}
@Override
public DavResourceIterator getMembers() {
final Path dir = ResourcePathUtils.getPhysicalPath(this);
try {
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
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 DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
final DavResource resource = factory.createResource(childLocator, session);
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 (DecryptFailedRuntimeException e) {
} catch (DecryptFailedException e) {
LOG.warn("Decryption of resource failed: " + childPath);
continue;
}
@@ -127,65 +215,118 @@ class EncryptedDir extends AbstractEncryptedNode {
@Override
public void removeMember(DavResource member) throws DavException {
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
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 {
if (Files.exists(memberPath)) {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
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);
}
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
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).getDirFilePath();
} 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).getDirFilePath();
} 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); final FileLock lock = c.lock()) {
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
}
@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;
}
}
}

View File

@@ -10,15 +10,18 @@ package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
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;
@@ -36,15 +39,36 @@ import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedFile extends AbstractEncryptedNode {
class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
protected final CryptoWarningHandler cryptoWarningHandler;
public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
super(factory, locator, session, lockManager, cryptor);
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;
if (Files.isRegularFile(filePath)) {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.tryLock(0L, FILE_HEADER_LENGTH, true)) {
final Long contentLength = cryptor.decryptedContentLength(c);
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 (IOException e) {
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
} catch (MacAuthenticationFailedException e) {
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
// don't add content length DAV property
}
}
}
@Override
@@ -69,11 +93,10 @@ class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
if (Files.isRegularFile(filePath)) {
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
@@ -86,32 +109,42 @@ class EncryptedFile extends AbstractEncryptedNode {
} catch (MacAuthenticationFailedException e) {
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
throw new IOException("Error decrypting file " + filePath.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);
}
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getFilePath();
} else {
dstPath = dest.filePath;
}
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);
}
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).getFilePath();
} 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);
}
}

View File

@@ -16,7 +16,6 @@ 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;
@@ -56,9 +55,9 @@ class EncryptedFilePart extends EncryptedFile {
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, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler);
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor, Path filePath) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
@@ -126,25 +125,23 @@ class EncryptedFilePart extends EncryptedFile {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.isRegularFile(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);
assert Files.isRegularFile(filePath);
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
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.trace("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + filePath.toString(), e);
}
}
@@ -165,18 +162,16 @@ class EncryptedFilePart extends EncryptedFile {
@Override
public void run() {
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isRegularFile(path) && Files.isReadable(path)) {
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + path.toString(), e);
assert Files.isRegularFile(filePath);
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + filePath.toString(), e);
}
}

View File

@@ -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 = 96;
/**
* 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 -&gt; 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);
}
};
}

View File

@@ -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.channels.FileLock;
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 FileLock lock = c.lock()) {
c.write(ByteBuffer.wrap(bytes));
}
}
private byte[] readAllBytesAtomically(Path path) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
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;
}
}
}

View File

@@ -9,22 +9,28 @@
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.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.DavProperty;
import org.cryptomator.crypto.Cryptor;
class NonExistingNode extends AbstractEncryptedNode {
public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
private final Path filePath;
private final Path dirFilePath;
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) {
super(factory, locator, session, lockManager, cryptor, null);
this.filePath = filePath;
this.dirFilePath = dirFilePath;
}
@Override
@@ -37,6 +43,11 @@ class NonExistingNode extends AbstractEncryptedNode {
return false;
}
@Override
public long getModificationTime() {
return -1;
}
@Override
public void spool(OutputContext outputContext) throws IOException {
throw new UnsupportedOperationException("Resource doesn't exist.");
@@ -58,8 +69,26 @@ class NonExistingNode extends AbstractEncryptedNode {
}
@Override
protected void determineProperties() {
// do nothing.
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.");
}
public Path getFilePath() {
return filePath;
}
public Path getDirFilePath() {
return dirFilePath;
}
}

View File

@@ -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;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
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());
}
}

View File

@@ -47,8 +47,8 @@ public class WebDavServlet extends AbstractWebdavServlet {
final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
backgroundTaskExecutor = Executors.newCachedThreadPool();
davSessionProvider = new DavSessionProviderImpl();
davLocatorFactory = new DavLocatorFactoryImpl(fsRoot, cryptor);
davResourceFactory = new DavResourceFactoryImpl(cryptor, cryptoWarningHandler, backgroundTaskExecutor);
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot);
}
@Override

View File

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

View File

@@ -15,17 +15,12 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
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 javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -42,28 +37,26 @@ 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.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.UnsupportedVaultException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
import 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 {
/**
* 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;
@@ -80,8 +73,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;
@@ -135,6 +128,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);
@@ -151,17 +145,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) {
@@ -169,7 +167,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);
@@ -187,7 +185,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);
}
@@ -224,17 +227,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) {
@@ -249,6 +251,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);
@@ -272,125 +282,71 @@ 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(64);
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 content length:
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
headerBuf.position(16);
headerBuf.get(encryptedContentLengthBytes);
// read stored header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(32);
headerBuf.get(storedHeaderMac);
// calculate mac over first 32 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.rewind();
headerBuf.limit(32);
headerMac.update(headerBuf);
final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
if (!macMatches) {
throw new MacAuthenticationFailedException("MAC authentication failed.");
}
return decryptContentLength(encryptedContentLengthBytes, iv);
}
private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) {
try {
final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes);
final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
return fileSizeBuffer.getLong();
} catch (IllegalBlockSizeException | BadPaddingException e) {
@@ -398,85 +354,93 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
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[] encryptContentLength(long contentLength, 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(fileSizeBuffer.array());
} 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 boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
// init mac:
final Mac calculatedMac = this.hmacSha256(hMacMasterKey);
// read stored mac:
encryptedFile.position(16);
final ByteBuffer storedMac = ByteBuffer.allocate(calculatedMac.getMacLength());
final int numMacBytesRead = encryptedFile.read(storedMac);
// check validity of header:
if (numMacBytesRead != calculatedMac.getMacLength()) {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
final int headerBytesRead = encryptedFile.read(headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
// go to begin of content:
encryptedFile.position(64);
// read header mac:
final byte[] storedHeaderMac = new byte[32];
headerBuf.position(32);
headerBuf.get(storedHeaderMac);
// calculated MAC
// read content mac:
final byte[] storedContentMac = new byte[32];
headerBuf.position(64);
headerBuf.get(storedContentMac);
// calculate mac over first 32 bytes of header:
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerBuf.position(0);
headerBuf.limit(32);
headerMac.update(headerBuf);
// calculate mac over content:
encryptedFile.position(96l);
final Mac contentMac = this.hmacSha256(hMacMasterKey);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, calculatedMac);
final InputStream macIn = new MacInputStream(in, contentMac);
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
return MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal());
return headerMacMatches && contentMacMatches;
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
// read 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) {
// read header:
encryptedFile.position(0l);
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
final int headerBytesRead = encryptedFile.read(headerBuf);
if (headerBytesRead != headerBuf.capacity()) {
throw new IOException("Failed to read file header.");
}
// 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 content length:
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
headerBuf.position(16);
headerBuf.get(encryptedContentLengthBytes);
final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv);
// read content
// read header mac:
final byte[] headerMac = new byte[32];
headerBuf.position(32);
headerBuf.get(headerMac);
// read content mac:
final byte[] contentMac = new byte[32];
headerBuf.position(64);
headerBuf.get(contentMac);
// decrypt content
encryptedFile.position(96l);
final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey);
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final InputStream macIn = new MacInputStream(in, calculatedMac);
final InputStream macIn = new MacInputStream(in, calculatedContentMac);
final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
@@ -484,7 +448,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
IOUtils.copyLarge(macIn, new NullOutputStream());
// compare (in constant time):
final boolean macMatches = MessageDigest.isEqual(storedMac.array(), calculatedMac.doFinal());
final boolean macMatches = MessageDigest.isEqual(contentMac, calculatedContentMac.doFinal());
if (!macMatches) {
// This exception will be thrown AFTER we sent the decrypted content to the user.
// This has two advantages:
@@ -500,7 +464,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
// read iv:
encryptedFile.position(0);
encryptedFile.position(0l);
final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
final int numIvBytesRead = encryptedFile.read(countingIv);
@@ -516,7 +480,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB
// fast forward stream:
encryptedFile.position(64l + beginOfFirstRelevantBlock);
encryptedFile.position(96l + beginOfFirstRelevantBlock);
// generate cipher:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
@@ -527,30 +491,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
}
/**
* header = {16 byte iv, 16 byte filesize, 32 byte headerMac, 32 byte contentMac}
*/
@Override
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.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
encryptedFile.write(countingIv);
final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
final byte[] iv = ivBuf.array();
// init crypto stuff:
final Mac mac = this.hmacSha256(hMacMasterKey);
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
headerBuf.limit(96);
encryptedFile.write(headerBuf);
// init mac buffer and skip 32 bytes
final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
encryptedFile.write(macBuffer);
// 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);
// write content:
// content encryption:
final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
final Mac contentMac = this.hmacSha256(hMacMasterKey);
final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
final OutputStream macOut = new MacOutputStream(out, mac);
final OutputStream macOut = new MacOutputStream(out, contentMac);
final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
@@ -558,48 +521,37 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
try {
plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
} catch (CounterAwareInputLimitReachedException ex) {
encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE);
// no additional padding needed here, as 64GiB is a multiple of 128bit
encryptedFile.truncate(0l);
throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
}
// 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]);
// for filesizes of up to 16GiB: append a few blocks of fake data:
if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) {
final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks);
final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH);
for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(emptyBytes);
}
// add random length padding to obfuscate file length:
final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
final long minAdditionalBlocks = 4;
final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs)
final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks
final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks);
final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) {
blockSizeBufferedOut.write(randomPadding);
}
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:
headerBuf.clear();
headerBuf.put(iv);
headerBuf.put(encryptContentLength(plaintextSize, iv));
headerBuf.flip();
final Mac headerMac = this.hmacSha256(hMacMasterKey);
headerMac.update(headerBuf);
headerBuf.limit(96);
headerBuf.put(headerMac.doFinal());
headerBuf.put(contentMac.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);
}
};
}
}

View File

@@ -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,8 @@ 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.
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
int FILE_NAME_IV_LENGTH = 5;
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
}

View File

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

View File

@@ -5,20 +5,18 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.atomic.AtomicLong;
import javax.crypto.Mac;
/**
* Updates a {@link Mac} with the bytes read from this stream.
* Throws an exception, if more than (2^32)-1 16 byte blocks will be encrypted (would result in an counter overflow).<br/>
* From https://tools.ietf.org/html/rfc3686: <cite> Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks</cite>
*/
class CounterAwareInputStream extends FilterInputStream {
static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l;
static final long SIXTY_FOUR_GIGABYE = ((1l << 32) - 1) * 16;
private final AtomicLong counter;
/**
* @param in Stream from which to read contents, which will update the Mac.
* @param mac Mac to be updated during writes.
*/
public CounterAwareInputStream(InputStream in) {
super(in);

View File

@@ -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 + "}");
}

View File

@@ -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 = 1;
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;
}

View File

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

View File

@@ -15,14 +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;
@@ -30,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());
@@ -46,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.
@@ -80,7 +80,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -112,7 +112,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -144,7 +144,7 @@ public class Aes256CryptorTest {
final Aes256Cryptor cryptor = new Aes256Cryptor();
// encrypt:
final ByteBuffer encryptedData = ByteBuffer.allocate(96);
final ByteBuffer encryptedData = ByteBuffer.allocate(256);
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -183,7 +183,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) (96 + plaintextData.length * 1.2));
final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
cryptor.encryptFile(plaintextIn, encryptedOut);
IOUtils.closeQuietly(plaintextIn);
@@ -207,49 +207,30 @@ public class Aes256CryptorTest {
@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);
}
}
}

View File

@@ -12,7 +12,7 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.6.0</version>
<version>0.7.2</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>

View File

@@ -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);
}
}

View File

@@ -0,0 +1,85 @@
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 boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.isAuthentic(encryptedFile);
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
return cryptor.decryptFile(encryptedFile, plaintextFile);
}
@Override
public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
return cryptor.decryptRange(encryptedFile, plaintextFile, pos, length);
}
@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();
}
}

View File

@@ -12,18 +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.
@@ -34,47 +36,44 @@ 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 true, if the stored MAC matches the calculated one.
@@ -100,14 +99,4 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
/**
* @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);
}

View File

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

View File

@@ -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);
}
}
}

View File

@@ -4,35 +4,24 @@ 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.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
import org.cryptomator.crypto.exceptions.WrongPasswordException;
public class SamplingDecorator implements Cryptor, CryptorIOSampling {
public class SamplingCryptorDecorator extends AbstractCryptorDecorator implements CryptorIOSampling {
private final Cryptor cryptor;
private final AtomicLong encryptedBytes;
private final AtomicLong decryptedBytes;
private SamplingDecorator(Cryptor cryptor) {
this.cryptor = cryptor;
private SamplingCryptorDecorator(Cryptor cryptor) {
super(cryptor);
encryptedBytes = new AtomicLong();
decryptedBytes = new AtomicLong();
}
public static Cryptor decorate(Cryptor cryptor) {
return new SamplingDecorator(cryptor);
}
@Override
public void swipeSensitiveData() {
cryptor.swipeSensitiveData();
return new SamplingCryptorDecorator(cryptor);
}
@Override
@@ -55,38 +44,6 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
/* 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 boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
return cryptor.isAuthentic(encryptedFile);
}
@Override
public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
@@ -105,21 +62,6 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
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;

View File

@@ -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();
}

View File

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

View File

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -0,0 +1,60 @@
<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.7.2</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-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="deb" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx: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:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

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

View File

@@ -0,0 +1,60 @@
<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.7.2</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-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="dmg" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx: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:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 361 KiB

View 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;

View File

@@ -0,0 +1,61 @@
<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.7.2</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-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="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0">
<fx:property name="settingsPath" value="./settings.json" />
<fx:property name="logPath" value="cryptomator.log" />
</fx:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="false" menu="false" shortcut="false" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

View File

@@ -0,0 +1,60 @@
<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.7.2</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-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="exe" outdir="${project.build.directory}" outfile="Cryptomator-${project.parent.version}" verbose="true">
<fx:application name="${javafx.application.name}" version="${project.version}" mainClass="${exec.mainClass}" />
<fx:info title="${javafx.application.name}" vendor="cryptomator.org" copyright="cryptomator.org" license="MIT" category="Utility" />
<fx:platform javafx="2.2+" j2se="8.0" >
<fx:property name="logPath" value="%appdata%/Cryptomator/cryptomator.log" />
</fx:platform>
<fx:resources>
<fx:fileset dir="../target/" includes="Cryptomator-${project.parent.version}.jar" />
</fx:resources>
<fx:permissions elevated="false" />
<fx:preferences install="true" />
</fx:deploy>
</target>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -11,7 +11,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.6.0</version>
<version>0.7.2</version>
<packaging>pom</packaging>
<name>Cryptomator</name>
@@ -186,6 +186,33 @@
<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>
</profiles>
<build>
<plugins>
<plugin>

View File

@@ -12,17 +12,11 @@
<parent>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
<version>0.6.0</version>
<version>0.7.2</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>
@@ -59,7 +53,7 @@
<artifactId>guice</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
@@ -74,50 +68,20 @@
</execution>
</executions>
<configuration>
<outputDirectory>${project.parent.build.directory}</outputDirectory>
<finalName>Cryptomator-${project.parent.version}</finalName>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${javafx.application.name}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<archive>
<manifestEntries>
<Main-Class>${exec.mainClass}</Main-Class>
<Main-Class>org.cryptomator.ui.Cryptomator</Main-Class>
<Implementation-Version>${project.version}</Implementation-Version>
</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>

View File

@@ -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;
@@ -65,6 +64,7 @@ public class MainApplication extends Application {
this.executorService = executorService;
this.controllerFactory = controllerFactory;
this.closer = closer;
Cryptomator.addShutdownTask(closer::close);
appRef.set(this);
}
@@ -82,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);
@@ -162,18 +160,6 @@ 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();
}
}
/**

View File

@@ -19,7 +19,7 @@ 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;
@@ -88,7 +88,7 @@ public class MainModule extends AbstractModule {
@Provides
Cryptor getCryptor() {
return SamplingDecorator.decorate(new Aes256Cryptor());
return SamplingCryptorDecorator.decorate(new Aes256Cryptor());
}
@Provides

View File

@@ -10,16 +10,19 @@ 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 +52,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 +85,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);
@@ -91,23 +110,33 @@ public class ChangePasswordController implements Initializable {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword);
Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING);
} catch (DecryptFailedException | IOException ex) {
messageLabel.setText(rb.getString("changePassword.errorMessage.decryptionFailed"));
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 +147,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.

View File

@@ -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);
}

View File

@@ -19,20 +19,25 @@ 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 +68,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 +101,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,14 +118,15 @@ 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:
@@ -117,18 +137,31 @@ public class UnlockController implements Initializable {
} catch (DecryptFailedException | 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();
}

View File

@@ -34,12 +34,16 @@ 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;
@@ -97,6 +101,7 @@ public class WelcomeController implements Initializable {
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(() -> {

View File

@@ -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);
}
}

View File

@@ -13,6 +13,8 @@ import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.ui.util.DeferredClosable;
@@ -94,7 +96,11 @@ public class Vault implements Serializable {
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);
}
setUnlocked(false);
namesOfResourcesWithInvalidMac.clear();
}

View File

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

View File

@@ -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();
}
}

View File

@@ -140,7 +140,7 @@ public class SingleInstanceManager {
if (key.isAcceptable()) {
final SocketChannel accepted = channel.accept();
if (accepted != null) {
LOG.info("accepted incoming connection");
LOG.debug("accepted incoming connection");
accepted.configureBlocking(false);
accepted.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
@@ -157,7 +157,7 @@ public class SingleInstanceManager {
if (!state.write.hasRemaining()) {
state.write = null;
}
LOG.debug("wrote welcome. switching to read only.");
LOG.trace("wrote welcome. switching to read only.");
key.interestOps(SelectionKey.OP_READ);
}
@@ -247,7 +247,7 @@ public class SingleInstanceManager {
try {
channel = SocketChannel.open();
channel.configureBlocking(false);
LOG.info("connecting to instance {}", port.get());
LOG.debug("connecting to instance {}", port.get());
channel.connect(new InetSocketAddress(InetAddress.getLoopbackAddress(), port.get()));
SocketChannel fChannel = channel;
@@ -255,7 +255,7 @@ public class SingleInstanceManager {
return Optional.empty();
}
LOG.info("connected to instance {}", port.get());
LOG.debug("connected to instance {}", port.get());
final byte[] bytes = applicationKey.getBytes();
ByteBuffer buf = ByteBuffer.allocate(bytes.length);
@@ -313,7 +313,7 @@ public class SingleInstanceManager {
final int port = ((InetSocketAddress) channel.getLocalAddress()).getPort();
Preferences.userNodeForPackage(Cryptomator.class).putInt(applicationKey, port);
LOG.info("InstanceManager bound to port {}", port);
LOG.debug("InstanceManager bound to port {}", port);
Selector selector = Selector.open();
channel.register(selector, SelectionKey.OP_ACCEPT);

View File

@@ -14,6 +14,7 @@ import static java.lang.String.format;
import java.io.IOException;
import org.apache.commons.io.IOUtils;
import org.apache.logging.log4j.util.Strings;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -77,7 +78,15 @@ public final class CommandResult {
private void logDebugInfo() {
if (LOG.isDebugEnabled()) {
LOG.debug("Command execution finished. Exit code: {}\n" + "Output:\n" + "{}\n" + "Error:\n" + "{}\n", process.exitValue(), stdout, stderr);
if (Strings.isEmpty(stderr) && Strings.isEmpty(stdout)) {
LOG.debug("Command execution finished. Exit code: {}", process.exitValue());
} else if (Strings.isEmpty(stderr)) {
LOG.debug("Command execution finished. Exit code: {}\nOutput: {}", process.exitValue(), stdout);
} else if (Strings.isEmpty(stdout)) {
LOG.debug("Command execution finished. Exit code: {}\nError: {}", process.exitValue(), stderr);
} else {
LOG.debug("Command execution finished. Exit code: {}\n Output: {}\nError: {}", process.exitValue(), stdout, stderr);
}
}
}

View File

@@ -6,6 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
* Markus Kreusch - Refactored WebDavMounter to use strategy pattern
* Mohit Raju - Added fallback schema-name "webdav" when opening file managers
******************************************************************************/
package org.cryptomator.ui.util.mount;
@@ -40,20 +41,45 @@ final class LinuxGvfsWebDavMounter implements WebDavMounterStrategy {
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final Script mountScript = Script.fromLines(
"set -x",
"gvfs-mount \"dav:$DAV_SSP\"",
"xdg-open \"dav:$DAV_SSP\"")
"gvfs-mount \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
final Script testMountStillExistsScript = Script.fromLines(
"set -x",
"test `gvfs-mount --list | grep \"$DAV_SSP\" | wc -l` -eq 1")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
final Script unmountScript = Script.fromLines(
"set -x",
"gvfs-mount -u \"dav:$DAV_SSP\"")
.addEnv("DAV_SSP", uri.getRawSchemeSpecificPart());
mountScript.execute();
try{
openMountWithWebdavUri("dav:"+uri.getRawSchemeSpecificPart()).execute();
}catch(CommandFailedException exception){
openMountWithWebdavUri("webdav:"+uri.getRawSchemeSpecificPart()).execute();
}
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
boolean mountStillExists;
try {
testMountStillExistsScript.execute();
mountStillExists = true;
} catch(CommandFailedException e) {
mountStillExists = false;
}
// only attempt unmount if user didn't unmount manually:
if (mountStillExists) {
unmountScript.execute();
}
}
};
}
private Script openMountWithWebdavUri(String webdavUri){
return Script.fromLines(
"set -x",
"xdg-open \"$DAV_URI\"")
.addEnv("DAV_URI", webdavUri);
}
}

View File

@@ -10,6 +10,8 @@
package org.cryptomator.ui.util.mount;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.util.UUID;
import org.apache.commons.lang3.SystemUtils;
@@ -46,7 +48,10 @@ final class MacOsXWebDavMounter implements WebDavMounterStrategy {
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
// only attempt unmount if user didn't unmount manually:
if (Files.exists(FileSystems.getDefault().getPath(path))) {
unmountScript.execute();
}
}
};
}

View File

@@ -12,6 +12,8 @@ package org.cryptomator.ui.util.mount;
import static org.cryptomator.ui.util.command.Script.fromLines;
import java.net.URI;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -24,13 +26,11 @@ import org.cryptomator.ui.util.command.Script;
* A {@link WebDavMounterStrategy} utilizing the "net use" command.
* <p>
* Tested on Windows 7 but should also work on Windows 8.
*
* @author Markus Kreusch
*/
final class WindowsWebDavMounter implements WebDavMounterStrategy {
private static final Pattern WIN_MOUNT_DRIVELETTER_PATTERN = Pattern.compile("\\s*([A-Z]:)\\s*");
private static final int MAX_MOUNT_ATTEMPTS = 10;
private static final int MAX_MOUNT_ATTEMPTS = 8;
@Override
public boolean shouldWork() {
@@ -39,51 +39,70 @@ final class WindowsWebDavMounter implements WebDavMounterStrategy {
@Override
public void warmUp(int serverPort) {
try {
final Script proxyBypassCmd = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net\" /f");
proxyBypassCmd.execute();
final Script mountCmd = fromLines("net use * http://0--1.ipv6-literal.net:" + serverPort + "/bill-gates-mom-uses-goto /persistent:no");
mountCmd.execute();
} catch (CommandFailedException e) {
// will most certainly throw an exception, because this is a fake WebDav path. But now windows has some DNS things cached :)
}
// no-op
}
@Override
public WebDavMount mount(URI uri, String name) throws CommandFailedException {
final Script mountScript = fromLines("net use * http://0--1.ipv6-literal.net:%PORT%%DAV_PATH% /persistent:no");
mountScript.addEnv("PORT", String.valueOf(uri.getPort())).addEnv("DAV_PATH", uri.getRawPath());
String driveLetter = null;
// The ugliness of the following 20 lines is solely windows' fault. Deal with it.
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
try {
final CommandResult mountResult = mountScript.execute(10, TimeUnit.SECONDS);
driveLetter = getDriveLetter(mountResult.getStdOut());
break;
} catch (CommandFailedException ex) {
if (i == MAX_MOUNT_ATTEMPTS - 1) {
throw ex;
} else {
try {
// retry after 500ms
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
CommandResult mountResult;
try {
final Script mountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
mountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
mountResult = mountScript.execute(5, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
final Script localhostMountScript = fromLines("net use * \\\\localhost@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
localhostMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script ipv6literaltMountScript = fromLines("net use * \\\\0--1.ipv6-literal.net@%DAV_PORT%\\DavWWWRoot%DAV_UNC_PATH% /persistent:no");
ipv6literaltMountScript.addEnv("DAV_PORT", String.valueOf(uri.getPort())).addEnv("DAV_UNC_PATH", uri.getRawPath().replace('/', '\\'));
final Script proxyBypassScript = fromLines("reg add \"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\" /v \"ProxyOverride\" /d \"<local>;0--1.ipv6-literal.net;0--1.ipv6-literal.net:%DAV_PORT%\" /f");
proxyBypassScript.addEnv("DAV_PORT", String.valueOf(uri.getPort()));
mountResult = bypassProxyAndRetryMount(localhostMountScript, ipv6literaltMountScript, proxyBypassScript);
}
final String driveLetter = getDriveLetter(mountResult.getStdOut());
final Script openExplorerScript = fromLines("start explorer.exe " + driveLetter);
openExplorerScript.execute();
final Script unmountScript = fromLines("net use " + driveLetter + " /delete").addEnv("DRIVE_LETTER", driveLetter);
return new AbstractWebDavMount() {
@Override
public void unmount() throws CommandFailedException {
unmountScript.execute();
// only attempt unmount if user didn't unmount manually:
if (isVolumeMounted(driveLetter)) {
unmountScript.execute();
}
}
};
}
private boolean isVolumeMounted(String driveLetter) {
for (Path path : FileSystems.getDefault().getRootDirectories()) {
if (path.toString().startsWith(driveLetter)) {
return true;
}
}
return false;
}
private CommandResult bypassProxyAndRetryMount(Script localhostMountScript, Script ipv6literalMountScript, Script proxyBypassScript) throws CommandFailedException {
CommandFailedException latestException = null;
for (int i = 0; i < MAX_MOUNT_ATTEMPTS; i++) {
try {
// wait a moment before next attempt
Thread.sleep(5000);
proxyBypassScript.execute();
// alternate localhost and 0--1.ipv6literal.net
final Script mountScript = (i % 2 == 0) ? localhostMountScript : ipv6literalMountScript;
return mountScript.execute(3, TimeUnit.SECONDS);
} catch (CommandFailedException ex) {
latestException = ex;
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new CommandFailedException(ex);
}
}
throw latestException;
}
private String getDriveLetter(String result) throws CommandFailedException {
final Matcher matcher = WIN_MOUNT_DRIVELETTER_PATTERN.matcher(result);
if (matcher.find()) {

View File

@@ -17,6 +17,9 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.text.Text?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" xmlns:fx="http://javafx.com/fxml">
@@ -43,10 +46,15 @@
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 3 -->
<Button fx:id="changePasswordButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
<Button fx:id="changePasswordButton" text="%changePassword.button.change" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2">
<children>
<Text fx:id="messageText" />
<Hyperlink fx:id="downloadsPageLink" text="%changePassword.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" />
</children>
</TextFlow>
</children>
</GridPane>

View File

@@ -18,6 +18,9 @@
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.text.TextFlow?>
<?import javafx.scene.control.Hyperlink?>
<?import javafx.scene.text.Text?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockController" xmlns:fx="http://javafx.com/fxml">
<padding>
@@ -45,7 +48,12 @@
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
<TextFlow GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" >
<children>
<Text fx:id="messageText" />
<Hyperlink fx:id="downloadsPageLink" text="%unlock.label.downloadsPageLink" visible="false" onAction="#didClickDownloadsLink" />
</children>
</TextFlow>
</children>
</GridPane>

View File

@@ -28,20 +28,26 @@ initialize.button.ok=Create vault
# unlock.fxml
unlock.label.password=Password
unlock.label.mountName=Drive name
unlock.label.downloadsPageLink=All Cryptomator versions
unlock.button.unlock=Unlock vault
unlock.errorMessage.wrongPassword=Wrong password.
unlock.errorMessage.decryptionFailed=Decryption failed.
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
unlock.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.
unlock.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator.
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# change_password.fxml
changePassword.label.oldPassword=Old password
changePassword.label.newPassword=New password
changePassword.label.retypePassword=Retype password
changePassword.button.unlock=Change password
changePassword.label.downloadsPageLink=All Cryptomator versions
changePassword.button.change=Change password
changePassword.errorMessage.wrongPassword=Wrong password.
changePassword.errorMessage.decryptionFailed=Decryption failed.
changePassword.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
changePassword.errorMessage.unsupportedVersion.vaultOlderThanSoftware=Unsupported vault. This vault has been created with an older version of Cryptomator.
changePassword.errorMessage.unsupportedVersion.softwareOlderThanVault=Unsupported vault. This vault has been created with a newer version of Cryptomator.
changePassword.infoMessage.success=Password changed.
# unlocked.fxml

View File

@@ -7,7 +7,7 @@
Contributors:
Markus Kreusch - switched to log4j 2
-->
<Configuration status="WARN">
<Configuration status="WARN" packages="org.cryptomator.ui.logging">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
@@ -18,16 +18,21 @@
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
<ThresholdFilter level="WARN" onMatch="ACCEPT" onMismatch="DENY" />
</Console>
<!-- <File fileName="${sys:logPath}" ...> not feasible for paths like ~/foo/bar -->
<ConfigurableFile name="File" pathPropertyName="logPath">
<PatternLayout pattern="%16d %-5p [%c{1}:%L] %m%n" />
</ConfigurableFile>
</Appenders>
<Loggers>
<!-- show our own debug messages: -->
<Logger name="org.cryptomator" level="DEBUG"/>
<Logger name="org.cryptomator" level="DEBUG" />
<!-- mute dependencies: -->
<Root level="INFO">
<AppenderRef ref="Console" />
<AppenderRef ref="StdErr" />
<AppenderRef ref="File" />
</Root>
</Loggers>
</Configuration>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 564 B