diff --git a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java index a773b0b15..925953123 100644 --- a/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java +++ b/main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedDir.java @@ -36,6 +36,8 @@ import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import org.apache.jackrabbit.webdav.property.ResourceType; import org.cryptomator.crypto.Cryptor; +import org.cryptomator.crypto.exceptions.CounterOverflowException; +import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.webdav.exceptions.DavRuntimeException; import org.cryptomator.webdav.exceptions.DecryptFailedRuntimeException; import org.cryptomator.webdav.exceptions.IORuntimeException; @@ -85,6 +87,12 @@ class EncryptedDir extends AbstractEncryptedNode { } 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()); } 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 b0e9f3faf..dfc73d78c 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 @@ -2,6 +2,7 @@ package org.cryptomator.webdav.jackrabbit; import java.io.EOFException; import java.io.IOException; +import java.nio.channels.ClosedByInterruptException; import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; @@ -171,6 +172,8 @@ class EncryptedFilePart extends EncryptedFile { if (!authentic) { cryptoWarningHandler.macAuthFailed(locator.getResourcePath()); } + } catch (ClosedByInterruptException ex) { + LOG.debug("Couldn't finish MAC verification due to interruption of worker thread."); } catch (IOException e) { LOG.error("IOException during MAC verification of " + path.toString(), e); } 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 1ac7f3f65..fcc0e2ebd 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 @@ -46,7 +46,10 @@ import org.apache.commons.lang3.StringUtils; import org.bouncycastle.crypto.generators.SCrypt; import org.cryptomator.crypto.AbstractCryptor; import org.cryptomator.crypto.CryptorIOSupport; +import org.cryptomator.crypto.aes256.CounterAwareInputStream.CounterAwareInputLimitReachedException; +import org.cryptomator.crypto.exceptions.CounterOverflowException; import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.MacAuthenticationFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; @@ -510,10 +513,10 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction! long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH; long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock; - countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock); + countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, (int) firstRelevantBlock); // int-cast is possible, as max file size is 64GiB // fast forward stream: - encryptedFile.position(64 + beginOfFirstRelevantBlock); + encryptedFile.position(64l + beginOfFirstRelevantBlock); // generate cipher: final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE); @@ -525,13 +528,13 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo } @Override - public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException { + public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException { // truncate file encryptedFile.truncate(0); // use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file. final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH)); - countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l); + countingIv.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0); encryptedFile.write(countingIv); // init crypto stuff: @@ -550,18 +553,29 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo final OutputStream macOut = new MacOutputStream(out, mac); final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher); final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH); - final Long plaintextSize = IOUtils.copyLarge(plaintextFile, blockSizeBufferedOut); + final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile); + final Long plaintextSize; + try { + plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut); + } catch (CounterAwareInputLimitReachedException ex) { + encryptedFile.truncate(64l + CounterAwareInputStream.SIXTY_FOUR_GIGABYE); + encryptedContentLength(encryptedFile, CounterAwareInputStream.SIXTY_FOUR_GIGABYE); + // no additional padding needed here, as 64GiB is a multiple of 128bit + throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow."); + } // ensure total byte count is a multiple of the block size, in CTR mode: final int remainderToFillLastBlock = AES_BLOCK_LENGTH - (int) (plaintextSize % AES_BLOCK_LENGTH); blockSizeBufferedOut.write(new byte[remainderToFillLastBlock]); - // append a few blocks of fake data: - final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH); - final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks); - final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH); - for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) { - blockSizeBufferedOut.write(emptyBytes); + // for filesizes of up to 16GiB: append a few blocks of fake data: + if (plaintextSize < (long) (Integer.MAX_VALUE / 4) * AES_BLOCK_LENGTH) { + final int numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH); + final int upToTenPercentFakeBlocks = (int) Math.ceil(Math.random() * 0.1 * numberOfPlaintextBlocks); + final byte[] emptyBytes = this.randomData(AES_BLOCK_LENGTH); + for (int i = 0; i < upToTenPercentFakeBlocks; i += AES_BLOCK_LENGTH) { + blockSizeBufferedOut.write(emptyBytes); + } } blockSizeBufferedOut.flush(); diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java new file mode 100644 index 000000000..ede51e3f8 --- /dev/null +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java @@ -0,0 +1,59 @@ +package org.cryptomator.crypto.aes256; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicLong; + +import javax.crypto.Mac; + +/** + * Updates a {@link Mac} with the bytes read from this stream. + */ +class CounterAwareInputStream extends FilterInputStream { + + static final long SIXTY_FOUR_GIGABYE = 1024l * 1024l * 1024l * 64l; + + private final AtomicLong counter; + + /** + * @param in Stream from which to read contents, which will update the Mac. + * @param mac Mac to be updated during writes. + */ + public CounterAwareInputStream(InputStream in) { + super(in); + this.counter = new AtomicLong(0l); + } + + @Override + public int read() throws IOException { + int b = in.read(); + if (b != -1) { + final long currentValue = counter.incrementAndGet(); + failWhen64GibReached(currentValue); + } + return b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + int read = in.read(b, off, len); + if (read > 0) { + final long currentValue = counter.addAndGet(read); + failWhen64GibReached(currentValue); + } + return read; + } + + private void failWhen64GibReached(long currentValue) throws CounterAwareInputLimitReachedException { + if (currentValue > SIXTY_FOUR_GIGABYE) { + throw new CounterAwareInputLimitReachedException(); + } + } + + static class CounterAwareInputLimitReachedException extends IOException { + private static final long serialVersionUID = -1905012809288019359L; + + } + +} diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java index 0bec34ce1..0abbe5b32 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java @@ -25,7 +25,9 @@ class MacInputStream extends FilterInputStream { @Override public int read() throws IOException { int b = in.read(); - mac.update((byte) b); + if (b != -1) { + mac.update((byte) b); + } return b; } diff --git a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java index 38f881c15..0ee932d81 100644 --- a/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java +++ b/main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java @@ -21,6 +21,7 @@ import java.util.Map; import org.apache.commons.io.IOUtils; import org.cryptomator.crypto.CryptorIOSupport; import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; import org.junit.Assert; @@ -70,7 +71,7 @@ public class Aes256CryptorTest { } @Test - public void testIntegrityAuthentication() throws IOException, DecryptFailedException { + public void testIntegrityAuthentication() throws IOException, DecryptFailedException, EncryptFailedException { // our test plaintext data: final byte[] plaintextData = "Hello World".getBytes(); final InputStream plaintextIn = new ByteArrayInputStream(plaintextData); @@ -102,7 +103,7 @@ public class Aes256CryptorTest { } @Test(expected = DecryptFailedException.class) - public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException { + public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException { // our test plaintext data: final byte[] plaintextData = "Hello World".getBytes(); final InputStream plaintextIn = new ByteArrayInputStream(plaintextData); @@ -134,7 +135,7 @@ public class Aes256CryptorTest { } @Test - public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { + public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException { // our test plaintext data: final byte[] plaintextData = "Hello World".getBytes(); final InputStream plaintextIn = new ByteArrayInputStream(plaintextData); @@ -169,7 +170,7 @@ public class Aes256CryptorTest { } @Test - public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException { + public void testPartialDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException, EncryptFailedException { // our test plaintext data: final byte[] plaintextData = new byte[65536 * Integer.BYTES]; final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData); diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java index d997af8f3..bcf28cf90 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java @@ -16,6 +16,7 @@ import java.nio.file.DirectoryStream.Filter; import java.nio.file.Path; import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; @@ -97,7 +98,7 @@ public interface Cryptor extends SensitiveDataSwipeListener { /** * @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it. */ - Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException; + Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException; /** * @return A filter, that returns true for encrypted files, i.e. if the file is an actual user payload and not a supporting diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java index cec189598..a6461373a 100644 --- a/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java @@ -10,6 +10,7 @@ import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.StringUtils; import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.EncryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; import org.cryptomator.crypto.exceptions.WrongPasswordException; @@ -99,7 +100,7 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling { } @Override - public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException { + public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException { final InputStream countingInputStream = new CountingInputStream(encryptedBytes, plaintextFile); return cryptor.encryptFile(countingInputStream, encryptedFile); } diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/CounterOverflowException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/CounterOverflowException.java new file mode 100644 index 000000000..113b0a31c --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/CounterOverflowException.java @@ -0,0 +1,10 @@ +package org.cryptomator.crypto.exceptions; + +public class CounterOverflowException extends EncryptFailedException { + private static final long serialVersionUID = 380066751064534731L; + + public CounterOverflowException(String msg) { + super(msg); + } + +} diff --git a/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java new file mode 100644 index 000000000..62d981f92 --- /dev/null +++ b/main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/EncryptFailedException.java @@ -0,0 +1,9 @@ +package org.cryptomator.crypto.exceptions; + +public class EncryptFailedException extends StorageCryptingException { + private static final long serialVersionUID = -3855673600374897828L; + + public EncryptFailedException(String msg) { + super(msg); + } +} \ No newline at end of file