From 4cf872f9167c05e2b61e586140639c9376233044 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 14 May 2015 07:37:56 +0200 Subject: [PATCH 1/4] directory moving --- .../webdav/jackrabbit/CryptoLocator.java | 66 +++++++++++++++++-- .../jackrabbit/CryptoResourceFactory.java | 23 ++++++- .../webdav/jackrabbit/EncryptedDir.java | 41 ++++++++---- .../webdav/jackrabbit/ResourcePathUtils.java | 28 -------- 4 files changed, 108 insertions(+), 50 deletions(-) delete mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java 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 index 12cf9a761..3f998870a 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java @@ -1,10 +1,21 @@ 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.charset.StandardCharsets; import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; 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.DavResourceLocator; @@ -69,7 +80,7 @@ class CryptoLocator implements DavResourceLocator { @Override public String getRepositoryPath() { if (isRootLocation()) { - return getDirectoryPath(); + return getEncryptedRootDirectoryPath(); } try { final String plaintextPath = getResourcePath(); @@ -88,18 +99,61 @@ class CryptoLocator implements DavResourceLocator { * 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). + * @throws IOException */ - public String getDirectoryPath() { - final String ciphertextPath = cryptor.encryptDirectoryPath(getResourcePath(), FileSystems.getDefault().getSeparator()); - return rootPath.resolve(ciphertextPath).toString(); + public String getDirectoryPath(boolean create) throws IOException { + if (isRootLocation()) { + return getEncryptedRootDirectoryPath(); + } else { + final List cleartextPathComponents = Arrays.asList(StringUtils.split(getResourcePath(), "/")); + return getEncryptedDirectoryPath(rootPath, cleartextPathComponents, false).toString(); + } + } + + private Path getEncryptedDirectoryPath(Path encryptedParentDirectoryPath, List cleartextSubPathComponents, boolean create) throws IOException { + if (cleartextSubPathComponents.size() == 0) { + return encryptedParentDirectoryPath; + } else { + final String nextPathComponent = cleartextSubPathComponents.get(0); + final List remainingSubPathComponents = cleartextSubPathComponents.subList(1, cleartextSubPathComponents.size()); + final String fullEncryptedSubdirectoryPath = getEncryptedDirectoryPath(encryptedParentDirectoryPath, nextPathComponent, create); + return getEncryptedDirectoryPath(rootPath.resolve(fullEncryptedSubdirectoryPath), remainingSubPathComponents, create); + } + } + + private String getEncryptedDirectoryPath(Path encryptedParentDirectoryPath, String cleartextDirectoryName, boolean create) throws IOException { + final String encryptedDirectoryName = this.cryptor.encryptFilename(cleartextDirectoryName, this.factory); + // TODO file extensions... + final Path directoryFile = encryptedParentDirectoryPath.resolve(encryptedDirectoryName + ".dir"); + if (Files.exists(directoryFile)) { + try (final FileChannel c = FileChannel.open(directoryFile, 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); + final String directoryUuid = buffer.asCharBuffer().toString(); + return this.cryptor.encryptDirectoryPath(directoryUuid, FileSystems.getDefault().getSeparator()); + } + } else if (create) { + try (final FileChannel c = FileChannel.open(directoryFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { + final String directoryUuid = UUID.randomUUID().toString(); + final ByteBuffer buf = ByteBuffer.wrap(directoryUuid.getBytes(StandardCharsets.UTF_8)); + c.write(buf); + return this.cryptor.encryptDirectoryPath(directoryUuid, FileSystems.getDefault().getSeparator()); + } + } else { + throw new FileNotFoundException(directoryFile.toString()); + } + } + + private String getEncryptedRootDirectoryPath() { + return this.cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator()); } public Path getEncryptedFilePath() { return FileSystems.getDefault().getPath(getRepositoryPath()); } - public Path getEncryptedDirectoryPath() { - return FileSystems.getDefault().getPath(getDirectoryPath()); + public Path getEncryptedDirectoryPath(boolean create) throws IOException { + return FileSystems.getDefault().getPath(getDirectoryPath(create)); } /* other stuff */ 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 index 6a965f5e4..990c9029d 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java @@ -1,5 +1,7 @@ package org.cryptomator.webdav.jackrabbit; +import java.io.FileNotFoundException; +import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; @@ -17,6 +19,7 @@ import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.lock.LockManager; import org.apache.jackrabbit.webdav.lock.SimpleLockManager; import org.cryptomator.crypto.Cryptor; +import org.cryptomator.webdav.exceptions.IORuntimeException; import org.eclipse.jetty.http.HttpHeader; public class CryptoResourceFactory implements DavResourceFactory { @@ -52,10 +55,17 @@ public class CryptoResourceFactory implements DavResourceFactory { 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()); + Path dirpath = null; + try { + dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath(DavMethods.METHOD_MKCOL.equals(request.getMethod()))); + } catch (FileNotFoundException e) { + // no-op + } catch (IOException e) { + throw new IORuntimeException(e); + } final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); - if (Files.isDirectory(dirpath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) { + if (Files.isDirectory(dirpath)) { return createDirectory(locator, request.getDavSession()); } else if (Files.isRegularFile(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { response.setStatus(HttpStatus.SC_PARTIAL_CONTENT); @@ -69,7 +79,14 @@ public class CryptoResourceFactory implements DavResourceFactory { 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()); + Path dirpath = null; + try { + dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath(false)); + } catch (FileNotFoundException e) { + // no-op + } catch (IOException e) { + throw new IORuntimeException(e); + } if (Files.isDirectory(dirpath)) { return createDirectory(locator, session); 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 aec5a60e8..7fbeb6202 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 @@ -8,6 +8,7 @@ ******************************************************************************/ package org.cryptomator.webdav.jackrabbit; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.SeekableByteChannel; import java.nio.file.AtomicMoveNotSupportedException; @@ -53,7 +54,11 @@ class EncryptedDir extends AbstractEncryptedNode { @Override protected Path getPhysicalPath() { - return locator.getEncryptedDirectoryPath(); + try { + return locator.getEncryptedDirectoryPath(false); + } catch (IOException e) { + throw new IORuntimeException(e); + } } @Override @@ -63,13 +68,17 @@ class EncryptedDir extends AbstractEncryptedNode { @Override public boolean exists() { - return Files.isDirectory(locator.getEncryptedDirectoryPath()); + try { + return Files.isDirectory(locator.getEncryptedDirectoryPath(false)); + } catch (IOException e) { + return false; + } } @Override public long getModificationTime() { try { - return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath()).toMillis(); + return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath(false)).toMillis(); } catch (IOException e) { return -1; } @@ -94,8 +103,7 @@ class EncryptedDir extends AbstractEncryptedNode { private void addMemberDir(CryptoLocator childLocator, InputContext inputContext) throws DavException { try { - Files.createDirectories(childLocator.getEncryptedFilePath()); - Files.createDirectories(childLocator.getEncryptedDirectoryPath()); + Files.createDirectories(childLocator.getEncryptedDirectoryPath(true)); } catch (SecurityException e) { throw new DavException(DavServletResponse.SC_FORBIDDEN, e); } catch (IOException e) { @@ -126,7 +134,7 @@ class EncryptedDir extends AbstractEncryptedNode { @Override public DavResourceIterator getMembers() { try { - final DirectoryStream directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(), cryptor.getPayloadFilesFilter()); + final DirectoryStream directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(false), cryptor.getPayloadFilesFilter()); final List result = new ArrayList<>(); for (final Path childPath : directoryStream) { @@ -162,11 +170,13 @@ class EncryptedDir extends AbstractEncryptedNode { private void removeMember(AbstractEncryptedNode member) { try { + Files.deleteIfExists(member.getLocator().getEncryptedFilePath()); if (member.isCollection()) { member.getMembers().forEachRemaining(m -> securelyRemoveMemberOfCollection(member, m)); - Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath()); + Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath(false)); } - Files.deleteIfExists(member.getLocator().getEncryptedFilePath()); + } catch (FileNotFoundException e) { + // no-op } catch (IOException e) { throw new IORuntimeException(e); } @@ -182,8 +192,8 @@ class EncryptedDir extends AbstractEncryptedNode { @Override public void move(AbstractEncryptedNode dest) throws DavException, IOException { - final Path srcDir = this.locator.getEncryptedDirectoryPath(); - final Path dstDir = dest.locator.getEncryptedDirectoryPath(); + final Path srcDir = this.locator.getEncryptedDirectoryPath(false); + final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); final Path srcFile = this.locator.getEncryptedFilePath(); final Path dstFile = dest.locator.getEncryptedFilePath(); @@ -205,8 +215,8 @@ class EncryptedDir extends AbstractEncryptedNode { @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 srcDir = this.locator.getEncryptedDirectoryPath(false); + final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); final Path srcFile = this.locator.getEncryptedFilePath(); final Path dstFile = dest.locator.getEncryptedFilePath(); @@ -233,7 +243,12 @@ class EncryptedDir extends AbstractEncryptedNode { @Override protected void determineProperties() { - final Path path = locator.getEncryptedDirectoryPath(); + Path path; + try { + path = locator.getEncryptedDirectoryPath(false); + } catch (IOException e) { + throw new IORuntimeException(e); + } properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); if (Files.exists(path)) { 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 deleted file mode 100644 index 6f77bff1f..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/ResourcePathUtils.java +++ /dev/null @@ -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()); - } - -} From be369b480bf3feba3f71c3f77a2eb4ae79812cfd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 14 May 2015 21:48:02 +0200 Subject: [PATCH 2/4] some more destruction... --- .../jackrabbit/AbstractEncryptedNode.java | 7 +- .../jackrabbit/CleartextLocatorFactory.java | 112 +++++++++++ .../jackrabbit/CryptoResourceFactory.java | 179 +++++++++++++----- .../webdav/jackrabbit/EncryptedDir.java | 176 ++++++++--------- .../webdav/jackrabbit/EncryptedFile.java | 92 ++++----- .../webdav/jackrabbit/EncryptedFilePart.java | 64 +++---- .../webdav/jackrabbit/NonExistingNode.java | 3 +- .../webdav/jackrabbit/WebDavServlet.java | 4 +- .../crypto/aes256/Aes256Cryptor.java | 4 +- .../java/org/cryptomator/crypto/Cryptor.java | 4 +- 10 files changed, 411 insertions(+), 234 deletions(-) create mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CleartextLocatorFactory.java 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 1715ed810..d22e7a3bb 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 @@ -44,20 +44,19 @@ 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 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) { this.factory = factory; this.locator = locator; this.session = session; this.lockManager = lockManager; this.cryptor = cryptor; this.properties = new DavPropertySet(); - this.determineProperties(); } protected abstract Path getPhysicalPath(); @@ -89,7 +88,7 @@ abstract class AbstractEncryptedNode implements DavResource { } @Override - public CryptoLocator getLocator() { + public DavResourceLocator getLocator() { return locator; } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CleartextLocatorFactory.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CleartextLocatorFactory.java new file mode 100644 index 000000000..dc313c8d2 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CleartextLocatorFactory.java @@ -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 + ")"; + } + + } + +} 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 index 990c9029d..44e85364f 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java @@ -1,13 +1,19 @@ 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.charset.StandardCharsets; 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; @@ -18,99 +24,168 @@ import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.lock.LockManager; import org.apache.jackrabbit.webdav.lock.SimpleLockManager; +import org.apache.logging.log4j.util.Strings; import org.cryptomator.crypto.Cryptor; -import org.cryptomator.webdav.exceptions.IORuntimeException; +import org.cryptomator.crypto.CryptorMetadataSupport; import org.eclipse.jetty.http.HttpHeader; -public class CryptoResourceFactory implements DavResourceFactory { +public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadataSupport { private final LockManager lockManager = new SimpleLockManager(); private final Cryptor cryptor; private final CryptoWarningHandler cryptoWarningHandler; private final ExecutorService backgroundTaskExecutor; + private final Path dataRoot; + private final Path metadataRoot; - CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) { + CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String fsRoot) { this.cryptor = cryptor; this.cryptoWarningHandler = cryptoWarningHandler; this.backgroundTaskExecutor = backgroundTaskExecutor; + this.dataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("d"); + this.metadataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("m"); } @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()); + if (DavMethods.METHOD_MKCOL.equals(request.getMethod()) || locator.isRootLocation()) { + final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); + return createDirectory(locator, request.getDavSession(), dirpath); } - } - @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()); - Path dirpath = null; - try { - dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath(DavMethods.METHOD_MKCOL.equals(request.getMethod()))); - } catch (FileNotFoundException e) { - // no-op - } catch (IOException e) { - throw new IORuntimeException(e); - } + final Path filepath = getEncryptedFilePath(locator.getResourcePath()); final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); - - if (Files.isDirectory(dirpath)) { - return createDirectory(locator, request.getDavSession()); + if (filepath.getFileName().toString().endsWith(".dir")) { + final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); + return createDirectory(locator, request.getDavSession(), dirpath); } 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); + return createFilePart(locator, request.getDavSession(), request, filepath); } else if (Files.isRegularFile(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) { - return createFile(locator, request.getDavSession()); + return createFile(locator, request.getDavSession(), filepath); } else { return createNonExisting(locator, request.getDavSession()); } } - private DavResource createResource(CryptoLocator locator, DavSession session) throws DavException { - final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath()); - Path dirpath = null; - try { - dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath(false)); - } catch (FileNotFoundException e) { - // no-op - } catch (IOException e) { - throw new IORuntimeException(e); + @Override + public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { + if (locator.isRootLocation()) { + final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); + return createDirectory(locator, session, dirpath); } - if (Files.isDirectory(dirpath)) { - return createDirectory(locator, session); + final Path filepath = getEncryptedFilePath(locator.getResourcePath()); + if (filepath.getFileName().toString().endsWith(".dir")) { + final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); + return createDirectory(locator, session, dirpath); } else if (Files.isRegularFile(filepath)) { - return createFile(locator, session); + return createFile(locator, session, filepath); } 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); + /** + * @return Absolute file path for a given cleartext file resourcePath. + * @throws IOException + */ + Path getEncryptedFilePath(String relativeCleartextPath) throws DavException { + final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); + final Path parent = getEncryptedDirectoryPath(parentCleartextPath); + final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); + try { + final String encryptedFilename = cryptor.encryptFilename(cleartextFilename, this); + return parent.resolve(encryptedFilename); + } 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); + /** + * @return Absolute directory path for a given cleartext directory resourcePath. + * @throws IOException + */ + Path getEncryptedDirectoryPath(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 = getEncryptedDirectoryPath(parentCleartextPath); + final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); + final String encryptedFilename = cryptor.encryptFilename(cleartextFilename, CryptoResourceFactory.this); + 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 EncryptedDir createDirectory(CryptoLocator locator, DavSession session) { - return new EncryptedDir(this, locator, session, lockManager, cryptor); + private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) { + return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath); } - private NonExistingNode createNonExisting(CryptoLocator locator, DavSession session) { + private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) { + return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath); + } + + private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path dirPath) { + return new EncryptedDir(this, locator, session, lockManager, cryptor, dirPath); + } + + private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) { return new NonExistingNode(this, locator, session, lockManager, cryptor); } + /* 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(); + } + } + + @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)); + writeAllBytesAtomically(metadataFile, 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; + } else { + return readAllBytesAtomically(metadataFile); + } + } + } 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 7fbeb6202..6117bbb96 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 @@ -11,16 +11,16 @@ package org.cryptomator.webdav.jackrabbit; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.channels.SeekableByteChannel; -import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; @@ -37,9 +37,9 @@ 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.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -47,18 +47,20 @@ import org.slf4j.LoggerFactory; class EncryptedDir extends AbstractEncryptedNode { private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class); + private final Path directoryPath; - public EncryptedDir(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { + public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path directoryPath) { super(factory, locator, session, lockManager, cryptor); + if (directoryPath == null || !Files.isDirectory(directoryPath)) { + throw new IllegalArgumentException("directoryPath must be an existing directory, but was " + directoryPath); + } + this.directoryPath = directoryPath; + determineProperties(); } @Override protected Path getPhysicalPath() { - try { - return locator.getEncryptedDirectoryPath(false); - } catch (IOException e) { - throw new IORuntimeException(e); - } + return directoryPath; } @Override @@ -68,17 +70,14 @@ class EncryptedDir extends AbstractEncryptedNode { @Override public boolean exists() { - try { - return Files.isDirectory(locator.getEncryptedDirectoryPath(false)); - } catch (IOException e) { - return false; - } + assert Files.isDirectory(directoryPath); + return true; } @Override public long getModificationTime() { try { - return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath(false)).toMillis(); + return Files.getLastModifiedTime(directoryPath).toMillis(); } catch (IOException e) { return -1; } @@ -101,19 +100,18 @@ class EncryptedDir extends AbstractEncryptedNode { } } - private void addMemberDir(CryptoLocator childLocator, InputContext inputContext) throws DavException { + private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException { try { - Files.createDirectories(childLocator.getEncryptedDirectoryPath(true)); + // the following invokation will create nonexisting directories: + factory.getEncryptedDirectoryPath(childLocator.getResourcePath()); } catch (SecurityException e) { throw new DavException(DavServletResponse.SC_FORBIDDEN, e); - } catch (IOException e) { - LOG.error("Failed to create subdirectory.", e); - throw new IORuntimeException(e); } } - private void addMemberFile(CryptoLocator childLocator, InputContext inputContext) throws DavException { - try (final SeekableByteChannel channel = Files.newByteChannel(childLocator.getEncryptedFilePath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException { + final Path filePath = factory.getEncryptedFilePath(childLocator.getResourcePath()); + 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); @@ -134,17 +132,17 @@ class EncryptedDir extends AbstractEncryptedNode { @Override public DavResourceIterator getMembers() { try { - final DirectoryStream directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(false), cryptor.getPayloadFilesFilter()); + final DirectoryStream directoryStream = Files.newDirectoryStream(directoryPath, cryptor.getPayloadFilesFilter()); final List 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 String cleartextFilename = cryptor.decryptFilename(childPath.getFileName().toString(), factory); + final String cleartextFilepath = FilenameUtils.concat(getResourcePath(), cleartextFilename); + final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath); final DavResource resource = factory.createResource(childLocator, session); result.add(resource); - } catch (DecryptFailedRuntimeException e) { + } catch (DecryptFailedException e) { LOG.warn("Decryption of resource failed: " + childPath); continue; } @@ -160,7 +158,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 { @@ -168,13 +166,19 @@ class EncryptedDir extends AbstractEncryptedNode { } } - private void removeMember(AbstractEncryptedNode member) { + private void removeMember(AbstractEncryptedNode member) throws DavException { try { - Files.deleteIfExists(member.getLocator().getEncryptedFilePath()); if (member.isCollection()) { - member.getMembers().forEachRemaining(m -> securelyRemoveMemberOfCollection(member, m)); - Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath(false)); + // remove sub-members recursively before deleting own directory + for (Iterator iterator = member.getMembers(); iterator.hasNext();) { + DavResource m = iterator.next(); + member.removeMember(m); + } + final Path memberDirectoryPath = factory.getEncryptedDirectoryPath(member.getResourcePath()); + Files.deleteIfExists(memberDirectoryPath); } + final Path memberPath = factory.getEncryptedFilePath(member.getResourcePath()); + Files.deleteIfExists(memberPath); } catch (FileNotFoundException e) { // no-op } catch (IOException e) { @@ -182,58 +186,52 @@ class EncryptedDir extends AbstractEncryptedNode { } } - 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(false); - final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); - 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); - } + throw new UnsupportedOperationException("not yet implemented"); + // final Path srcDir = this.locator.getEncryptedDirectoryPath(false); + // final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); + // 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(false); - final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); - 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); - } + throw new UnsupportedOperationException("not yet implemented"); + // final Path srcDir = this.locator.getEncryptedDirectoryPath(false); + // final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); + // 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 @@ -243,23 +241,15 @@ class EncryptedDir extends AbstractEncryptedNode { @Override protected void determineProperties() { - Path path; - try { - path = locator.getEncryptedDirectoryPath(false); - } catch (IOException e) { - throw new IORuntimeException(e); - } properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); - if (Files.exists(path)) { - try { - final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); - properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime()))); - properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime()))); - } catch (IOException e) { - LOG.error("Error determining metadata " + path.toString(), e); - // don't add any further properties - } + try { + final BasicFileAttributes attrs = Files.readAttributes(directoryPath, BasicFileAttributes.class); + properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime()))); + properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime()))); + } catch (IOException e) { + LOG.error("Error determining metadata " + directoryPath.toString(), e); + // don't add any further properties } } 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 30a3cd160..d218db846 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,17 +11,15 @@ 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.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; @@ -42,15 +40,21 @@ class EncryptedFile extends AbstractEncryptedNode { private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class); protected final CryptoWarningHandler cryptoWarningHandler; + protected final Path filePath; - public EncryptedFile(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) { + public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) { super(factory, locator, session, lockManager, cryptor); + if (filePath == null) { + throw new IllegalArgumentException("filePath must not be null"); + } this.cryptoWarningHandler = cryptoWarningHandler; + this.filePath = filePath; + this.determineProperties(); } @Override protected Path getPhysicalPath() { - return locator.getEncryptedFilePath(); + return filePath; } @Override @@ -75,11 +79,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 +95,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 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(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 +115,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(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime()))); properties.add(new DefaultDavProperty(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,38 +128,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); - } + throw new UnsupportedOperationException("not yet implemented"); + // 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); - } + throw new UnsupportedOperationException("not yet implemented"); + // 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 697a4422b..454bda21f 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 @@ -55,9 +55,9 @@ class EncryptedFilePart extends EncryptedFile { private final Set> requestedContentRanges = new HashSet>(); - 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 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 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); } } 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 859cd943c..5a2e8e726 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 @@ -14,6 +14,7 @@ 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; @@ -22,7 +23,7 @@ import org.cryptomator.crypto.Cryptor; class NonExistingNode extends AbstractEncryptedNode { - public NonExistingNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { + public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { super(factory, locator, session, lockManager, cryptor); } 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 8b5fbc86f..608606eb0 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 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 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 b33a344bc..2ec02b1fc 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 @@ -288,8 +288,8 @@ 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); 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 bf7a5f8ba..55c637e5d 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 @@ -47,11 +47,11 @@ 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. From 0d969432c27e7852cd2dd20847926f094fd8cf52 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 15 May 2015 18:13:34 +0200 Subject: [PATCH 3/4] some more flat hierarchy fixes --- main/core/pom.xml | 6 + .../jackrabbit/AbstractEncryptedNode.java | 4 +- .../webdav/jackrabbit/CryptoLocator.java | 220 ------------------ .../jackrabbit/CryptoLocatorFactory.java | 102 -------- .../jackrabbit/CryptoResourceFactory.java | 108 +++++---- .../webdav/jackrabbit/EncryptedDir.java | 93 ++++++-- .../EncryptedDirDuringCreation.java | 114 +++++++++ .../webdav/jackrabbit/EncryptedFile.java | 2 +- .../jackrabbit}/FileNamingConventions.java | 53 +++-- .../webdav/jackrabbit/FilenameTranslator.java | 117 ++++++++++ .../jackrabbit}/LongFilenameMetadata.java | 2 +- .../webdav/jackrabbit/NonExistingNode.java | 5 - .../webdav/jackrabbit/WebDavServlet.java | 15 ++ .../crypto/aes256/Aes256Cryptor.java | 77 +----- .../aes256/AesCryptographicConfiguration.java | 8 + .../crypto/aes256/Aes256CryptorTest.java | 32 +-- .../crypto/AbstractCryptorDecorator.java | 15 +- .../java/org/cryptomator/crypto/Cryptor.java | 15 +- .../crypto/CryptorMetadataSupport.java | 31 --- .../crypto/PathCachingCryptorDecorator.java | 9 +- 20 files changed, 453 insertions(+), 575 deletions(-) delete mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java delete mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java create mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java rename main/{crypto-aes/src/main/java/org/cryptomator/crypto/aes256 => core/src/main/java/org/cryptomator/webdav/jackrabbit}/FileNamingConventions.java (59%) create mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FilenameTranslator.java rename main/{crypto-aes/src/main/java/org/cryptomator/crypto/aes256 => core/src/main/java/org/cryptomator/webdav/jackrabbit}/LongFilenameMetadata.java (97%) delete mode 100644 main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java diff --git a/main/core/pom.xml b/main/core/pom.xml index 85fd9f6a1..2a28061fd 100644 --- a/main/core/pom.xml +++ b/main/core/pom.xml @@ -64,5 +64,11 @@ org.apache.commons commons-lang3 + + + + com.fasterxml.jackson.core + jackson-databind + 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 d22e7a3bb..ead8ea246 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 @@ -111,8 +111,6 @@ abstract class AbstractEncryptedNode implements DavResource { } } - protected abstract void determineProperties(); - @Override public DavPropertyName[] getPropertyNames() { return getProperties().getPropertyNames(); @@ -182,7 +180,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); 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 deleted file mode 100644 index 3f998870a..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java +++ /dev/null @@ -1,220 +0,0 @@ -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.charset.StandardCharsets; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -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.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 getEncryptedRootDirectoryPath(); - } - 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). - * @throws IOException - */ - public String getDirectoryPath(boolean create) throws IOException { - if (isRootLocation()) { - return getEncryptedRootDirectoryPath(); - } else { - final List cleartextPathComponents = Arrays.asList(StringUtils.split(getResourcePath(), "/")); - return getEncryptedDirectoryPath(rootPath, cleartextPathComponents, false).toString(); - } - } - - private Path getEncryptedDirectoryPath(Path encryptedParentDirectoryPath, List cleartextSubPathComponents, boolean create) throws IOException { - if (cleartextSubPathComponents.size() == 0) { - return encryptedParentDirectoryPath; - } else { - final String nextPathComponent = cleartextSubPathComponents.get(0); - final List remainingSubPathComponents = cleartextSubPathComponents.subList(1, cleartextSubPathComponents.size()); - final String fullEncryptedSubdirectoryPath = getEncryptedDirectoryPath(encryptedParentDirectoryPath, nextPathComponent, create); - return getEncryptedDirectoryPath(rootPath.resolve(fullEncryptedSubdirectoryPath), remainingSubPathComponents, create); - } - } - - private String getEncryptedDirectoryPath(Path encryptedParentDirectoryPath, String cleartextDirectoryName, boolean create) throws IOException { - final String encryptedDirectoryName = this.cryptor.encryptFilename(cleartextDirectoryName, this.factory); - // TODO file extensions... - final Path directoryFile = encryptedParentDirectoryPath.resolve(encryptedDirectoryName + ".dir"); - if (Files.exists(directoryFile)) { - try (final FileChannel c = FileChannel.open(directoryFile, 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); - final String directoryUuid = buffer.asCharBuffer().toString(); - return this.cryptor.encryptDirectoryPath(directoryUuid, FileSystems.getDefault().getSeparator()); - } - } else if (create) { - try (final FileChannel c = FileChannel.open(directoryFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { - final String directoryUuid = UUID.randomUUID().toString(); - final ByteBuffer buf = ByteBuffer.wrap(directoryUuid.getBytes(StandardCharsets.UTF_8)); - c.write(buf); - return this.cryptor.encryptDirectoryPath(directoryUuid, FileSystems.getDefault().getSeparator()); - } - } else { - throw new FileNotFoundException(directoryFile.toString()); - } - } - - private String getEncryptedRootDirectoryPath() { - return this.cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator()); - } - - public Path getEncryptedFilePath() { - return FileSystems.getDefault().getPath(getRepositoryPath()); - } - - public Path getEncryptedDirectoryPath(boolean create) throws IOException { - return FileSystems.getDefault().getPath(getDirectoryPath(create)); - } - - /* 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 deleted file mode 100644 index 7e910de80..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocatorFactory.java +++ /dev/null @@ -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(); - } - } -} 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 index 44e85364f..bcffb89d6 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java @@ -26,42 +26,49 @@ import org.apache.jackrabbit.webdav.lock.LockManager; import org.apache.jackrabbit.webdav.lock.SimpleLockManager; import org.apache.logging.log4j.util.Strings; import org.cryptomator.crypto.Cryptor; -import org.cryptomator.crypto.CryptorMetadataSupport; import org.eclipse.jetty.http.HttpHeader; -public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadataSupport { +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 Path metadataRoot; + private final FilenameTranslator filenameTranslator; - CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String fsRoot) { + 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 = FileSystems.getDefault().getPath(fsRoot).resolve("d"); - this.metadataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("m"); + 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 (DavMethods.METHOD_MKCOL.equals(request.getMethod()) || locator.isRootLocation()) { - final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); + if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) { + final String parentResourcePath = FilenameUtils.getFullPathNoEndSeparator(locator.getResourcePath()); + final Path parentDirectoryPath = createEncryptedDirectoryPath(parentResourcePath); + return new EncryptedDirDuringCreation(this, locator, request.getDavSession(), lockManager, cryptor, filenameTranslator, parentDirectoryPath); + } + + if (locator.isRootLocation()) { + final Path dirpath = createEncryptedDirectoryPath(""); return createDirectory(locator, request.getDavSession(), dirpath); } final Path filepath = getEncryptedFilePath(locator.getResourcePath()); + final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath()); final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); - if (filepath.getFileName().toString().endsWith(".dir")) { - final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); - return createDirectory(locator, request.getDavSession(), dirpath); - } else if (Files.isRegularFile(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { + if (Files.exists(dirFilePath)) { + final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath()); + return createDirectory(locator, request.getDavSession(), dirPath); + } 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.isRegularFile(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) { + } else if (Files.exists(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) { return createFile(locator, request.getDavSession(), filepath); } else { return createNonExisting(locator, request.getDavSession()); @@ -71,31 +78,63 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat @Override public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { if (locator.isRootLocation()) { - final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); + final Path dirpath = createEncryptedDirectoryPath(""); return createDirectory(locator, session, dirpath); } final Path filepath = getEncryptedFilePath(locator.getResourcePath()); - if (filepath.getFileName().toString().endsWith(".dir")) { - final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath()); - return createDirectory(locator, session, dirpath); - } else if (Files.isRegularFile(filepath)) { + final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath()); + if (Files.exists(dirFilePath)) { + final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath()); + return createDirectory(locator, session, dirPath); + } else if (Files.exists(filepath)) { return createFile(locator, session, filepath); } else { return createNonExisting(locator, session); } } + DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException { + try { + final String directoryId = new String(readAllBytesAtomically(existingDirectoryFile), StandardCharsets.UTF_8); + final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator()); + final Path dirpath = dataRoot.resolve(directory); + return createDirectory(locator, session, dirpath); + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + } + + DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException { + return createFile(locator, session, existingFile); + } + + /** + * @return Absolute file path for a given cleartext file resourcePath. + * @throws IOException + */ + private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException { + final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); + final Path parent = createEncryptedDirectoryPath(parentCleartextPath); + final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); + try { + final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); + return parent.resolve(encryptedFilename); + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } + } + /** * @return Absolute file path for a given cleartext file resourcePath. * @throws IOException */ - Path getEncryptedFilePath(String relativeCleartextPath) throws DavException { + private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException { final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); - final Path parent = getEncryptedDirectoryPath(parentCleartextPath); + final Path parent = createEncryptedDirectoryPath(parentCleartextPath); final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); try { - final String encryptedFilename = cryptor.encryptFilename(cleartextFilename, this); + final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); return parent.resolve(encryptedFilename); } catch (IOException e) { throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); @@ -106,7 +145,7 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat * @return Absolute directory path for a given cleartext directory resourcePath. * @throws IOException */ - Path getEncryptedDirectoryPath(String relativeCleartextPath) throws DavException { + private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException { assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/"); try { final Path result; @@ -116,9 +155,9 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat result = dataRoot.resolve(fixedRootDirectory); } else { final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); - final Path parent = getEncryptedDirectoryPath(parentCleartextPath); + final Path parent = createEncryptedDirectoryPath(parentCleartextPath); final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); - final String encryptedFilename = cryptor.encryptFilename(cleartextFilename, CryptoResourceFactory.this); + final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); final Path directoryFile = parent.resolve(encryptedFilename); final String directoryId; if (Files.exists(directoryFile)) { @@ -146,7 +185,7 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat } private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path dirPath) { - return new EncryptedDir(this, locator, session, lockManager, cryptor, dirPath); + return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, dirPath); } private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) { @@ -169,23 +208,4 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat } } - @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)); - writeAllBytesAtomically(metadataFile, 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; - } else { - return readAllBytesAtomically(metadataFile); - } - } - } 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 6117bbb96..acf254bb4 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,7 +10,11 @@ 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.DirectoryStream; import java.nio.file.Files; import java.nio.file.Path; @@ -19,6 +23,7 @@ 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; @@ -41,20 +46,23 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.webdav.exceptions.DavRuntimeException; import org.cryptomator.webdav.exceptions.IORuntimeException; +import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class EncryptedDir extends AbstractEncryptedNode { +class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions { private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class); private final Path directoryPath; + private final FilenameTranslator filenameTranslator; - public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path directoryPath) { + public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path directoryPath) { super(factory, locator, session, lockManager, cryptor); if (directoryPath == null || !Files.isDirectory(directoryPath)) { throw new IllegalArgumentException("directoryPath must be an existing directory, but was " + directoryPath); } this.directoryPath = directoryPath; + this.filenameTranslator = filenameTranslator; determineProperties(); } @@ -100,47 +108,77 @@ class EncryptedDir extends AbstractEncryptedNode { } } + @Deprecated private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException { + LOG.warn("Invokation of addMemberDir(DavResourceLocator childLocator, InputContext inputContext)"); try { - // the following invokation will create nonexisting directories: - factory.getEncryptedDirectoryPath(childLocator.getResourcePath()); + final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath()); + final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName); + final Path dirFilePath = directoryPath.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) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException { - final Path filePath = factory.getEncryptedFilePath(childLocator.getResourcePath()); - 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); + try { + final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath()); + final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); + final Path filePath = directoryPath.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 directoryStream = Files.newDirectoryStream(directoryPath, cryptor.getPayloadFilesFilter()); + final DirectoryStream directoryStream = Files.newDirectoryStream(directoryPath, DIRECTORY_CONTENT_FILTER); final List result = new ArrayList<>(); for (final Path childPath : directoryStream) { try { - final String cleartextFilename = cryptor.decryptFilename(childPath.getFileName().toString(), factory); + 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 = factory.createResource(childLocator, session); + final DavResource resource; + if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) { + resource = factory.createChildDirectoryResource(childLocator, session, childPath); + } else { + assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT); + resource = factory.createChildFileResource(childLocator, session, childPath); + } result.add(resource); } catch (DecryptFailedException e) { LOG.warn("Decryption of resource failed: " + childPath); @@ -168,16 +206,21 @@ class EncryptedDir extends AbstractEncryptedNode { private void removeMember(AbstractEncryptedNode member) throws DavException { try { - if (member.isCollection()) { + 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 iterator = member.getMembers(); iterator.hasNext();) { DavResource m = iterator.next(); member.removeMember(m); } - final Path memberDirectoryPath = factory.getEncryptedDirectoryPath(member.getResourcePath()); - Files.deleteIfExists(memberDirectoryPath); + Files.deleteIfExists(subDir.directoryPath); + ciphertextFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); + } else { + ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); } - final Path memberPath = factory.getEncryptedFilePath(member.getResourcePath()); + final Path memberPath = directoryPath.resolve(ciphertextFilename); Files.deleteIfExists(memberPath); } catch (FileNotFoundException e) { // no-op @@ -239,7 +282,7 @@ class EncryptedDir extends AbstractEncryptedNode { // do nothing } - @Override + @Deprecated protected void determineProperties() { properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java new file mode 100644 index 000000000..cce4bab0b --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * 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.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.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.util.UUID; + +import org.apache.commons.io.FilenameUtils; +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.DavServletResponse; +import org.apache.jackrabbit.webdav.DavSession; +import org.apache.jackrabbit.webdav.io.InputContext; +import org.apache.jackrabbit.webdav.io.OutputContext; +import org.apache.jackrabbit.webdav.lock.LockManager; +import org.cryptomator.crypto.Cryptor; + +class EncryptedDirDuringCreation extends AbstractEncryptedNode { + + private final Path parentDir; + private final FilenameTranslator filenameTranslator; + + public EncryptedDirDuringCreation(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path parentDir) { + super(factory, locator, session, lockManager, cryptor); + this.parentDir = parentDir; + this.filenameTranslator = filenameTranslator; + } + + public void doCreate() throws DavException { + try { + final String cleartextDirName = FilenameUtils.getName(locator.getResourcePath()); + final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName); + final Path dirFilePath = parentDir.resolve(ciphertextDirName); + final String 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))); + } catch (FileAlreadyExistsException e) { + throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); + } + final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId); + Files.createDirectories(directoryPath); + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + + @Override + protected Path getPhysicalPath() { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public boolean exists() { + return false; + } + + @Override + public boolean isCollection() { + return true; + } + + @Override + public long getModificationTime() { + return Instant.now().toEpochMilli(); + } + + @Override + public void spool(OutputContext outputContext) throws IOException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public void addMember(DavResource resource, InputContext inputContext) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public DavResourceIterator getMembers() { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public void removeMember(DavResource member) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public void move(AbstractEncryptedNode destination) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + @Override + public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + +} 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 d218db846..968478030 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 @@ -100,7 +100,7 @@ class EncryptedFile extends AbstractEncryptedNode { } } - @Override + @Deprecated protected void determineProperties() { if (Files.isRegularFile(filePath)) { try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) { diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java similarity index 59% rename from main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java rename to main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java index ca6da8eea..7963a84b9 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/FileNamingConventions.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java @@ -6,23 +6,18 @@ * 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.
* Parent folder path uses up to 58 chars (sha256 -> 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.
@@ -31,14 +26,24 @@ interface FileNamingConventions { int ENCRYPTED_FILENAME_LENGTH_LIMIT = 136; /** - * For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + * For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. */ - String BASIC_FILE_EXT = ".aes"; + String DIR_EXT = ".dir"; /** - * For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + * For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. */ - String LONG_NAME_FILE_EXT = ".lng.aes"; + 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 +61,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 +80,14 @@ interface FileNamingConventions { }; + /** + * Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}. + */ + Filter DIRECTORY_CONTENT_FILTER = new Filter() { + @Override + public boolean accept(Path entry) throws IOException { + return ENCRYPTED_FILE_MATCHER.matches(entry); + } + }; + } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FilenameTranslator.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FilenameTranslator.java new file mode 100644 index 000000000..f951ef3e5 --- /dev/null +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FilenameTranslator.java @@ -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.
+ * This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.
+ *
+ * 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); + } + } + } + +} diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LongFilenameMetadata.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/LongFilenameMetadata.java similarity index 97% rename from main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LongFilenameMetadata.java rename to main/core/src/main/java/org/cryptomator/webdav/jackrabbit/LongFilenameMetadata.java index db1e5cbbd..77cd116cc 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LongFilenameMetadata.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/LongFilenameMetadata.java @@ -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; 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 5a2e8e726..27cd3adbc 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 @@ -67,11 +67,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."); 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 608606eb0..cef5f2a4d 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 @@ -8,6 +8,7 @@ ******************************************************************************/ package org.cryptomator.webdav.jackrabbit; +import java.io.IOException; import java.util.Collection; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -16,11 +17,14 @@ import java.util.concurrent.TimeUnit; import javax.servlet.ServletConfig; import javax.servlet.ServletException; +import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavLocatorFactory; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceFactory; +import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSessionProvider; import org.apache.jackrabbit.webdav.WebdavRequest; +import org.apache.jackrabbit.webdav.WebdavResponse; import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet; import org.cryptomator.crypto.Cryptor; @@ -67,6 +71,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); 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 2ec02b1fc..03b669549 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 @@ -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 @@ -296,71 +290,20 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi 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.
- * 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.
- * This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.
- *
- * 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.
- *
- * 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 getPayloadFilesFilter() { - return new Filter() { - @Override - public boolean accept(Path entry) throws IOException { - return ENCRYPTED_FILE_MATCHER.matches(entry); - } - }; - } - } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java index 852248b9b..b31dcfe99 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java @@ -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(); + } 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 a04303fb2..ae55c916d 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 @@ -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 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); - } - - } - } 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 index ab44886f0..9f91ddac1 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java @@ -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 getPayloadFilesFilter() { - return cryptor.getPayloadFilesFilter(); - } - @Override public void destroy() throws DestroyFailedException { cryptor.destroy(); 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 55c637e5d..c2c479f5e 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 @@ -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; @@ -57,22 +55,18 @@ public interface Cryptor extends Destroyable { * 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 true for encrypted files, i.e. if the file is an actual user payload and not a supporting metadata file of the {@link Cryptor}. - */ - Filter getPayloadFilesFilter(); - } diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java deleted file mode 100644 index c3564fb32..000000000 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/CryptorMetadataSupport.java +++ /dev/null @@ -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 null if no such group exists. - */ - 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 index ba955a799..d22c350db 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/PathCachingCryptorDecorator.java @@ -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; } From ea9c8eee834fd9dd4d66dade3915d1c917daad87 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 15 May 2015 23:17:24 +0200 Subject: [PATCH 4/4] yet another refactoring session (functionality restored now) --- .../jackrabbit/AbstractEncryptedNode.java | 15 +- .../jackrabbit/CryptoResourceFactory.java | 76 +++---- .../webdav/jackrabbit/EncryptedDir.java | 209 ++++++++++++------ .../EncryptedDirDuringCreation.java | 114 ---------- .../webdav/jackrabbit/EncryptedFile.java | 67 +++--- .../jackrabbit/FileNamingConventions.java | 9 +- .../webdav/jackrabbit/NonExistingNode.java | 26 ++- .../webdav/jackrabbit/WebDavServlet.java | 24 +- 8 files changed, 254 insertions(+), 286 deletions(-) delete mode 100644 main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java 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 ead8ea246..c9f6158ed 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 @@ -48,19 +48,19 @@ abstract class AbstractEncryptedNode implements DavResource { protected final DavSession session; protected final LockManager lockManager; protected final Cryptor cryptor; + protected final Path filePath; protected final DavPropertySet properties; - protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { + protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) { this.factory = factory; this.locator = locator; this.session = session; this.lockManager = lockManager; this.cryptor = cryptor; + this.filePath = filePath; this.properties = new DavPropertySet(); } - protected abstract Path getPhysicalPath(); - @Override public String getComplianceClass() { return DAV_COMPLIANCE_CLASSES; @@ -73,7 +73,7 @@ abstract class AbstractEncryptedNode implements DavResource { @Override public boolean exists() { - return Files.exists(getPhysicalPath()); + return Files.exists(filePath); } @Override @@ -105,7 +105,7 @@ 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; } @@ -133,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()); } 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 index bcffb89d6..8c7614eec 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java @@ -5,6 +5,7 @@ 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; @@ -48,61 +49,46 @@ public class CryptoResourceFactory implements DavResourceFactory, FileNamingConv @Override public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { - if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) { - final String parentResourcePath = FilenameUtils.getFullPathNoEndSeparator(locator.getResourcePath()); - final Path parentDirectoryPath = createEncryptedDirectoryPath(parentResourcePath); - return new EncryptedDirDuringCreation(this, locator, request.getDavSession(), lockManager, cryptor, filenameTranslator, parentDirectoryPath); - } - if (locator.isRootLocation()) { - final Path dirpath = createEncryptedDirectoryPath(""); - return createDirectory(locator, request.getDavSession(), dirpath); + return createRootDirectory(locator, request.getDavSession()); } - final Path filepath = getEncryptedFilePath(locator.getResourcePath()); + final Path filePath = getEncryptedFilePath(locator.getResourcePath()); final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath()); final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); - if (Files.exists(dirFilePath)) { - final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath()); - return createDirectory(locator, request.getDavSession(), dirPath); - } else if (Files.exists(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { + 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); + 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 { - return createNonExisting(locator, request.getDavSession()); + // e.g. for MOVE operations: + return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath); } } @Override public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { if (locator.isRootLocation()) { - final Path dirpath = createEncryptedDirectoryPath(""); - return createDirectory(locator, session, dirpath); + return createRootDirectory(locator, session); } - final Path filepath = getEncryptedFilePath(locator.getResourcePath()); + final Path filePath = getEncryptedFilePath(locator.getResourcePath()); final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath()); if (Files.exists(dirFilePath)) { - final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath()); - return createDirectory(locator, session, dirPath); - } else if (Files.exists(filepath)) { - return createFile(locator, session, filepath); + return createDirectory(locator, session, dirFilePath); + } else if (Files.exists(filePath)) { + return createFile(locator, session, filePath); } else { - return createNonExisting(locator, session); + // e.g. for MOVE operations: + return createNonExisting(locator, session, filePath, dirFilePath); } } DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException { - try { - final String directoryId = new String(readAllBytesAtomically(existingDirectoryFile), StandardCharsets.UTF_8); - final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator()); - final Path dirpath = dataRoot.resolve(directory); - return createDirectory(locator, session, dirpath); - } catch (IOException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); - } + return createDirectory(locator, session, existingDirectoryFile); } DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException { @@ -184,12 +170,28 @@ public class CryptoResourceFactory implements DavResourceFactory, FileNamingConv return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath); } - private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path dirPath) { - return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, dirPath); + 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 NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) { - return new NonExistingNode(this, locator, session, lockManager, cryptor); + 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 */ 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 acf254bb4..30f337547 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 @@ -15,9 +15,11 @@ 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; import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; @@ -27,6 +29,7 @@ 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; @@ -53,21 +56,44 @@ import org.slf4j.LoggerFactory; class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions { private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class); - private final Path directoryPath; private final FilenameTranslator filenameTranslator; + private String directoryId; + private Path directoryPath; - public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path directoryPath) { - super(factory, locator, session, lockManager, cryptor); - if (directoryPath == null || !Files.isDirectory(directoryPath)) { - throw new IllegalArgumentException("directoryPath must be an existing directory, but was " + directoryPath); - } - this.directoryPath = directoryPath; + public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) { + super(factory, locator, session, lockManager, cryptor, filePath); this.filenameTranslator = filenameTranslator; determineProperties(); } - @Override - protected Path getPhysicalPath() { + /** + * @return Path or null, 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 null, 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; } @@ -76,16 +102,15 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention return true; } - @Override - public boolean exists() { - assert Files.isDirectory(directoryPath); - return true; - } - @Override public long getModificationTime() { try { - return Files.getLastModifiedTime(directoryPath).toMillis(); + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + return -1; + } else { + return Files.getLastModifiedTime(dirPath).toMillis(); + } } catch (IOException e) { return -1; } @@ -108,13 +133,15 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention } } - @Deprecated private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException { - LOG.warn("Invokation of addMemberDir(DavResourceLocator childLocator, InputContext inputContext)"); + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } try { final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath()); final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName); - final Path dirFilePath = directoryPath.resolve(ciphertextDirName); + 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)) { @@ -138,10 +165,14 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention } 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 = directoryPath.resolve(ciphertextFilename); + 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) { @@ -164,7 +195,11 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention @Override public DavResourceIterator getMembers() { try { - final DirectoryStream directoryStream = Files.newDirectoryStream(directoryPath, DIRECTORY_CONTENT_FILTER); + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + final DirectoryStream directoryStream = Files.newDirectoryStream(dirPath, DIRECTORY_CONTENT_FILTER); final List result = new ArrayList<>(); for (final Path childPath : directoryStream) { @@ -205,6 +240,10 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention } private void removeMember(AbstractEncryptedNode member) throws DavException { + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } try { final String cleartextFilename = FilenameUtils.getName(member.getResourcePath()); final String ciphertextFilename; @@ -215,12 +254,15 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention DavResource m = iterator.next(); member.removeMember(m); } - Files.deleteIfExists(subDir.directoryPath); + final Path subDirPath = subDir.getDirectoryPath(); + if (subDirPath != null) { + Files.deleteIfExists(subDirPath); + } ciphertextFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); } else { ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); } - final Path memberPath = directoryPath.resolve(ciphertextFilename); + final Path memberPath = dirPath.resolve(ciphertextFilename); Files.deleteIfExists(memberPath); } catch (FileNotFoundException e) { // no-op @@ -231,50 +273,81 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention @Override public void move(AbstractEncryptedNode dest) throws DavException, IOException { - throw new UnsupportedOperationException("not yet implemented"); - // final Path srcDir = this.locator.getEncryptedDirectoryPath(false); - // final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); - // 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); - // } + // when moving a directory we only need to move the file (actual dir is ID-dependent and won't change) + final Path srcPath = filePath; + final Path dstPath; + if (dest instanceof NonExistingNode) { + dstPath = ((NonExistingNode) dest).getDirFilePath(); + } else { + dstPath = dest.filePath; + } + + // move: + Files.createDirectories(dstPath.getParent()); + try { + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING); + } } @Override public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException { - throw new UnsupportedOperationException("not yet implemented"); - // final Path srcDir = this.locator.getEncryptedDirectoryPath(false); - // final Path dstDir = dest.locator.getEncryptedDirectoryPath(true); - // 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); - // } + final Path dstDirFilePath; + if (dest instanceof NonExistingNode) { + dstDirFilePath = ((NonExistingNode) dest).getDirFilePath(); + } else { + dstDirFilePath = dest.filePath; + } + + // copy dirFile: + final String srcDirId = getDirectoryId(); + if (srcDirId == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + final String dstDirId = UUID.randomUUID().toString(); + try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { + c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8))); + } + + // copy actual dir: + if (!shallow) { + copyDirectoryContents(srcDirId, dstDirId); + } else { + final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId); + Files.createDirectories(dstDirPath); + } + } + + private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException { + final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId); + final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId); + Files.createDirectories(dstDirPath); + final DirectoryStream 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); + } + } } @Override @@ -287,11 +360,13 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); try { - final BasicFileAttributes attrs = Files.readAttributes(directoryPath, BasicFileAttributes.class); - properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime()))); - properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime()))); + if (Files.exists(filePath)) { + final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class); + properties.add(new DefaultDavProperty(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime()))); + properties.add(new DefaultDavProperty(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime()))); + } } catch (IOException e) { - LOG.error("Error determining metadata " + directoryPath.toString(), e); + LOG.error("Error determining metadata " + filePath, e); // don't add any further properties } } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java deleted file mode 100644 index cce4bab0b..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDirDuringCreation.java +++ /dev/null @@ -1,114 +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.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.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.Instant; -import java.util.UUID; - -import org.apache.commons.io.FilenameUtils; -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.DavServletResponse; -import org.apache.jackrabbit.webdav.DavSession; -import org.apache.jackrabbit.webdav.io.InputContext; -import org.apache.jackrabbit.webdav.io.OutputContext; -import org.apache.jackrabbit.webdav.lock.LockManager; -import org.cryptomator.crypto.Cryptor; - -class EncryptedDirDuringCreation extends AbstractEncryptedNode { - - private final Path parentDir; - private final FilenameTranslator filenameTranslator; - - public EncryptedDirDuringCreation(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path parentDir) { - super(factory, locator, session, lockManager, cryptor); - this.parentDir = parentDir; - this.filenameTranslator = filenameTranslator; - } - - public void doCreate() throws DavException { - try { - final String cleartextDirName = FilenameUtils.getName(locator.getResourcePath()); - final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName); - final Path dirFilePath = parentDir.resolve(ciphertextDirName); - final String 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))); - } catch (FileAlreadyExistsException e) { - throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED); - } - final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId); - Files.createDirectories(directoryPath); - } catch (IOException e) { - throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - - @Override - protected Path getPhysicalPath() { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - - @Override - public boolean exists() { - return false; - } - - @Override - public boolean isCollection() { - return true; - } - - @Override - public long getModificationTime() { - return Instant.now().toEpochMilli(); - } - - @Override - public void spool(OutputContext outputContext) throws IOException { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - - @Override - public void addMember(DavResource resource, InputContext inputContext) throws DavException { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - - @Override - public DavResourceIterator getMembers() { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - - @Override - public void removeMember(DavResource member) throws DavException { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - - @Override - public void move(AbstractEncryptedNode destination) throws DavException { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - - @Override - public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException { - throw new UnsupportedOperationException("Resource doesn't exist."); - } - -} 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 968478030..5f20bc661 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,8 +11,10 @@ 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; @@ -40,23 +42,16 @@ class EncryptedFile extends AbstractEncryptedNode { private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class); protected final CryptoWarningHandler cryptoWarningHandler; - protected final Path filePath; public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) { - super(factory, locator, session, lockManager, cryptor); + super(factory, locator, session, lockManager, cryptor, filePath); if (filePath == null) { throw new IllegalArgumentException("filePath must not be null"); } this.cryptoWarningHandler = cryptoWarningHandler; - this.filePath = filePath; this.determineProperties(); } - @Override - protected Path getPhysicalPath() { - return filePath; - } - @Override public boolean isCollection() { return false; @@ -128,40 +123,36 @@ class EncryptedFile extends AbstractEncryptedNode { @Override public void move(AbstractEncryptedNode dest) throws DavException, IOException { - throw new UnsupportedOperationException("not yet implemented"); - // 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); - // } + final Path srcPath = filePath; + final Path dstPath; + if (dest instanceof NonExistingNode) { + dstPath = ((NonExistingNode) dest).getFilePath(); + } else { + dstPath = dest.filePath; + } + + try { + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING); + } } @Override public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException { - throw new UnsupportedOperationException("not yet implemented"); - // 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); - // } + final Path srcPath = filePath; + final Path dstPath; + if (dest instanceof NonExistingNode) { + dstPath = ((NonExistingNode) dest).getFilePath(); + } else { + dstPath = dest.filePath; + } + + try { + Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + } } } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java index 7963a84b9..49a9493a4 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/FileNamingConventions.java @@ -21,9 +21,14 @@ interface FileNamingConventions { /** * Maximum path length on some file systems or cloud storage providers is restricted.
* Parent folder path uses up to 58 chars (sha256 -> 32 bytes base32 encoded to 56 bytes + two slashes). That in mind we don't want the total path to be longer than 255 chars.
- * 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; + + /** + * Dummy file, on which file attributes can be stored for the root directory. + */ + String ROOT_FILE = "root"; /** * For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. 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 27cd3adbc..8d194a1cb 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 @@ -19,17 +19,18 @@ 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, DavResourceLocator 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 @@ -77,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; + } + } 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 cef5f2a4d..ee9893e86 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 @@ -8,7 +8,6 @@ ******************************************************************************/ package org.cryptomator.webdav.jackrabbit; -import java.io.IOException; import java.util.Collection; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -17,14 +16,11 @@ import java.util.concurrent.TimeUnit; import javax.servlet.ServletConfig; import javax.servlet.ServletException; -import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavLocatorFactory; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceFactory; -import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSessionProvider; import org.apache.jackrabbit.webdav.WebdavRequest; -import org.apache.jackrabbit.webdav.WebdavResponse; import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet; import org.cryptomator.crypto.Cryptor; @@ -71,16 +67,16 @@ 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 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) {