mirror of
https://github.com/cryptomator/cryptomator.git
synced 2024-11-23 12:09:45 +00:00
- locking file header during creation,
- suggesting range request for files > 32MiB only
This commit is contained in:
parent
8845efb983
commit
0e3513e86d
@ -13,6 +13,7 @@ import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.BasicFileAttributeView;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.List;
|
||||
|
||||
@ -32,6 +33,7 @@ import org.apache.jackrabbit.webdav.property.DavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyName;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertyNameSet;
|
||||
import org.apache.jackrabbit.webdav.property.DavPropertySet;
|
||||
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
|
||||
import org.apache.jackrabbit.webdav.property.PropEntry;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.cryptomator.webdav.exceptions.IORuntimeException;
|
||||
@ -59,6 +61,15 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
this.cryptor = cryptor;
|
||||
this.filePath = filePath;
|
||||
this.properties = new DavPropertySet();
|
||||
if (filePath != null && Files.exists(filePath)) {
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + filePath.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,22 +143,24 @@ abstract class AbstractEncryptedNode implements DavResource {
|
||||
|
||||
LOG.info("Set property {}", property.getName());
|
||||
|
||||
try {
|
||||
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(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(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
|
||||
if (Files.exists(filePath)) {
|
||||
try {
|
||||
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(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(filePath, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS);
|
||||
attrView.setTimes(lastModifiedTime, null, null);
|
||||
LOG.info("Updating Last Modified Date: {}", lastModifiedTime.toString());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ import org.apache.logging.log4j.util.Strings;
|
||||
import org.cryptomator.crypto.Cryptor;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
|
||||
public class CryptoResourceFactory implements DavResourceFactory, FileNamingConventions {
|
||||
public class CryptoResourceFactory implements DavResourceFactory, FileConstants {
|
||||
|
||||
private final LockManager lockManager = new SimpleLockManager();
|
||||
private final Cryptor cryptor;
|
||||
|
@ -13,7 +13,6 @@ 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;
|
||||
@ -21,7 +20,6 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
@ -53,7 +51,7 @@ import org.eclipse.jetty.util.StringUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class EncryptedDir extends AbstractEncryptedNode implements FileNamingConventions {
|
||||
class EncryptedDir extends AbstractEncryptedNode implements FileConstants {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedDir.class);
|
||||
private final FilenameTranslator filenameTranslator;
|
||||
@ -63,7 +61,8 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
|
||||
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();
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -173,8 +172,8 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
|
||||
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);
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.WRITE, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); final FileLock lock = c.lock(0L, FILE_HEADER_LENGTH, false)) {
|
||||
cryptor.encryptFile(inputContext.getInputStream(), c);
|
||||
} catch (SecurityException e) {
|
||||
throw new DavException(DavServletResponse.SC_FORBIDDEN, e);
|
||||
} catch (CounterOverflowException e) {
|
||||
@ -355,20 +354,4 @@ class EncryptedDir extends AbstractEncryptedNode implements FileNamingConvention
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected void determineProperties() {
|
||||
properties.add(new ResourceType(ResourceType.COLLECTION));
|
||||
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
|
||||
try {
|
||||
if (Files.exists(filePath)) {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + filePath, e);
|
||||
// don't add any further properties
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,13 +10,15 @@ package org.cryptomator.webdav.jackrabbit;
|
||||
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.nio.channels.SeekableByteChannel;
|
||||
import java.nio.file.AtomicMoveNotSupportedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
|
||||
import org.apache.jackrabbit.webdav.DavException;
|
||||
import org.apache.jackrabbit.webdav.DavResource;
|
||||
@ -37,7 +39,7 @@ import org.eclipse.jetty.http.HttpHeaderValue;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class EncryptedFile extends AbstractEncryptedNode {
|
||||
class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFile.class);
|
||||
|
||||
@ -49,7 +51,24 @@ class EncryptedFile extends AbstractEncryptedNode {
|
||||
throw new IllegalArgumentException("filePath must not be null");
|
||||
}
|
||||
this.cryptoWarningHandler = cryptoWarningHandler;
|
||||
this.determineProperties();
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ, StandardOpenOption.DSYNC); final FileLock lock = c.tryLock(0L, FILE_HEADER_LENGTH, true)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(c);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
if (contentLength > RANGE_REQUEST_LOWER_LIMIT) {
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
}
|
||||
} catch (OverlappingFileLockException e) {
|
||||
// file header currently locked, report -1 for unknown size.
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, -1l));
|
||||
} catch (IOException 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.");
|
||||
// don't add content length DAV property
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -95,32 +114,6 @@ class EncryptedFile extends AbstractEncryptedNode {
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected void determineProperties() {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
|
||||
final Long contentLength = cryptor.decryptedContentLength(channel);
|
||||
properties.add(new DefaultDavProperty<Long>(DavPropertyName.GETCONTENTLENGTH, contentLength));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error reading filesize " + filePath.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
} catch (MacAuthenticationFailedException e) {
|
||||
LOG.warn("Content length couldn't be determined due to MAC authentication violation.");
|
||||
// don't add content length DAV property
|
||||
}
|
||||
|
||||
try {
|
||||
final BasicFileAttributes attrs = Files.readAttributes(filePath, BasicFileAttributes.class);
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
|
||||
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
|
||||
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
|
||||
} catch (IOException e) {
|
||||
LOG.error("Error determining metadata " + filePath.toString(), e);
|
||||
throw new IORuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void move(AbstractEncryptedNode dest) throws DavException, IOException {
|
||||
final Path srcPath = filePath;
|
||||
|
@ -16,7 +16,17 @@ import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
interface FileNamingConventions {
|
||||
interface FileConstants {
|
||||
|
||||
/**
|
||||
* Number of bytes in the file header.
|
||||
*/
|
||||
long FILE_HEADER_LENGTH = 96;
|
||||
|
||||
/**
|
||||
* Allow range requests for files > 32MiB.
|
||||
*/
|
||||
long RANGE_REQUEST_LOWER_LIMIT = 32 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Maximum path length on some file systems or cloud storage providers is restricted.<br/>
|
@ -16,7 +16,7 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
class FilenameTranslator implements FileNamingConventions {
|
||||
class FilenameTranslator implements FileConstants {
|
||||
|
||||
private final Cryptor cryptor;
|
||||
private final Path dataRoot;
|
||||
@ -46,7 +46,7 @@ class FilenameTranslator implements FileNamingConventions {
|
||||
|
||||
/**
|
||||
* 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/>
|
||||
* This means that we need a workaround for filenames longer than the limit defined in {@link FileConstants#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.
|
||||
|
@ -67,17 +67,6 @@ 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);
|
||||
|
@ -323,7 +323,6 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
|
||||
headerBuf.position(16);
|
||||
headerBuf.get(encryptedContentLengthBytes);
|
||||
final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv);
|
||||
|
||||
// read stored header mac:
|
||||
final byte[] storedHeaderMac = new byte[32];
|
||||
@ -341,7 +340,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
throw new MacAuthenticationFailedException("MAC authentication failed.");
|
||||
}
|
||||
|
||||
return fileSize;
|
||||
return decryptContentLength(encryptedContentLengthBytes, iv);
|
||||
}
|
||||
|
||||
private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) {
|
||||
@ -505,12 +504,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
|
||||
ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
|
||||
final byte[] iv = ivBuf.array();
|
||||
|
||||
// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac)
|
||||
// prefilled with "zero" content length for impatient processes, which want to know the size, before file has been completely written:
|
||||
// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content
|
||||
final ByteBuffer headerBuf = ByteBuffer.allocate(96);
|
||||
headerBuf.position(16);
|
||||
headerBuf.put(encryptContentLength(0l, iv));
|
||||
headerBuf.flip();
|
||||
headerBuf.limit(96);
|
||||
encryptedFile.write(headerBuf);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user