diff --git a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java index 76e92e185..ef1b29b65 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java +++ b/main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java @@ -40,7 +40,6 @@ public final class WebDavServer { private static final int MAX_THREADS = 200; private static final int MIN_THREADS = 4; private static final int THREAD_IDLE_SECONDS = 20; - private static final int CONNECTION_IDLE_MILLIS = 100; // idle connection slow down random access on WebDAVFS for some reason. reconnect overhead can be tolerated private final Server server; private final ServerConnector localConnector; private final ContextHandlerCollection servletCollection; @@ -51,7 +50,6 @@ public final class WebDavServer { server = new Server(tp); localConnector = new ServerConnector(server); localConnector.setHost(LOCALHOST); - localConnector.setIdleTimeout(CONNECTION_IDLE_MILLIS); servletCollection = new ContextHandlerCollection(); if (SystemUtils.IS_OS_WINDOWS) { diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java index 45ce34aa0..633143aef 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java @@ -108,8 +108,9 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants { if (outputContext.hasStream()) { final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath()); cryptor.decryptFile(c, outputContext.getOutputStream(), authenticate); + outputContext.getOutputStream().flush(); } - outputContext.getOutputStream().flush(); + } catch (EOFException e) { LOG.warn("Unexpected end of stream (possibly client hung up)."); } diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java index 586203939..b9d241799 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java @@ -41,7 +41,7 @@ class EncryptedFilePart extends EncryptedFile { } else if (upper == null) { range = new ImmutablePair(lower, contentLength - 1); } else { - range = new ImmutablePair(lower, upper); + range = new ImmutablePair(lower, Math.min(upper, contentLength - 1)); } } catch (NumberFormatException e) { throw new IllegalArgumentException("Invalid byte range: " + requestRange, e); @@ -51,27 +51,31 @@ class EncryptedFilePart extends EncryptedFile { @Override public void spool(OutputContext outputContext) throws IOException { assert Files.isRegularFile(filePath); - assert this.contentLength != null; + assert contentLength != null; - final Long rangeLength = range.getRight() - range.getLeft() + 1; + final Long rangeLength = range.getRight() - range.getLeft(); outputContext.setModificationTime(Files.getLastModifiedTime(filePath).toMillis()); - if (rangeLength <= 0) { + if (rangeLength <= 0 || range.getLeft() > contentLength - 1) { // unsatisfiable content range: outputContext.setContentLength(0); - outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getRight(), range.getRight(), contentLength)); - LOG.debug("Unsatisfiable content range: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength)); + outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), "bytes */" + contentLength); + LOG.debug("Requested content range unsatisfiable: " + getContentRangeHeader(range.getLeft(), range.getRight(), contentLength)); return; } else { outputContext.setContentLength(rangeLength); outputContext.setProperty(HttpHeader.CONTENT_RANGE.asString(), getContentRangeHeader(range.getLeft(), range.getRight(), contentLength)); } + assert range.getLeft() > 0; + assert range.getLeft() < contentLength; + assert range.getRight() < contentLength; + try (final FileChannel c = FileChannel.open(filePath, StandardOpenOption.READ)) { if (outputContext.hasStream()) { final boolean authenticate = !cryptoWarningHandler.ignoreMac(getLocator().getResourcePath()); cryptor.decryptRange(c, outputContext.getOutputStream(), range.getLeft(), rangeLength, authenticate); + outputContext.getOutputStream().flush(); } - outputContext.getOutputStream().flush(); } catch (EOFException e) { if (LOG.isDebugEnabled()) { LOG.trace("Unexpected end of stream during delivery of partial content (client hung up)."); diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/SilentlyFailingFileLock.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/SilentlyFailingFileLock.java index d136159f2..8bb30d570 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/SilentlyFailingFileLock.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/SilentlyFailingFileLock.java @@ -39,7 +39,7 @@ class SilentlyFailingFileLock implements AutoCloseable { lock = channel.tryLock(position, size, shared); } catch (IOException | OverlappingFileLockException e) { if (LOG.isDebugEnabled()) { - LOG.warn("Unable to lock file."); + LOG.trace("Unable to lock file."); } } finally { this.lock = lock; diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java index f4f279f5c..e504190d3 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java @@ -95,7 +95,7 @@ public class WebDavServlet extends AbstractWebdavServlet { super.doPut(request, response, resource); if (LOG.isDebugEnabled()) { long t1 = System.nanoTime(); - LOG.debug("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms"); + LOG.trace("PUT TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms"); } } @@ -111,7 +111,7 @@ public class WebDavServlet extends AbstractWebdavServlet { } if (LOG.isDebugEnabled()) { long t1 = System.nanoTime(); - LOG.debug("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms"); + LOG.trace("GET TIME: " + (t1 - t0) / 1000 / 1000.0 + " ms"); } } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index de203e824..838e25801 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -37,7 +37,6 @@ import javax.crypto.spec.SecretKeySpec; import javax.security.auth.DestroyFailedException; import javax.security.auth.Destroyable; -import org.apache.commons.io.IOUtils; import org.bouncycastle.crypto.generators.SCrypt; import org.cryptomator.crypto.Cryptor; import org.cryptomator.crypto.exceptions.DecryptFailedException; @@ -46,7 +45,6 @@ import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.UnsupportedVaultException; import org.cryptomator.crypto.exceptions.WrongPasswordException; -import org.cryptomator.crypto.io.SeekableByteChannelInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -548,9 +546,11 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { final byte[] fileKeyBytes = new byte[32]; final byte[] decryptedSensitiveHeaderContentBytes = decryptHeaderData(encryptedSensitiveHeaderContentBytes, iv); final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes); - sensitiveHeaderContentBuf.position(Long.BYTES); // skip file size + final Long fileSize = sensitiveHeaderContentBuf.getLong(); sensitiveHeaderContentBuf.get(fileKeyBytes); + assert pos + length < fileSize; + // find first relevant block: final long startBlock = pos / CONTENT_MAC_BLOCK; // floor final long startByte = startBlock * (CONTENT_MAC_BLOCK + 32) + 104; @@ -571,40 +571,51 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { try { // reading ciphered input and MACs interleaved: long bytesWritten = 0; - final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - byte[] buffer = new byte[CONTENT_MAC_BLOCK + 32]; + final ByteBuffer buf = ByteBuffer.allocate(CONTENT_MAC_BLOCK + 32); int n = 0; long blockNum = startBlock; - while ((n = IOUtils.read(in, buffer)) > 0 && bytesWritten < length) { + while ((n = readFromChannel(encryptedFile, buf)) > 0 && bytesWritten < length) { if (n < 32) { throw new DecryptFailedException("Invalid file content, missing MAC."); } + buf.flip(); + final ByteBuffer ciphertextBuf = buf.asReadOnlyBuffer(); + ciphertextBuf.limit(n - 32); + // check MAC of current block: if (authenticate) { + final byte[] storedMac = new byte[contentMac.getMacLength()]; + final ByteBuffer storedMacBuf = buf.asReadOnlyBuffer(); + storedMacBuf.position(n - 32); + storedMacBuf.get(storedMac); contentMac.update(iv); contentMac.update(longToByteArray(blockNum)); - contentMac.update(buffer, 0, n - 32); + contentMac.update(ciphertextBuf); + ciphertextBuf.rewind(); final byte[] calculatedMac = contentMac.doFinal(); - final byte[] storedMac = new byte[32]; - System.arraycopy(buffer, n - 32, storedMac, 0, 32); if (!MessageDigest.isEqual(calculatedMac, storedMac)) { throw new MacAuthenticationFailedException("Content MAC authentication failed."); } } // decrypt block: - final byte[] plaintext = cipher.update(buffer, 0, n - 32); + final ByteBuffer plaintextBuf = ByteBuffer.allocate(cipher.getOutputSize(ciphertextBuf.remaining())); + cipher.update(ciphertextBuf, plaintextBuf); + plaintextBuf.flip(); final int offset = (bytesWritten == 0) ? (int) offsetFromFirstBlock : 0; final long pending = length - bytesWritten; - final int available = plaintext.length - offset; + final int available = plaintextBuf.remaining() - offset; final int currentBatch = (int) Math.min(pending, available); - plaintextFile.write(plaintext, offset, currentBatch); + plaintextFile.write(plaintextBuf.array(), offset, currentBatch); bytesWritten += currentBatch; blockNum++; + buf.rewind(); } return bytesWritten; + } catch (ShortBufferException e) { + throw new IllegalStateException("Output buffer size known to fit.", e); } finally { destroyQuietly(fileKey); } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthLimitingOutputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthLimitingOutputStream.java index c4088f5b5..8d52f32b8 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthLimitingOutputStream.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthLimitingOutputStream.java @@ -19,7 +19,7 @@ public class LengthLimitingOutputStream extends FilterOutputStream { public void write(int b) throws IOException { if (bytesWritten < limit) { out.write(b); - bytesWritten++; + increaseNumberOfWrittenBytes(1); } } @@ -29,7 +29,7 @@ public class LengthLimitingOutputStream extends FilterOutputStream { final int adjustedLen = (int) Math.min(len, bytesAvailable); if (adjustedLen > 0) { out.write(b, off, adjustedLen); - bytesWritten += adjustedLen; + increaseNumberOfWrittenBytes(adjustedLen); } } @@ -37,4 +37,11 @@ public class LengthLimitingOutputStream extends FilterOutputStream { return bytesWritten; } + private void increaseNumberOfWrittenBytes(int amount) throws IOException { + bytesWritten += amount; + if (bytesWritten >= limit) { + out.flush(); + } + } + } diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscatingInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscatingInputStream.java index 377d9209d..e41f1c728 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscatingInputStream.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscatingInputStream.java @@ -109,18 +109,6 @@ public class LengthObfuscatingInputStream extends FilterInputStream { throw new IOException("Skip not supported"); } - @Override - public int available() throws IOException { - final int inputAvailable = in.available(); - if (inputAvailable > 0) { - return inputAvailable; - } else { - // remaining padding - choosePaddingLengthOnce(); - return paddingLength - paddingBytesRead; - } - } - @Override public boolean markSupported() { return false; diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/io/SeekableByteChannelInputStream.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/io/SeekableByteChannelInputStream.java deleted file mode 100644 index a441d7be4..000000000 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/io/SeekableByteChannelInputStream.java +++ /dev/null @@ -1,90 +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.io; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; - -public class SeekableByteChannelInputStream extends InputStream { - private final SeekableByteChannel channel; - private volatile long markedPos = 0; - - public SeekableByteChannelInputStream(SeekableByteChannel channel) { - this.channel = channel; - } - - @Override - public int read() throws IOException { - final ByteBuffer buffer = ByteBuffer.allocate(1); - final int read = channel.read(buffer); - if (read == 1) { - return buffer.get(0); - } else { - return -1; - } - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - final ByteBuffer buffer = ByteBuffer.wrap(b, off, len); - return channel.read(buffer); - } - - @Override - public int available() throws IOException { - long available = channel.size() - channel.position(); - if (available > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } else { - return (int) available; - } - } - - @Override - public long skip(long n) throws IOException { - final long pos = channel.position(); - final long max = channel.size(); - final long maxSkip = max - pos; - final long actualSkip = Math.min(n, maxSkip); - channel.position(channel.position() + actualSkip); - return actualSkip; - } - - @Override - public void close() throws IOException { - channel.close(); - super.close(); - } - - @Override - public synchronized void mark(int readlimit) { - try { - markedPos = channel.position(); - } catch (IOException e) { - markedPos = 0; - } - } - - @Override - public synchronized void reset() throws IOException { - channel.position(markedPos); - } - - public synchronized void resetTo(long position) throws IOException { - channel.position(position); - } - - @Override - public boolean markSupported() { - return true; - } - -} \ No newline at end of file diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/io/SeekableByteChannelOutputStream.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/io/SeekableByteChannelOutputStream.java deleted file mode 100644 index bdac2c594..000000000 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/io/SeekableByteChannelOutputStream.java +++ /dev/null @@ -1,64 +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.io; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.SeekableByteChannel; - -public class SeekableByteChannelOutputStream extends OutputStream { - - private final SeekableByteChannel channel; - - public SeekableByteChannelOutputStream(SeekableByteChannel channel) { - this.channel = channel; - } - - @Override - public void write(int b) throws IOException { - final byte actualByte = (byte) (b & 0x000000FF); - final ByteBuffer buffer = ByteBuffer.allocate(1); - buffer.put(actualByte); - channel.write(buffer); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - final ByteBuffer buffer = ByteBuffer.wrap(b, off, len); - channel.write(buffer); - } - - @Override - public void close() throws IOException { - channel.close(); - } - - /** - * @see SeekableByteChannel#truncate(long) - */ - public void truncate(long size) throws IOException { - channel.truncate(size); - } - - /** - * @see SeekableByteChannel#position() - */ - public long position() throws IOException { - return channel.position(); - } - - /** - * @see SeekableByteChannel#position(long) - */ - public void position(long newPosition) throws IOException { - channel.position(newPosition); - } - -}