Merge branch 'webdav-directory-moving'

This commit is contained in:
Sebastian Stenzel
2015-05-21 18:50:56 +02:00
22 changed files with 784 additions and 703 deletions
+6
View File
@@ -64,5 +64,11 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
@@ -44,24 +44,23 @@ abstract class AbstractEncryptedNode implements DavResource {
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
protected final CryptoResourceFactory factory;
protected final CryptoLocator locator;
protected final DavResourceLocator locator;
protected final DavSession session;
protected final LockManager lockManager;
protected final Cryptor cryptor;
protected final Path filePath;
protected final DavPropertySet properties;
protected AbstractEncryptedNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) {
this.factory = factory;
this.locator = locator;
this.session = session;
this.lockManager = lockManager;
this.cryptor = cryptor;
this.filePath = filePath;
this.properties = new DavPropertySet();
this.determineProperties();
}
protected abstract Path getPhysicalPath();
@Override
public String getComplianceClass() {
return DAV_COMPLIANCE_CLASSES;
@@ -74,7 +73,7 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public boolean exists() {
return Files.exists(getPhysicalPath());
return Files.exists(filePath);
}
@Override
@@ -89,7 +88,7 @@ abstract class AbstractEncryptedNode implements DavResource {
}
@Override
public CryptoLocator getLocator() {
public DavResourceLocator getLocator() {
return locator;
}
@@ -106,14 +105,12 @@ abstract class AbstractEncryptedNode implements DavResource {
@Override
public long getModificationTime() {
try {
return Files.getLastModifiedTime(getPhysicalPath()).toMillis();
return Files.getLastModifiedTime(filePath).toMillis();
} catch (IOException e) {
return -1;
}
}
protected abstract void determineProperties();
@Override
public DavPropertyName[] getPropertyNames() {
return getProperties().getPropertyNames();
@@ -136,17 +133,16 @@ abstract class AbstractEncryptedNode implements DavResource {
LOG.info("Set property {}", property.getName());
try {
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);
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(null, null, createTime);
LOG.info("Updating Creation Date: {}", createTime.toString());
} else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) {
final String lastModifiedTimeStr = (String) property.getValue();
final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr);
final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
attrView.setTimes(lastModifiedTime, null, null);
LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
}
@@ -183,7 +179,7 @@ abstract class AbstractEncryptedNode implements DavResource {
return null;
}
final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
try {
return getFactory().createResource(parentLocator, session);
@@ -0,0 +1,112 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavLocatorFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.util.EncodeUtil;
import org.apache.logging.log4j.util.Strings;
public class CleartextLocatorFactory implements DavLocatorFactory {
private final String pathPrefix;
public CleartextLocatorFactory(String pathPrefix) {
this.pathPrefix = pathPrefix;
}
// resourcePath == repositoryPath. No encryption here.
@Override
public DavResourceLocator createResourceLocator(String prefix, String href) {
final String fullPrefix = prefix.endsWith("/") ? prefix : prefix + "/";
final String relativeHref = StringUtils.removeStart(href, fullPrefix);
final String relativeCleartextPath = EncodeUtil.unescape(StringUtils.removeStart(relativeHref, "/"));
return new CleartextLocator(relativeCleartextPath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String resourcePath) {
return new CleartextLocator(resourcePath);
}
@Override
public DavResourceLocator createResourceLocator(String prefix, String workspacePath, String path, boolean isResourcePath) {
return new CleartextLocator(path);
}
private class CleartextLocator implements DavResourceLocator {
private final String relativeCleartextPath;
private CleartextLocator(String relativeCleartextPath) {
this.relativeCleartextPath = FilenameUtils.normalizeNoEndSeparator(relativeCleartextPath, true);
}
@Override
public String getPrefix() {
return pathPrefix;
}
@Override
public String getResourcePath() {
return relativeCleartextPath;
}
@Override
public String getWorkspacePath() {
return null;
}
@Override
public String getWorkspaceName() {
return null;
}
@Override
public boolean isSameWorkspace(DavResourceLocator locator) {
return false;
}
@Override
public boolean isSameWorkspace(String workspaceName) {
return false;
}
@Override
public String getHref(boolean isCollection) {
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
final String fullPrefix = pathPrefix.endsWith("/") ? pathPrefix : pathPrefix + "/";
final String href = fullPrefix.concat(encodedResourcePath);
assert !href.endsWith("/");
if (isCollection) {
return href.concat("/");
} else {
return href;
}
}
@Override
public boolean isRootLocation() {
return Strings.isEmpty(relativeCleartextPath);
}
@Override
public DavLocatorFactory getFactory() {
return CleartextLocatorFactory.this;
}
@Override
public String getRepositoryPath() {
return relativeCleartextPath;
}
@Override
public String toString() {
return "Locator: " + relativeCleartextPath + " (Prefix: " + pathPrefix + ")";
}
}
}
@@ -1,166 +0,0 @@
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;
}
}
}
@@ -1,102 +0,0 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
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 metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
Files.createDirectories(metadataDir);
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
c.write(ByteBuffer.wrap(encryptedMetadata));
}
}
@Override
public byte[] readMetadata(String metadataGroup) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
if (!Files.isReadable(metadataFile)) {
return null;
}
try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
return buffer.array();
}
}
}
@@ -1,11 +1,20 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.FilenameUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
@@ -16,84 +25,189 @@ import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.apache.logging.log4j.util.Strings;
import org.cryptomator.crypto.Cryptor;
import org.eclipse.jetty.http.HttpHeader;
public class CryptoResourceFactory implements DavResourceFactory {
public class CryptoResourceFactory implements DavResourceFactory, FileNamingConventions {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
private final CryptoWarningHandler cryptoWarningHandler;
private final ExecutorService backgroundTaskExecutor;
private final Path dataRoot;
private final FilenameTranslator filenameTranslator;
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) {
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) {
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
this.cryptor = cryptor;
this.cryptoWarningHandler = cryptoWarningHandler;
this.backgroundTaskExecutor = backgroundTaskExecutor;
this.dataRoot = vaultRootPath.resolve("d");
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
if (locator instanceof CryptoLocator) {
return createResource((CryptoLocator) locator, request, response);
if (locator.isRootLocation()) {
return createRootDirectory(locator, request.getDavSession());
}
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession(), dirFilePath);
} else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request, filePath);
} else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession(), filePath);
} else {
throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName());
// e.g. for MOVE operations:
return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath);
}
}
@Override
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (locator instanceof CryptoLocator) {
return createResource((CryptoLocator) locator, session);
if (locator.isRootLocation()) {
return createRootDirectory(locator, session);
}
final Path filePath = getEncryptedFilePath(locator.getResourcePath());
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
if (Files.exists(dirFilePath)) {
return createDirectory(locator, session, dirFilePath);
} else if (Files.exists(filePath)) {
return createFile(locator, session, filePath);
} else {
throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName());
// e.g. for MOVE operations:
return createNonExisting(locator, session, filePath, dirFilePath);
}
}
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());
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
return createDirectory(locator, session, existingDirectoryFile);
}
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());
DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
return createFile(locator, session, existingFile);
}
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws IOException
*/
private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
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);
/**
* @return Absolute file path for a given cleartext file resourcePath.
* @throws IOException
*/
private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
try {
final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
return parent.resolve(encryptedFilename);
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
private EncryptedFile createFilePart(CryptoLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor);
/**
* @return Absolute directory path for a given cleartext directory resourcePath.
* @throws IOException
*/
private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException {
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
try {
final Path result;
if (Strings.isEmpty(relativeCleartextPath)) {
// root level
final String fixedRootDirectory = cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(fixedRootDirectory);
} else {
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
final Path directoryFile = parent.resolve(encryptedFilename);
final String directoryId;
if (Files.exists(directoryFile)) {
directoryId = new String(readAllBytesAtomically(directoryFile), StandardCharsets.UTF_8);
} else {
directoryId = UUID.randomUUID().toString();
writeAllBytesAtomically(directoryFile, directoryId.getBytes(StandardCharsets.UTF_8));
}
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
result = dataRoot.resolve(directory);
}
Files.createDirectories(result);
return result;
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
private EncryptedFile createFile(CryptoLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler);
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath);
}
private EncryptedDir createDirectory(CryptoLocator locator, DavSession session) {
return new EncryptedDir(this, locator, session, lockManager, cryptor);
private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {
return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
}
private NonExistingNode createNonExisting(CryptoLocator locator, DavSession session) {
return new NonExistingNode(this, locator, session, lockManager, cryptor);
private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException {
final Path rootFile = dataRoot.resolve(ROOT_FILE);
final Path rootDir = filenameTranslator.getEncryptedDirectoryPath("");
try {
// make sure, root dir always exists.
// create dir first (because it fails silently, if alreay existing)
Files.createDirectories(rootDir);
Files.createFile(rootFile);
} catch (FileAlreadyExistsException e) {
// no-op
} catch (IOException e) {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
}
return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE));
}
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) {
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath);
}
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session, Path filePath, Path dirFilePath) {
return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath);
}
/* IO support */
private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
c.write(ByteBuffer.wrap(bytes));
}
}
private byte[] readAllBytesAtomically(Path path) throws IOException {
try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
return buffer.array();
}
}
}
@@ -8,8 +8,13 @@
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
@@ -18,9 +23,13 @@ import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.UUID;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
@@ -36,24 +45,56 @@ import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.apache.jackrabbit.webdav.property.ResourceType;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.CounterOverflowException;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.webdav.exceptions.DavRuntimeException;
import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
class EncryptedDir extends AbstractEncryptedNode {
class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
private final FilenameTranslator filenameTranslator;
private String directoryId;
private Path directoryPath;
public EncryptedDir(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
this.filenameTranslator = filenameTranslator;
determineProperties();
}
@Override
protected Path getPhysicalPath() {
return locator.getEncryptedDirectoryPath();
/**
* @return Path or <code>null</code>, if directory does not yet exist.
*/
protected synchronized String getDirectoryId() {
if (directoryId == null) {
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
directoryId = new String(buffer.array(), StandardCharsets.UTF_8);
} catch (FileNotFoundException e) {
directoryId = null;
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
return directoryId;
}
/**
* @return Path or <code>null</code>, if directory does not yet exist.
*/
private synchronized Path getDirectoryPath() {
if (directoryPath == null) {
final String dirId = getDirectoryId();
if (dirId != null) {
directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
}
}
return directoryPath;
}
@Override
@@ -61,15 +102,15 @@ class EncryptedDir extends AbstractEncryptedNode {
return true;
}
@Override
public boolean exists() {
return Files.isDirectory(locator.getEncryptedDirectoryPath());
}
@Override
public long getModificationTime() {
try {
return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath()).toMillis();
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
return -1;
} else {
return Files.getLastModifiedTime(dirPath).toMillis();
}
} catch (IOException e) {
return -1;
}
@@ -92,51 +133,89 @@ class EncryptedDir extends AbstractEncryptedNode {
}
}
private void addMemberDir(CryptoLocator childLocator, InputContext inputContext) throws DavException {
private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
Files.createDirectories(childLocator.getEncryptedFilePath());
Files.createDirectories(childLocator.getEncryptedDirectoryPath());
final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName);
final Path dirFilePath = dirPath.resolve(ciphertextDirName);
final String directoryId;
if (Files.exists(dirFilePath)) {
try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
directoryId = new String(buffer.array(), StandardCharsets.UTF_8);
}
} else {
directoryId = UUID.randomUUID().toString();
try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
c.write(ByteBuffer.wrap(directoryId.getBytes(StandardCharsets.UTF_8)));
}
}
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
Files.createDirectories(directoryPath);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (IOException e) {
LOG.error("Failed to create subdirectory.", e);
throw new IORuntimeException(e);
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
}
}
private void addMemberFile(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);
private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
final Path filePath = dirPath.resolve(ciphertextFilename);
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
cryptor.encryptFile(inputContext.getInputStream(), channel);
} catch (SecurityException e) {
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
} catch (CounterOverflowException e) {
// lets indicate this to the client as a "file too big" error
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
} catch (EncryptFailedException e) {
LOG.error("Encryption failed for unknown reasons.", e);
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
} finally {
IOUtils.closeQuietly(inputContext.getInputStream());
}
} catch (IOException e) {
LOG.error("Failed to create file.", e);
throw new IORuntimeException(e);
} catch (CounterOverflowException e) {
// lets indicate this to the client as a "file too big" error
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
} catch (EncryptFailedException e) {
LOG.error("Encryption failed for unknown reasons.", e);
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
} finally {
IOUtils.closeQuietly(inputContext.getInputStream());
}
}
@Override
public DavResourceIterator getMembers() {
try {
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(), cryptor.getPayloadFilesFilter());
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER);
final List<DavResource> result = new ArrayList<>();
for (final Path childPath : directoryStream) {
try {
final DavResourceLocator childLocator = locator.getFactory().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);
final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString());
final String cleartextFilepath = FilenameUtils.concat(getResourcePath(), cleartextFilename);
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
final DavResource resource;
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
resource = factory.createChildDirectoryResource(childLocator, session, childPath);
} else {
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
resource = factory.createChildFileResource(childLocator, session, childPath);
}
result.add(resource);
} catch (DecryptFailedRuntimeException e) {
} catch (DecryptFailedException e) {
LOG.warn("Decryption of resource failed: " + childPath);
continue;
}
@@ -152,7 +231,7 @@ class EncryptedDir extends AbstractEncryptedNode {
}
@Override
public void removeMember(DavResource member) {
public void removeMember(DavResource member) throws DavException {
if (member instanceof AbstractEncryptedNode) {
removeMember((AbstractEncryptedNode) member);
} else {
@@ -160,69 +239,114 @@ class EncryptedDir extends AbstractEncryptedNode {
}
}
private void removeMember(AbstractEncryptedNode member) {
private void removeMember(AbstractEncryptedNode member) throws DavException {
final Path dirPath = getDirectoryPath();
if (dirPath == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
try {
if (member.isCollection()) {
member.getMembers().forEachRemaining(m -> securelyRemoveMemberOfCollection(member, m));
Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath());
final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
final String ciphertextFilename;
if (member instanceof EncryptedDir) {
final EncryptedDir subDir = (EncryptedDir) member;
// remove sub-members recursively before deleting own directory
for (Iterator<DavResource> iterator = member.getMembers(); iterator.hasNext();) {
DavResource m = iterator.next();
member.removeMember(m);
}
final Path subDirPath = subDir.getDirectoryPath();
if (subDirPath != null) {
Files.deleteIfExists(subDirPath);
}
ciphertextFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
} else {
ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
}
Files.deleteIfExists(member.getLocator().getEncryptedFilePath());
final Path memberPath = dirPath.resolve(ciphertextFilename);
Files.deleteIfExists(memberPath);
} catch (FileNotFoundException e) {
// no-op
} 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());
// when moving a directory we only need to move the file (actual dir is ID-dependent and won't change)
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getDirFilePath();
} else {
dstPath = dest.filePath;
}
// move:
Files.createDirectories(dstDir);
Files.createDirectories(dstPath.getParent());
try {
Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING);
Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING);
Files.move(srcPath, dstPath, 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());
final Path dstDirFilePath;
if (dest instanceof NonExistingNode) {
dstDirFilePath = ((NonExistingNode) dest).getDirFilePath();
} else {
dstDirFilePath = dest.filePath;
}
// 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);
// copy dirFile:
final String srcDirId = getDirectoryId();
if (srcDirId == null) {
throw new DavException(DavServletResponse.SC_NOT_FOUND);
}
final String dstDirId = UUID.randomUUID().toString();
try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8)));
}
// copy actual dir:
if (!shallow) {
copyDirectoryContents(srcDirId, dstDirId);
} else {
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
Files.createDirectories(dstDirPath);
}
}
private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException {
final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId);
final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId);
Files.createDirectories(dstDirPath);
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER);
for (final Path srcChildPath : directoryStream) {
final String childName = srcChildPath.getFileName().toString();
final Path dstChildPath = dstDirPath.resolve(childName);
if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) {
try {
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
} else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) {
final String srcSubdirId;
try (final FileChannel c = FileChannel.open(srcChildPath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
srcSubdirId = new String(buffer.array(), StandardCharsets.UTF_8);
}
final String dstSubdirId = UUID.randomUUID().toString();
try (final FileChannel c = FileChannel.open(dstChildPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC);
final FileLock lock = c.lock()) {
c.write(ByteBuffer.wrap(dstSubdirId.getBytes(StandardCharsets.UTF_8)));
}
copyDirectoryContents(srcSubdirId, dstSubdirId);
}
}
}
@@ -231,20 +355,19 @@ class EncryptedDir extends AbstractEncryptedNode {
// do nothing
}
@Override
@Deprecated
protected void determineProperties() {
final Path path = locator.getEncryptedDirectoryPath();
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
if (Files.exists(path)) {
try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
try {
if (Files.exists(filePath)) {
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
// don't add any further properties
}
} catch (IOException e) {
LOG.error("Error determining metadata " + filePath, e);
// don't add any further properties
}
}
@@ -21,7 +21,7 @@ import java.nio.file.attribute.BasicFileAttributes;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavServletResponse;
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;
@@ -43,14 +43,13 @@ class EncryptedFile extends AbstractEncryptedNode {
protected final CryptoWarningHandler cryptoWarningHandler;
public EncryptedFile(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) {
super(factory, locator, session, lockManager, cryptor);
public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) {
super(factory, locator, session, lockManager, cryptor, filePath);
if (filePath == null) {
throw new IllegalArgumentException("filePath must not be null");
}
this.cryptoWarningHandler = cryptoWarningHandler;
}
@Override
protected Path getPhysicalPath() {
return locator.getEncryptedFilePath();
this.determineProperties();
}
@Override
@@ -75,11 +74,10 @@ class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = locator.getEncryptedFilePath();
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
if (Files.isRegularFile(filePath)) {
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
if (contentLength != null) {
outputContext.setContentLength(contentLength);
@@ -92,20 +90,19 @@ class EncryptedFile extends AbstractEncryptedNode {
} catch (MacAuthenticationFailedException e) {
cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
throw new IOException("Error decrypting file " + filePath.toString(), e);
}
}
}
@Override
@Deprecated
protected void determineProperties() {
final Path path = locator.getEncryptedFilePath();
if (Files.exists(path)) {
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
if (Files.isRegularFile(filePath)) {
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long contentLength = cryptor.decryptedContentLength(channel);
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
} catch (IOException e) {
LOG.error("Error reading filesize " + path.toString(), e);
LOG.error("Error reading filesize " + filePath.toString(), e);
throw new IORuntimeException(e);
} catch (MacAuthenticationFailedException e) {
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
@@ -113,12 +110,12 @@ class EncryptedFile extends AbstractEncryptedNode {
}
try {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
LOG.error("Error determining metadata " + filePath.toString(), e);
throw new IORuntimeException(e);
}
}
@@ -126,37 +123,35 @@ 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());
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getFilePath();
} else {
dstPath = dest.filePath;
}
// move:
try {
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(srcPath, dstPath, 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());
final Path srcPath = filePath;
final Path dstPath;
if (dest instanceof NonExistingNode) {
dstPath = ((NonExistingNode) dest).getFilePath();
} else {
dstPath = dest.filePath;
}
// copy:
try {
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING);
}
}
@@ -55,9 +55,9 @@ class EncryptedFilePart extends EncryptedFile {
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
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);
public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
ExecutorService backgroundTaskExecutor, Path filePath) {
super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
@@ -125,25 +125,23 @@ class EncryptedFilePart extends EncryptedFile {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = locator.getEncryptedFilePath();
if (Files.isRegularFile(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
if (outputContext.hasStream()) {
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + path.toString(), e);
assert Files.isRegularFile(filePath);
outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis());
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
if (outputContext.hasStream()) {
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (DecryptFailedException e) {
throw new IOException("Error decrypting file " + filePath.toString(), e);
}
}
@@ -153,9 +151,9 @@ class EncryptedFilePart extends EncryptedFile {
private class MacAuthenticationJob implements Runnable {
private final CryptoLocator locator;
private final DavResourceLocator locator;
public MacAuthenticationJob(final CryptoLocator locator) {
public MacAuthenticationJob(final DavResourceLocator locator) {
if (locator == null) {
throw new IllegalArgumentException("locator must not be null.");
}
@@ -164,18 +162,16 @@ class EncryptedFilePart extends EncryptedFile {
@Override
public void run() {
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);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + path.toString(), e);
assert Files.isRegularFile(filePath);
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
final boolean authentic = cryptor.isAuthentic(channel);
if (!authentic) {
cryptoWarningHandler.macAuthFailed(locator.getResourcePath());
}
} catch (ClosedByInterruptException ex) {
LOG.debug("Couldn't finish MAC verification due to interruption of worker thread.");
} catch (IOException e) {
LOG.error("IOException during MAC verification of " + filePath.toString(), e);
}
}
@@ -6,39 +6,49 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.aes256;
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.regex.Pattern;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
import org.apache.commons.lang3.StringUtils;
interface FileNamingConventions {
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
/**
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
* Parent folder path uses up to 58 chars (sha256 -&gt; 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.<br/>
* 128 chars would be enought for up to 80 plaintext chars. Also we need up to 8 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
* 128 chars would be enought for up to 80 plaintext chars. Also we need up to 9 chars for our file extension. So lets use {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT}.
*/
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 136;
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 137;
/**
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
* Dummy file, on which file attributes can be stored for the root directory.
*/
String BASIC_FILE_EXT = ".aes";
String ROOT_FILE = "root";
/**
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
* For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_NAME_FILE_EXT = ".lng.aes";
String DIR_EXT = ".dir";
/**
* For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_DIR_EXT = ".lng.dir";
/**
* For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String FILE_EXT = ".file";
/**
* For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
*/
String LONG_FILE_EXT = ".lng.file";
/**
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
@@ -56,11 +66,17 @@ interface FileNamingConventions {
@Override
public boolean matches(Path path) {
final String filename = path.getFileName().toString();
if (StringUtils.endsWithIgnoreCase(filename, LONG_NAME_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_NAME_FILE_EXT);
if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT);
return LONG_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, BASIC_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, BASIC_FILE_EXT);
} else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT);
return BASIC_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT);
return LONG_NAME_PATTERN.matcher(basename).matches();
} else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT);
return BASIC_NAME_PATTERN.matcher(basename).matches();
} else {
return false;
@@ -69,4 +85,14 @@ interface FileNamingConventions {
};
/**
* Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}.
*/
Filter<Path> DIRECTORY_CONTENT_FILTER = new Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return ENCRYPTED_FILE_MATCHER.matches(entry);
}
};
}
@@ -0,0 +1,117 @@
package org.cryptomator.webdav.jackrabbit;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import com.fasterxml.jackson.databind.ObjectMapper;
class FilenameTranslator implements FileNamingConventions {
private final Cryptor cryptor;
private final Path dataRoot;
private final Path metadataRoot;
private final ObjectMapper objectMapper = new ObjectMapper();
public FilenameTranslator(Cryptor cryptor, Path vaultRoot) {
this.cryptor = cryptor;
this.dataRoot = vaultRoot.resolve("d");
this.metadataRoot = vaultRoot.resolve("m");
}
/* file and directory name en/decryption */
public Path getEncryptedDirectoryPath(String directoryId) {
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
return dataRoot.resolve(encrypted);
}
public String getEncryptedFilename(String cleartextFilename) throws IOException {
return getEncryptedFilename(cleartextFilename, FILE_EXT, LONG_FILE_EXT);
}
public String getEncryptedDirName(String cleartextDirName) throws IOException {
return getEncryptedFilename(cleartextDirName, DIR_EXT, LONG_DIR_EXT);
}
/**
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
* This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
* <br/>
* For filenames longer than this limit we use a metadata file containing the full encrypted paths. For the actual filename a unique alternative is created by concatenating the metadata filename
* and a unique id.
*/
private String getEncryptedFilename(String cleartextFilename, String basicExt, String longExt) throws IOException {
final String ivAndCiphertext = cryptor.encryptFilename(cleartextFilename);
if (ivAndCiphertext.length() + basicExt.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
final String longFilename = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + longExt;
this.writeMetadata(metadataGroup, metadata);
return longFilename;
} else {
return ivAndCiphertext + basicExt;
}
}
public String getCleartextFilename(String encryptedFilename) throws DecryptFailedException, IOException {
final String ciphertext;
if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_FILE_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_FILE_EXT);
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, FILE_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, FILE_EXT);
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_DIR_EXT)) {
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_DIR_EXT);
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, DIR_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, DIR_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + encryptedFilename);
}
return cryptor.decryptFilename(ciphertext);
}
/* Long name metadata files */
private void writeMetadata(String metadataGroup, LongFilenameMetadata metadata) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
Files.createDirectories(metadataDir);
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
byte[] bytes = objectMapper.writeValueAsBytes(metadata);
c.write(ByteBuffer.wrap(bytes));
}
}
private LongFilenameMetadata readMetadata(String metadataGroup) throws IOException {
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
if (!Files.isReadable(metadataFile)) {
return new LongFilenameMetadata();
} else {
try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
c.read(buffer);
return objectMapper.readValue(buffer.array(), LongFilenameMetadata.class);
}
}
}
}
@@ -6,7 +6,7 @@
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto.aes256;
package org.cryptomator.webdav.jackrabbit;
import java.io.Serializable;
import java.util.UUID;
@@ -14,21 +14,23 @@ import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceIterator;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.InputContext;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.apache.jackrabbit.webdav.property.DavProperty;
import org.cryptomator.crypto.Cryptor;
class NonExistingNode extends AbstractEncryptedNode {
public NonExistingNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
}
private final Path filePath;
private final Path dirFilePath;
@Override
protected Path getPhysicalPath() {
throw new UnsupportedOperationException("Resource doesn't exist.");
public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) {
super(factory, locator, session, lockManager, cryptor, null);
this.filePath = filePath;
this.dirFilePath = dirFilePath;
}
@Override
@@ -66,11 +68,6 @@ class NonExistingNode extends AbstractEncryptedNode {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
protected void determineProperties() {
// do nothing.
}
@Override
public void move(AbstractEncryptedNode destination) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
@@ -81,4 +78,17 @@ class NonExistingNode extends AbstractEncryptedNode {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
@Override
public void setProperty(DavProperty<?> property) throws DavException {
throw new UnsupportedOperationException("Resource doesn't exist.");
}
public Path getFilePath() {
return filePath;
}
public Path getDirFilePath() {
return dirFilePath;
}
}
@@ -1,28 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.webdav.jackrabbit;
import java.nio.file.FileSystems;
import java.nio.file.Path;
final class ResourcePathUtils {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");
}
public static Path getPhysicalFilePath(CryptoLocator locator) {
return FileSystems.getDefault().getPath(locator.getRepositoryPath());
}
public static Path getPhysicalDirectoryPath(CryptoLocator locator) {
return FileSystems.getDefault().getPath(locator.getDirectoryPath());
}
}
@@ -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 CryptoLocatorFactory(fsRoot, cryptor);
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor);
davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath()); // CryptoLocatorFactory(fsRoot, cryptor);
davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot);
}
@Override
@@ -67,6 +67,17 @@ public class WebDavServlet extends AbstractWebdavServlet {
}
}
// @Override
// protected void doMkCol(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
// if (resource instanceof EncryptedDirDuringCreation) {
// EncryptedDirDuringCreation dir = (EncryptedDirDuringCreation) resource;
// dir.doCreate();
// response.setStatus(DavServletResponse.SC_CREATED);
// } else {
//
// }
// }
@Override
protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) {
return !resource.exists() || request.matchesIfHeader(resource);
@@ -15,15 +15,12 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.UUID;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -40,10 +37,8 @@ import javax.security.auth.Destroyable;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.output.NullOutputStream;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.crypto.generators.SCrypt;
import org.cryptomator.crypto.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;
@@ -55,10 +50,9 @@ import org.cryptomator.crypto.exceptions.WrongPasswordException;
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, FileNamingConventions {
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
/**
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded
@@ -288,79 +282,28 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
}
@Override
public String encryptDirectoryPath(String cleartextPath, String nativePathSep) {
final byte[] cleartextBytes = cleartextPath.getBytes(StandardCharsets.UTF_8);
public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) {
final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8);
byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
final byte[] hashed = sha256().digest(encryptedBytes);
final String encryptedThenHashedPath = ENCRYPTED_FILENAME_CODEC.encodeAsString(hashed);
return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2);
}
/**
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
* This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
* <br/>
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No cryptographically secure hash is needed here. We just want an uniform
* distribution for better load balancing. All encrypted filenames with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
* alternative names are stored.<br/>
* <br/>
* These alternative names consist of the checksum, a unique id and a special file extension defined in {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
*/
@Override
public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
public String encryptFilename(String cleartextName) {
final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
// encrypt:
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 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;
}
return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
}
@Override
public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws DecryptFailedException, IOException {
final String ciphertext;
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 LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup);
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
} else if (ciphertextName.endsWith(BASIC_FILE_EXT)) {
ciphertext = StringUtils.removeEndIgnoreCase(ciphertextName, BASIC_FILE_EXT);
} else {
throw new IllegalArgumentException("Unsupported path component: " + ciphertextName);
}
// decrypt:
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertextName);
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(primaryMasterKey, hMacMasterKey, encryptedBytes);
return new String(cleartextBytes, StandardCharsets.UTF_8);
}
private LongFilenameMetadata getMetadata(CryptorMetadataSupport ioSupport, String metadataGroup) throws IOException {
final byte[] fileContent = ioSupport.readMetadata(metadataGroup);
if (fileContent == null) {
return new LongFilenameMetadata();
} else {
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
}
}
private void storeMetadata(CryptorMetadataSupport ioSupport, String metadataGroup, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
ioSupport.writeMetadata(metadataGroup, objectMapper.writeValueAsBytes(metadata));
}
@Override
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
// read header:
@@ -616,14 +559,4 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
return plaintextSize;
}
@Override
public Filter<Path> getPayloadFilesFilter() {
return new Filter<Path>() {
@Override
public boolean accept(Path entry) throws IOException {
return ENCRYPTED_FILE_MATCHER.matches(entry);
}
};
}
}
@@ -8,6 +8,9 @@
******************************************************************************/
package org.cryptomator.crypto.aes256;
import org.apache.commons.codec.binary.Base32;
import org.apache.commons.codec.binary.BaseNCodec;
interface AesCryptographicConfiguration {
/**
@@ -78,4 +81,9 @@ interface AesCryptographicConfiguration {
*/
int AES_BLOCK_LENGTH = 16;
/**
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
*/
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
}
@@ -15,13 +15,10 @@ import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.SeekableByteChannel;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.security.auth.DestroyFailedException;
import org.apache.commons.io.IOUtils;
import org.cryptomator.crypto.CryptorMetadataSupport;
import org.cryptomator.crypto.exceptions.DecryptFailedException;
import org.cryptomator.crypto.exceptions.EncryptFailedException;
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
@@ -210,7 +207,6 @@ public class Aes256CryptorTest {
@Test
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
final CryptorMetadataSupport ioSupportMock = new CryptoIOSupportMock();
final Aes256Cryptor cryptor = new Aes256Cryptor();
// directory paths
@@ -222,35 +218,19 @@ public class Aes256CryptorTest {
// long file names
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt";
final String encryptedPath2a = cryptor.encryptFilename(originalPath2, ioSupportMock);
final String encryptedPath2b = cryptor.encryptFilename(originalPath2, ioSupportMock);
final String encryptedPath2a = cryptor.encryptFilename(originalPath2);
final String encryptedPath2b = cryptor.encryptFilename(originalPath2);
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a, ioSupportMock);
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a);
Assert.assertEquals(originalPath2, decryptedPath2);
// block size length file names
final String originalPath3 = "aaaabbbbccccdddd";
final String encryptedPath3a = cryptor.encryptFilename(originalPath3, ioSupportMock);
final String encryptedPath3b = cryptor.encryptFilename(originalPath3, ioSupportMock);
final String encryptedPath3a = cryptor.encryptFilename(originalPath3);
final String encryptedPath3b = cryptor.encryptFilename(originalPath3);
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a, ioSupportMock);
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a);
Assert.assertEquals(originalPath3, decryptedPath3);
}
private static class CryptoIOSupportMock implements CryptorMetadataSupport {
private final Map<String, byte[]> map = new HashMap<>();
@Override
public void writeMetadata(String metadataGroup, byte[] encryptedMetadata) {
map.put(metadataGroup, encryptedMetadata);
}
@Override
public byte[] readMetadata(String metadataGroup) {
return map.get(metadataGroup);
}
}
}
@@ -4,8 +4,6 @@ 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;
@@ -40,13 +38,13 @@ public class AbstractCryptorDecorator implements Cryptor {
}
@Override
public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
return cryptor.encryptFilename(cleartextName, ioSupport);
public String encryptFilename(String cleartextName) {
return cryptor.encryptFilename(cleartextName);
}
@Override
public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException {
return cryptor.decryptFilename(ciphertextName, ioSupport);
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
return cryptor.decryptFilename(ciphertextName);
}
@Override
@@ -74,11 +72,6 @@ public class AbstractCryptorDecorator implements Cryptor {
return cryptor.encryptFile(plaintextFile, encryptedFile);
}
@Override
public Filter<Path> getPayloadFilesFilter() {
return cryptor.getPayloadFilesFilter();
}
@Override
public void destroy() throws DestroyFailedException {
cryptor.destroy();
@@ -12,8 +12,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.DirectoryStream.Filter;
import java.nio.file.Path;
import javax.security.auth.Destroyable;
@@ -47,32 +45,28 @@ public interface Cryptor extends Destroyable {
/**
* 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), whose path components are separated by '/'
* @param cleartextDirectoryId 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.
* @return Encrypted path.
*/
String encryptDirectoryPath(String cleartextPath, String nativePathSep);
String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep);
/**
* Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir.
*
* @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 encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException;
String encryptFilename(String cleartextName);
/**
* 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;
String decryptFilename(String ciphertextName) throws DecryptFailedException;
/**
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
@@ -105,9 +99,4 @@ public interface Cryptor extends Destroyable {
*/
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
/**
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting metadata file of the {@link Cryptor}.
*/
Filter<Path> getPayloadFilesFilter();
}
@@ -1,31 +0,0 @@
/*******************************************************************************
* Copyright (c) 2014 Sebastian Stenzel
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
******************************************************************************/
package org.cryptomator.crypto;
import java.io.IOException;
/**
* Methods that may be called by the Cryptor when accessing a path.
*/
public interface CryptorMetadataSupport {
/**
* Persists encryptedMetadata in a metadata group.
*
* @param metadataFilename File relative to
* @throws IOException
*/
void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException;
/**
* @return Previously written metadata stored in the given metadata group or <code>null</code> if no such group exists.
*/
byte[] readMetadata(String metadataGroup) throws IOException;
}
@@ -1,6 +1,5 @@
package org.cryptomator.crypto;
import java.io.IOException;
import java.util.Map;
import org.apache.commons.collections4.BidiMap;
@@ -38,22 +37,22 @@ public class PathCachingCryptorDecorator extends AbstractCryptorDecorator {
}
@Override
public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
public String encryptFilename(String cleartextName) {
if (nameCache.containsKey(cleartextName)) {
return nameCache.get(cleartextName);
} else {
final String ciphertextName = cryptor.encryptFilename(cleartextName, ioSupport);
final String ciphertextName = cryptor.encryptFilename(cleartextName);
nameCache.put(cleartextName, ciphertextName);
return ciphertextName;
}
}
@Override
public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException {
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
if (nameCache.containsValue(ciphertextName)) {
return nameCache.getKey(ciphertextName);
} else {
final String cleartextName = cryptor.decryptFilename(ciphertextName, ioSupport);
final String cleartextName = cryptor.decryptFilename(ciphertextName);
nameCache.put(cleartextName, ciphertextName);
return ciphertextName;
}