diff --git a/main/core/pom.xml b/main/core/pom.xml
index c7b66fa5a..85fd9f6a1 100644
--- a/main/core/pom.xml
+++ b/main/core/pom.xml
@@ -64,9 +64,5 @@
org.apache.commons
commons-lang3
-
- org.apache.commons
- commons-collections4
-
diff --git a/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java b/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java
index 210524596..df5e57c77 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/exceptions/IORuntimeException.java
@@ -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
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java
index f760c445b..1715ed810 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/AbstractEncryptedNode.java
@@ -9,11 +9,9 @@
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.FileTime;
import java.util.List;
@@ -21,7 +19,6 @@ 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;
@@ -46,14 +43,14 @@ abstract class AbstractEncryptedNode implements DavResource {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
- protected final DavResourceFactory factory;
- protected final DavResourceLocator locator;
+ protected final CryptoResourceFactory factory;
+ protected final CryptoLocator locator;
protected final DavSession session;
protected final LockManager lockManager;
protected final Cryptor cryptor;
protected final DavPropertySet properties;
- protected AbstractEncryptedNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ protected AbstractEncryptedNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
this.factory = factory;
this.locator = locator;
this.session = session;
@@ -63,6 +60,8 @@ abstract class AbstractEncryptedNode implements DavResource {
this.determineProperties();
}
+ protected abstract Path getPhysicalPath();
+
@Override
public String getComplianceClass() {
return DAV_COMPLIANCE_CLASSES;
@@ -75,8 +74,7 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public boolean exists() {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
- return Files.exists(path);
+ return Files.exists(getPhysicalPath());
}
@Override
@@ -91,7 +89,7 @@ abstract class AbstractEncryptedNode implements DavResource {
}
@Override
- public DavResourceLocator getLocator() {
+ public CryptoLocator getLocator() {
return locator;
}
@@ -107,9 +105,8 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public long getModificationTime() {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
try {
- return Files.getLastModifiedTime(path).toMillis();
+ return Files.getLastModifiedTime(getPhysicalPath()).toMillis();
} catch (IOException e) {
return -1;
}
@@ -139,7 +136,7 @@ abstract class AbstractEncryptedNode implements DavResource {
LOG.info("Set property {}", property.getName());
try {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
+ final Path path = getPhysicalPath();
if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) {
final String createDateStr = (String) property.getValue();
final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr);
@@ -196,49 +193,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 +266,7 @@ abstract class AbstractEncryptedNode implements DavResource {
}
@Override
- public DavResourceFactory getFactory() {
+ public CryptoResourceFactory getFactory() {
return factory;
}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java
deleted file mode 100644
index dc8761d1f..000000000
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/BidiLRUMap.java
+++ /dev/null
@@ -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 extends AbstractDualBidiMap {
-
- BidiLRUMap(int maxSize) {
- super(new LRUMap(maxSize), new LRUMap(maxSize));
- }
-
- protected BidiLRUMap(final Map normalMap, final Map reverseMap, final BidiMap inverseBidiMap) {
- super(normalMap, reverseMap, inverseBidiMap);
- }
-
- @Override
- protected BidiMap createBidiMap(Map normalMap, Map reverseMap, BidiMap inverseMap) {
- return new BidiLRUMap(normalMap, reverseMap, inverseMap);
- }
-
-}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java
new file mode 100644
index 000000000..12cf9a761
--- /dev/null
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java
@@ -0,0 +1,166 @@
+package org.cryptomator.webdav.jackrabbit;
+
+import java.io.IOException;
+import java.nio.file.FileSystems;
+import java.nio.file.Path;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
+import org.apache.jackrabbit.webdav.DavResourceLocator;
+import org.apache.jackrabbit.webdav.util.EncodeUtil;
+import org.apache.logging.log4j.util.Strings;
+import org.cryptomator.crypto.Cryptor;
+import org.cryptomator.webdav.exceptions.IORuntimeException;
+
+class CryptoLocator implements DavResourceLocator {
+
+ private final CryptoLocatorFactory factory;
+ private final Cryptor cryptor;
+ private final Path rootPath;
+ private final String prefix;
+ private final String resourcePath;
+
+ public CryptoLocator(CryptoLocatorFactory factory, Cryptor cryptor, Path rootPath, String prefix, String resourcePath) {
+ this.factory = factory;
+ this.cryptor = cryptor;
+ this.rootPath = rootPath;
+ this.prefix = prefix;
+ this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true);
+ }
+
+ /* path variants */
+
+ /**
+ * Returns the decrypted path without any trailing slash.
+ *
+ * @see #getHref(boolean)
+ * @return Plaintext resource path.
+ */
+ @Override
+ public String getResourcePath() {
+ return resourcePath;
+ }
+
+ /**
+ * Returns the decrypted path and adds URL-encoding.
+ *
+ * @param isCollection If true, a trailing slash will be appended.
+ * @see #getResourcePath()
+ * @return URL-encoded plaintext resource path.
+ */
+ @Override
+ public String getHref(boolean isCollection) {
+ final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
+ final String href = getPrefix().concat(encodedResourcePath);
+ assert !href.endsWith("/");
+ if (isCollection) {
+ return href.concat("/");
+ } else {
+ return href;
+ }
+ }
+
+ /**
+ * Returns the encrypted, absolute path on the local filesystem.
+ *
+ * @return Absolute, encrypted path as string (use {@link #getEncryptedFilePath()} for {@link Path}s).
+ */
+ @Override
+ public String getRepositoryPath() {
+ if (isRootLocation()) {
+ return getDirectoryPath();
+ }
+ try {
+ final String plaintextPath = getResourcePath();
+ final String plaintextDir = FilenameUtils.getPathNoEndSeparator(plaintextPath);
+ final String plaintextFilename = FilenameUtils.getName(plaintextPath);
+ final String ciphertextDir = cryptor.encryptDirectoryPath(plaintextDir, FileSystems.getDefault().getSeparator());
+ final String ciphertextFilename = cryptor.encryptFilename(plaintextFilename, factory);
+ final String ciphertextPath = ciphertextDir + FileSystems.getDefault().getSeparator() + ciphertextFilename;
+ return rootPath.resolve(ciphertextPath).toString();
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the encrypted, absolute path on the local filesystem to the directory represented by this locator.
+ *
+ * @return Absolute, encrypted path as string (use {@link #getEncryptedDirectoryPath()} for {@link Path}s).
+ */
+ public String getDirectoryPath() {
+ final String ciphertextPath = cryptor.encryptDirectoryPath(getResourcePath(), FileSystems.getDefault().getSeparator());
+ return rootPath.resolve(ciphertextPath).toString();
+ }
+
+ public Path getEncryptedFilePath() {
+ return FileSystems.getDefault().getPath(getRepositoryPath());
+ }
+
+ public Path getEncryptedDirectoryPath() {
+ return FileSystems.getDefault().getPath(getDirectoryPath());
+ }
+
+ /* other stuff */
+
+ @Override
+ public String getPrefix() {
+ return prefix;
+ }
+
+ @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 boolean isRootLocation() {
+ return Strings.isEmpty(getResourcePath());
+ }
+
+ @Override
+ public CryptoLocatorFactory getFactory() {
+ return factory;
+ }
+
+ /* hashcode and equals */
+
+ @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 CryptoLocator) {
+ final CryptoLocator other = (CryptoLocator) obj;
+ final EqualsBuilder builder = new EqualsBuilder();
+ builder.append(this.factory, other.factory);
+ builder.append(this.prefix, other.prefix);
+ builder.append(this.resourcePath, other.resourcePath);
+ return builder.isEquals();
+ } else {
+ return false;
+ }
+ }
+
+}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java
new file mode 100644
index 000000000..7b6e68c06
--- /dev/null
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java
@@ -0,0 +1,92 @@
+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.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.cryptomator.crypto.Cryptor;
+import org.cryptomator.crypto.CryptorMetadataSupport;
+import org.cryptomator.crypto.exceptions.DecryptFailedException;
+import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
+import org.cryptomator.webdav.exceptions.IORuntimeException;
+
+class CryptoLocatorFactory implements DavLocatorFactory, CryptorMetadataSupport {
+
+ private final Path dataRoot;
+ private final Path metadataRoot;
+ private final Cryptor cryptor;
+
+ CryptoLocatorFactory(String fsRoot, Cryptor cryptor) {
+ this.dataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("d");
+ this.metadataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("m");
+ this.cryptor = cryptor;
+ }
+
+ @Override
+ public CryptoLocator 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 CryptoLocator(this, cryptor, dataRoot, fullPrefix, resourcePath);
+ }
+
+ /**
+ * @throws DecryptFailedRuntimeException, which should be a checked exception, but Jackrabbit doesn't allow that.
+ */
+ @Override
+ public CryptoLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
+ if (!isResourcePath) {
+ throw new UnsupportedOperationException("Can not decrypt " + path + " without knowing plaintext parent path.");
+ }
+ final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
+ return new CryptoLocator(this, cryptor, dataRoot, fullPrefix, path);
+ }
+
+ @Override
+ public CryptoLocator 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);
+ }
+ }
+
+ public DavResourceLocator createSubresourceLocator(CryptoLocator parentResource, String ciphertextChildName) {
+ try {
+ final String plaintextFilename = cryptor.decryptFilename(ciphertextChildName, this);
+ final String plaintextPath = FilenameUtils.concat(parentResource.getResourcePath(), plaintextFilename);
+ return createResourceLocator(parentResource.getPrefix(), parentResource.getWorkspacePath(), plaintextPath);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ } catch (DecryptFailedException e) {
+ throw new DecryptFailedRuntimeException(e);
+ }
+ }
+
+ /* metadata storage */
+
+ @Override
+ public void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException {
+ final Path metaDataFile = metadataRoot.resolve(metadataGroup);
+ Files.write(metaDataFile, encryptedMetadata, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
+
+ }
+
+ @Override
+ public byte[] readMetadata(String metadataGroup) throws IOException {
+ final Path metaDataFile = metadataRoot.resolve(metadataGroup);
+ if (!Files.isReadable(metaDataFile)) {
+ return null;
+ } else {
+ return Files.readAllBytes(metaDataFile);
+ }
+ }
+}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java
new file mode 100644
index 000000000..6a965f5e4
--- /dev/null
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java
@@ -0,0 +1,99 @@
+package org.cryptomator.webdav.jackrabbit;
+
+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.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;
+
+public class CryptoResourceFactory implements DavResourceFactory {
+
+ private final LockManager lockManager = new SimpleLockManager();
+ private final Cryptor cryptor;
+ private final CryptoWarningHandler cryptoWarningHandler;
+ private final ExecutorService backgroundTaskExecutor;
+
+ CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) {
+ this.cryptor = cryptor;
+ this.cryptoWarningHandler = cryptoWarningHandler;
+ this.backgroundTaskExecutor = backgroundTaskExecutor;
+ }
+
+ @Override
+ public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
+ if (locator instanceof CryptoLocator) {
+ return createResource((CryptoLocator) locator, request, response);
+ } else {
+ throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName());
+ }
+ }
+
+ @Override
+ public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
+ if (locator instanceof CryptoLocator) {
+ return createResource((CryptoLocator) locator, session);
+ } else {
+ throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName());
+ }
+ }
+
+ private DavResource createResource(CryptoLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
+ final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath());
+ final Path dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath());
+ final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
+
+ if (Files.isDirectory(dirpath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
+ return createDirectory(locator, request.getDavSession());
+ } else if (Files.isRegularFile(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
+ response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
+ return createFilePart(locator, request.getDavSession(), request);
+ } else if (Files.isRegularFile(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
+ return createFile(locator, request.getDavSession());
+ } else {
+ return createNonExisting(locator, request.getDavSession());
+ }
+ }
+
+ private DavResource createResource(CryptoLocator locator, DavSession session) throws DavException {
+ final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath());
+ final Path dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath());
+
+ if (Files.isDirectory(dirpath)) {
+ return createDirectory(locator, session);
+ } else if (Files.isRegularFile(filepath)) {
+ return createFile(locator, session);
+ } else {
+ return createNonExisting(locator, session);
+ }
+ }
+
+ private EncryptedFile createFilePart(CryptoLocator locator, DavSession session, DavServletRequest request) {
+ return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor);
+ }
+
+ private EncryptedFile createFile(CryptoLocator locator, DavSession session) {
+ return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler);
+ }
+
+ private EncryptedDir createDirectory(CryptoLocator locator, DavSession session) {
+ return new EncryptedDir(this, locator, session, lockManager, cryptor);
+ }
+
+ private NonExistingNode createNonExisting(CryptoLocator locator, DavSession session) {
+ return new NonExistingNode(this, locator, session, lockManager, cryptor);
+ }
+
+}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavLocatorFactoryImpl.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavLocatorFactoryImpl.java
deleted file mode 100644
index 71ca8e476..000000000
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavLocatorFactoryImpl.java
+++ /dev/null
@@ -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 pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); //
-
- 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;
- }
- }
-
- }
-
-}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavResourceFactoryImpl.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavResourceFactoryImpl.java
deleted file mode 100644
index fac7e9072..000000000
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/DavResourceFactoryImpl.java
+++ /dev/null
@@ -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);
- }
-
-}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java
index 925953123..aec5a60e8 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java
@@ -10,11 +10,11 @@ package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
+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;
@@ -23,7 +23,6 @@ import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
-import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceIteratorImpl;
import org.apache.jackrabbit.webdav.DavResourceLocator;
@@ -48,28 +47,55 @@ class EncryptedDir extends AbstractEncryptedNode {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
- public EncryptedDir(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ public EncryptedDir(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
}
+ @Override
+ protected Path getPhysicalPath() {
+ return locator.getEncryptedDirectoryPath();
+ }
+
@Override
public boolean isCollection() {
return true;
}
@Override
- public void addMember(DavResource resource, InputContext inputContext) throws DavException {
- if (resource.isCollection()) {
- this.addMemberDir(resource, inputContext);
- } else {
- this.addMemberFile(resource, inputContext);
+ public boolean exists() {
+ return Files.isDirectory(locator.getEncryptedDirectoryPath());
+ }
+
+ @Override
+ public long getModificationTime() {
+ try {
+ return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath()).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(CryptoLocator childLocator, InputContext inputContext) throws DavException {
try {
- Files.createDirectories(childPath);
+ Files.createDirectories(childLocator.getEncryptedFilePath());
+ Files.createDirectories(childLocator.getEncryptedDirectoryPath());
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
@@ -78,9 +104,8 @@ class EncryptedDir extends AbstractEncryptedNode {
}
}
- 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)) {
+ private void addMemberFile(CryptoLocator childLocator, InputContext inputContext) throws DavException {
+ try (final SeekableByteChannel channel = Files.newByteChannel(childLocator.getEncryptedFilePath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
cryptor.encryptFile(inputContext.getInputStream(), channel);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
@@ -100,14 +125,15 @@ class EncryptedDir extends AbstractEncryptedNode {
@Override
public DavResourceIterator getMembers() {
- final Path dir = ResourcePathUtils.getPhysicalPath(this);
try {
- final DirectoryStream directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
+ final DirectoryStream directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(), cryptor.getPayloadFilesFilter());
final List result = new ArrayList<>();
for (final Path childPath : directoryStream) {
try {
- final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), childPath.toString(), false);
+ final DavResourceLocator childLocator = locator.getFactory().createSubresourceLocator(locator, childPath.getFileName().toString());
+ // final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(),
+ // locator.getWorkspacePath(), childPath.toString(), false);
final DavResource resource = factory.createResource(childLocator, session);
result.add(resource);
} catch (DecryptFailedRuntimeException e) {
@@ -126,19 +152,80 @@ class EncryptedDir extends AbstractEncryptedNode {
}
@Override
- public void removeMember(DavResource member) throws DavException {
- final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
+ public void removeMember(DavResource member) {
+ if (member instanceof AbstractEncryptedNode) {
+ removeMember((AbstractEncryptedNode) member);
+ } else {
+ throw new IllegalArgumentException("Unsupported resource type: " + member.getClass().getName());
+ }
+ }
+
+ private void removeMember(AbstractEncryptedNode member) {
try {
- if (Files.exists(memberPath)) {
- Files.walkFileTree(memberPath, new DeletingFileVisitor());
+ if (member.isCollection()) {
+ member.getMembers().forEachRemaining(m -> securelyRemoveMemberOfCollection(member, m));
+ Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath());
}
- } catch (SecurityException e) {
- throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
+ Files.deleteIfExists(member.getLocator().getEncryptedFilePath());
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
+ private void securelyRemoveMemberOfCollection(DavResource collection, DavResource member) {
+ try {
+ collection.removeMember(member);
+ } catch (DavException e) {
+ throw new IllegalStateException("DavException should not be thrown by collections of type EncryptedDir. Collections is of type " + collection.getClass().getName());
+ }
+ }
+
+ @Override
+ public void move(AbstractEncryptedNode dest) throws DavException, IOException {
+ final Path srcDir = this.locator.getEncryptedDirectoryPath();
+ final Path dstDir = dest.locator.getEncryptedDirectoryPath();
+ final Path srcFile = this.locator.getEncryptedFilePath();
+ final Path dstFile = dest.locator.getEncryptedFilePath();
+
+ // check for conflicts:
+ if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) {
+ throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString());
+ }
+
+ // move:
+ Files.createDirectories(dstDir);
+ try {
+ Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING);
+ Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ @Override
+ public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
+ final Path srcDir = this.locator.getEncryptedDirectoryPath();
+ final Path dstDir = dest.locator.getEncryptedDirectoryPath();
+ final Path srcFile = this.locator.getEncryptedFilePath();
+ final Path dstFile = dest.locator.getEncryptedFilePath();
+
+ // check for conflicts:
+ if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) {
+ throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString());
+ }
+
+ // copy:
+ Files.createDirectories(dstDir);
+ try {
+ Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+ Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
@Override
public void spool(OutputContext outputContext) throws IOException {
// do nothing
@@ -146,7 +233,7 @@ class EncryptedDir extends AbstractEncryptedNode {
@Override
protected void determineProperties() {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
+ final Path path = locator.getEncryptedDirectoryPath();
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1));
if (Files.exists(path)) {
@@ -161,31 +248,4 @@ class EncryptedDir extends AbstractEncryptedNode {
}
}
- /**
- * Deletes all files and folders, it visits.
- */
- private static class DeletingFileVisitor extends SimpleFileVisitor {
-
- @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;
- }
-
- }
-
}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java
index 09905a0b6..be76d711f 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java
@@ -11,16 +11,17 @@ package org.cryptomator.webdav.jackrabbit;
import java.io.EOFException;
import java.io.IOException;
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.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
@@ -42,11 +43,16 @@ class EncryptedFile extends AbstractEncryptedNode {
protected final CryptoWarningHandler cryptoWarningHandler;
- public EncryptedFile(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
+ public EncryptedFile(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
super(factory, locator, session, lockManager, cryptor);
this.cryptoWarningHandler = cryptoWarningHandler;
}
+ @Override
+ protected Path getPhysicalPath() {
+ return locator.getEncryptedFilePath();
+ }
+
@Override
public boolean isCollection() {
return false;
@@ -69,7 +75,7 @@ class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
+ final Path path = locator.getEncryptedFilePath();
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
@@ -93,7 +99,7 @@ class EncryptedFile extends AbstractEncryptedNode {
@Override
protected void determineProperties() {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
+ final Path path = locator.getEncryptedFilePath();
if (Files.exists(path)) {
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
@@ -115,4 +121,40 @@ class EncryptedFile extends AbstractEncryptedNode {
}
}
+ @Override
+ public void move(AbstractEncryptedNode dest) throws DavException, IOException {
+ final Path src = this.locator.getEncryptedFilePath();
+ final Path dst = dest.locator.getEncryptedFilePath();
+
+ // 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:
+ try {
+ Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
+ } catch (AtomicMoveNotSupportedException e) {
+ Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
+ }
+ }
+
+ @Override
+ public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException {
+ final Path src = this.locator.getEncryptedFilePath();
+ final Path dst = dest.locator.getEncryptedFilePath();
+
+ // 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:
+ 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);
+ }
+ }
+
}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java
index dfc73d78c..697a4422b 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java
@@ -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,7 +55,7 @@ class EncryptedFilePart extends EncryptedFile {
private final Set> requestedContentRanges = new HashSet>();
- public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
+ public EncryptedFilePart(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
@@ -126,7 +125,7 @@ class EncryptedFilePart extends EncryptedFile {
@Override
public void spool(OutputContext outputContext) throws IOException {
- final Path path = ResourcePathUtils.getPhysicalPath(this);
+ final Path path = locator.getEncryptedFilePath();
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
@@ -154,9 +153,9 @@ class EncryptedFilePart extends EncryptedFile {
private class MacAuthenticationJob implements Runnable {
- private final DavResourceLocator locator;
+ private final CryptoLocator locator;
- public MacAuthenticationJob(final DavResourceLocator locator) {
+ public MacAuthenticationJob(final CryptoLocator locator) {
if (locator == null) {
throw new IllegalArgumentException("locator must not be null.");
}
@@ -165,7 +164,7 @@ class EncryptedFilePart extends EncryptedFile {
@Override
public void run() {
- final Path path = ResourcePathUtils.getPhysicalPath(locator);
+ final Path path = locator.getEncryptedFilePath();
if (Files.isRegularFile(path) && Files.isReadable(path)) {
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java
index f58f20a2c..859cd943c 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/NonExistingNode.java
@@ -9,12 +9,11 @@
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;
@@ -23,10 +22,15 @@ import org.cryptomator.crypto.Cryptor;
class NonExistingNode extends AbstractEncryptedNode {
- public NonExistingNode(DavResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
+ public NonExistingNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
}
+ @Override
+ protected Path getPhysicalPath() {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
@Override
public boolean exists() {
return false;
@@ -37,6 +41,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.");
@@ -62,4 +71,14 @@ class NonExistingNode extends AbstractEncryptedNode {
// do nothing.
}
+ @Override
+ public void move(AbstractEncryptedNode destination) throws DavException {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
+ @Override
+ public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
+ throw new UnsupportedOperationException("Resource doesn't exist.");
+ }
+
}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java
index 71f65fcd3..6f77bff1f 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java
@@ -11,21 +11,18 @@ 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) {
+ public static Path getPhysicalFilePath(CryptoLocator locator) {
return FileSystems.getDefault().getPath(locator.getRepositoryPath());
}
+ public static Path getPhysicalDirectoryPath(CryptoLocator locator) {
+ return FileSystems.getDefault().getPath(locator.getDirectoryPath());
+ }
+
}
diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java
index 48752c20c..8b5fbc86f 100644
--- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java
+++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java
@@ -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 CryptoLocatorFactory(fsRoot, cryptor);
+ davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor);
}
@Override
diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java
index fcc0e2ebd..fc9e1b77a 100644
--- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java
+++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java
@@ -22,9 +22,7 @@ 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;
@@ -44,8 +42,8 @@ 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.CryptorMetadataSupport;
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
@@ -59,7 +57,7 @@ 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, FileNamingConventions {
/**
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction
@@ -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);
}
@@ -249,6 +252,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,18 +283,12 @@ 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 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);
- }
+ public String encryptDirectoryPath(String cleartextPath, String nativePathSep) {
+ final byte[] cleartextBytes = cleartextPath.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);
}
/**
@@ -301,19 +306,19 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
* 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);
+ @Override
+ public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
+ final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
// encrypt:
- final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(aesKey, macKey, cleartextBytes);
+ final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, 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);
+ final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
+ final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup);
+ final String alternativeFileName = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
+ this.storeMetadata(ioSupport, metadataGroup, metadata);
return alternativeFileName;
} else {
return ivAndCiphertext + BASIC_FILE_EXT;
@@ -321,47 +326,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
@Override
- public String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException {
- try {
- final String[] encryptedPathComps = StringUtils.split(encryptedPath, encryptedPathSep);
- final List 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);
- }
- }
-
- /**
- * @see #encryptPathComponent(String, SecretKey, CryptorIOSupport)
- */
- private String decryptPathComponent(final String encrypted, final SecretKey aesKey, final SecretKey macKey, CryptorIOSupport ioSupport) throws IOException, InvalidKeyException, DecryptFailedException {
+ public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws DecryptFailedException, IOException {
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);
+ if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
+ final String basename = StringUtils.removeEnd(ciphertextName, LONG_NAME_FILE_EXT);
+ final String metadataGroup = 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);
+ final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
- } else if (encrypted.endsWith(BASIC_FILE_EXT)) {
- ciphertext = StringUtils.removeEndIgnoreCase(encrypted, BASIC_FILE_EXT);
+ } else if (ciphertextName.endsWith(BASIC_FILE_EXT)) {
+ ciphertext = StringUtils.removeEndIgnoreCase(ciphertextName, BASIC_FILE_EXT);
} else {
- throw new IllegalArgumentException("Unsupported path component: " + encrypted);
+ throw new IllegalArgumentException("Unsupported path component: " + ciphertextName);
}
// decrypt:
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
- final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(aesKey, macKey, encryptedBytes);
+ 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);
+ private LongFilenameMetadata getMetadata(CryptorMetadataSupport ioSupport, String metadataGroup) throws IOException {
+ final byte[] fileContent = ioSupport.readMetadata(metadataGroup);
if (fileContent == null) {
return new LongFilenameMetadata();
} else {
@@ -369,8 +356,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
}
}
- private void storeMetadata(CryptorIOSupport ioSupport, String metadataFile, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
- ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
+ private void storeMetadata(CryptorMetadataSupport ioSupport, String metadataGroup, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
+ ioSupport.writeMetadata(metadataGroup, objectMapper.writeValueAsBytes(metadata));
}
@Override
diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java
index 4a4d9b3f1..023f28767 100644
--- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java
+++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesSivCipherUtil.java
@@ -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);
diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java
index 82e6b6aa7..36ee0ccfe 100644
--- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java
+++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java
@@ -32,11 +32,6 @@ interface FileNamingConventions {
*/
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.
*/
@@ -47,12 +42,6 @@ interface FileNamingConventions {
*/
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.
*/
diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java
index 0ee932d81..8c8a2bc7a 100644
--- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java
+++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java
@@ -18,8 +18,10 @@ 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.CryptorMetadataSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
@@ -30,12 +32,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 {
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 +48,12 @@ public class Aes256CryptorTest {
}
@Test
- public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
+ public void testWrongPassword() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, DestroyFailedException {
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.
@@ -207,46 +209,44 @@ public class Aes256CryptorTest {
@Test
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
- final CryptorIOSupport ioSupportMock = new CryptoIOSupportMock();
+ final CryptorMetadataSupport 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, ioSupportMock);
+ final String encryptedPath2b = cryptor.encryptFilename(originalPath2, ioSupportMock);
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
- final String decryptedPath2 = cryptor.decryptPath(encryptedPath2a, '/', '/', ioSupportMock);
+ final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a, ioSupportMock);
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, ioSupportMock);
+ final String encryptedPath3b = cryptor.encryptFilename(originalPath3, ioSupportMock);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
- final String decryptedPath3 = cryptor.decryptPath(encryptedPath3a, '/', '/', ioSupportMock);
+ final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a, ioSupportMock);
Assert.assertEquals(originalPath3, decryptedPath3);
}
- private static class CryptoIOSupportMock implements CryptorIOSupport {
+ private static class CryptoIOSupportMock implements CryptorMetadataSupport {
private final Map map = new HashMap<>();
@Override
- public void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) {
+ public void writeMetadata(String encryptedPath, byte[] encryptedMetadata) {
map.put(encryptedPath, encryptedMetadata);
}
@Override
- public byte[] readPathSpecificMetadata(String encryptedPath) {
+ public byte[] readMetadata(String encryptedPath) {
return map.get(encryptedPath);
}
diff --git a/main/crypto-api/pom.xml b/main/crypto-api/pom.xml
index bd0a115c3..3a8c08e03 100644
--- a/main/crypto-api/pom.xml
+++ b/main/crypto-api/pom.xml
@@ -27,5 +27,9 @@
org.apache.commons
commons-lang3
+
+ org.apache.commons
+ commons-collections4
+
\ No newline at end of file
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptor.java
deleted file mode 100644
index d51ad70c3..000000000
--- a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptor.java
+++ /dev/null
@@ -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 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);
- }
-
-}
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java
new file mode 100644
index 000000000..0a1b9b8b6
--- /dev/null
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java
@@ -0,0 +1,90 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.Path;
+
+import javax.security.auth.DestroyFailedException;
+
+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 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 {
+ cryptor.decryptMasterKey(in, password);
+ }
+
+ @Override
+ public String encryptDirectoryPath(String cleartextPath, String nativePathSep) {
+ return cryptor.encryptDirectoryPath(cleartextPath, nativePathSep);
+ }
+
+ @Override
+ public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
+ return cryptor.encryptFilename(cleartextName, ioSupport);
+ }
+
+ @Override
+ public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException {
+ return cryptor.decryptFilename(ciphertextName, 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 {
+ 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 Filter getPayloadFilesFilter() {
+ return cryptor.getPayloadFilesFilter();
+ }
+
+ @Override
+ public void destroy() throws DestroyFailedException {
+ cryptor.destroy();
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return cryptor.isDestroyed();
+ }
+
+}
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java
index bcf28cf90..5a2cd25a0 100644
--- a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java
@@ -15,6 +15,8 @@ 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.UnsupportedKeyLengthException;
@@ -23,7 +25,7 @@ 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.
@@ -42,33 +44,38 @@ public interface Cryptor extends SensitiveDataSwipeListener {
void decryptMasterKey(InputStream in, CharSequence password) throws DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, IOException;
/**
- * 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
+ * @param cleartextPath A relative path (UTF-8 encoded), whose path components are separated by '/'
+ * @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.
- * @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.
+ * @return Encrypted path.
*/
- String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
+ String encryptDirectoryPath(String cleartextPath, 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.
- * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
+ * @param cleartextName A plaintext filename without any preceeding directory paths.
+ * @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this
+ * support object.
+ * @return Encrypted filename.
+ * @throws IOException If ioSupport throws an IOException
*/
- String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) throws DecryptFailedException;
+ String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException;
+
+ /**
+ * Decrypts the name of a file.
+ *
+ * @param ciphertextName A ciphertext filename without any preceeding directory paths.
+ * @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this
+ * support object.
+ * @return Decrypted filename.
+ * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
+ * @throws IOException If ioSupport throws an IOException
+ */
+ String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
@@ -106,8 +113,4 @@ public interface Cryptor extends SensitiveDataSwipeListener {
*/
Filter getPayloadFilesFilter();
- void addSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
-
- void removeSensitiveDataSwipeListener(SensitiveDataSwipeListener listener);
-
}
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java
similarity index 56%
rename from main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java
rename to main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java
index 2709441bc..c3564fb32 100644
--- a/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorIOSupport.java
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java
@@ -13,19 +13,19 @@ import java.io.IOException;
/**
* Methods that may be called by the Cryptor when accessing a path.
*/
-public interface CryptorIOSupport {
+public interface CryptorMetadataSupport {
/**
- * Persists encryptedMetadata to the given encryptedPath.
+ * Persists encryptedMetadata in a metadata group.
*
- * @param encryptedPath A relative path
+ * @param metadataFilename File relative to
* @throws IOException
*/
- void writePathSpecificMetadata(String encryptedPath, byte[] encryptedMetadata) throws IOException;
+ void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException;
/**
- * @return Previously written encryptedMetadata stored at the given encryptedPath or null if no such file exists.
+ * @return Previously written metadata stored in the given metadata group or null if no such group exists.
*/
- byte[] readPathSpecificMetadata(String encryptedPath) throws IOException;
+ byte[] readMetadata(String metadataGroup) throws IOException;
}
\ No newline at end of file
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java
new file mode 100644
index 000000000..ba955a799
--- /dev/null
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java
@@ -0,0 +1,79 @@
+package org.cryptomator.crypto;
+
+import java.io.IOException;
+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 pathCache = new LRUMap<>(MAX_CACHED_PATHS); //
+ private final BidiMap nameCache = new BidiLRUMap<>(MAX_CACHED_NAMES); //
+
+ private PathCachingCryptorDecorator(Cryptor cryptor) {
+ super(cryptor);
+ }
+
+ public static Cryptor decorate(Cryptor cryptor) {
+ return new PathCachingCryptorDecorator(cryptor);
+ }
+
+ /* Cryptor */
+
+ @Override
+ public String encryptDirectoryPath(String cleartextPath, String nativePathSep) {
+ if (pathCache.containsKey(cleartextPath)) {
+ return pathCache.get(cleartextPath);
+ } else {
+ final String ciphertextPath = cryptor.encryptDirectoryPath(cleartextPath, nativePathSep);
+ pathCache.put(cleartextPath, ciphertextPath);
+ return ciphertextPath;
+ }
+ }
+
+ @Override
+ public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
+ if (nameCache.containsKey(cleartextName)) {
+ return nameCache.get(cleartextName);
+ } else {
+ final String ciphertextName = cryptor.encryptFilename(cleartextName, ioSupport);
+ nameCache.put(cleartextName, ciphertextName);
+ return ciphertextName;
+ }
+ }
+
+ @Override
+ public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException {
+ if (nameCache.containsValue(ciphertextName)) {
+ return nameCache.getKey(ciphertextName);
+ } else {
+ final String cleartextName = cryptor.decryptFilename(ciphertextName, ioSupport);
+ nameCache.put(cleartextName, ciphertextName);
+ return ciphertextName;
+ }
+ }
+
+ private static class BidiLRUMap extends AbstractDualBidiMap {
+
+ BidiLRUMap(int maxSize) {
+ super(new LRUMap(maxSize), new LRUMap(maxSize));
+ }
+
+ protected BidiLRUMap(final Map normalMap, final Map reverseMap, final BidiMap inverseBidiMap) {
+ super(normalMap, reverseMap, inverseBidiMap);
+ }
+
+ @Override
+ protected BidiMap createBidiMap(Map normalMap, Map reverseMap, BidiMap inverseMap) {
+ return new BidiLRUMap(normalMap, reverseMap, inverseMap);
+ }
+
+ }
+
+}
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java
similarity index 56%
rename from main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java
rename to main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java
index a6461373a..8f056054b 100644
--- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java
+++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingCryptorDecorator.java
@@ -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 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;
diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SensitiveDataSwipeListener.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SensitiveDataSwipeListener.java
deleted file mode 100644
index c98cceb99..000000000
--- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SensitiveDataSwipeListener.java
+++ /dev/null
@@ -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();
-
-}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainModule.java b/main/ui/src/main/java/org/cryptomator/ui/MainModule.java
index bfa815eab..30f3d78bf 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/MainModule.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/MainModule.java
@@ -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
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java
index 0d392b4db..a7fa2a0a4 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java
@@ -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);
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java
index 1a708e30f..877d18a39 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java
@@ -30,6 +30,8 @@ import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
+import javax.security.auth.DestroyFailedException;
+
import org.apache.commons.lang3.CharUtils;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
@@ -106,7 +108,7 @@ public class UnlockController implements Initializable {
vault.getCryptor().decryptMasterKey(masterKeyInputStream, password);
if (!vault.startServer()) {
messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
- vault.getCryptor().swipeSensitiveData();
+ vault.getCryptor().destroy();
return;
}
// at this point we know for sure, that the masterkey can be decrypted, so lets make a backup:
@@ -129,6 +131,10 @@ public class UnlockController implements Initializable {
progressIndicator.setVisible(false);
messageLabel.setText(rb.getString("unlock.errorMessage.unsupportedKeyLengthInstallJCE"));
LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex);
+ } catch (DestroyFailedException e) {
+ setControlsDisabled(false);
+ progressIndicator.setVisible(false);
+ LOG.error("Destruction of cryptor throw an exception.", e);
} finally {
passwordField.swipe();
}
diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
index 9b20e48cc..2a65ad2aa 100644
--- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
+++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java
@@ -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();
}