Browse Source

Using 96 bit of random data and a 32 bit counter (as specified in https://tools.ietf.org/html/rfc3686#section-4). Thus maximum file size supported by Cryptomator is 64GiB, but decreasing risk of IV collisions to 1 : 2^48

Sebastian Stenzel 10 years ago
parent
commit
652c4cbafb

+ 8 - 0
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());
 		}

+ 3 - 0
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);
 				}

+ 25 - 11
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();
 

+ 59 - 0
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;
+
+	}
+
+}

+ 3 - 1
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;
 	}
 

+ 5 - 4
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);

+ 2 - 1
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 <code>true</code> for encrypted files, i.e. if the file is an actual user payload and not a supporting

+ 2 - 1
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);
 	}

+ 10 - 0
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);
+	}
+
+}

+ 9 - 0
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);
+	}
+}