Browse Source

new, more secure encryption scheme
- fixed flaw reported by Stan Drapkin (SecurityDriven.NET) reducing effective key size to 96 bit
- multiple file content MACs for 1MB blocks, preventing chosen ciphertext attacks, as authentication now happens before decryption
- allowing files bigger than 64GiB

Sebastian Stenzel 9 years ago
parent
commit
45cf87d089

+ 2 - 5
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/CryptoResourceFactory.java

@@ -5,7 +5,6 @@ import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.concurrent.ExecutorService;
 
 import org.apache.commons.httpclient.HttpStatus;
 import org.apache.commons.io.FilenameUtils;
@@ -28,15 +27,13 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
 	private final LockManager lockManager = new SimpleLockManager();
 	private final Cryptor cryptor;
 	private final CryptoWarningHandler cryptoWarningHandler;
-	private final ExecutorService backgroundTaskExecutor;
 	private final Path dataRoot;
 	private final FilenameTranslator filenameTranslator;
 
-	CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, ExecutorService backgroundTaskExecutor, String vaultRoot) {
+	CryptoResourceFactory(Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler, String vaultRoot) {
 		Path vaultRootPath = FileSystems.getDefault().getPath(vaultRoot);
 		this.cryptor = cryptor;
 		this.cryptoWarningHandler = cryptoWarningHandler;
-		this.backgroundTaskExecutor = backgroundTaskExecutor;
 		this.dataRoot = vaultRootPath.resolve("d");
 		this.filenameTranslator = new FilenameTranslator(cryptor, vaultRootPath);
 	}
@@ -151,7 +148,7 @@ public class CryptoResourceFactory implements DavResourceFactory, FileConstants
 	}
 
 	private EncryptedFile createFilePart(DavResourceLocator locator, DavSession session, DavServletRequest request, Path filePath) {
-		return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, backgroundTaskExecutor, filePath);
+		return new EncryptedFilePart(this, locator, session, request, lockManager, cryptor, cryptoWarningHandler, filePath);
 	}
 
 	private EncryptedFile createFile(DavResourceLocator locator, DavSession session, Path filePath) {

+ 3 - 1
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFile.java

@@ -107,7 +107,9 @@ class EncryptedFile extends AbstractEncryptedNode implements FileConstants {
 			} catch (EOFException e) {
 				LOG.warn("Unexpected end of stream (possibly client hung up).");
 			} catch (MacAuthenticationFailedException e) {
-				cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
+				LOG.warn("File integrity violation for " + getLocator().getResourcePath());
+				throw new IOException("Error decrypting file " + filePath.toString(), e);
+				// cryptoWarningHandler.macAuthFailed(getLocator().getResourcePath());
 			} catch (DecryptFailedException e) {
 				throw new IOException("Error decrypting file " + filePath.toString(), e);
 			}

+ 1 - 59
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/EncryptedFilePart.java

@@ -2,15 +2,12 @@ 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;
 import java.nio.file.StandardOpenOption;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeUnit;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.ImmutablePair;
@@ -27,9 +24,6 @@ import org.eclipse.jetty.http.HttpHeader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.common.cache.Cache;
-import com.google.common.cache.CacheBuilder;
-
 /**
  * Delivers only the requested range of bytes from a file.
  * 
@@ -41,7 +35,6 @@ class EncryptedFilePart extends EncryptedFile {
 	private static final String BYTE_UNIT_PREFIX = "bytes=";
 	private static final char RANGE_SET_SEP = ',';
 	private static final char RANGE_SEP = '-';
-	private static final Cache<DavResourceLocator, MacAuthenticationJob> cachedMacAuthenticationJobs = CacheBuilder.newBuilder().expireAfterWrite(10, TimeUnit.MINUTES).build();
 
 	/**
 	 * e.g. range -500 (gets the last 500 bytes) -> (-1, 500)
@@ -56,22 +49,13 @@ class EncryptedFilePart extends EncryptedFile {
 	private final Set<Pair<Long, Long>> requestedContentRanges = new HashSet<Pair<Long, Long>>();
 
 	public EncryptedFilePart(CryptoResourceFactory factory, DavResourceLocator locator, DavSession session, DavServletRequest request, LockManager lockManager, Cryptor cryptor, CryptoWarningHandler cryptoWarningHandler,
-			ExecutorService backgroundTaskExecutor, Path filePath) {
+			Path filePath) {
 		super(factory, locator, session, lockManager, cryptor, cryptoWarningHandler, filePath);
 		final String rangeHeader = request.getHeader(HttpHeader.RANGE.asString());
 		if (rangeHeader == null) {
 			throw new IllegalArgumentException("HTTP request doesn't contain a range header");
 		}
 		determineByteRanges(rangeHeader);
-
-		synchronized (cachedMacAuthenticationJobs) {
-			if (cachedMacAuthenticationJobs.getIfPresent(locator) == null) {
-				final MacAuthenticationJob macAuthJob = new MacAuthenticationJob(locator);
-				cachedMacAuthenticationJobs.put(locator, macAuthJob);
-				backgroundTaskExecutor.submit(macAuthJob);
-			}
-		}
-
 	}
 
 	private void determineByteRanges(String rangeHeader) {
@@ -149,46 +133,4 @@ class EncryptedFilePart extends EncryptedFile {
 		return String.format("%d-%d/%d", firstByte, lastByte, completeLength);
 	}
 
-	private class MacAuthenticationJob implements Runnable {
-
-		private final DavResourceLocator locator;
-
-		public MacAuthenticationJob(final DavResourceLocator locator) {
-			if (locator == null) {
-				throw new IllegalArgumentException("locator must not be null.");
-			}
-			this.locator = locator;
-		}
-
-		@Override
-		public void run() {
-			assert Files.isRegularFile(filePath);
-			try (final SeekableByteChannel channel = Files.newByteChannel(filePath, StandardOpenOption.READ)) {
-				final boolean authentic = cryptor.isAuthentic(channel);
-				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 " + filePath.toString(), e);
-			}
-		}
-
-		@Override
-		public int hashCode() {
-			return locator.hashCode();
-		}
-
-		@Override
-		public boolean equals(Object obj) {
-			if (obj instanceof MacAuthenticationJob) {
-				final MacAuthenticationJob other = (MacAuthenticationJob) obj;
-				return this.locator.equals(other.locator);
-			} else {
-				return false;
-			}
-		}
-	}
-
 }

+ 1 - 22
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/WebDavServlet.java

@@ -9,9 +9,6 @@
 package org.cryptomator.webdav.jackrabbit;
 
 import java.util.Collection;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 
 import javax.servlet.ServletConfig;
 import javax.servlet.ServletException;
@@ -33,7 +30,6 @@ public class WebDavServlet extends AbstractWebdavServlet {
 	private DavResourceFactory davResourceFactory;
 	private final Cryptor cryptor;
 	private final CryptoWarningHandler cryptoWarningHandler;
-	private ExecutorService backgroundTaskExecutor;
 
 	public WebDavServlet(final Cryptor cryptor, final Collection<String> failingMacCollection) {
 		super();
@@ -45,26 +41,9 @@ public class WebDavServlet extends AbstractWebdavServlet {
 	public void init(ServletConfig config) throws ServletException {
 		super.init(config);
 		final String fsRoot = config.getInitParameter(CFG_FS_ROOT);
-		backgroundTaskExecutor = Executors.newCachedThreadPool();
 		davSessionProvider = new DavSessionProviderImpl();
 		davLocatorFactory = new CleartextLocatorFactory(config.getServletContext().getContextPath());
-		davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, backgroundTaskExecutor, fsRoot);
-	}
-
-	@Override
-	public void destroy() {
-		backgroundTaskExecutor.shutdown();
-		try {
-			final boolean tasksFinished = backgroundTaskExecutor.awaitTermination(2, TimeUnit.SECONDS);
-			if (!tasksFinished) {
-				backgroundTaskExecutor.shutdownNow();
-			}
-		} catch (InterruptedException e) {
-			backgroundTaskExecutor.shutdownNow();
-			Thread.currentThread().interrupt();
-		} finally {
-			super.destroy();
-		}
+		davResourceFactory = new CryptoResourceFactory(cryptor, cryptoWarningHandler, fsRoot);
 	}
 
 	@Override

+ 97 - 109
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java

@@ -8,7 +8,6 @@
  ******************************************************************************/
 package org.cryptomator.crypto.aes256;
 
-import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -36,11 +35,8 @@ import javax.security.auth.DestroyFailedException;
 import javax.security.auth.Destroyable;
 
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.io.output.NullOutputStream;
 import org.bouncycastle.crypto.generators.SCrypt;
 import org.cryptomator.crypto.Cryptor;
-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;
@@ -211,7 +207,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 		} catch (InvalidKeyException ex) {
 			throw new IllegalArgumentException("Invalid key.", ex);
 		} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
-			throw new IllegalStateException("Algorithm/Padding should exist and accept GCM specs.", ex);
+			throw new IllegalStateException("Algorithm/Padding should exist.", ex);
 		}
 	}
 
@@ -308,7 +304,7 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 	public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException {
 		// read header:
 		encryptedFile.position(0);
-		final ByteBuffer headerBuf = ByteBuffer.allocate(64);
+		final ByteBuffer headerBuf = ByteBuffer.allocate(96);
 		final int headerBytesRead = encryptedFile.read(headerBuf);
 		if (headerBytesRead != headerBuf.capacity()) {
 			return null;
@@ -326,13 +322,13 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 
 		// read stored header mac:
 		final byte[] storedHeaderMac = new byte[32];
-		headerBuf.position(32);
+		headerBuf.position(64);
 		headerBuf.get(storedHeaderMac);
 
-		// calculate mac over first 32 bytes of header:
+		// calculate mac over first 64 bytes of header:
 		final Mac headerMac = this.hmacSha256(hMacMasterKey);
 		headerBuf.rewind();
-		headerBuf.limit(32);
+		headerBuf.limit(64);
 		headerMac.update(headerBuf);
 
 		final boolean macMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
@@ -340,70 +336,29 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 			throw new MacAuthenticationFailedException("MAC authentication failed.");
 		}
 
-		return decryptContentLength(encryptedContentLengthBytes, iv);
+		final byte[] decryptedContentLengthBytes = decryptHeaderData(encryptedContentLengthBytes, iv);
+		final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedContentLengthBytes);
+		return fileSizeBuffer.getLong();
 	}
 
-	private long decryptContentLength(byte[] encryptedContentLengthBytes, byte[] iv) {
+	private byte[] decryptHeaderData(byte[] ciphertextBytes, byte[] iv) {
 		try {
 			final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
-			final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedContentLengthBytes);
-			final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
-			return fileSizeBuffer.getLong();
+			return sizeCipher.doFinal(ciphertextBytes);
 		} catch (IllegalBlockSizeException | BadPaddingException e) {
 			throw new IllegalStateException(e);
 		}
 	}
 
-	private byte[] encryptContentLength(long contentLength, byte[] iv) {
+	private byte[] encryptHeaderData(byte[] plaintextBytes, byte[] iv) {
 		try {
-			final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
-			fileSizeBuffer.putLong(contentLength);
 			final Cipher sizeCipher = aesCbcCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
-			return sizeCipher.doFinal(fileSizeBuffer.array());
+			return sizeCipher.doFinal(plaintextBytes);
 		} catch (IllegalBlockSizeException | BadPaddingException e) {
 			throw new IllegalStateException("Block size must be valid, as padding is requested. BadPaddingException not possible in encrypt mode.", e);
 		}
 	}
 
-	@Override
-	public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
-		// read header:
-		encryptedFile.position(0l);
-		final ByteBuffer headerBuf = ByteBuffer.allocate(96);
-		final int headerBytesRead = encryptedFile.read(headerBuf);
-		if (headerBytesRead != headerBuf.capacity()) {
-			throw new IOException("Failed to read file header.");
-		}
-
-		// read header mac:
-		final byte[] storedHeaderMac = new byte[32];
-		headerBuf.position(32);
-		headerBuf.get(storedHeaderMac);
-
-		// read content mac:
-		final byte[] storedContentMac = new byte[32];
-		headerBuf.position(64);
-		headerBuf.get(storedContentMac);
-
-		// calculate mac over first 32 bytes of header:
-		final Mac headerMac = this.hmacSha256(hMacMasterKey);
-		headerBuf.position(0);
-		headerBuf.limit(32);
-		headerMac.update(headerBuf);
-
-		// calculate mac over content:
-		encryptedFile.position(96l);
-		final Mac contentMac = this.hmacSha256(hMacMasterKey);
-		final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
-		final InputStream macIn = new MacInputStream(in, contentMac);
-		IOUtils.copyLarge(macIn, new NullOutputStream());
-
-		// compare (in constant time):
-		final boolean headerMacMatches = MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal());
-		final boolean contentMacMatches = MessageDigest.isEqual(storedContentMac, contentMac.doFinal());
-		return headerMacMatches && contentMacMatches;
-	}
-
 	@Override
 	public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
 		// read header:
@@ -419,44 +374,73 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 		headerBuf.position(0);
 		headerBuf.get(iv);
 
+		// derive nonce used in counter mode from IV by setting last 64bit to 0:
+		final ByteBuffer nonceBuf = ByteBuffer.wrap(iv.clone());
+		nonceBuf.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0);
+		final byte[] nonce = nonceBuf.array();
+
 		// read content length:
 		final byte[] encryptedContentLengthBytes = new byte[AES_BLOCK_LENGTH];
 		headerBuf.position(16);
 		headerBuf.get(encryptedContentLengthBytes);
-		final Long fileSize = decryptContentLength(encryptedContentLengthBytes, iv);
+		final byte[] decryptedContentLengthBytes = decryptHeaderData(encryptedContentLengthBytes, iv);
+		final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedContentLengthBytes);
+		final Long fileSize = fileSizeBuffer.getLong();
 
-		// read header mac:
-		final byte[] headerMac = new byte[32];
+		// read content key:
+		final byte[] encryptedContentKeyBytes = new byte[32];
 		headerBuf.position(32);
-		headerBuf.get(headerMac);
+		headerBuf.get(encryptedContentKeyBytes);
+		final byte[] contentKeyBytes = decryptHeaderData(encryptedContentKeyBytes, iv);
 
-		// read content mac:
-		final byte[] contentMac = new byte[32];
+		// read header mac:
+		final byte[] storedHeaderMac = new byte[32];
 		headerBuf.position(64);
-		headerBuf.get(contentMac);
+		headerBuf.get(storedHeaderMac);
+
+		// calculate mac over first 64 bytes of header:
+		final Mac headerMac = this.hmacSha256(hMacMasterKey);
+		headerBuf.position(0);
+		headerBuf.limit(64);
+		headerMac.update(headerBuf);
 
-		// decrypt content
+		// check header integrity:
+		if (!MessageDigest.isEqual(storedHeaderMac, headerMac.doFinal())) {
+			throw new MacAuthenticationFailedException("Header MAC authentication failed.");
+		}
+
+		// content decryption:
 		encryptedFile.position(96l);
-		final Mac calculatedContentMac = this.hmacSha256(hMacMasterKey);
-		final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.DECRYPT_MODE);
+		final SecretKey contentKey = new SecretKeySpec(contentKeyBytes, AES_KEY_ALGORITHM);
+		final Cipher cipher = this.aesCtrCipher(contentKey, nonce, Cipher.DECRYPT_MODE);
+		final Mac contentMac = this.hmacSha256(hMacMasterKey);
+
+		// reading ciphered input and MACs interleaved:
+		long bytesDecrypted = 0;
 		final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
-		final InputStream macIn = new MacInputStream(in, calculatedContentMac);
-		final InputStream cipheredIn = new CipherInputStream(macIn, cipher);
-		final long bytesDecrypted = IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
+		byte[] buffer = new byte[1024 * 1024 + 32];
+		int n = 0;
+		while ((n = IOUtils.read(in, buffer)) > 0) {
+			if (n < 32) {
+				throw new DecryptFailedException("Invalid file content, missing MAC.");
+			}
 
-		// drain remaining bytes to /dev/null to complete MAC calculation:
-		IOUtils.copyLarge(macIn, new NullOutputStream());
+			// check MAC of current block:
+			contentMac.update(buffer, 0, n - 32);
+			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.");
+			}
 
-		// compare (in constant time):
-		final boolean macMatches = MessageDigest.isEqual(contentMac, calculatedContentMac.doFinal());
-		if (!macMatches) {
-			// This exception will be thrown AFTER we sent the decrypted content to the user.
-			// This has two advantages:
-			// - we don't need to read files twice
-			// - we can still restore files suffering from non-malicious bit rotting
-			// Anyway me MUST make sure to warn the user. This will be done by the UI when catching this exception.
-			throw new MacAuthenticationFailedException("MAC authentication failed.");
+			// decrypt block:
+			final byte[] plaintext = cipher.update(buffer, 0, n - 32);
+			final int plaintextLengthWithoutPadding = (int) Math.min(plaintext.length, fileSize - bytesDecrypted); // plaintext.length is known to be a 32 bit int
+			plaintextFile.write(plaintext, 0, plaintextLengthWithoutPadding);
+			bytesDecrypted += plaintextLengthWithoutPadding;
 		}
+		destroyQuietly(contentKey);
 
 		return bytesDecrypted;
 	}
@@ -492,61 +476,65 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration {
 	}
 
 	/**
-	 * header = {16 byte iv, 16 byte filesize, 32 byte headerMac, 32 byte contentMac}
+	 * header = {16 byte iv, 16 byte filesize, 32 byte contentKey, 32 byte headerMac}
 	 */
 	@Override
 	public Long encryptFile(InputStream plaintextFile, SeekableByteChannel encryptedFile) throws IOException, EncryptFailedException {
 		// truncate file
 		encryptedFile.truncate(0l);
 
-		// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
-		final ByteBuffer ivBuf = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
-		ivBuf.putInt(AES_BLOCK_LENGTH - Integer.BYTES, 0);
-		final byte[] iv = ivBuf.array();
+		// choose a random IV:
+		final byte[] iv = randomData(AES_BLOCK_LENGTH);
 
-		// 96 byte header buffer (16 IV, 16 size, 32 headerMac, 32 contentMac), filled after writing the content
+		// derive nonce used in counter mode from IV by setting last 64bit to 0:
+		final ByteBuffer nonceBuf = ByteBuffer.wrap(iv.clone());
+		nonceBuf.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0);
+		final byte[] nonce = nonceBuf.array();
+
+		// choose a random content key:
+		final byte[] contentKeyBytes = randomData(32);
+
+		// 96 byte header buffer (16 IV, 16 size, 32 content key, 32 headerMac), filled after writing the content
 		final ByteBuffer headerBuf = ByteBuffer.allocate(96);
 		headerBuf.limit(96);
 		encryptedFile.write(headerBuf);
 
+		// add random length padding to obfuscate file length:
+		final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
+		final LengthObfuscationInputStream in = new LengthObfuscationInputStream(plaintextFile, randomPadding);
+
 		// content encryption:
-		final Cipher cipher = this.aesCtrCipher(primaryMasterKey, iv, Cipher.ENCRYPT_MODE);
+		final SecretKey contentKey = new SecretKeySpec(contentKeyBytes, AES_KEY_ALGORITHM);
+		final Cipher cipher = this.aesCtrCipher(contentKey, nonce, Cipher.ENCRYPT_MODE);
 		final Mac contentMac = this.hmacSha256(hMacMasterKey);
 		final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
 		final OutputStream macOut = new MacOutputStream(out, contentMac);
+		@SuppressWarnings("resource")
 		final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
-		final OutputStream blockSizeBufferedOut = new BufferedOutputStream(cipheredOut, AES_BLOCK_LENGTH);
-		final InputStream lengthLimitingIn = new CounterAwareInputStream(plaintextFile);
-		final Long plaintextSize;
-		try {
-			plaintextSize = IOUtils.copyLarge(lengthLimitingIn, blockSizeBufferedOut);
-		} catch (CounterAwareInputLimitReachedException ex) {
-			encryptedFile.truncate(0l);
-			throw new CounterOverflowException("File size exceeds limit (64Gib). Aborting to prevent counter overflow.");
-		}
 
-		// add random length padding to obfuscate file length:
-		final long numberOfPlaintextBlocks = (int) Math.ceil(plaintextSize / AES_BLOCK_LENGTH);
-		final long minAdditionalBlocks = 4;
-		final long maxAdditionalBlocks = Math.min(numberOfPlaintextBlocks >> 3, 1024 * 1024); // 12,5% of original blocks, but not more than 1M blocks (16MiBs)
-		final long availableBlocks = (1l << 32) - numberOfPlaintextBlocks; // before reaching limit of 2^32 blocks
-		final long additionalBlocks = (long) Math.min(Math.random() * Math.max(minAdditionalBlocks, maxAdditionalBlocks), availableBlocks);
-		final byte[] randomPadding = this.randomData(AES_BLOCK_LENGTH);
-		for (int i = 0; i < additionalBlocks; i += AES_BLOCK_LENGTH) {
-			blockSizeBufferedOut.write(randomPadding);
+		// writing ciphered output and MACs interleaved:
+		byte[] buffer = new byte[1024 * 1024];
+		int n = 0;
+		while ((n = IOUtils.read(in, buffer)) > 0) {
+			cipheredOut.write(buffer, 0, n);
+			final byte[] mac = contentMac.doFinal();
+			out.write(mac);
 		}
-		blockSizeBufferedOut.flush();
+		destroyQuietly(contentKey);
 
 		// create and write header:
+		final long plaintextSize = in.getRealInputLength();
+		final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
+		fileSizeBuffer.putLong(plaintextSize);
 		headerBuf.clear();
 		headerBuf.put(iv);
-		headerBuf.put(encryptContentLength(plaintextSize, iv));
+		headerBuf.put(encryptHeaderData(fileSizeBuffer.array(), iv));
+		headerBuf.put(encryptHeaderData(contentKeyBytes, iv));
 		headerBuf.flip();
 		final Mac headerMac = this.hmacSha256(hMacMasterKey);
 		headerMac.update(headerBuf);
 		headerBuf.limit(96);
 		headerBuf.put(headerMac.doFinal());
-		headerBuf.put(contentMac.doFinal());
 		headerBuf.flip();
 		encryptedFile.position(0);
 		encryptedFile.write(headerBuf);

+ 1 - 1
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java

@@ -74,7 +74,7 @@ interface AesCryptographicConfiguration {
 	 * 
 	 * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
 	 */
-	String AES_CBC_CIPHER = "AES/CBC/PKCS5Padding";
+	String AES_CBC_CIPHER = "AES/CBC/NoPadding";
 
 	/**
 	 * AES block size is 128 bit or 16 bytes.

+ 0 - 57
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/CounterAwareInputStream.java

@@ -1,57 +0,0 @@
-package org.cryptomator.crypto.aes256;
-
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Throws an exception, if more than (2^32)-1 16 byte blocks will be encrypted (would result in an counter overflow).<br/>
- * From https://tools.ietf.org/html/rfc3686: <cite> Using the encryption process described in section 2.1, this construction permits each packet to consist of up to: (2^32)-1 blocks</cite>
- */
-class CounterAwareInputStream extends FilterInputStream {
-
-	static final long SIXTY_FOUR_GIGABYE = ((1l << 32) - 1) * 16;
-
-	private final AtomicLong counter;
-
-	/**
-	 * @param in Stream from which to read contents, which will update the Mac.
-	 */
-	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;
-
-	}
-
-}

+ 128 - 0
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/LengthObfuscationInputStream.java

@@ -0,0 +1,128 @@
+package org.cryptomator.crypto.aes256;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Not thread-safe!
+ */
+public class LengthObfuscationInputStream extends FilterInputStream {
+
+	private final byte[] padding;
+	private int paddingLength = -1;
+	private long inputBytesRead = 0;
+	private int paddingBytesRead = 0;
+
+	LengthObfuscationInputStream(InputStream in, byte[] padding) {
+		super(in);
+		this.padding = padding;
+	}
+
+	long getRealInputLength() {
+		return inputBytesRead;
+	}
+
+	private void choosePaddingLengthOnce() {
+		if (paddingLength == -1) {
+			long upperBound = Math.min(inputBytesRead / 10, 16 * 1024 * 1024); // 10% of original bytes, but not more than 16MiBs
+			paddingLength = (int) (Math.random() * upperBound);
+		}
+	}
+
+	@Override
+	public int read() throws IOException {
+		final int b = in.read();
+		if (b != -1) {
+			// stream available:
+			inputBytesRead++;
+			return b;
+		} else {
+			choosePaddingLengthOnce();
+			return readFromPadding();
+		}
+	}
+
+	private int readFromPadding() {
+		if (paddingLength == -1) {
+			throw new IllegalStateException("No padding length chosen yet.");
+		}
+
+		if (paddingBytesRead < paddingLength) {
+			// padding available:
+			return padding[paddingBytesRead++ % padding.length];
+		} else {
+			// end of stream AND padding
+			return -1;
+		}
+	}
+
+	@Override
+	public int read(byte[] b, int off, int len) throws IOException {
+		final int n = in.read(b, 0, len);
+		final int bytesRead = Math.max(0, n); // EOF -> 0
+		inputBytesRead += bytesRead;
+
+		if (bytesRead == len) {
+			return bytesRead;
+		} else if (bytesRead < len) {
+			choosePaddingLengthOnce();
+			final int additionalBytesNeeded = len - bytesRead;
+			final int m = readFromPadding(b, bytesRead, additionalBytesNeeded);
+			final int additionalBytesRead = Math.max(0, m); // EOF -> 0
+			return (n == -1 && m == -1) ? -1 : bytesRead + additionalBytesRead;
+		} else {
+			// bytesRead > len:
+			throw new IllegalStateException("read more bytes than requested.");
+		}
+	}
+
+	private int readFromPadding(byte[] b, int off, int len) {
+		if (paddingLength == -1) {
+			throw new IllegalStateException("No padding length chosen yet.");
+		}
+
+		final int remainingPadding = paddingLength - paddingBytesRead;
+		if (remainingPadding > len) {
+			// padding available:
+			for (int i = 0; i < len; i++) {
+				b[off + i] = padding[paddingBytesRead + i % padding.length];
+			}
+			paddingBytesRead += len;
+			return len;
+		} else if (remainingPadding > 0) {
+			// partly available:
+			for (int i = 0; i < remainingPadding; i++) {
+				b[off + i] = padding[paddingBytesRead + i % padding.length];
+			}
+			paddingBytesRead += remainingPadding;
+			return remainingPadding;
+		} else {
+			// end of stream AND padding
+			return -1;
+		}
+	}
+
+	@Override
+	public long skip(long n) throws IOException {
+		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;
+	}
+
+}

+ 1 - 0
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java

@@ -9,6 +9,7 @@ import javax.crypto.Mac;
 /**
  * Updates a {@link Mac} with the bytes read from this stream.
  */
+@Deprecated
 class MacInputStream extends FilterInputStream {
 
 	private final Mac mac;

+ 0 - 32
main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java

@@ -70,38 +70,6 @@ public class Aes256CryptorTest {
 		}
 	}
 
-	@Test
-	public void testIntegrityAuthentication() throws IOException, DecryptFailedException, EncryptFailedException {
-		// our test plaintext data:
-		final byte[] plaintextData = "Hello World".getBytes();
-		final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
-
-		// init cryptor:
-		final Aes256Cryptor cryptor = new Aes256Cryptor();
-
-		// encrypt:
-		final ByteBuffer encryptedData = ByteBuffer.allocate(256);
-		final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
-		cryptor.encryptFile(plaintextIn, encryptedOut);
-		IOUtils.closeQuietly(plaintextIn);
-		IOUtils.closeQuietly(encryptedOut);
-
-		encryptedData.position(0);
-
-		// toggle one bit inf first content byte:
-		encryptedData.position(64);
-		final byte fifthByte = encryptedData.get();
-		encryptedData.position(64);
-		encryptedData.put((byte) (fifthByte ^ 0x01));
-
-		encryptedData.position(0);
-
-		// check mac (should return false)
-		final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
-		final boolean authentic = cryptor.isAuthentic(encryptedIn);
-		Assert.assertFalse(authentic);
-	}
-
 	@Test(expected = DecryptFailedException.class)
 	public void testIntegrityViolationDuringDecryption() throws IOException, DecryptFailedException, EncryptFailedException {
 		// our test plaintext data:

+ 0 - 5
main/crypto-api/src/main/java/org/cryptomator/crypto/AbstractCryptorDecorator.java

@@ -52,11 +52,6 @@ public class AbstractCryptorDecorator implements Cryptor {
 		return cryptor.decryptedContentLength(encryptedFile);
 	}
 
-	@Override
-	public boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException {
-		return cryptor.isAuthentic(encryptedFile);
-	}
-
 	@Override
 	public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
 		return cryptor.decryptFile(encryptedFile, plaintextFile);

+ 0 - 5
main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java

@@ -75,11 +75,6 @@ public interface Cryptor extends Destroyable {
 	 */
 	Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException, MacAuthenticationFailedException;
 
-	/**
-	 * @return true, if the stored MAC matches the calculated one.
-	 */
-	boolean isAuthentic(SeekableByteChannel encryptedFile) throws IOException;
-
 	/**
 	 * @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
 	 * @throws DecryptFailedException If decryption failed