浏览代码

support for forced decryption ignoring failed authentication

Sebastian Stenzel 9 年之前
父节点
当前提交
2e5264bac2
共有 16 个文件被更改,包括 166 次插入36 次删除
  1. 2 1
      main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentCryptor.java
  2. 2 2
      main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java
  3. 9 5
      main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java
  4. 2 1
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java
  5. 8 2
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java
  6. 21 0
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemDelegate.java
  7. 2 2
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemFactory.java
  8. 13 0
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoNode.java
  9. 4 2
      main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java
  10. 1 1
      main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java
  11. 4 4
      main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java
  12. 48 3
      main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java
  13. 36 2
      main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemComponentIntegrationTest.java
  14. 9 8
      main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemTest.java
  15. 1 1
      main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoReadableFileTest.java
  16. 4 2
      main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java

+ 2 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentCryptor.java

@@ -32,9 +32,10 @@ public interface FileContentCryptor {
 	 * @param header The full fixed-length header of an encrypted file. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
 	 * @param firstCiphertextByte Position of the first ciphertext byte passed to the decryptor. If the decryptor can not fast-forward to the requested byte, an exception is thrown.
 	 *            If firstCiphertextByte is an invalid starting point, i.e. doesn't align with the decryptors internal block size, an IllegalArgumentException will be thrown.
+	 * @param authenticate Skip authentication by setting this flag to <code>false</code>. Should be <code>true</code> by default.
 	 * @return A possibly new FileContentDecryptor instance which is capable of decrypting ciphertexts associated with the given file header.
 	 */
-	FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) throws IllegalArgumentException;
+	FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) throws IllegalArgumentException;
 
 	/**
 	 * @param header The full fixed-length header of an encrypted file or {@link Optional#empty()}. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.

+ 2 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java

@@ -50,14 +50,14 @@ public class FileContentCryptorImpl implements FileContentCryptor {
 	}
 
 	@Override
-	public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) {
+	public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
 		if (header.remaining() != getHeaderSize()) {
 			throw new IllegalArgumentException("Invalid header.");
 		}
 		if (firstCiphertextByte % (CHUNK_SIZE + MAC_SIZE) != 0) {
 			throw new IllegalArgumentException("Invalid starting point for decryption.");
 		}
-		return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte);
+		return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte, authenticate);
 	}
 
 	@Override

+ 9 - 5
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java

@@ -41,13 +41,15 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
 	private final FifoParallelDataProcessor<ByteBuffer> dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS, NUM_THREADS + READ_AHEAD);
 	private final ThreadLocal<Mac> hmacSha256;
 	private final FileHeader header;
+	private final boolean authenticate;
 	private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE + MAC_SIZE);
 	private long chunkNumber = 0;
 
-	public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte) {
+	public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
 		final ThreadLocalMac hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
 		this.hmacSha256 = hmacSha256;
 		this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
+		this.authenticate = authenticate;
 		this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
 	}
 
@@ -141,10 +143,12 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
 		@Override
 		public ByteBuffer call() {
 			try {
-				Mac mac = hmacSha256.get();
-				mac.update(ciphertextChunk.asReadOnlyBuffer());
-				if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
-					throw new AuthenticationFailedException();
+				if (authenticate) {
+					Mac mac = hmacSha256.get();
+					mac.update(ciphertextChunk.asReadOnlyBuffer());
+					if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
+						throw new AuthenticationFailedException();
+					}
 				}
 
 				Cipher cipher = ThreadLocalAesCtrCipher.get();

+ 2 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFile.java

@@ -40,7 +40,8 @@ public class CryptoFile extends CryptoNode implements File {
 
 	@Override
 	public ReadableFile openReadable() {
-		return new CryptoReadableFile(cryptor.getFileContentCryptor(), physicalFile().openReadable());
+		boolean authenticate = !fileSystem().delegate().shouldSkipAuthentication(toString());
+		return new CryptoReadableFile(cryptor.getFileContentCryptor(), physicalFile().openReadable(), authenticate);
 	}
 
 	@Override

+ 8 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java

@@ -27,10 +27,12 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 	private static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
 
 	private final Folder physicalRoot;
+	private final CryptoFileSystemDelegate delegate;
 
-	public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CharSequence passphrase) throws InvalidPassphraseException {
+	public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CryptoFileSystemDelegate delegate, CharSequence passphrase) throws InvalidPassphraseException {
 		super(null, "", cryptor);
 		this.physicalRoot = physicalRoot;
+		this.delegate = delegate;
 		final File masterkeyFile = physicalRoot.file(MASTERKEY_FILENAME);
 		if (masterkeyFile.exists()) {
 			final boolean unlocked = decryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
@@ -68,6 +70,10 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 		}
 	}
 
+	CryptoFileSystemDelegate delegate() {
+		return delegate;
+	}
+
 	@Override
 	protected File physicalFile() {
 		return physicalDataRoot().file(ROOT_DIR_FILE);
@@ -107,7 +113,7 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
 
 	@Override
 	public String toString() {
-		return physicalRoot + ":::/";
+		return "/";
 	}
 
 }

+ 21 - 0
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemDelegate.java

@@ -0,0 +1,21 @@
+package org.cryptomator.filesystem.crypto;
+
+public interface CryptoFileSystemDelegate {
+
+	/**
+	 * Reports the path for resources, that could not be decrypted due to authentication errors.
+	 * 
+	 * @param cleartextPath Unix-style vault-relative path
+	 */
+	void authenticationFailed(String cleartextPath);
+
+	/**
+	 * Allows the delegate to deactivate authentication during decryption.
+	 * This bears the risk of CCAs, thus this method should only return <code>true</code> for data recovery purposes.
+	 * 
+	 * @param cleartextPath Unix-style vault-relative path
+	 * @return Must always <b>default to <code>false</code></b>, except when authentication should be skipped.
+	 */
+	boolean shouldSkipAuthentication(String cleartextPath);
+
+}

+ 2 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystemFactory.java

@@ -24,9 +24,9 @@ public class CryptoFileSystemFactory {
 		this.blockAlignedFileSystemFactory = blockAlignedFileSystemFactory;
 	}
 
-	public FileSystem get(Folder root, CharSequence passphrase) {
+	public FileSystem get(Folder root, CharSequence passphrase, CryptoFileSystemDelegate delegate) {
 		final FileSystem nameShorteningFs = shorteningFileSystemFactory.get(root);
-		final FileSystem cryptoFs = new CryptoFileSystem(nameShorteningFs, cryptorProvider.get(), passphrase);
+		final FileSystem cryptoFs = new CryptoFileSystem(nameShorteningFs, cryptorProvider.get(), delegate, passphrase);
 		return blockAlignedFileSystemFactory.get(cryptoFs);
 	}
 }

+ 13 - 0
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoNode.java

@@ -37,6 +37,11 @@ abstract class CryptoNode implements Node {
 		return parent.physicalFolder().file(encryptedName());
 	}
 
+	@Override
+	public CryptoFileSystem fileSystem() {
+		return (CryptoFileSystem) Node.super.fileSystem();
+	}
+
 	@Override
 	public Optional<CryptoFolder> parent() {
 		return Optional.of(parent);
@@ -74,4 +79,12 @@ abstract class CryptoNode implements Node {
 		}
 	}
 
+	/**
+	 * Unix-style cleartext path rooted at the vault's top-level directory.
+	 * 
+	 * @return Vault-relative cleartext path.
+	 */
+	@Override
+	public abstract String toString();
+
 }

+ 4 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java

@@ -29,14 +29,16 @@ class CryptoReadableFile implements ReadableFile {
 	private final ByteBuffer header;
 	private final FileContentCryptor cryptor;
 	private final ReadableFile file;
+	private final boolean authenticate;
 	private FileContentDecryptor decryptor;
 	private Future<Void> readAheadTask;
 	private ByteBuffer bufferedCleartext = EMPTY_BUFFER;
 
-	public CryptoReadableFile(FileContentCryptor cryptor, ReadableFile file) {
+	public CryptoReadableFile(FileContentCryptor cryptor, ReadableFile file, boolean authenticate) {
 		this.header = ByteBuffer.allocate(cryptor.getHeaderSize());
 		this.cryptor = cryptor;
 		this.file = file;
+		this.authenticate = authenticate;
 		file.position(0);
 		file.read(header);
 		header.flip();
@@ -73,7 +75,7 @@ class CryptoReadableFile implements ReadableFile {
 			bufferedCleartext = EMPTY_BUFFER;
 		}
 		long ciphertextPos = cryptor.toCiphertextPos(position);
-		decryptor = cryptor.createFileContentDecryptor(header.asReadOnlyBuffer(), ciphertextPos);
+		decryptor = cryptor.createFileContentDecryptor(header.asReadOnlyBuffer(), ciphertextPos, authenticate);
 		readAheadTask = executorService.submit(new CiphertextReader(file, decryptor, header.remaining() + ciphertextPos));
 	}
 

+ 1 - 1
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java

@@ -29,7 +29,7 @@ class NoFileContentCryptor implements FileContentCryptor {
 	}
 
 	@Override
-	public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) {
+	public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte, boolean authenticate) {
 		if (header.remaining() != getHeaderSize()) {
 			throw new IllegalArgumentException("Invalid header size.");
 		}

+ 4 - 4
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java

@@ -53,7 +53,7 @@ public class FileContentCryptorImplTest {
 		FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
 
 		ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
-		cryptor.createFileContentDecryptor(tooShortHeader, 0);
+		cryptor.createFileContentDecryptor(tooShortHeader, 0, true);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -75,7 +75,7 @@ public class FileContentCryptorImplTest {
 		FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
 
 		ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
-		cryptor.createFileContentDecryptor(header, 3);
+		cryptor.createFileContentDecryptor(header, 3, true);
 	}
 
 	@Test(expected = IllegalArgumentException.class)
@@ -110,7 +110,7 @@ public class FileContentCryptorImplTest {
 		ciphertext.flip();
 
 		ByteBuffer plaintext = ByteBuffer.allocate(100);
-		try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0)) {
+		try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
 			decryptor.append(ciphertext);
 			decryptor.append(FileContentCryptor.EOF);
 			ByteBuffer buf;
@@ -163,7 +163,7 @@ public class FileContentCryptorImplTest {
 
 		final Thread fileReader;
 		final long decStart = System.nanoTime();
-		try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0)) {
+		try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
 			fileReader = new Thread(() -> {
 				try (FileChannel fc = FileChannel.open(tmpFile, StandardOpenOption.READ)) {
 					ByteBuffer ciphertext = ByteBuffer.allocate(654321);

+ 48 - 3
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java

@@ -19,6 +19,7 @@ import javax.crypto.SecretKey;
 import javax.crypto.spec.SecretKeySpec;
 
 import org.bouncycastle.util.encoders.Base64;
+import org.cryptomator.crypto.engine.AuthenticationFailedException;
 import org.cryptomator.crypto.engine.FileContentCryptor;
 import org.cryptomator.crypto.engine.FileContentDecryptor;
 import org.cryptomator.crypto.engine.FileContentEncryptor;
@@ -47,7 +48,51 @@ public class FileContentDecryptorImplTest {
 		final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
 		final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHQ==");
 
-		try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0)) {
+		try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
+			decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
+			decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
+			decryptor.append(FileContentCryptor.EOF);
+
+			ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
+			ByteBuffer buf;
+			while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
+				ByteBuffers.copy(buf, result);
+			}
+
+			Assert.assertArrayEquals("hello world".getBytes(), result.array());
+		}
+	}
+
+	@Test(expected = AuthenticationFailedException.class)
+	public void testManipulatedDecryption() throws InterruptedException {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
+		final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
+		final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
+		final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHq==");
+
+		try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
+			decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
+			decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
+			decryptor.append(FileContentCryptor.EOF);
+
+			ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
+			ByteBuffer buf;
+			while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
+				ByteBuffers.copy(buf, result);
+			}
+		}
+	}
+
+	@Test
+	public void testManipulatedDecryptionWithSuppressedAuthentication() throws InterruptedException {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
+		final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
+		final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
+		final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHq==");
+
+		try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, false)) {
 			decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
 			decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
 			decryptor.append(FileContentCryptor.EOF);
@@ -69,7 +114,7 @@ public class FileContentDecryptorImplTest {
 		final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
 		final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
 
-		try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0)) {
+		try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0, true)) {
 			decryptor.cancelWithException(new IOException("can not do"));
 			decryptor.cleartext();
 		}
@@ -113,7 +158,7 @@ public class FileContentDecryptorImplTest {
 
 		for (int i = 3; i >= 0; i--) {
 			final int ciphertextPos = (int) cryptor.toCiphertextPos(i * 32768);
-			try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, ciphertextPos)) {
+			try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, ciphertextPos, true)) {
 				final Thread ciphertextReader = new Thread(() -> {
 					try {
 						ciphertext.position(ciphertextPos);

+ 36 - 2
main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemComponentIntegrationTest.java

@@ -15,6 +15,7 @@ import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mockito;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -24,13 +25,15 @@ public class CryptoFileSystemComponentIntegrationTest {
 
 	private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemComponentIntegrationTest.class);
 
+	private CryptoFileSystemDelegate cryptoDelegate;
 	private FileSystem ciphertextFs;
 	private FileSystem cleartextFs;
 
 	@Before
 	public void setupFileSystems() {
+		cryptoDelegate = Mockito.mock(CryptoFileSystemDelegate.class);
 		ciphertextFs = new InMemoryFileSystem();
-		cleartextFs = cryptoFsComp.cryptoFileSystemFactory().get(ciphertextFs, "TopSecret");
+		cleartextFs = cryptoFsComp.cryptoFileSystemFactory().get(ciphertextFs, "TopSecret", cryptoDelegate);
 		cleartextFs.create();
 	}
 
@@ -77,7 +80,38 @@ public class CryptoFileSystemComponentIntegrationTest {
 		}
 	}
 
-	@Test(timeout = 2000000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
+	@Test
+	public void testForcedDecryptionOfManipulatedFile() {
+		// write test content to encrypted file
+		try (WritableFile writable = cleartextFs.file("test1.txt").openWritable()) {
+			writable.write(ByteBuffer.wrap("Hello World".getBytes()));
+		}
+
+		File physicalFile = ciphertextFs.folder("d").folders().findAny().get().folders().findAny().get().files().findAny().get();
+		Assert.assertTrue(physicalFile.exists());
+
+		// toggle last bit
+		try (WritableFile writable = physicalFile.openWritable(); ReadableFile readable = physicalFile.openReadable()) {
+			ByteBuffer buf = ByteBuffer.allocate((int) readable.size());
+			readable.read(buf);
+			buf.array()[buf.limit() - 1] ^= 0x01;
+			buf.flip();
+			writable.write(buf);
+		}
+
+		// whitelist
+		Mockito.when(cryptoDelegate.shouldSkipAuthentication("/test1.txt")).thenReturn(true);
+
+		// read test content from decrypted file
+		try (ReadableFile readable = cleartextFs.file("test1.txt").openReadable()) {
+			ByteBuffer buf = ByteBuffer.allocate(11);
+			readable.read(buf);
+			buf.flip();
+			Assert.assertArrayEquals("Hello World".getBytes(), buf.array());
+		}
+	}
+
+	@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
 	public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
 		File file = cleartextFs.file("benchmark.test");
 

+ 9 - 8
main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemTest.java

@@ -27,6 +27,7 @@ import org.cryptomator.filesystem.WritableFile;
 import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
 import org.junit.Assert;
 import org.junit.Test;
+import org.mockito.Mockito;
 
 public class CryptoFileSystemTest {
 
@@ -45,7 +46,7 @@ public class CryptoFileSystemTest {
 		Assert.assertFalse(physicalDataRoot.exists());
 
 		// init crypto fs:
-		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		Assert.assertTrue(masterkeyFile.exists());
 		Assert.assertTrue(masterkeyBkupFile.exists());
 		fs.create();
@@ -66,7 +67,7 @@ public class CryptoFileSystemTest {
 		Assert.assertFalse(masterkeyBkupFile.exists());
 
 		// first initialization:
-		new CryptoFileSystem(physicalFs, cryptor, "foo");
+		new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		Assert.assertTrue(masterkeyBkupFile.exists());
 		final Instant bkupDateT0 = masterkeyBkupFile.lastModified();
 
@@ -75,7 +76,7 @@ public class CryptoFileSystemTest {
 		Thread.sleep(1);
 
 		// second initialization:
-		new CryptoFileSystem(physicalFs, cryptor, "foo");
+		new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		Assert.assertTrue(masterkeyBkupFile.exists());
 		final Instant bkupDateT1 = masterkeyBkupFile.lastModified();
 
@@ -88,7 +89,7 @@ public class CryptoFileSystemTest {
 		final Cryptor cryptor = new NoCryptor();
 		final FileSystem physicalFs = new InMemoryFileSystem();
 		final Folder physicalDataRoot = physicalFs.folder("d");
-		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		fs.create();
 
 		// add another encrypted folder:
@@ -108,7 +109,7 @@ public class CryptoFileSystemTest {
 		// mock stuff and prepare crypto FS:
 		final Cryptor cryptor = new NoCryptor();
 		final FileSystem physicalFs = new InMemoryFileSystem();
-		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		fs.create();
 
 		// create foo/bar/ and then move foo/ to baz/:
@@ -131,7 +132,7 @@ public class CryptoFileSystemTest {
 		// mock stuff and prepare crypto FS:
 		final Cryptor cryptor = new NoCryptor();
 		final FileSystem physicalFs = new InMemoryFileSystem();
-		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		fs.create();
 
 		// create foo/bar/ and then try to move foo/bar/ to foo/
@@ -141,12 +142,12 @@ public class CryptoFileSystemTest {
 		fooBarFolder.moveTo(fooFolder);
 	}
 
-	@Test(timeout = 10000000)
+	@Test(timeout = 10000)
 	public void testWriteAndReadEncryptedFile() {
 		// mock stuff and prepare crypto FS:
 		final Cryptor cryptor = new NoCryptor();
 		final FileSystem physicalFs = new InMemoryFileSystem();
-		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
+		final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, Mockito.mock(CryptoFileSystemDelegate.class), "foo");
 		fs.create();
 
 		// write test content to file

+ 1 - 1
main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoReadableFileTest.java

@@ -30,7 +30,7 @@ public class CryptoReadableFileTest {
 		}).thenThrow(new UncheckedIOException(new IOException("failed.")));
 
 		@SuppressWarnings("resource")
-		ReadableFile cryptoReadableFile = new CryptoReadableFile(fileContentCryptor, underlyingFile);
+		ReadableFile cryptoReadableFile = new CryptoReadableFile(fileContentCryptor, underlyingFile, true);
 		cryptoReadableFile.read(ByteBuffer.allocate(1));
 	}
 

+ 4 - 2
main/filesystem-invariants-tests/src/test/java/org/cryptomator/filesystem/invariants/FileSystemFactories.java

@@ -13,9 +13,11 @@ import java.util.List;
 import org.cryptomator.crypto.engine.impl.CryptorImpl;
 import org.cryptomator.filesystem.FileSystem;
 import org.cryptomator.filesystem.crypto.CryptoFileSystem;
+import org.cryptomator.filesystem.crypto.CryptoFileSystemDelegate;
 import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
 import org.cryptomator.filesystem.invariants.FileSystemFactories.FileSystemFactory;
 import org.cryptomator.filesystem.nio.NioFileSystem;
+import org.mockito.Mockito;
 
 class FileSystemFactories implements Iterable<FileSystemFactory> {
 
@@ -48,11 +50,11 @@ class FileSystemFactories implements Iterable<FileSystemFactory> {
 	}
 
 	private FileSystem createCryptoFileSystemInMemory() {
-		return new CryptoFileSystem(createInMemoryFileSystem(), new CryptorImpl(RANDOM_MOCK), "aPassphrase");
+		return new CryptoFileSystem(createInMemoryFileSystem(), new CryptorImpl(RANDOM_MOCK), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase");
 	}
 
 	private FileSystem createCryptoFileSystemNio() {
-		return new CryptoFileSystem(createNioFileSystem(), new CryptorImpl(RANDOM_MOCK), "aPassphrase");
+		return new CryptoFileSystem(createNioFileSystem(), new CryptorImpl(RANDOM_MOCK), Mockito.mock(CryptoFileSystemDelegate.class), "aPassphrase");
 	}
 
 	private void add(String name, FileSystemFactory factory) {