- Support for HTTP Range header fields, thus vastly improved performance for video streaming

- Simplified cryptor implementation for partial decryption
This commit is contained in:
Sebastian Stenzel 2014-12-21 16:54:47 +01:00
parent f76091ddc0
commit 1d05e878ab
26 changed files with 293 additions and 393 deletions

View File

@ -19,7 +19,6 @@ If you want to take a look at the current beta version, go ahead and download [C
## Security
- Default key length is 256 bit (falls back to 128 bit, if JCE isn't installed)
- PBKDF2 key generation
- 4096 bit internal masterkey
- Cryptographically secure random numbers for salts, IVs and the masterkey of course
- Sensitive data is swiped from the heap asap
- Lightweight: Complexity kills security
@ -31,16 +30,12 @@ If you want to take a look at the current beta version, go ahead and download [C
## Dependencies
- Java 8
- Maven
- Awesome 3rd party open source libraries (Apache Commons, Apache Jackrabbit, Jetty, Jackson, ...)
- see pom.xml ;-)
## TODO
### Core
- Support for HTTP range requests
### UI
- Automount of WebDAV volumes for Win/Tux
- Native L&F
- Drive icons in WebDAV volumes
- Change password functionality
- Better explanations on UI

View File

@ -23,9 +23,9 @@ import org.eclipse.jetty.util.thread.ThreadPool;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class WebDAVServer {
public final class WebDavServer {
private static final Logger LOG = LoggerFactory.getLogger(WebDAVServer.class);
private static final Logger LOG = LoggerFactory.getLogger(WebDavServer.class);
private static final String LOCALHOST = "::1";
private static final int MAX_PENDING_REQUESTS = 200;
private static final int MAX_THREADS = 200;
@ -34,7 +34,7 @@ public final class WebDAVServer {
private final Server server;
private int port;
public WebDAVServer() {
public WebDavServer() {
final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
server = new Server(tp);
@ -50,9 +50,10 @@ public final class WebDAVServer {
connector.setHost(LOCALHOST);
final String contextPath = "/";
final String servletPathSpec = "/*";
final ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS);
context.addServlet(getMiltonServletHolder(workDir, contextPath, cryptor), "/*");
context.addServlet(getWebDavServletHolder(workDir, contextPath, cryptor), servletPathSpec);
context.setContextPath(contextPath);
server.setHandler(context);
@ -81,7 +82,7 @@ public final class WebDAVServer {
return server.isStopped();
}
private ServletHolder getMiltonServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
private ServletHolder getWebDavServletHolder(final String workDir, final String contextPath, final Cryptor cryptor) {
final ServletHolder result = new ServletHolder("Cryptomator-WebDAV-Servlet", new WebDavServlet(cryptor));
result.setInitParameter(WebDavServlet.CFG_FS_ROOT, workDir);
result.setInitParameter(WebDavServlet.CFG_HTTP_ROOT, contextPath);

View File

@ -0,0 +1,36 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavServletResponse;
import org.apache.jackrabbit.webdav.DavSession;
abstract class AbstractSessionAwareWebDavResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final DavSession session = request.getDavSession();
if (session != null && session instanceof WebDavSession) {
return createDavResource(locator, (WebDavSession) session, request, response);
} else {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
}
}
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session, DavServletRequest request, DavServletResponse response) throws DavException;
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
if (session != null && session instanceof WebDavSession) {
return createDavResource(locator, (WebDavSession) session);
} else {
throw new DavException(DavServletResponse.SC_INTERNAL_SERVER_ERROR, "Unsupported session type.");
}
}
protected abstract DavResource createDavResource(DavResourceLocator locator, WebDavSession session);
}

View File

@ -8,7 +8,7 @@ import org.apache.commons.collections4.map.LRUMap;
final class BidiLRUMap<K, V> extends AbstractDualBidiMap<K, V> {
public BidiLRUMap(int maxSize) {
BidiLRUMap(int maxSize) {
super(new LRUMap<K, V>(maxSize), new LRUMap<V, K>(maxSize));
}

View File

@ -21,14 +21,14 @@ import org.cryptomator.crypto.Cryptor;
import org.cryptomator.crypto.CryptorIOSupport;
import org.cryptomator.crypto.SensitiveDataSwipeListener;
public class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
class WebDavLocatorFactory extends AbstractLocatorFactory implements SensitiveDataSwipeListener, CryptorIOSupport {
private static final int MAX_CACHED_PATHS = 10000;
private final Path fsRoot;
private final Cryptor cryptor;
private final BidiMap<String, String> pathCache = new BidiLRUMap<>(MAX_CACHED_PATHS); // <decryptedPath, encryptedPath>
public WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
WebDavLocatorFactory(String fsRoot, String httpRoot, Cryptor cryptor) {
super(httpRoot);
this.fsRoot = FileSystems.getDefault().getPath(fsRoot);
this.cryptor = cryptor;

View File

@ -11,6 +11,7 @@ package org.cryptomator.webdav.jackrabbit;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavMethods;
import org.apache.jackrabbit.webdav.DavResource;
@ -24,28 +25,32 @@ import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedDir;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFile;
import org.cryptomator.webdav.jackrabbit.resources.EncryptedFilePart;
import org.cryptomator.webdav.jackrabbit.resources.NonExistingNode;
import org.cryptomator.webdav.jackrabbit.resources.PathUtils;
import org.cryptomator.webdav.jackrabbit.resources.ResourcePathUtils;
import org.eclipse.jetty.http.HttpHeader;
public class WebDavResourceFactory implements DavResourceFactory {
class WebDavResourceFactory implements DavResourceFactory {
private final LockManager lockManager = new SimpleLockManager();
private final Cryptor cryptor;
public WebDavResourceFactory(Cryptor cryptor) {
WebDavResourceFactory(Cryptor cryptor) {
this.cryptor = cryptor;
}
@Override
public DavResource createResource(DavResourceLocator locator, DavServletRequest request, DavServletResponse response) throws DavException {
final Path path = PathUtils.getPhysicalPath(locator);
final Path path = ResourcePathUtils.getPhysicalPath(locator);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (Files.exists(path)) {
return createResource(locator, request.getDavSession());
} else if (DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else if (DavMethods.METHOD_PUT.equals(request.getMethod())) {
if (Files.isRegularFile(path) && DavMethods.METHOD_GET.equals(request.getMethod()) && rangeHeader != null) {
response.setStatus(HttpStatus.SC_PARTIAL_CONTENT);
return createFilePart(locator, request.getDavSession(), request);
} else if (Files.isRegularFile(path) || DavMethods.METHOD_PUT.equals(request.getMethod())) {
return createFile(locator, request.getDavSession());
} else if (Files.isDirectory(path) || DavMethods.METHOD_MKCOL.equals(request.getMethod())) {
return createDirectory(locator, request.getDavSession());
} else {
return createNonExisting(locator, request.getDavSession());
}
@ -53,17 +58,21 @@ public class WebDavResourceFactory implements DavResourceFactory {
@Override
public DavResource createResource(DavResourceLocator locator, DavSession session) throws DavException {
final Path path = PathUtils.getPhysicalPath(locator);
final Path path = ResourcePathUtils.getPhysicalPath(locator);
if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else if (Files.isRegularFile(path)) {
if (Files.isRegularFile(path)) {
return createFile(locator, session);
} else if (Files.isDirectory(path)) {
return createDirectory(locator, session);
} else {
return createNonExisting(locator, session);
}
}
private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request) {
return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor);
}
private EncryptedFile createFile(DavResourceLocator locator, DavSession session) {
return new EncryptedFile(this, locator, session, lockManager, cryptor);
}

View File

@ -9,8 +9,15 @@
package org.cryptomator.webdav.jackrabbit;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.WebdavRequest;
public class WebDavSession implements DavSession {
class WebDavSession implements DavSession {
private final WebdavRequest request;
WebDavSession(WebdavRequest request) {
this.request = request;
}
@Override
public void addReference(Object reference) {
@ -42,4 +49,8 @@ public class WebDavSession implements DavSession {
}
public WebdavRequest getRequest() {
return request;
}
}

View File

@ -12,12 +12,12 @@ import org.apache.jackrabbit.webdav.DavException;
import org.apache.jackrabbit.webdav.DavSessionProvider;
import org.apache.jackrabbit.webdav.WebdavRequest;
public class WebDavSessionProvider implements DavSessionProvider {
class WebDavSessionProvider implements DavSessionProvider {
@Override
public boolean attachSession(WebdavRequest request) throws DavException {
// every user gets a session
request.setDavSession(new WebDavSession());
// every request gets a session
request.setDavSession(new WebDavSession(request));
return true;
}

View File

@ -38,7 +38,7 @@ import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public abstract class AbstractEncryptedNode implements DavResource {
abstract class AbstractEncryptedNode implements DavResource {
private static final Logger LOG = LoggerFactory.getLogger(AbstractEncryptedNode.class);
private static final String DAV_COMPLIANCE_CLASSES = "1, 2";
@ -72,7 +72,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public boolean exists() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
return Files.exists(path);
}
@ -104,7 +104,7 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public long getModificationTime() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
try {
return Files.getLastModifiedTime(path).toMillis();
} catch (IOException e) {
@ -173,8 +173,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public void move(DavResource dest) throws DavException {
final Path src = PathUtils.getPhysicalPath(this);
final Path dst = PathUtils.getPhysicalPath(dest);
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {
@ -195,8 +195,8 @@ public abstract class AbstractEncryptedNode implements DavResource {
@Override
public void copy(DavResource dest, boolean shallow) throws DavException {
final Path src = PathUtils.getPhysicalPath(this);
final Path dst = PathUtils.getPhysicalPath(dest);
final Path src = ResourcePathUtils.getPhysicalPath(this);
final Path dst = ResourcePathUtils.getPhysicalPath(dest);
try {
// check for conflicts:
if (Files.exists(dst) && Files.getLastModifiedTime(dst).toMillis() > Files.getLastModifiedTime(src).toMillis()) {

View File

@ -64,7 +64,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
}
private void addMemberDir(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = PathUtils.getPhysicalPath(resource);
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
try {
Files.createDirectories(childPath);
} catch (SecurityException e) {
@ -76,7 +76,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
}
private void addMemberFile(DavResource resource, InputContext inputContext) throws DavException {
final Path childPath = PathUtils.getPhysicalPath(resource);
final Path childPath = ResourcePathUtils.getPhysicalPath(resource);
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(childPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
@ -94,7 +94,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
public DavResourceIterator getMembers() {
final Path dir = PathUtils.getPhysicalPath(this);
final Path dir = ResourcePathUtils.getPhysicalPath(this);
try {
final DirectoryStream<Path> directoryStream = Files.newDirectoryStream(dir, cryptor.getPayloadFilesFilter());
final List<DavResource> result = new ArrayList<>();
@ -116,7 +116,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
public void removeMember(DavResource member) throws DavException {
final Path memberPath = PathUtils.getPhysicalPath(member);
final Path memberPath = ResourcePathUtils.getPhysicalPath(member);
try {
Files.walkFileTree(memberPath, new DeletingFileVisitor());
} catch (SecurityException e) {
@ -133,7 +133,7 @@ public class EncryptedDir extends AbstractEncryptedNode {
@Override
protected void determineProperties() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
properties.add(new ResourceType(ResourceType.COLLECTION));
properties.add(new DefaultDavProperty<Integer>(DavPropertyName.ISCOLLECTION, 1));
if (Files.exists(path)) {

View File

@ -30,6 +30,8 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName;
import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -63,9 +65,10 @@ public class EncryptedFile extends AbstractEncryptedNode {
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
outputContext.setProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
@ -81,13 +84,12 @@ public class EncryptedFile extends AbstractEncryptedNode {
} finally {
IOUtils.closeQuietly(channel);
}
}
}
@Override
protected void determineProperties() {
final Path path = PathUtils.getPhysicalPath(this);
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
SeekableByteChannel channel = null;
try {
@ -98,6 +100,7 @@ public class EncryptedFile extends AbstractEncryptedNode {
final BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
properties.add(new DefaultDavProperty<String>(DavPropertyName.CREATIONDATE, FileTimeUtils.toRfc1123String(attrs.creationTime())));
properties.add(new DefaultDavProperty<String>(DavPropertyName.GETLASTMODIFIED, FileTimeUtils.toRfc1123String(attrs.lastModifiedTime())));
properties.add(new HttpHeaderProperty(HttpHeader.ACCEPT_RANGES.asString(), HttpHeaderValue.BYTES.asString()));
} catch (IOException e) {
LOG.error("Error determining metadata " + path.toString(), e);
throw new IORuntimeException(e);

View File

@ -0,0 +1,144 @@
package org.cryptomator.webdav.jackrabbit.resources;
import java.io.EOFException;
import java.io.IOException;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.HashSet;
import java.util.Set;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.jackrabbit.webdav.DavResourceFactory;
import org.apache.jackrabbit.webdav.DavResourceLocator;
import org.apache.jackrabbit.webdav.DavServletRequest;
import org.apache.jackrabbit.webdav.DavSession;
import org.apache.jackrabbit.webdav.io.OutputContext;
import org.apache.jackrabbit.webdav.lock.LockManager;
import org.cryptomator.crypto.Cryptor;
import org.cryptomator.webdav.exceptions.IORuntimeException;
import org.eclipse.jetty.http.HttpHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Delivers only the requested range of bytes from a file.
*
* @see {@link https://tools.ietf.org/html/rfc7233#section-4}
*/
public class EncryptedFilePart extends EncryptedFile {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedFilePart.class);
private static final String BYTE_UNIT_PREFIX = "bytes=";
private static final char RANGE_SET_SEP = ',';
private static final char RANGE_SEP = '-';
/**
* e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
*/
private static final Long SUFFIX_BYTE_RANGE_LOWER = -1L;
/**
* e.g. range 500- (gets all bytes from 500) -> (500, MAX_LONG)
*/
private static final Long SUFFIX_BYTE_RANGE_UPPER = Long.MAX_VALUE;
private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
public EncryptedFilePart(DavResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor) {
super(factory, locator, session, lockManager, cryptor);
final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
if (rangeHeader == null) {
throw new IllegalArgumentException("HTTP request doesn't contain a range header");
}
determineByteRanges(rangeHeader);
}
private void determineByteRanges(String rangeHeader) {
final String byteRangeSet = StringUtils.removeStartIgnoreCase(rangeHeader, BYTE_UNIT_PREFIX);
final String[] byteRanges = StringUtils.split(byteRangeSet, RANGE_SET_SEP);
if (byteRanges.length == 0) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
for (final String byteRange : byteRanges) {
final String[] bytePos = StringUtils.splitPreserveAllTokens(byteRange, RANGE_SEP);
if (bytePos.length != 2 || bytePos[0].isEmpty() && bytePos[1].isEmpty()) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
final Long lower = bytePos[0].isEmpty() ? SUFFIX_BYTE_RANGE_LOWER : Long.valueOf(bytePos[0]);
final Long upper = bytePos[1].isEmpty() ? SUFFIX_BYTE_RANGE_UPPER : Long.valueOf(bytePos[1]);
if (lower > upper) {
throw new IllegalArgumentException("Invalid range: " + rangeHeader);
}
requestedContentRanges.add(new ImmutablePair<Long, Long>(lower, upper));
}
}
/**
* @return One range, that spans all requested ranges.
*/
private Pair<Long, Long> getUnionRange(Long fileSize) {
final long lastByte = fileSize - 1;
final MutablePair<Long, Long> result = new MutablePair<Long, Long>();
for (Pair<Long, Long> range : requestedContentRanges) {
final long left;
final long right;
if (SUFFIX_BYTE_RANGE_LOWER.equals(range.getLeft())) {
left = lastByte - range.getRight();
right = lastByte;
} else if (SUFFIX_BYTE_RANGE_UPPER.equals(range.getRight())) {
left = range.getLeft();
right = lastByte;
} else {
left = range.getLeft();
right = range.getRight();
}
if (result.getLeft() == null || left < result.getLeft()) {
result.setLeft(left);
}
if (result.getRight() == null || right > result.getRight()) {
result.setRight(right);
}
}
return result;
}
@Override
public void spool(OutputContext outputContext) throws IOException {
final Path path = ResourcePathUtils.getPhysicalPath(this);
if (Files.exists(path)) {
outputContext.setModificationTime(Files.getLastModifiedTime(path).toMillis());
SeekableByteChannel channel = null;
try {
channel = Files.newByteChannel(path, StandardOpenOption.READ);
final Long fileSize = cryptor.decryptedContentLength(channel);
final Pair<Long, Long> range = getUnionRange(fileSize);
final Long rangeLength = range.getRight() - range.getLeft() + 1;
outputContext.setContentLength(rangeLength);
outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), fileSize));
if (outputContext.hasStream()) {
cryptor.decryptRange(channel, outputContext.getOutputStream(), range.getLeft(), rangeLength);
}
} catch (EOFException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
}
} catch (IOException e) {
LOG.error("Error reading file " + path.toString(), e);
throw new IORuntimeException(e);
} finally {
IOUtils.closeQuietly(channel);
}
}
}
private String getContentRangeHeader(long firstByte, long lastByte, long completeLength) {
return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
}
}

View File

@ -14,13 +14,13 @@ import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.temporal.Temporal;
public final class FileTimeUtils {
final class FileTimeUtils {
private FileTimeUtils() {
throw new IllegalStateException("not instantiable");
}
public static String toRfc1123String(FileTime time) {
static String toRfc1123String(FileTime time) {
final Temporal date = OffsetDateTime.ofInstant(time.toInstant(), ZoneOffset.UTC);
return DateTimeFormatter.RFC_1123_DATE_TIME.format(date);
}

View File

@ -0,0 +1,20 @@
package org.cryptomator.webdav.jackrabbit.resources;
import org.apache.jackrabbit.webdav.property.AbstractDavProperty;
import org.apache.jackrabbit.webdav.property.DavPropertyName;
class HttpHeaderProperty extends AbstractDavProperty<String> {
private final String value;
public HttpHeaderProperty(String key, String value) {
super(DavPropertyName.create(key), true);
this.value = value;
}
@Override
public String getValue() {
return value;
}
}

View File

@ -14,9 +14,9 @@ import java.nio.file.Path;
import org.apache.jackrabbit.webdav.DavResource;
import org.apache.jackrabbit.webdav.DavResourceLocator;
public final class PathUtils {
public final class ResourcePathUtils {
private PathUtils() {
private ResourcePathUtils() {
throw new IllegalStateException("not instantiable");
}

View File

@ -32,6 +32,7 @@ import java.util.zip.CRC32;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
@ -398,8 +399,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final OutputStream cipheredOut = new CipherOutputStream(plaintextFile, cipher);
return IOUtils.copyLarge(in, cipheredOut);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile);
}
@Override
@ -416,8 +417,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// seek relevant position and update iv:
long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
long numberOfRelevantBlocks = 1 + length / AES_BLOCK_LENGTH;
long numberOfRelevantBytes = numberOfRelevantBlocks * AES_BLOCK_LENGTH;
long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
@ -431,9 +430,8 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
// read content
final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
final OutputStream rangedOut = new RangeFilterOutputStream(plaintextFile, offsetInsideFirstRelevantBlock, length);
final OutputStream cipheredOut = new CipherOutputStream(rangedOut, cipher);
return IOUtils.copyLarge(in, cipheredOut, 0, numberOfRelevantBytes);
final InputStream cipheredIn = new CipherInputStream(in, cipher);
return IOUtils.copyLarge(cipheredIn, plaintextFile, offsetInsideFirstRelevantBlock, length);
}
@Override

View File

@ -1,46 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.io.OutputStream;
class LimitFilterOutputStream extends java.io.FilterOutputStream {
private final long limit;
private long bytesWritten;
LimitFilterOutputStream(OutputStream out, long limit) {
super(out);
if (limit < 0) {
throw new IllegalArgumentException("Limit must be greater than or equal 0.");
}
this.limit = limit;
}
@Override
public void write(int b) throws IOException {
this.write(new byte[] {(byte) b});
}
@Override
public void write(byte[] b) throws IOException {
this.write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
final long adjustedLength = Math.min(bytesRemainingUntilReachingLimit(), len);
// adjustedLength is <= len, so it must be INT and we can safely cast:
out.write(b, off, (int) adjustedLength);
bytesWritten += adjustedLength;
}
private long bytesRemainingUntilReachingLimit() {
if (bytesWritten < limit) {
return limit - bytesWritten;
} else {
return 0l;
}
}
}

View File

@ -1,49 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.IOException;
import java.io.OutputStream;
class OffsetFilterOutputStream extends java.io.FilterOutputStream {
private final long offset;
private long bytesWritten;
OffsetFilterOutputStream(OutputStream out, long offset) {
super(out);
if (offset < 0) {
throw new IllegalArgumentException("Offset must be greater than or equal 0.");
}
this.offset = offset;
}
@Override
public void write(int b) throws IOException {
this.write(new byte[] {(byte) b});
}
@Override
public void write(byte[] b) throws IOException {
this.write(b, 0, b.length);
}
@Override
public synchronized void write(byte[] b, int off, int len) throws IOException {
final long adjustedOffset = remainingOffset() + off;
final long adjustedLength = len - remainingOffset();
if (adjustedOffset < b.length && adjustedLength <= b.length) {
// b.length is INT, so by definition adjustedOffset and adjustedLength must be INT too and we can safely cast:
out.write(b, (int) adjustedOffset, (int) adjustedLength);
}
bytesWritten += len;
}
private long remainingOffset() {
if (bytesWritten < offset) {
return offset - bytesWritten;
} else {
return 0l;
}
}
}

View File

@ -1,21 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* Passthrough of all bytes except for certain bytes at the begin and end of the stream, which will get cut off.
*/
class RangeFilterOutputStream extends FilterOutputStream {
RangeFilterOutputStream(OutputStream out, long offset, long limit) {
super(new OffsetFilterOutputStream(new LimitFilterOutputStream(out, limit), offset));
}
@Override
public void write(byte b[], int off, int len) throws IOException {
out.write(b, off, len);
}
}

View File

@ -14,6 +14,7 @@ import java.io.IOException;
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 java.util.Random;
@ -95,9 +96,9 @@ public class Aes256CryptorTest {
@Test
public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
// our test plaintext data:
final byte[] plaintextData = new byte[500 * Integer.BYTES];
final byte[] plaintextData = new byte[65536 * Integer.BYTES];
final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData);
for (int i = 0; i < 500; i++) {
for (int i = 0; i < 65536; i++) {
bbIn.putInt(i);
}
final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
@ -115,18 +116,14 @@ public class Aes256CryptorTest {
// decrypt:
final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 313 * Integer.BYTES, 50 * Integer.BYTES);
final Long numDecryptedBytes = cryptor.decryptRange(encryptedIn, plaintextOut, 25000 * Integer.BYTES, 30000 * Integer.BYTES);
IOUtils.closeQuietly(encryptedIn);
IOUtils.closeQuietly(plaintextOut);
Assert.assertTrue(numDecryptedBytes > 0);
// check decrypted data:
final byte[] result = plaintextOut.toByteArray();
final byte[] expected = new byte[50 * Integer.BYTES];
final ByteBuffer bbOut = ByteBuffer.wrap(expected);
for (int i = 313; i < 363; i++) {
bbOut.putInt(i);
}
final byte[] expected = Arrays.copyOfRange(plaintextData, 25000 * Integer.BYTES, 55000 * Integer.BYTES);
Assert.assertArrayEquals(expected, result);
}

View File

@ -1,63 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
public class LimitFilterOutputStreamTest {
@Test
public void testNoLimit() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new LimitFilterOutputStream(out, Long.MAX_VALUE);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testLimit43() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new LimitFilterOutputStream(out, 43l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 43);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testLimit307() throws IOException {
final byte[] testData = createTestData(512);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new LimitFilterOutputStream(out, 307l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 307);
Assert.assertArrayEquals(expected, out.toByteArray());
}
private byte[] createTestData(int length) {
final byte[] testData = new byte[length];
for (int i = 0; i < length; i++) {
testData[i] = (byte) i;
}
return testData;
}
}

View File

@ -1,63 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
public class OffsetFilterOutputStreamTest {
@Test
public void testNoOffset() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new OffsetFilterOutputStream(out, 0l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testOffset43() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new OffsetFilterOutputStream(out, 43l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 43, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testOffset307() throws IOException {
final byte[] testData = createTestData(512);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new OffsetFilterOutputStream(out, 307l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 307, 512);
Assert.assertArrayEquals(expected, out.toByteArray());
}
private byte[] createTestData(int length) {
final byte[] testData = new byte[length];
for (int i = 0; i < length; i++) {
testData[i] = (byte) i;
}
return testData;
}
}

View File

@ -1,76 +0,0 @@
package org.cryptomator.crypto.aes256;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import org.apache.commons.io.IOUtils;
import org.junit.Assert;
import org.junit.Test;
public class RangeFilterOutputStreamTest {
@Test
public void testNoOffsetUnlimited() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 0l, Long.MAX_VALUE);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testNoOffsetButLimit() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 0l, 97l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 0, 97);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testNoLimitButOffset() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 43l, Long.MAX_VALUE);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 43, 256);
Assert.assertArrayEquals(expected, out.toByteArray());
}
@Test
public void testOffsettedAndLimited() throws IOException {
final byte[] testData = createTestData(256);
final InputStream in = new ByteArrayInputStream(testData);
final ByteArrayOutputStream out = new ByteArrayOutputStream();
final OutputStream decorator = new RangeFilterOutputStream(out, 43l, 57l);
IOUtils.copy(in, decorator);
final byte[] expected = Arrays.copyOfRange(testData, 43, 100);
Assert.assertArrayEquals(expected, out.toByteArray());
}
private byte[] createTestData(int length) {
final byte[] testData = new byte[length];
for (int i = 0; i < length; i++) {
testData[i] = (byte) i;
}
return testData;
}
}

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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 -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- 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 -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.cryptomator</groupId>
<artifactId>main</artifactId>
@ -29,6 +26,7 @@
<!-- dependency versions -->
<log4j.version>2.1</log4j.version>
<slf4j.version>1.7.7</slf4j.version>
<junit.version>4.11</junit.version>
<commons-io.version>2.4</commons-io.version>
<commons-collections.version>4.0</commons-collections.version>
@ -61,6 +59,11 @@
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
@ -141,7 +144,7 @@
<module>core</module>
<module>ui</module>
</modules>
<build>
<plugins>
<plugin>

View File

@ -20,7 +20,7 @@ import org.cryptomator.ui.util.MasterKeyFilter;
import org.cryptomator.ui.util.mount.CommandFailedException;
import org.cryptomator.ui.util.mount.WebDavMount;
import org.cryptomator.ui.util.mount.WebDavMounter;
import org.cryptomator.webdav.WebDAVServer;
import org.cryptomator.webdav.WebDavServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -34,7 +34,7 @@ public class Directory implements Serializable {
private static final long serialVersionUID = 3754487289683599469L;
private static final Logger LOG = LoggerFactory.getLogger(Directory.class);
private final WebDAVServer server = new WebDAVServer();
private final WebDavServer server = new WebDavServer();
private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
private final Path path;
@ -137,7 +137,7 @@ public class Directory implements Serializable {
this.unlocked.set(unlocked);
}
public WebDAVServer getServer() {
public WebDavServer getServer() {
return server;
}

View File

@ -33,6 +33,7 @@ final class FallbackWebDavMounter implements WebDavMounterStrategy {
private void displayMountInstructions() {
// TODO display message to user pointing to cryptomator.org/mounting#mount which describes what to do
// Machine-readable mount instructions: http://tools.ietf.org/html/rfc4709#page-5 :-)
}
private void displayUnmountInstructions() {