mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-21 12:11:28 +00:00
some more flat hierarchy fixes
This commit is contained in:
@@ -64,5 +64,11 @@
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
@@ -111,8 +111,6 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void determineProperties();
|
||||
|
||||
@Override
|
||||
public DavPropertyName[] getPropertyNames() {
|
||||
return getProperties().getPropertyNames();
|
||||
@@ -182,7 +180,7 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
final String parentResource = FilenameUtils.getPath(locator.getResourcePath());
|
||||
final String parentResource = FilenameUtils.getPathNoEndSeparator(locator.getResourcePath());
|
||||
final DavResourceLocator parentLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), parentResource);
|
||||
try {
|
||||
return getFactory().createResource(parentLocator, session);
|
||||
|
||||
@@ -1,220 +0,0 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.util.EncodeUtil;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
|
||||
class CryptoLocator implements DavResourceLocator {
|
||||
|
||||
private final CryptoLocatorFactory factory;
|
||||
private final Cryptor cryptor;
|
||||
private final Path rootPath;
|
||||
private final String prefix;
|
||||
private final String resourcePath;
|
||||
|
||||
public CryptoLocator(CryptoLocatorFactory factory, Cryptor cryptor, Path rootPath, String prefix, String resourcePath) {
|
||||
this.factory = factory;
|
||||
this.cryptor = cryptor;
|
||||
this.rootPath = rootPath;
|
||||
this.prefix = prefix;
|
||||
this.resourcePath = FilenameUtils.normalizeNoEndSeparator(resourcePath, true);
|
||||
}
|
||||
|
||||
/* path variants */
|
||||
|
||||
/**
|
||||
* Returns the decrypted path without any trailing slash.
|
||||
*
|
||||
* @see #getHref(boolean)
|
||||
* @return Plaintext resource path.
|
||||
*/
|
||||
@Override
|
||||
public String getResourcePath() {
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the decrypted path and adds URL-encoding.
|
||||
*
|
||||
* @param isCollection If true, a trailing slash will be appended.
|
||||
* @see #getResourcePath()
|
||||
* @return URL-encoded plaintext resource path.
|
||||
*/
|
||||
@Override
|
||||
public String getHref(boolean isCollection) {
|
||||
final String encodedResourcePath = EncodeUtil.escapePath(getResourcePath());
|
||||
final String href = getPrefix().concat(encodedResourcePath);
|
||||
assert !href.endsWith("/");
|
||||
if (isCollection) {
|
||||
return href.concat("/");
|
||||
} else {
|
||||
return href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encrypted, absolute path on the local filesystem.
|
||||
*
|
||||
* @return Absolute, encrypted path as string (use {@link #getEncryptedFilePath()} for {@link Path}s).
|
||||
*/
|
||||
@Override
|
||||
public String getRepositoryPath() {
|
||||
if (isRootLocation()) {
|
||||
return getEncryptedRootDirectoryPath();
|
||||
}
|
||||
try {
|
||||
final String plaintextPath = getResourcePath();
|
||||
final String plaintextDir = FilenameUtils.getPathNoEndSeparator(plaintextPath);
|
||||
final String plaintextFilename = FilenameUtils.getName(plaintextPath);
|
||||
final String ciphertextDir = cryptor.encryptDirectoryPath(plaintextDir, FileSystems.getDefault().getSeparator());
|
||||
final String ciphertextFilename = cryptor.encryptFilename(plaintextFilename, factory);
|
||||
final String ciphertextPath = ciphertextDir + FileSystems.getDefault().getSeparator() + ciphertextFilename;
|
||||
return rootPath.resolve(ciphertextPath).toString();
|
||||
} catch (IOException e) {
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the encrypted, absolute path on the local filesystem to the directory represented by this locator.
|
||||
*
|
||||
* @return Absolute, encrypted path as string (use {@link #getEncryptedDirectoryPath()} for {@link Path}s).
|
||||
* @throws IOException
|
||||
*/
|
||||
public String getDirectoryPath(boolean create) throws IOException {
|
||||
if (isRootLocation()) {
|
||||
return getEncryptedRootDirectoryPath();
|
||||
} else {
|
||||
final List<String> cleartextPathComponents = Arrays.asList(StringUtils.split(getResourcePath(), "/"));
|
||||
return getEncryptedDirectoryPath(rootPath, cleartextPathComponents, false).toString();
|
||||
}
|
||||
}
|
||||
|
||||
private Path getEncryptedDirectoryPath(Path encryptedParentDirectoryPath, List<String> cleartextSubPathComponents, boolean create) throws IOException {
|
||||
if (cleartextSubPathComponents.size() == 0) {
|
||||
return encryptedParentDirectoryPath;
|
||||
} else {
|
||||
final String nextPathComponent = cleartextSubPathComponents.get(0);
|
||||
final List<String> remainingSubPathComponents = cleartextSubPathComponents.subList(1, cleartextSubPathComponents.size());
|
||||
final String fullEncryptedSubdirectoryPath = getEncryptedDirectoryPath(encryptedParentDirectoryPath, nextPathComponent, create);
|
||||
return getEncryptedDirectoryPath(rootPath.resolve(fullEncryptedSubdirectoryPath), remainingSubPathComponents, create);
|
||||
}
|
||||
}
|
||||
|
||||
private String getEncryptedDirectoryPath(Path encryptedParentDirectoryPath, String cleartextDirectoryName, boolean create) throws IOException {
|
||||
final String encryptedDirectoryName = this.cryptor.encryptFilename(cleartextDirectoryName, this.factory);
|
||||
// TODO file extensions...
|
||||
final Path directoryFile = encryptedParentDirectoryPath.resolve(encryptedDirectoryName + ".dir");
|
||||
if (Files.exists(directoryFile)) {
|
||||
try (final FileChannel c = FileChannel.open(directoryFile, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||
c.read(buffer);
|
||||
final String directoryUuid = buffer.asCharBuffer().toString();
|
||||
return this.cryptor.encryptDirectoryPath(directoryUuid, FileSystems.getDefault().getSeparator());
|
||||
}
|
||||
} else if (create) {
|
||||
try (final FileChannel c = FileChannel.open(directoryFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
|
||||
final String directoryUuid = UUID.randomUUID().toString();
|
||||
final ByteBuffer buf = ByteBuffer.wrap(directoryUuid.getBytes(StandardCharsets.UTF_8));
|
||||
c.write(buf);
|
||||
return this.cryptor.encryptDirectoryPath(directoryUuid, FileSystems.getDefault().getSeparator());
|
||||
}
|
||||
} else {
|
||||
throw new FileNotFoundException(directoryFile.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private String getEncryptedRootDirectoryPath() {
|
||||
return this.cryptor.encryptDirectoryPath("", FileSystems.getDefault().getSeparator());
|
||||
}
|
||||
|
||||
public Path getEncryptedFilePath() {
|
||||
return FileSystems.getDefault().getPath(getRepositoryPath());
|
||||
}
|
||||
|
||||
public Path getEncryptedDirectoryPath(boolean create) throws IOException {
|
||||
return FileSystems.getDefault().getPath(getDirectoryPath(create));
|
||||
}
|
||||
|
||||
/* other stuff */
|
||||
|
||||
@Override
|
||||
public String getPrefix() {
|
||||
return prefix;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspacePath() {
|
||||
return isRootLocation() ? null : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWorkspaceName() {
|
||||
return getPrefix();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(DavResourceLocator locator) {
|
||||
return (locator == null) ? false : isSameWorkspace(locator.getWorkspaceName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSameWorkspace(String workspaceName) {
|
||||
return getWorkspaceName().equals(workspaceName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRootLocation() {
|
||||
return Strings.isEmpty(getResourcePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CryptoLocatorFactory getFactory() {
|
||||
return factory;
|
||||
}
|
||||
|
||||
/* hashcode and equals */
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final HashCodeBuilder builder = new HashCodeBuilder();
|
||||
builder.append(prefix);
|
||||
builder.append(resourcePath);
|
||||
return builder.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof CryptoLocator) {
|
||||
final CryptoLocator other = (CryptoLocator) obj;
|
||||
final EqualsBuilder builder = new EqualsBuilder();
|
||||
builder.append(this.factory, other.factory);
|
||||
builder.append(this.prefix, other.prefix);
|
||||
builder.append(this.resourcePath, other.resourcePath);
|
||||
return builder.isEquals();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,42 +26,49 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorMetadataSupport;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadataSupport {
|
||||
public class CryptoResourceFactory implements DavResourceFactory, FileNamingConventions {
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
private final CryptoWarningHandler cryptoWarningHandler;
|
||||
private final ExecutorService backgroundTaskExecutor;
|
||||
private final Path dataRoot;
|
||||
private final Path metadataRoot;
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
|
||||
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String fsRoot) {
|
||||
CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) {
|
||||
Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
|
||||
this.cryptor = cryptor;
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
this.backgroundTaskExecutor = backgroundTaskExecutor;
|
||||
this.dataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("d");
|
||||
this.metadataRoot = FileSystems.getDefault().getPath(fsRoot).resolve("m");
|
||||
this.dataRoot = vaultRootPath.resolve("d");
|
||||
this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
|
||||
if (DavMethods.METHOD_MKCOL.equals(request.getMethod()) || locator.isRootLocation()) {
|
||||
final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath());
|
||||
if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
|
||||
final String parentResourcePath = FilenameUtils.getFullPathNoEndSeparator(locator.getResourcePath());
|
||||
final Path parentDirectoryPath = createEncryptedDirectoryPath(parentResourcePath);
|
||||
return new EncryptedDirDuringCreation(this, locator, request.getDavSession(), lockManager, cryptor, filenameTranslator, parentDirectoryPath);
|
||||
}
|
||||
|
||||
if (locator.isRootLocation()) {
|
||||
final Path dirpath = createEncryptedDirectoryPath("");
|
||||
return createDirectory(locator, request.getDavSession(), dirpath);
|
||||
}
|
||||
|
||||
final Path filepath = getEncryptedFilePath(locator.getResourcePath());
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
|
||||
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
|
||||
if (filepath.getFileName().toString().endsWith(".dir")) {
|
||||
final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath());
|
||||
return createDirectory(locator, request.getDavSession(), dirpath);
|
||||
} else if (Files.isRegularFile(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
if (Files.exists(dirFilePath)) {
|
||||
final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath());
|
||||
return createDirectory(locator, request.getDavSession(), dirPath);
|
||||
} else if (Files.exists(filepath) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
|
||||
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
|
||||
return createFilePart(locator, request.getDavSession(), request, filepath);
|
||||
} else if (Files.isRegularFile(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
} else if (Files.exists(filepath) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
|
||||
return createFile(locator, request.getDavSession(), filepath);
|
||||
} else {
|
||||
return createNonExisting(locator, request.getDavSession());
|
||||
@@ -71,31 +78,63 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat
|
||||
@Override
|
||||
public final DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
|
||||
if (locator.isRootLocation()) {
|
||||
final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath());
|
||||
final Path dirpath = createEncryptedDirectoryPath("");
|
||||
return createDirectory(locator, session, dirpath);
|
||||
}
|
||||
|
||||
final Path filepath = getEncryptedFilePath(locator.getResourcePath());
|
||||
if (filepath.getFileName().toString().endsWith(".dir")) {
|
||||
final Path dirpath = getEncryptedDirectoryPath(locator.getResourcePath());
|
||||
return createDirectory(locator, session, dirpath);
|
||||
} else if (Files.isRegularFile(filepath)) {
|
||||
final Path dirFilePath = getEncryptedDirectoryFilePath(locator.getResourcePath());
|
||||
if (Files.exists(dirFilePath)) {
|
||||
final Path dirPath = createEncryptedDirectoryPath(locator.getResourcePath());
|
||||
return createDirectory(locator, session, dirPath);
|
||||
} else if (Files.exists(filepath)) {
|
||||
return createFile(locator, session, filepath);
|
||||
} else {
|
||||
return createNonExisting(locator, session);
|
||||
}
|
||||
}
|
||||
|
||||
DavResource createChildDirectoryResource(DavResourceLocator locator, DavSession session, Path existingDirectoryFile) throws DavException {
|
||||
try {
|
||||
final String directoryId = new String(readAllBytesAtomically(existingDirectoryFile), StandardCharsets.UTF_8);
|
||||
final String directory = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
final Path dirpath = dataRoot.resolve(directory);
|
||||
return createDirectory(locator, session, dirpath);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
DavResource createChildFileResource(DavResourceLocator locator, DavSession session, Path existingFile) throws DavException {
|
||||
return createFile(locator, session, existingFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws IOException
|
||||
*/
|
||||
private Path getEncryptedFilePath(String relativeCleartextPath) throws DavException {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
try {
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
return parent.resolve(encryptedFilename);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Absolute file path for a given cleartext file resourcePath.
|
||||
* @throws IOException
|
||||
*/
|
||||
Path getEncryptedFilePath(String relativeCleartextPath) throws DavException {
|
||||
private Path getEncryptedDirectoryFilePath(String relativeCleartextPath) throws DavException {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath);
|
||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
try {
|
||||
final String encryptedFilename = cryptor.encryptFilename(cleartextFilename, this);
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
|
||||
return parent.resolve(encryptedFilename);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
@@ -106,7 +145,7 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat
|
||||
* @return Absolute directory path for a given cleartext directory resourcePath.
|
||||
* @throws IOException
|
||||
*/
|
||||
Path getEncryptedDirectoryPath(String relativeCleartextPath) throws DavException {
|
||||
private Path createEncryptedDirectoryPath(String relativeCleartextPath) throws DavException {
|
||||
assert Strings.isEmpty(relativeCleartextPath) || !relativeCleartextPath.endsWith("/");
|
||||
try {
|
||||
final Path result;
|
||||
@@ -116,9 +155,9 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat
|
||||
result = dataRoot.resolve(fixedRootDirectory);
|
||||
} else {
|
||||
final String parentCleartextPath = FilenameUtils.getPathNoEndSeparator(relativeCleartextPath);
|
||||
final Path parent = getEncryptedDirectoryPath(parentCleartextPath);
|
||||
final Path parent = createEncryptedDirectoryPath(parentCleartextPath);
|
||||
final String cleartextFilename = FilenameUtils.getName(relativeCleartextPath);
|
||||
final String encryptedFilename = cryptor.encryptFilename(cleartextFilename, CryptoResourceFactory.this);
|
||||
final String encryptedFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
|
||||
final Path directoryFile = parent.resolve(encryptedFilename);
|
||||
final String directoryId;
|
||||
if (Files.exists(directoryFile)) {
|
||||
@@ -146,7 +185,7 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat
|
||||
}
|
||||
|
||||
private EncryptedDir createDirectory(DavResourceLocator locator, DavSession session, Path dirPath) {
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor, dirPath);
|
||||
return new EncryptedDir(this, locator, session, lockManager, cryptor, filenameTranslator, dirPath);
|
||||
}
|
||||
|
||||
private NonExistingNode createNonExisting(DavResourceLocator locator, DavSession session) {
|
||||
@@ -169,23 +208,4 @@ public class CryptoResourceFactory implements DavResourceFactory, CryptorMetadat
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
Files.createDirectories(metadataDir);
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
writeAllBytesAtomically(metadataFile, encryptedMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readMetadata(String metadataGroup) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
if (!Files.isReadable(metadataFile)) {
|
||||
return null;
|
||||
} else {
|
||||
return readAllBytesAtomically(metadataFile);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,11 @@ package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -19,6 +23,7 @@ import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
@@ -41,20 +46,23 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.webdav.exceptions.DavRuntimeException;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class EncryptedDir extends AbstractEncryptedNode {
|
||||
class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
private final Path directoryPath;
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
|
||||
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, Path directoryPath) {
|
||||
public EncryptedDir(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path directoryPath) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
if (directoryPath == null || !Files.isDirectory(directoryPath)) {
|
||||
throw new IllegalArgumentException("directoryPath must be an existing directory, but was " + directoryPath);
|
||||
}
|
||||
this.directoryPath = directoryPath;
|
||||
this.filenameTranslator = filenameTranslator;
|
||||
determineProperties();
|
||||
}
|
||||
|
||||
@@ -100,47 +108,77 @@ class EncryptedDir extends AbstractEncryptedNode {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
private void addMemberDir(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
|
||||
LOG.warn("Invokation of addMemberDir(DavResourceLocator childLocator, InputContext inputContext)");
|
||||
try {
|
||||
// the following invokation will create nonexisting directories:
|
||||
factory.getEncryptedDirectoryPath(childLocator.getResourcePath());
|
||||
final String cleartextDirName = FilenameUtils.getName(childLocator.getResourcePath());
|
||||
final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName);
|
||||
final Path dirFilePath = directoryPath.resolve(ciphertextDirName);
|
||||
final String directoryId;
|
||||
if (Files.exists(dirFilePath)) {
|
||||
try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||
c.read(buffer);
|
||||
directoryId = new String(buffer.array(), StandardCharsets.UTF_8);
|
||||
}
|
||||
} else {
|
||||
directoryId = UUID.randomUUID().toString();
|
||||
try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
|
||||
c.write(ByteBuffer.wrap(directoryId.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
Files.createDirectories(directoryPath);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addMemberFile(DavResourceLocator childLocator, InputContext inputContext) throws DavException {
|
||||
final Path filePath = factory.getEncryptedFilePath(childLocator.getResourcePath());
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), channel);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
try {
|
||||
final String cleartextFilename = FilenameUtils.getName(childLocator.getResourcePath());
|
||||
final String ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
final Path filePath = directoryPath.resolve(ciphertextFilename);
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), channel);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (CounterOverflowException e) {
|
||||
// lets indicate this to the client as a "file too big" error
|
||||
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
|
||||
} catch (EncryptFailedException e) {
|
||||
LOG.error("Encryption failed for unknown reasons.", e);
|
||||
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Failed to create file.", e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (CounterOverflowException e) {
|
||||
// lets indicate this to the client as a "file too big" error
|
||||
throw new DavException(DavServletResponse.SC_INSUFFICIENT_SPACE_ON_RESOURCE, e);
|
||||
} catch (EncryptFailedException e) {
|
||||
LOG.error("Encryption failed for unknown reasons.", e);
|
||||
throw new IllegalStateException("Encryption failed for unknown reasons.", e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputContext.getInputStream());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
try {
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directoryPath, cryptor.getPayloadFilesFilter());
|
||||
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(directoryPath, DIRECTORY_CONTENT_FILTER);
|
||||
final List<DavResource> result = new ArrayList<>();
|
||||
|
||||
for (final Path childPath : directoryStream) {
|
||||
try {
|
||||
final String cleartextFilename = cryptor.decryptFilename(childPath.getFileName().toString(), factory);
|
||||
final String cleartextFilename = filenameTranslator.getCleartextFilename(childPath.getFileName().toString());
|
||||
final String cleartextFilepath = FilenameUtils.concat(getResourcePath(), cleartextFilename);
|
||||
final DavResourceLocator childLocator = locator.getFactory().createResourceLocator(locator.getPrefix(), locator.getWorkspacePath(), cleartextFilepath);
|
||||
final DavResource resource = factory.createResource(childLocator, session);
|
||||
final DavResource resource;
|
||||
if (StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), DIR_EXT)) {
|
||||
resource = factory.createChildDirectoryResource(childLocator, session, childPath);
|
||||
} else {
|
||||
assert StringUtil.endsWithIgnoreCase(childPath.getFileName().toString(), FILE_EXT);
|
||||
resource = factory.createChildFileResource(childLocator, session, childPath);
|
||||
}
|
||||
result.add(resource);
|
||||
} catch (DecryptFailedException e) {
|
||||
LOG.warn("Decryption of resource failed: " + childPath);
|
||||
@@ -168,16 +206,21 @@ class EncryptedDir extends AbstractEncryptedNode {
|
||||
|
||||
private void removeMember(AbstractEncryptedNode member) throws DavException {
|
||||
try {
|
||||
if (member.isCollection()) {
|
||||
final String cleartextFilename = FilenameUtils.getName(member.getResourcePath());
|
||||
final String ciphertextFilename;
|
||||
if (member instanceof EncryptedDir) {
|
||||
final EncryptedDir subDir = (EncryptedDir) member;
|
||||
// remove sub-members recursively before deleting own directory
|
||||
for (Iterator<DavResource> iterator = member.getMembers(); iterator.hasNext();) {
|
||||
DavResource m = iterator.next();
|
||||
member.removeMember(m);
|
||||
}
|
||||
final Path memberDirectoryPath = factory.getEncryptedDirectoryPath(member.getResourcePath());
|
||||
Files.deleteIfExists(memberDirectoryPath);
|
||||
Files.deleteIfExists(subDir.directoryPath);
|
||||
ciphertextFilename = filenameTranslator.getEncryptedDirName(cleartextFilename);
|
||||
} else {
|
||||
ciphertextFilename = filenameTranslator.getEncryptedFilename(cleartextFilename);
|
||||
}
|
||||
final Path memberPath = factory.getEncryptedFilePath(member.getResourcePath());
|
||||
final Path memberPath = directoryPath.resolve(ciphertextFilename);
|
||||
Files.deleteIfExists(memberPath);
|
||||
} catch (FileNotFoundException e) {
|
||||
// no-op
|
||||
@@ -239,7 +282,7 @@ class EncryptedDir extends AbstractEncryptedNode {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
protected void determineProperties() {
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.io.FilenameUtils;
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceIterator;
|
||||
import org.apache.jackrabbit.webdav.DavResourceLocator;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSession;
|
||||
import org.apache.jackrabbit.webdav.io.InputContext;
|
||||
import org.apache.jackrabbit.webdav.io.OutputContext;
|
||||
import org.apache.jackrabbit.webdav.lock.LockManager;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
|
||||
class EncryptedDirDuringCreation extends AbstractEncryptedNode {
|
||||
|
||||
private final Path parentDir;
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
|
||||
public EncryptedDirDuringCreation(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, LockManager lockManager, Cryptor cryptor, FilenameTranslator filenameTranslator, Path parentDir) {
|
||||
super(factory, locator, session, lockManager, cryptor);
|
||||
this.parentDir = parentDir;
|
||||
this.filenameTranslator = filenameTranslator;
|
||||
}
|
||||
|
||||
public void doCreate() throws DavException {
|
||||
try {
|
||||
final String cleartextDirName = FilenameUtils.getName(locator.getResourcePath());
|
||||
final String ciphertextDirName = filenameTranslator.getEncryptedDirName(cleartextDirName);
|
||||
final Path dirFilePath = parentDir.resolve(ciphertextDirName);
|
||||
final String directoryId = UUID.randomUUID().toString();
|
||||
try (final FileChannel c = FileChannel.open(dirFilePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
|
||||
c.write(ByteBuffer.wrap(directoryId.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (FileAlreadyExistsException e) {
|
||||
throw new DavException(DavServletResponse.SC_METHOD_NOT_ALLOWED);
|
||||
}
|
||||
final Path directoryPath = filenameTranslator.getEncryptedDirectoryPath(directoryId);
|
||||
Files.createDirectories(directoryPath);
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Path getPhysicalPath() {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean exists() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCollection() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getModificationTime() {
|
||||
return Instant.now().toEpochMilli();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void spool(OutputContext outputContext) throws IOException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addMember(DavResource resource, InputContext inputContext) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public DavResourceIterator getMembers() {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeMember(DavResource member) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode destination) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copy(AbstractEncryptedNode destination, boolean shallow) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class EncryptedFile extends AbstractEncryptedNode {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
protected void determineProperties() {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
||||
|
||||
@@ -6,23 +6,18 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.PathMatcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
interface FileNamingConventions {
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
/**
|
||||
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
|
||||
* 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.<br/>
|
||||
@@ -31,14 +26,24 @@ interface FileNamingConventions {
|
||||
int ENCRYPTED_FILENAME_LENGTH_LIMIT = 136;
|
||||
|
||||
/**
|
||||
* For plaintext file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
* For encrypted directory names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String BASIC_FILE_EXT = ".aes";
|
||||
String DIR_EXT = ".dir";
|
||||
|
||||
/**
|
||||
* For plaintext file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
* For encrypted direcotry names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_NAME_FILE_EXT = ".lng.aes";
|
||||
String LONG_DIR_EXT = ".lng.dir";
|
||||
|
||||
/**
|
||||
* For encrypted file names <= {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String FILE_EXT = ".file";
|
||||
|
||||
/**
|
||||
* For encrypted file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars.
|
||||
*/
|
||||
String LONG_FILE_EXT = ".lng.file";
|
||||
|
||||
/**
|
||||
* Length of prefix in file names > {@value #ENCRYPTED_FILENAME_LENGTH_LIMIT} chars used to determine the corresponding metadata file.
|
||||
@@ -56,11 +61,17 @@ interface FileNamingConventions {
|
||||
@Override
|
||||
public boolean matches(Path path) {
|
||||
final String filename = path.getFileName().toString();
|
||||
if (StringUtils.endsWithIgnoreCase(filename, LONG_NAME_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_NAME_FILE_EXT);
|
||||
if (StringUtils.endsWithIgnoreCase(filename, LONG_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_FILE_EXT);
|
||||
return LONG_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, BASIC_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, BASIC_FILE_EXT);
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, FILE_EXT);
|
||||
return BASIC_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, LONG_DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, LONG_DIR_EXT);
|
||||
return LONG_NAME_PATTERN.matcher(basename).matches();
|
||||
} else if (StringUtils.endsWithIgnoreCase(filename, DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(filename, DIR_EXT);
|
||||
return BASIC_NAME_PATTERN.matcher(basename).matches();
|
||||
} else {
|
||||
return false;
|
||||
@@ -69,4 +80,14 @@ interface FileNamingConventions {
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter to determine files of interest in encrypted directory. Based on {@link #ENCRYPTED_FILE_MATCHER}.
|
||||
*/
|
||||
Filter<Path> DIRECTORY_CONTENT_FILTER = new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
class FilenameTranslator implements FileNamingConventions {
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final Path dataRoot;
|
||||
private final Path metadataRoot;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
public FilenameTranslator(Cryptor cryptor, Path vaultRoot) {
|
||||
this.cryptor = cryptor;
|
||||
this.dataRoot = vaultRoot.resolve("d");
|
||||
this.metadataRoot = vaultRoot.resolve("m");
|
||||
}
|
||||
|
||||
/* file and directory name en/decryption */
|
||||
|
||||
public Path getEncryptedDirectoryPath(String directoryId) {
|
||||
final String encrypted = cryptor.encryptDirectoryPath(directoryId, FileSystems.getDefault().getSeparator());
|
||||
return dataRoot.resolve(encrypted);
|
||||
}
|
||||
|
||||
public String getEncryptedFilename(String cleartextFilename) throws IOException {
|
||||
return getEncryptedFilename(cleartextFilename, FILE_EXT, LONG_FILE_EXT);
|
||||
}
|
||||
|
||||
public String getEncryptedDirName(String cleartextDirName) throws IOException {
|
||||
return getEncryptedFilename(cleartextDirName, DIR_EXT, LONG_DIR_EXT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* For filenames longer than this limit we use a metadata file containing the full encrypted paths. For the actual filename a unique alternative is created by concatenating the metadata filename
|
||||
* and a unique id.
|
||||
*/
|
||||
private String getEncryptedFilename(String cleartextFilename, String basicExt, String longExt) throws IOException {
|
||||
final String ivAndCiphertext = cryptor.encryptFilename(cleartextFilename);
|
||||
if (ivAndCiphertext.length() + basicExt.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
final String longFilename = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + longExt;
|
||||
this.writeMetadata(metadataGroup, metadata);
|
||||
return longFilename;
|
||||
} else {
|
||||
return ivAndCiphertext + basicExt;
|
||||
}
|
||||
}
|
||||
|
||||
public String getCleartextFilename(String encryptedFilename) throws DecryptFailedException, IOException {
|
||||
final String ciphertext;
|
||||
if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_FILE_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, FILE_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, FILE_EXT);
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, LONG_DIR_EXT)) {
|
||||
final String basename = StringUtils.removeEndIgnoreCase(encryptedFilename, LONG_DIR_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = readMetadata(metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (StringUtils.endsWithIgnoreCase(encryptedFilename, DIR_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(encryptedFilename, DIR_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + encryptedFilename);
|
||||
}
|
||||
return cryptor.decryptFilename(ciphertext);
|
||||
}
|
||||
|
||||
/* Long name metadata files */
|
||||
|
||||
private void writeMetadata(String metadataGroup, LongFilenameMetadata metadata) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
Files.createDirectories(metadataDir);
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.DSYNC); final FileLock lock = c.lock()) {
|
||||
byte[] bytes = objectMapper.writeValueAsBytes(metadata);
|
||||
c.write(ByteBuffer.wrap(bytes));
|
||||
}
|
||||
}
|
||||
|
||||
private LongFilenameMetadata readMetadata(String metadataGroup) throws IOException {
|
||||
final Path metadataDir = metadataRoot.resolve(metadataGroup.substring(0, 2));
|
||||
final Path metadataFile = metadataDir.resolve(metadataGroup.substring(2));
|
||||
if (!Files.isReadable(metadataFile)) {
|
||||
return new LongFilenameMetadata();
|
||||
} else {
|
||||
try (final FileChannel c = FileChannel.open(metadataFile, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.lock(0L, Long.MAX_VALUE, true)) {
|
||||
final ByteBuffer buffer = ByteBuffer.allocate((int) c.size());
|
||||
c.read(buffer);
|
||||
return objectMapper.readValue(buffer.array(), LongFilenameMetadata.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
@@ -67,11 +67,6 @@ class NonExistingNode extends AbstractEncryptedNode {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void determineProperties() {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode destination) throws DavException {
|
||||
throw new UnsupportedOperationException("Resource doesn't exist.");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
@@ -16,11 +17,14 @@ import java.util.concurrent.TimeUnit;
|
||||
import javax.servlet.ServletConfig;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavLocatorFactory;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
import org.apache.jackrabbit.webdav.DavResourceFactory;
|
||||
import org.apache.jackrabbit.webdav.DavServletResponse;
|
||||
import org.apache.jackrabbit.webdav.DavSessionProvider;
|
||||
import org.apache.jackrabbit.webdav.WebdavRequest;
|
||||
import org.apache.jackrabbit.webdav.WebdavResponse;
|
||||
import org.apache.jackrabbit.webdav.server.AbstractWebdavServlet;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
|
||||
@@ -67,6 +71,17 @@ public class WebDavServlet extends AbstractWebdavServlet {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doMkCol(WebdavRequest request, WebdavResponse response, DavResource resource) throws IOException, DavException {
|
||||
if (resource instanceof EncryptedDirDuringCreation) {
|
||||
EncryptedDirDuringCreation dir = (EncryptedDirDuringCreation) resource;
|
||||
dir.doCreate();
|
||||
response.setStatus(DavServletResponse.SC_CREATED);
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isPreconditionValid(WebdavRequest request, DavResource resource) {
|
||||
return !resource.exists() || request.matchesIfHeader(resource);
|
||||
|
||||
@@ -15,15 +15,12 @@ import java.io.OutputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
@@ -40,10 +37,8 @@ import javax.security.auth.Destroyable;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.commons.io.output.NullOutputStream;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.bouncycastle.crypto.generators.SCrypt;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.crypto.CryptorMetadataSupport;
|
||||
import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException;
|
||||
import org.cryptomator.crypto.exceptions.CounterOverflowException;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
@@ -55,10 +50,9 @@ import org.cryptomator.crypto.exceptions.WrongPasswordException;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelInputStream;
|
||||
import org.cryptomator.crypto.io.SeekableByteChannelOutputStream;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, FileNamingConventions {
|
||||
public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
|
||||
/**
|
||||
* Defined in static initializer. Defaults to 256, but falls back to maximum value possible, if JCE Unlimited Strength Jurisdiction Policy Files isn't installed. Those files can be downloaded
|
||||
@@ -296,71 +290,20 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
|
||||
return encryptedThenHashedPath.substring(0, 2) + nativePathSep + encryptedThenHashedPath.substring(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Each path component, i.e. file or directory name separated by path separators, gets encrypted for its own.<br/>
|
||||
* Encryption will blow up the filename length due to aes block sizes, IVs and base32 encoding. The result may be too long for some old file systems.<br/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in {@link FileNamingConventions#ENCRYPTED_FILENAME_LENGTH_LIMIT}.<br/>
|
||||
* <br/>
|
||||
* In any case we will create the encrypted filename normally. For those, that are too long, we calculate a checksum. No cryptographically secure hash is needed here. We just want an uniform
|
||||
* distribution for better load balancing. All encrypted filenames with the same checksum will then share a metadata file, in which a lookup map between encrypted filenames and short unique
|
||||
* alternative names are stored.<br/>
|
||||
* <br/>
|
||||
* These alternative names consist of the checksum, a unique id and a special file extension defined in {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
|
||||
*/
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
|
||||
public String encryptFilename(String cleartextName) {
|
||||
final byte[] cleartextBytes = cleartextName.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
// encrypt:
|
||||
final byte[] encryptedBytes = AesSivCipherUtil.sivEncrypt(primaryMasterKey, hMacMasterKey, cleartextBytes);
|
||||
final String ivAndCiphertext = ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
|
||||
|
||||
if (ivAndCiphertext.length() + BASIC_FILE_EXT.length() > ENCRYPTED_FILENAME_LENGTH_LIMIT) {
|
||||
final String metadataGroup = ivAndCiphertext.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup);
|
||||
final String alternativeFileName = metadataGroup + metadata.getOrCreateUuidForEncryptedFilename(ivAndCiphertext).toString() + LONG_NAME_FILE_EXT;
|
||||
this.storeMetadata(ioSupport, metadataGroup, metadata);
|
||||
return alternativeFileName;
|
||||
} else {
|
||||
return ivAndCiphertext + BASIC_FILE_EXT;
|
||||
}
|
||||
return ENCRYPTED_FILENAME_CODEC.encodeAsString(encryptedBytes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws DecryptFailedException, IOException {
|
||||
final String ciphertext;
|
||||
if (ciphertextName.endsWith(LONG_NAME_FILE_EXT)) {
|
||||
final String basename = StringUtils.removeEnd(ciphertextName, LONG_NAME_FILE_EXT);
|
||||
final String metadataGroup = basename.substring(0, LONG_NAME_PREFIX_LENGTH);
|
||||
final String uuid = basename.substring(LONG_NAME_PREFIX_LENGTH);
|
||||
final LongFilenameMetadata metadata = this.getMetadata(ioSupport, metadataGroup);
|
||||
ciphertext = metadata.getEncryptedFilenameForUUID(UUID.fromString(uuid));
|
||||
} else if (ciphertextName.endsWith(BASIC_FILE_EXT)) {
|
||||
ciphertext = StringUtils.removeEndIgnoreCase(ciphertextName, BASIC_FILE_EXT);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported path component: " + ciphertextName);
|
||||
}
|
||||
|
||||
// decrypt:
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertext);
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
final byte[] encryptedBytes = ENCRYPTED_FILENAME_CODEC.decode(ciphertextName);
|
||||
final byte[] cleartextBytes = AesSivCipherUtil.sivDecrypt(primaryMasterKey, hMacMasterKey, encryptedBytes);
|
||||
|
||||
return new String(cleartextBytes, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LongFilenameMetadata getMetadata(CryptorMetadataSupport ioSupport, String metadataGroup) throws IOException {
|
||||
final byte[] fileContent = ioSupport.readMetadata(metadataGroup);
|
||||
if (fileContent == null) {
|
||||
return new LongFilenameMetadata();
|
||||
} else {
|
||||
return objectMapper.readValue(fileContent, LongFilenameMetadata.class);
|
||||
}
|
||||
}
|
||||
|
||||
private void storeMetadata(CryptorMetadataSupport ioSupport, String metadataGroup, LongFilenameMetadata metadata) throws JsonProcessingException, IOException {
|
||||
ioSupport.writeMetadata(metadataGroup, objectMapper.writeValueAsBytes(metadata));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
|
||||
// read header:
|
||||
@@ -616,14 +559,4 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration, Fi
|
||||
return plaintextSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return new Filter<Path>() {
|
||||
@Override
|
||||
public boolean accept(Path entry) throws IOException {
|
||||
return ENCRYPTED_FILE_MATCHER.matches(entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto.aes256;
|
||||
|
||||
import org.apache.commons.codec.binary.Base32;
|
||||
import org.apache.commons.codec.binary.BaseNCodec;
|
||||
|
||||
interface AesCryptographicConfiguration {
|
||||
|
||||
/**
|
||||
@@ -78,4 +81,9 @@ interface AesCryptographicConfiguration {
|
||||
*/
|
||||
int AES_BLOCK_LENGTH = 16;
|
||||
|
||||
/**
|
||||
* How to encode the encrypted file names safely. Base32 uses only alphanumeric characters and is case-insensitive.
|
||||
*/
|
||||
BaseNCodec ENCRYPTED_FILENAME_CODEC = new Base32();
|
||||
|
||||
}
|
||||
|
||||
@@ -15,13 +15,10 @@ import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.cryptomator.crypto.CryptorMetadataSupport;
|
||||
import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.EncryptFailedException;
|
||||
import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
|
||||
@@ -210,7 +207,6 @@ public class Aes256CryptorTest {
|
||||
|
||||
@Test
|
||||
public void testEncryptionOfFilenames() throws IOException, DecryptFailedException {
|
||||
final CryptorMetadataSupport ioSupportMock = new CryptoIOSupportMock();
|
||||
final Aes256Cryptor cryptor = new Aes256Cryptor();
|
||||
|
||||
// directory paths
|
||||
@@ -222,35 +218,19 @@ public class Aes256CryptorTest {
|
||||
// long file names
|
||||
final String str50chars = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee";
|
||||
final String originalPath2 = str50chars + str50chars + str50chars + str50chars + str50chars + "_isLongerThan255Chars.txt";
|
||||
final String encryptedPath2a = cryptor.encryptFilename(originalPath2, ioSupportMock);
|
||||
final String encryptedPath2b = cryptor.encryptFilename(originalPath2, ioSupportMock);
|
||||
final String encryptedPath2a = cryptor.encryptFilename(originalPath2);
|
||||
final String encryptedPath2b = cryptor.encryptFilename(originalPath2);
|
||||
Assert.assertEquals(encryptedPath2a, encryptedPath2b);
|
||||
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a, ioSupportMock);
|
||||
final String decryptedPath2 = cryptor.decryptFilename(encryptedPath2a);
|
||||
Assert.assertEquals(originalPath2, decryptedPath2);
|
||||
|
||||
// block size length file names
|
||||
final String originalPath3 = "aaaabbbbccccdddd";
|
||||
final String encryptedPath3a = cryptor.encryptFilename(originalPath3, ioSupportMock);
|
||||
final String encryptedPath3b = cryptor.encryptFilename(originalPath3, ioSupportMock);
|
||||
final String encryptedPath3a = cryptor.encryptFilename(originalPath3);
|
||||
final String encryptedPath3b = cryptor.encryptFilename(originalPath3);
|
||||
Assert.assertEquals(encryptedPath3a, encryptedPath3b);
|
||||
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a, ioSupportMock);
|
||||
final String decryptedPath3 = cryptor.decryptFilename(encryptedPath3a);
|
||||
Assert.assertEquals(originalPath3, decryptedPath3);
|
||||
}
|
||||
|
||||
private static class CryptoIOSupportMock implements CryptorMetadataSupport {
|
||||
|
||||
private final Map<String, byte[]> map = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void writeMetadata(String metadataGroup, byte[] encryptedMetadata) {
|
||||
map.put(metadataGroup, encryptedMetadata);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] readMetadata(String metadataGroup) {
|
||||
return map.get(metadataGroup);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
@@ -40,13 +38,13 @@ public class AbstractCryptorDecorator implements Cryptor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
|
||||
return cryptor.encryptFilename(cleartextName, ioSupport);
|
||||
public String encryptFilename(String cleartextName) {
|
||||
return cryptor.encryptFilename(cleartextName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException {
|
||||
return cryptor.decryptFilename(ciphertextName, ioSupport);
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
return cryptor.decryptFilename(ciphertextName);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,11 +72,6 @@ public class AbstractCryptorDecorator implements Cryptor {
|
||||
return cryptor.encryptFile(plaintextFile, encryptedFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter<Path> getPayloadFilesFilter() {
|
||||
return cryptor.getPayloadFilesFilter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws DestroyFailedException {
|
||||
cryptor.destroy();
|
||||
|
||||
@@ -12,8 +12,6 @@ import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.DirectoryStream.Filter;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import javax.security.auth.Destroyable;
|
||||
|
||||
@@ -57,22 +55,18 @@ public interface Cryptor extends Destroyable {
|
||||
* Encrypts the name of a file. See {@link #encryptDirectoryPath(String, char)} for parent dir.
|
||||
*
|
||||
* @param cleartextName A plaintext filename without any preceeding directory paths.
|
||||
* @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this support object.
|
||||
* @return Encrypted filename.
|
||||
* @throws IOException If ioSupport throws an IOException
|
||||
*/
|
||||
String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException;
|
||||
String encryptFilename(String cleartextName);
|
||||
|
||||
/**
|
||||
* Decrypts the name of a file.
|
||||
*
|
||||
* @param ciphertextName A ciphertext filename without any preceeding directory paths.
|
||||
* @param ioSupport Support object allowing the Cryptor to read and write its own metadata to a storage space associated with this support object.
|
||||
* @return Decrypted filename.
|
||||
* @throws DecryptFailedException If the decryption failed for various reasons (including wrong password).
|
||||
* @throws IOException If ioSupport throws an IOException
|
||||
*/
|
||||
String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException;
|
||||
String decryptFilename(String ciphertextName) throws DecryptFailedException;
|
||||
|
||||
/**
|
||||
* @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
|
||||
@@ -105,9 +99,4 @@ public interface Cryptor extends Destroyable {
|
||||
*/
|
||||
Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException;
|
||||
|
||||
/**
|
||||
* @return A filter, that returns <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting metadata file of the {@link Cryptor}.
|
||||
*/
|
||||
Filter<Path> getPayloadFilesFilter();
|
||||
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2014 Sebastian Stenzel
|
||||
* This file is licensed under the terms of the MIT license.
|
||||
* See the LICENSE.txt file for more info.
|
||||
*
|
||||
* Contributors:
|
||||
* Sebastian Stenzel - initial API and implementation
|
||||
******************************************************************************/
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Methods that may be called by the Cryptor when accessing a path.
|
||||
*/
|
||||
public interface CryptorMetadataSupport {
|
||||
|
||||
/**
|
||||
* Persists encryptedMetadata in a metadata group.
|
||||
*
|
||||
* @param metadataFilename File relative to
|
||||
* @throws IOException
|
||||
*/
|
||||
void writeMetadata(String metadataGroup, byte[] encryptedMetadata) throws IOException;
|
||||
|
||||
/**
|
||||
* @return Previously written metadata stored in the given metadata group or <code>null</code> if no such group exists.
|
||||
*/
|
||||
byte[] readMetadata(String metadataGroup) throws IOException;
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.cryptomator.crypto;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.collections4.BidiMap;
|
||||
@@ -38,22 +37,22 @@ public class PathCachingCryptorDecorator extends AbstractCryptorDecorator {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String encryptFilename(String cleartextName, CryptorMetadataSupport ioSupport) throws IOException {
|
||||
public String encryptFilename(String cleartextName) {
|
||||
if (nameCache.containsKey(cleartextName)) {
|
||||
return nameCache.get(cleartextName);
|
||||
} else {
|
||||
final String ciphertextName = cryptor.encryptFilename(cleartextName, ioSupport);
|
||||
final String ciphertextName = cryptor.encryptFilename(cleartextName);
|
||||
nameCache.put(cleartextName, ciphertextName);
|
||||
return ciphertextName;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decryptFilename(String ciphertextName, CryptorMetadataSupport ioSupport) throws IOException, DecryptFailedException {
|
||||
public String decryptFilename(String ciphertextName) throws DecryptFailedException {
|
||||
if (nameCache.containsValue(ciphertextName)) {
|
||||
return nameCache.getKey(ciphertextName);
|
||||
} else {
|
||||
final String cleartextName = cryptor.decryptFilename(ciphertextName, ioSupport);
|
||||
final String cleartextName = cryptor.decryptFilename(ciphertextName);
|
||||
nameCache.put(cleartextName, ciphertextName);
|
||||
return ciphertextName;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user