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 1715ed810..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 @@ -44,24 +44,23 @@ abstract class AbstractEncryptedNode implements DavResource { private static final String DAV_COMPLIANCE_CLASSES = "1, 2"; protected final CryptoResourceFactory factory; - protected final CryptoLocator locator; + protected final DavResourceLocator locator; protected final DavSession session; protected final LockManager lockManager; protected final Cryptor cryptor; + protected final Path filePath; protected final DavPropertySet properties; - protected AbstractEncryptedNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { + protected AbstractEncryptedNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath) { this.factory = factory; this.locator = locator; this.session = session; this.lockManager = lockManager; this.cryptor = cryptor; + this.filePath = filePath; this.properties = new DavPropertySet(); - this.determineProperties(); } - protected abstract Path getPhysicalPath(); - @Override public String getComplianceClass() { return DAV_COMPLIANCE_CLASSES; @@ -74,7 +73,7 @@ abstract class AbstractEncryptedNode implements DavResource { @Override public boolean exists() { - return Files.exists(getPhysicalPath()); + return Files.exists(filePath); } @Override @@ -89,7 +88,7 @@ abstract class AbstractEncryptedNode implements DavResource { } @Override - public CryptoLocator getLocator() { + public DavResourceLocator getLocator() { return locator; } @@ -106,14 +105,12 @@ abstract class AbstractEncryptedNode implements DavResource { @Override public long getModificationTime() { try { - return Files.getLastModifiedTime(getPhysicalPath()).toMillis(); + return Files.getLastModifiedTime(filePath).toMillis(); } catch (IOException e) { return -1; } } - protected abstract void determineProperties(); - @Override public DavPropertyName[] getPropertyNames() { return getProperties().getPropertyNames(); @@ -136,17 +133,16 @@ abstract class AbstractEncryptedNode implements DavResource { LOG.info("Set property {}", property.getName()); try { - final Path path = getPhysicalPath(); if (DavPropertyName.CREATIONDATE.equals(property.getName()) && property.getValue() instanceof String) { final String createDateStr = (String) property.getValue(); final FileTime createTime = FileTimeUtils.fromRfc1123String(createDateStr); - final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); attrView.setTimes(null, null, createTime); LOG.info("Updating Creation Date: {}", createTime.toString()); } else if (DavPropertyName.GETLASTMODIFIED.equals(property.getName()) && property.getValue() instanceof String) { final String lastModifiedTimeStr = (String) property.getValue(); final FileTime lastModifiedTime = FileTimeUtils.fromRfc1123String(lastModifiedTimeStr); - final BasicFileAttributeView attrView = Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); + final BasicFileAttributeView attrView = Files.getFileAttributeView(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS); attrView.setTimes(lastModifiedTime, null, null); LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString()); } @@ -183,7 +179,7 @@ abstract class AbstractEncryptedNode implements DavResource { return null; } - final String parentResource = FilenameUtils.getPath(locator.getResourcePath()); + final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath()); final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource); try { return getFactory().createResource(parentLocator, session); 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/CryptoLocator.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java deleted file mode 100644 index 12cf9a761..000000000 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoLocator.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.cryptomator.webdav.jackrabbit; - -import java.io.IOException; -import java.nio.file.FileSystems; -import java.nio.file.Path; - -import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.jackrabbit.webdav.DavResourceLocator; -import org.apache.jackrabbit.webdav.util.EncodeUtil; -import org.apache.logging.log4j.util.Strings; -import org.cryptomator.crypto.Cryptor; -import org.cryptomator.webdav.exceptions.IORuntimeException; - -class CryptoLocator implements DavResourceLocator { - - private final CryptoLocatorFactory factory; - private final Cryptor cryptor; - private final Path rootPath; - private final String prefix; - private final String resourcePath; - - public CryptoLocator(CryptoLocatorFactory factory, Cryptor cryptor, Path rootPath, String prefix, String resourcePath) { - this.factory = factory; - this.cryptor = cryptor; - this.rootPath = rootPath; - this.prefix = prefix; - this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true); - } - - /* path variants */ - - /** - * Returns the decrypted path without any trailing slash. - * - * @see #getHref(boolean) - * @return Plaintext resource path. - */ - @Override - public String getResourcePath() { - return resourcePath; - } - - /** - * Returns the decrypted path and adds URL-encoding. - * - * @param isCollection If true, a trailing slash will be appended. - * @see #getResourcePath() - * @return URL-encoded plaintext resource path. - */ - @Override - public String getHref(boolean isCollection) { - final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath()); - final String href = getPrefix().concat(encodedResourcePath); - assert !href.endsWith("/"); - if (isCollection) { - return href.concat("/"); - } else { - return href; - } - } - - /** - * Returns the encrypted, absolute path on the local filesystem. - * - * @return Absolute, encrypted path as string (use {@link #getEncryptedFilePath()} for {@link Path}s). - */ - @Override - public String getRepositoryPath() { - if (isRootLocation()) { - return getDirectoryPath(); - } - try { - final String plaintextPath = getResourcePath(); - final String plaintextDir = FilenameUtils.getPathNoEndSeparator(plaintextPath); - final String plaintextFilename = FilenameUtils.getName(plaintextPath); - final String ciphertextDir = cryptor.encryptDirectoryPath(plaintextDir, FileSystems.getDefault().getSeparator()); - final String ciphertextFilename = cryptor.encryptFilename(plaintextFilename, factory); - final String ciphertextPath = ciphertextDir + FileSystems.getDefault().getSeparator() + ciphertextFilename; - return rootPath.resolve(ciphertextPath).toString(); - } catch (IOException e) { - throw new IORuntimeException(e); - } - } - - /** - * Returns the encrypted, absolute path on the local filesystem to the directory represented by this locator. - * - * @return Absolute, encrypted path as string (use {@link #getEncryptedDirectoryPath()} for {@link Path}s). - */ - public String getDirectoryPath() { - final String ciphertextPath = cryptor.encryptDirectoryPath(getResourcePath(), FileSystems.getDefault().getSeparator()); - return rootPath.resolve(ciphertextPath).toString(); - } - - public Path getEncryptedFilePath() { - return FileSystems.getDefault().getPath(getRepositoryPath()); - } - - public Path getEncryptedDirectoryPath() { - return FileSystems.getDefault().getPath(getDirectoryPath()); - } - - /* other stuff */ - - @Override - public String getPrefix() { - return prefix; - } - - @Override - public String getWorkspacePath() { - return isRootLocation() ? null : ""; - } - - @Override - public String getWorkspaceName() { - return getPrefix(); - } - - @Override - public boolean isSameWorkspace(DavResourceLocator locator) { - return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName()); - } - - @Override - public boolean isSameWorkspace(String workspaceName) { - return getWorkspaceName().equals(workspaceName); - } - - @Override - public boolean isRootLocation() { - return Strings.isEmpty(getResourcePath()); - } - - @Override - public CryptoLocatorFactory getFactory() { - return factory; - } - - /* hashcode and equals */ - - @Override - public int hashCode() { - final HashCodeBuilder builder = new HashCodeBuilder(); - builder.append(prefix); - builder.append(resourcePath); - return builder.toHashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof CryptoLocator) { - final CryptoLocator other = (CryptoLocator) obj; - final EqualsBuilder builder = new EqualsBuilder(); - builder.append(this.factory, other.factory); - builder.append(this.prefix, other.prefix); - builder.append(this.resourcePath, other.resourcePath); - return builder.isEquals(); - } else { - return false; - } - } - -} 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 6a965f5e4..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 @@ -1,11 +1,20 @@ package org.cryptomator.webdav.jackrabbit; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.UUID; import java.util.concurrent.ExecutorService; import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.io.FilenameUtils; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavMethods; import org.apache.jackrabbit.webdav.DavResource; @@ -16,84 +25,189 @@ import org.apache.jackrabbit.webdav.DavServletResponse; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.lock.LockManager; import org.apache.jackrabbit.webdav.lock.SimpleLockManager; +import org.apache.logging.log4j.util.Strings; import org.cryptomator.crypto.Cryptor; import org.eclipse.jetty.http.HttpHeader; -public class CryptoResourceFactory implements DavResourceFactory { +public class CryptoResourceFactory implements DavResourceFactory, FileNamingConventions { private final LockManager lockManager = new SimpleLockManager(); private final Cryptor cryptor; private final CryptoWarningHandler cryptoWarningHandler; private final ExecutorService backgroundTaskExecutor; + private final Path dataRoot; + private final FilenameTranslator filenameTranslator; - CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor) { + CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) { + Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot); this.cryptor = cryptor; this.cryptoWarningHandler = cryptoWarningHandler; this.backgroundTaskExecutor = backgroundTaskExecutor; + this.dataRoot = vaultRootPath.resolve("d"); + this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath); } @Override public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { - if (locator instanceof CryptoLocator) { - return createResource((CryptoLocator) locator, request, response); + if (locator.isRootLocation()) { + return createRootDirectory(locator, request.getDavSession()); + } + + final Path filePath = getEncryptedFilePath(locator.getResourcePath()); + final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath()); + final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); + if (Files.exists(dirFilePath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) { + return createDirectory(locator, request.getDavSession(), dirFilePath); + } else if (Files.exists(filePath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { + response.setStatus(HttpStatus.SC_PARTIAL_CONTENT); + return createFilePart(locator, request.getDavSession(), request, filePath); + } else if (Files.exists(filePath) || DavMethods.METHOD_PUT.equals(request.getMethod())) { + return createFile(locator, request.getDavSession(), filePath); } else { - throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName()); + // e.g. for MOVE operations: + return createNonExisting(locator, request.getDavSession(), filePath, dirFilePath); } } @Override public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException { - if (locator instanceof CryptoLocator) { - return createResource((CryptoLocator) locator, session); + if (locator.isRootLocation()) { + return createRootDirectory(locator, session); + } + + final Path filePath = getEncryptedFilePath(locator.getResourcePath()); + final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath()); + if (Files.exists(dirFilePath)) { + return createDirectory(locator, session, dirFilePath); + } else if (Files.exists(filePath)) { + return createFile(locator, session, filePath); } else { - throw new IllegalArgumentException("Unsupported resource locator of type " + locator.getClass().getName()); + // e.g. for MOVE operations: + return createNonExisting(locator, session, filePath, dirFilePath); } } - private DavResource createResource(CryptoLocator locator, DavServletRequest request, DavServletResponse response) throws DavException { - final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath()); - final Path dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath()); - final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString()); + DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException { + return createDirectory(locator, session, existingDirectoryFile); + } - if (Files.isDirectory(dirpath) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) { - return createDirectory(locator, request.getDavSession()); - } else if (Files.isRegularFile(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) { - response.setStatus(HttpStatus.SC_PARTIAL_CONTENT); - return createFilePart(locator, request.getDavSession(), request); - } else if (Files.isRegularFile(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) { - return createFile(locator, request.getDavSession()); - } else { - return createNonExisting(locator, request.getDavSession()); + DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException { + return createFile(locator, session, existingFile); + } + + /** + * @return Absolute file path for a given cleartext file resourcePath. + * @throws IOException + */ + private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException { + final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); + final Path parent = createEncryptedDirectoryPath(parentCleartextPath); + final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); + try { + final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); + return parent.resolve(encryptedFilename); + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } - private DavResource createResource(CryptoLocator locator, DavSession session) throws DavException { - final Path filepath = FileSystems.getDefault().getPath(locator.getRepositoryPath()); - final Path dirpath = FileSystems.getDefault().getPath(locator.getDirectoryPath()); - - if (Files.isDirectory(dirpath)) { - return createDirectory(locator, session); - } else if (Files.isRegularFile(filepath)) { - return createFile(locator, session); - } else { - return createNonExisting(locator, session); + /** + * @return Absolute file path for a given cleartext file resourcePath. + * @throws IOException + */ + private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException { + final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); + final Path parent = createEncryptedDirectoryPath(parentCleartextPath); + final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); + try { + final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); + return parent.resolve(encryptedFilename); + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } - private EncryptedFile createFilePart(CryptoLocator locator, DavSession session, DavServletRequest request) { - return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor); + /** + * @return Absolute directory path for a given cleartext directory resourcePath. + * @throws IOException + */ + private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException { + assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/"); + try { + final Path result; + if (Strings.isEmpty(relativeCleartextPath)) { + // root level + final String fixedRootDirectory = cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator()); + result = dataRoot.resolve(fixedRootDirectory); + } else { + final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath); + final Path parent = createEncryptedDirectoryPath(parentCleartextPath); + final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath); + final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); + final Path directoryFile = parent.resolve(encryptedFilename); + final String directoryId; + if (Files.exists(directoryFile)) { + directoryId = new String(readAllBytesAtomically(directoryFile), StandardCharsets.UTF_8); + } else { + directoryId = UUID.randomUUID().toString(); + writeAllBytesAtomically(directoryFile, directoryId.getBytes(StandardCharsets.UTF_8)); + } + final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator()); + result = dataRoot.resolve(directory); + } + Files.createDirectories(result); + return result; + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); + } } - private EncryptedFile createFile(CryptoLocator locator, DavSession session) { - return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler); + private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) { + return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath); } - private EncryptedDir createDirectory(CryptoLocator locator, DavSession session) { - return new EncryptedDir(this, locator, session, lockManager, cryptor); + private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) { + return new EncryptedFile(this, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath); } - private NonExistingNode createNonExisting(CryptoLocator locator, DavSession session) { - return new NonExistingNode(this, locator, session, lockManager, cryptor); + private EncryptedDir createRootDirectory(DavResourceLocator locator, DavSession session) throws DavException { + final Path rootFile = dataRoot.resolve(ROOT_FILE); + final Path rootDir = filenameTranslator.getEncryptedDirectoryPath(""); + try { + // make sure, root dir always exists. + // create dir first (because it fails silently, if alreay existing) + Files.createDirectories(rootDir); + Files.createFile(rootFile); + } catch (FileAlreadyExistsException e) { + // no-op + } catch (IOException e) { + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR); + } + return createDirectory(locator, session, dataRoot.resolve(ROOT_FILE)); + } + + private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path filePath) { + return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, filePath); + } + + private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session, Path filePath, Path dirFilePath) { + return new NonExistingNode(this, locator, session, lockManager, cryptor, filePath, dirFilePath); + } + + /* IO support */ + + private void writeAllBytesAtomically(Path path, byte[] bytes) throws IOException { + try (final FileChannel c = FileChannel.open(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { + c.write(ByteBuffer.wrap(bytes)); + } + } + + private byte[] readAllBytesAtomically(Path path) throws IOException { + try (final FileChannel c = FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) { + final ByteBuffer buffer = ByteBuffer.allocate((int) c.size()); + c.read(buffer); + return buffer.array(); + } } } 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..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 @@ -8,8 +8,13 @@ ******************************************************************************/ package org.cryptomator.webdav.jackrabbit; +import java.io.FileNotFoundException; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; import java.nio.file.AtomicMoveNotSupportedException; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -18,9 +23,13 @@ import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; +import java.util.UUID; +import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceIterator; @@ -36,24 +45,56 @@ import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.ResourceType; import org.cryptomator.crypto.Cryptor; import org.cryptomator.crypto.exceptions.CounterOverflowException; +import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.webdav.exceptions.DavRuntimeException; -import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException; import org.cryptomator.webdav.exceptions.IORuntimeException; +import org.eclipse.jetty.util.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -class EncryptedDir extends AbstractEncryptedNode { +class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions { private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class); + private final FilenameTranslator filenameTranslator; + private String directoryId; + private Path directoryPath; - public EncryptedDir(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { - super(factory, locator, session, lockManager, cryptor); + public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path filePath) { + super(factory, locator, session, lockManager, cryptor, filePath); + this.filenameTranslator = filenameTranslator; + determineProperties(); } - @Override - protected Path getPhysicalPath() { - return locator.getEncryptedDirectoryPath(); + /** + * @return Path or 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; } @Override @@ -61,15 +102,15 @@ class EncryptedDir extends AbstractEncryptedNode { return true; } - @Override - public boolean exists() { - return Files.isDirectory(locator.getEncryptedDirectoryPath()); - } - @Override public long getModificationTime() { try { - return Files.getLastModifiedTime(locator.getEncryptedDirectoryPath()).toMillis(); + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + return -1; + } else { + return Files.getLastModifiedTime(dirPath).toMillis(); + } } catch (IOException e) { return -1; } @@ -92,51 +133,89 @@ class EncryptedDir extends AbstractEncryptedNode { } } - private void addMemberDir(CryptoLocator childLocator, InputContext inputContext) throws DavException { + private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException { + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } try { - Files.createDirectories(childLocator.getEncryptedFilePath()); - Files.createDirectories(childLocator.getEncryptedDirectoryPath()); + final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath()); + final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName); + final Path dirFilePath = dirPath.resolve(ciphertextDirName); + final String directoryId; + if (Files.exists(dirFilePath)) { + try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) { + final ByteBuffer buffer = ByteBuffer.allocate((int) c.size()); + c.read(buffer); + directoryId = new String(buffer.array(), StandardCharsets.UTF_8); + } + } else { + directoryId = UUID.randomUUID().toString(); + try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { + c.write(ByteBuffer.wrap(directoryId.getBytes(StandardCharsets.UTF_8))); + } + } + final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId); + Files.createDirectories(directoryPath); } catch (SecurityException e) { throw new DavException(DavServletResponse.SC_FORBIDDEN, e); } catch (IOException e) { - LOG.error("Failed to create subdirectory.", e); - throw new IORuntimeException(e); + throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e); } } - private void addMemberFile(CryptoLocator childLocator, InputContext inputContext) throws DavException { - try (final SeekableByteChannel channel = Files.newByteChannel(childLocator.getEncryptedFilePath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { - cryptor.encryptFile(inputContext.getInputStream(), channel); - } catch (SecurityException e) { - throw new DavException(DavServletResponse.SC_FORBIDDEN, e); + private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException { + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + try { + final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath()); + final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); + final Path filePath = dirPath.resolve(ciphertextFilename); + try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + cryptor.encryptFile(inputContext.getInputStream(), channel); + } catch (SecurityException e) { + throw new DavException(DavServletResponse.SC_FORBIDDEN, e); + } catch (CounterOverflowException e) { + // lets indicate this to the client as a "file too big" error + throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e); + } catch (EncryptFailedException e) { + LOG.error("Encryption failed for unknown reasons.", e); + throw new IllegalStateException("Encryption failed for unknown reasons.", e); + } finally { + IOUtils.closeQuietly(inputContext.getInputStream()); + } } catch (IOException e) { LOG.error("Failed to create file.", e); throw new IORuntimeException(e); - } catch (CounterOverflowException e) { - // lets indicate this to the client as a "file too big" error - throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e); - } catch (EncryptFailedException e) { - LOG.error("Encryption failed for unknown reasons.", e); - throw new IllegalStateException("Encryption failed for unknown reasons.", e); - } finally { - IOUtils.closeQuietly(inputContext.getInputStream()); } } @Override public DavResourceIterator getMembers() { try { - final DirectoryStream directoryStream = Files.newDirectoryStream(locator.getEncryptedDirectoryPath(), cryptor.getPayloadFilesFilter()); + 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) { try { - final DavResourceLocator childLocator = locator.getFactory().createSubresourceLocator(locator, childPath.getFileName().toString()); - // final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), - // locator.getWorkspacePath(), childPath.toString(), false); - final DavResource resource = factory.createResource(childLocator, session); + final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString()); + final String cleartextFilepath = FilenameUtils.concat(getResourcePath(), cleartextFilename); + final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath); + final DavResource resource; + if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) { + resource = factory.createChildDirectoryResource(childLocator, session, childPath); + } else { + assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT); + resource = factory.createChildFileResource(childLocator, session, childPath); + } result.add(resource); - } catch (DecryptFailedRuntimeException e) { + } catch (DecryptFailedException e) { LOG.warn("Decryption of resource failed: " + childPath); continue; } @@ -152,7 +231,7 @@ class EncryptedDir extends AbstractEncryptedNode { } @Override - public void removeMember(DavResource member) { + public void removeMember(DavResource member) throws DavException { if (member instanceof AbstractEncryptedNode) { removeMember((AbstractEncryptedNode) member); } else { @@ -160,69 +239,114 @@ class EncryptedDir extends AbstractEncryptedNode { } } - private void removeMember(AbstractEncryptedNode member) { + private void removeMember(AbstractEncryptedNode member) throws DavException { + final Path dirPath = getDirectoryPath(); + if (dirPath == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } try { - if (member.isCollection()) { - member.getMembers().forEachRemaining(m -> securelyRemoveMemberOfCollection(member, m)); - Files.deleteIfExists(member.getLocator().getEncryptedDirectoryPath()); + final String cleartextFilename = FilenameUtils.getName(member.getResourcePath()); + final String ciphertextFilename; + if (member instanceof EncryptedDir) { + final EncryptedDir subDir = (EncryptedDir) member; + // remove sub-members recursively before deleting own directory + for (Iterator iterator = member.getMembers(); iterator.hasNext();) { + DavResource m = iterator.next(); + member.removeMember(m); + } + final Path subDirPath = subDir.getDirectoryPath(); + if (subDirPath != null) { + Files.deleteIfExists(subDirPath); + } + ciphertextFilename = filenameTranslator.getEncryptedDirName(cleartextFilename); + } else { + ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename); } - Files.deleteIfExists(member.getLocator().getEncryptedFilePath()); + final Path memberPath = dirPath.resolve(ciphertextFilename); + Files.deleteIfExists(memberPath); + } catch (FileNotFoundException e) { + // no-op } catch (IOException e) { throw new IORuntimeException(e); } } - private void securelyRemoveMemberOfCollection(DavResource collection, DavResource member) { - try { - collection.removeMember(member); - } catch (DavException e) { - throw new IllegalStateException("DavException should not be thrown by collections of type EncryptedDir. Collections is of type " + collection.getClass().getName()); - } - } - @Override public void move(AbstractEncryptedNode dest) throws DavException, IOException { - final Path srcDir = this.locator.getEncryptedDirectoryPath(); - final Path dstDir = dest.locator.getEncryptedDirectoryPath(); - final Path srcFile = this.locator.getEncryptedFilePath(); - final Path dstFile = dest.locator.getEncryptedFilePath(); - - // check for conflicts: - if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString()); + // when moving a directory we only need to move the file (actual dir is ID-dependent and won't change) + final Path srcPath = filePath; + final Path dstPath; + if (dest instanceof NonExistingNode) { + dstPath = ((NonExistingNode) dest).getDirFilePath(); + } else { + dstPath = dest.filePath; } // move: - Files.createDirectories(dstDir); + Files.createDirectories(dstPath.getParent()); try { - Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException e) { - Files.move(srcDir, dstDir, StandardCopyOption.REPLACE_EXISTING); - Files.move(srcFile, dstFile, StandardCopyOption.REPLACE_EXISTING); + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING); } } @Override public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException { - final Path srcDir = this.locator.getEncryptedDirectoryPath(); - final Path dstDir = dest.locator.getEncryptedDirectoryPath(); - final Path srcFile = this.locator.getEncryptedFilePath(); - final Path dstFile = dest.locator.getEncryptedFilePath(); - - // check for conflicts: - if (Files.exists(dstDir) && Files.getLastModifiedTime(dstDir).toMillis() > Files.getLastModifiedTime(dstDir).toMillis()) { - throw new DavException(DavServletResponse.SC_CONFLICT, "Directory at destination already exists: " + dstDir.toString()); + final Path dstDirFilePath; + if (dest instanceof NonExistingNode) { + dstDirFilePath = ((NonExistingNode) dest).getDirFilePath(); + } else { + dstDirFilePath = dest.filePath; } - // copy: - Files.createDirectories(dstDir); - try { - Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } catch (AtomicMoveNotSupportedException e) { - Files.copy(srcDir, dstDir, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); - Files.copy(srcFile, dstFile, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + // copy dirFile: + final String srcDirId = getDirectoryId(); + if (srcDirId == null) { + throw new DavException(DavServletResponse.SC_NOT_FOUND); + } + final String dstDirId = UUID.randomUUID().toString(); + try (final FileChannel c = FileChannel.open(dstDirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) { + c.write(ByteBuffer.wrap(dstDirId.getBytes(StandardCharsets.UTF_8))); + } + + // copy actual dir: + if (!shallow) { + copyDirectoryContents(srcDirId, dstDirId); + } else { + final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId); + Files.createDirectories(dstDirPath); + } + } + + private void copyDirectoryContents(String srcDirId, String dstDirId) throws IOException { + final Path srcDirPath = filenameTranslator.getEncryptedDirectoryPath(srcDirId); + final Path dstDirPath = filenameTranslator.getEncryptedDirectoryPath(dstDirId); + Files.createDirectories(dstDirPath); + final DirectoryStream directoryStream = Files.newDirectoryStream(srcDirPath, DIRECTORY_CONTENT_FILTER); + for (final Path srcChildPath : directoryStream) { + final String childName = srcChildPath.getFileName().toString(); + final Path dstChildPath = dstDirPath.resolve(childName); + if (StringUtils.endsWithIgnoreCase(childName, FILE_EXT)) { + try { + Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (AtomicMoveNotSupportedException e) { + Files.copy(srcChildPath, dstChildPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + } + } else if (StringUtils.endsWithIgnoreCase(childName, DIR_EXT)) { + final String srcSubdirId; + try (final FileChannel c = FileChannel.open(srcChildPath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) { + final ByteBuffer buffer = ByteBuffer.allocate((int) c.size()); + c.read(buffer); + srcSubdirId = new String(buffer.array(), StandardCharsets.UTF_8); + } + final String dstSubdirId = UUID.randomUUID().toString(); + try (final FileChannel c = FileChannel.open(dstChildPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); + final FileLock lock = c.lock()) { + c.write(ByteBuffer.wrap(dstSubdirId.getBytes(StandardCharsets.UTF_8))); + } + copyDirectoryContents(srcSubdirId, dstSubdirId); + } } } @@ -231,20 +355,19 @@ class EncryptedDir extends AbstractEncryptedNode { // do nothing } - @Override + @Deprecated protected void determineProperties() { - final Path path = locator.getEncryptedDirectoryPath(); properties.add(new ResourceType(ResourceType.COLLECTION)); properties.add(new DefaultDavProperty(DavPropertyName.ISCOLLECTION, 1)); - if (Files.exists(path)) { - try { - final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); + try { + if (Files.exists(filePath)) { + final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class); properties.add(new DefaultDavProperty(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 } + } catch (IOException 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/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java index 30a3cd160..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 @@ -21,7 +21,7 @@ import java.nio.file.attribute.BasicFileAttributes; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceIterator; -import org.apache.jackrabbit.webdav.DavServletResponse; +import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; @@ -43,14 +43,13 @@ class EncryptedFile extends AbstractEncryptedNode { protected final CryptoWarningHandler cryptoWarningHandler; - public EncryptedFile(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler) { - super(factory, locator, session, lockManager, cryptor); + public EncryptedFile(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, Path filePath) { + super(factory, locator, session, lockManager, cryptor, filePath); + if (filePath == null) { + throw new IllegalArgumentException("filePath must not be null"); + } this.cryptoWarningHandler = cryptoWarningHandler; - } - - @Override - protected Path getPhysicalPath() { - return locator.getEncryptedFilePath(); + this.determineProperties(); } @Override @@ -75,11 +74,10 @@ class EncryptedFile extends AbstractEncryptedNode { @Override public void spool(OutputContext outputContext) throws IOException { - final Path path = locator.getEncryptedFilePath(); - if (Files.isRegularFile(path)) { - outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis()); + if (Files.isRegularFile(filePath)) { + outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis()); outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()); - try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) { + try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) { final Long contentLength = cryptor.decryptedContentLength(channel); if (contentLength != null) { outputContext.setContentLength(contentLength); @@ -92,20 +90,19 @@ class EncryptedFile extends AbstractEncryptedNode { } catch (MacAuthenticationFailedException e) { cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath()); } catch (DecryptFailedException e) { - throw new IOException("Error decrypting file " + path.toString(), e); + throw new IOException("Error decrypting file " + filePath.toString(), e); } } } - @Override + @Deprecated protected void determineProperties() { - final Path path = locator.getEncryptedFilePath(); - if (Files.exists(path)) { - try (final SeekableByteChannel channel = Files.newByteChannel(path, StandardOpenOption.READ)) { + if (Files.isRegularFile(filePath)) { + try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) { final Long contentLength = cryptor.decryptedContentLength(channel); properties.add(new DefaultDavProperty(DavPropertyName.GETCONTENTLENGTH, contentLength)); } catch (IOException e) { - LOG.error("Error reading filesize " + path.toString(), e); + LOG.error("Error reading filesize " + filePath.toString(), e); throw new IORuntimeException(e); } catch (MacAuthenticationFailedException e) { LOG.warn("Content length couldn't be determined due to MAC authentication violation."); @@ -113,12 +110,12 @@ class EncryptedFile extends AbstractEncryptedNode { } try { - final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); + final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class); properties.add(new DefaultDavProperty(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,37 +123,35 @@ class EncryptedFile extends AbstractEncryptedNode { @Override public void move(AbstractEncryptedNode dest) throws DavException, IOException { - final Path src = this.locator.getEncryptedFilePath(); - final Path dst = dest.locator.getEncryptedFilePath(); - - // check for conflicts: - if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) { - throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString()); + final Path srcPath = filePath; + final Path dstPath; + if (dest instanceof NonExistingNode) { + dstPath = ((NonExistingNode) dest).getFilePath(); + } else { + dstPath = dest.filePath; } - // move: try { - Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException e) { - Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING); + Files.move(srcPath, dstPath, StandardCopyOption.REPLACE_EXISTING); } } @Override public void copy(AbstractEncryptedNode dest, boolean shallow) throws DavException, IOException { - final Path src = this.locator.getEncryptedFilePath(); - final Path dst = dest.locator.getEncryptedFilePath(); - - // check for conflicts: - if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) { - throw new DavException(DavServletResponse.SC_CONFLICT, "File at destination already exists: " + dst.toString()); + final Path srcPath = filePath; + final Path dstPath; + if (dest instanceof NonExistingNode) { + dstPath = ((NonExistingNode) dest).getFilePath(); + } else { + dstPath = dest.filePath; } - // copy: try { - Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (AtomicMoveNotSupportedException e) { - Files.copy(src, dst, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + Files.copy(srcPath, dstPath, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); } } 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/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 55% 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..49a9493a4 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,39 +6,49 @@ * Contributors: * Sebastian Stenzel - initial API and implementation ******************************************************************************/ -package org.cryptomator.crypto.aes256; +package org.cryptomator.webdav.jackrabbit; +import java.io.IOException; +import java.nio.file.DirectoryStream.Filter; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.util.regex.Pattern; -import org.apache.commons.codec.binary.Base32; -import org.apache.commons.codec.binary.BaseNCodec; import org.apache.commons.lang3.StringUtils; interface FileNamingConventions { - /** - * How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive. - */ - BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32(); - /** * Maximum path length on some file systems or cloud storage providers is restricted.
* 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; /** - * For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + * Dummy file, on which file attributes can be stored for the root directory. */ - String BASIC_FILE_EXT = ".aes"; + String ROOT_FILE = "root"; /** - * For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + * For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. */ - String LONG_NAME_FILE_EXT = ".lng.aes"; + String DIR_EXT = ".dir"; + + /** + * For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + */ + String LONG_DIR_EXT = ".lng.dir"; + + /** + * For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + */ + String FILE_EXT = ".file"; + + /** + * For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars. + */ + String LONG_FILE_EXT = ".lng.file"; /** * Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file. @@ -56,11 +66,17 @@ interface FileNamingConventions { @Override public boolean matches(Path path) { final String filename = path.getFileName().toString(); - if (StringUtils.endsWithIgnoreCase(filename, LONG_NAME_FILE_EXT)) { - final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_NAME_FILE_EXT); + if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) { + final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT); return LONG_NAME_PATTERN.matcher(basename).matches(); - } else if (StringUtils.endsWithIgnoreCase(filename, BASIC_FILE_EXT)) { - final String basename = StringUtils.removeEndIgnoreCase(filename, BASIC_FILE_EXT); + } else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) { + final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT); + return BASIC_NAME_PATTERN.matcher(basename).matches(); + } else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) { + final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT); + return LONG_NAME_PATTERN.matcher(basename).matches(); + } else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) { + final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT); return BASIC_NAME_PATTERN.matcher(basename).matches(); } else { return false; @@ -69,4 +85,14 @@ interface FileNamingConventions { }; + /** + * Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}. + */ + Filter 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 859cd943c..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 @@ -14,21 +14,23 @@ import java.nio.file.Path; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.DavResource; import org.apache.jackrabbit.webdav.DavResourceIterator; +import org.apache.jackrabbit.webdav.DavResourceLocator; import org.apache.jackrabbit.webdav.DavSession; import org.apache.jackrabbit.webdav.io.InputContext; import org.apache.jackrabbit.webdav.io.OutputContext; import org.apache.jackrabbit.webdav.lock.LockManager; +import org.apache.jackrabbit.webdav.property.DavProperty; import org.cryptomator.crypto.Cryptor; class NonExistingNode extends AbstractEncryptedNode { - public NonExistingNode(CryptoResourceFactory factory, CryptoLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor) { - super(factory, locator, session, lockManager, cryptor); - } + private final Path filePath; + private final Path dirFilePath; - @Override - protected Path getPhysicalPath() { - throw new UnsupportedOperationException("Resource doesn't exist."); + public NonExistingNode(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path filePath, Path dirFilePath) { + super(factory, locator, session, lockManager, cryptor, null); + this.filePath = filePath; + this.dirFilePath = dirFilePath; } @Override @@ -66,11 +68,6 @@ class NonExistingNode extends AbstractEncryptedNode { throw new UnsupportedOperationException("Resource doesn't exist."); } - @Override - protected void determineProperties() { - // do nothing. - } - @Override public void move(AbstractEncryptedNode destination) throws DavException { throw new UnsupportedOperationException("Resource doesn't exist."); @@ -81,4 +78,17 @@ class NonExistingNode extends AbstractEncryptedNode { throw new UnsupportedOperationException("Resource doesn't exist."); } + @Override + public void setProperty(DavProperty property) throws DavException { + throw new UnsupportedOperationException("Resource doesn't exist."); + } + + public Path getFilePath() { + return filePath; + } + + public Path getDirFilePath() { + return dirFilePath; + } + } 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()); - } - -} 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..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 @@ -47,8 +47,8 @@ public class WebDavServlet extends AbstractWebdavServlet { final String fsRoot = config.getInitParameter(CFG_FS_ROOT); backgroundTaskExecutor = Executors.newCachedThreadPool(); davSessionProvider = new DavSessionProviderImpl(); - davLocatorFactory = new CryptoLocatorFactory(fsRoot, cryptor); - davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor); + davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath()); // CryptoLocatorFactory(fsRoot, cryptor); + davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot); } @Override @@ -67,6 +67,17 @@ public class WebDavServlet extends AbstractWebdavServlet { } } + // @Override + // protected void doMkCol(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException { + // if (resource instanceof EncryptedDirDuringCreation) { + // EncryptedDirDuringCreation dir = (EncryptedDirDuringCreation) resource; + // dir.doCreate(); + // response.setStatus(DavServletResponse.SC_CREATED); + // } else { + // + // } + // } + @Override protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) { return !resource.exists() || request.matchesIfHeader(resource); 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..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 @@ -288,79 +282,28 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi } @Override - public String encryptDirectoryPath(String cleartextPath, String nativePathSep) { - final byte[] cleartextBytes = cleartextPath.getBytes(StandardCharsets.UTF_8); + public String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep) { + final byte[] cleartextBytes = cleartextDirectoryId.getBytes(StandardCharsets.UTF_8); byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes); final byte[] hashed = sha256().digest(encryptedBytes); final String encryptedThenHashedPath = ENCRYPTED_FILENAME_CODEC.encodeAsString(hashed); return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2); } - /** - * Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.
- * 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 bf7a5f8ba..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; @@ -47,32 +45,28 @@ public interface Cryptor extends Destroyable { /** * Encrypts a given plaintext path representing a directory structure. See {@link #encryptFilename(String, CryptorMetadataSupport)} for contents inside directories. * - * @param cleartextPath A relative path (UTF-8 encoded), whose path components are separated by '/' + * @param cleartextDirectoryId A relative path (UTF-8 encoded), whose path components are separated by '/' * @param nativePathSep Path separator like "/" used on local file system. Must not be null, even if cleartextPath is a sole file name without any path separators. * @return Encrypted path. */ - String encryptDirectoryPath(String cleartextPath, String nativePathSep); + String encryptDirectoryPath(String cleartextDirectoryId, String nativePathSep); /** * Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir. * * @param cleartextName A plaintext filename without any preceeding directory paths. - * @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this support object. * @return Encrypted filename. - * @throws IOException If ioSupport throws an IOException */ - String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException; + String encryptFilename(String cleartextName); /** * Decrypts the name of a file. * * @param ciphertextName A ciphertext filename without any preceeding directory paths. - * @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this support object. * @return Decrypted filename. * @throws DecryptFailedException If the decryption failed for various reasons (including wrong password). - * @throws IOException If ioSupport throws an IOException */ - String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException; + String decryptFilename(String ciphertextName) throws DecryptFailedException; /** * @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file. @@ -105,9 +99,4 @@ public interface Cryptor extends Destroyable { */ Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException; - /** - * @return A filter, that returns 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; }