瀏覽代碼

pass I/O exceptions on producer side to the consumer, so that decryption fails, if reading the decrypted file fails.

Sebastian Stenzel 9 年之前
父節點
當前提交
cd72dae0d7

+ 12 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java

@@ -9,6 +9,7 @@
 package org.cryptomator.crypto.engine;
 
 import java.io.Closeable;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 
 import javax.security.auth.Destroyable;
@@ -31,6 +32,14 @@ public interface FileContentDecryptor extends Destroyable, Closeable {
 	 */
 	void append(ByteBuffer ciphertext) throws InterruptedException;
 
+	/**
+	 * Cancels decryption due to an exception in the thread responsible for appending ciphertext.
+	 * The exception will be the root cause of an {@link UncheckedIOException} thrown by {@link #cleartext()} when retrieving the decrypted result.
+	 * 
+	 * @param cause The exception making it impossible to {@link #append(ByteBuffer)} further ciphertext.
+	 */
+	void cancelWithException(Exception cause) throws InterruptedException;
+
 	/**
 	 * Returns the next decrypted cleartext in byte-by-byte FIFO order, meaning in the order ciphertext has been appended to this encryptor.
 	 * However the number and size of the cleartext byte buffers doesn't need to resemble the ciphertext buffers.
@@ -38,8 +47,10 @@ public interface FileContentDecryptor extends Destroyable, Closeable {
 	 * This method might block if no cleartext is available yet.
 	 * 
 	 * @return Decrypted cleartext or {@link FileContentCryptor#EOF}.
+	 * @throws AuthenticationFailedException On MAC mismatches
+	 * @throws UncheckedIOException In case of I/O exceptions, e.g. caused by previous {@link #cancelWithException(Exception)}.
 	 */
-	ByteBuffer cleartext() throws InterruptedException, AuthenticationFailedException;
+	ByteBuffer cleartext() throws InterruptedException, AuthenticationFailedException, UncheckedIOException;
 
 	/**
 	 * Clears file-specific sensitive information.

+ 11 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java

@@ -9,6 +9,7 @@
 package org.cryptomator.crypto.engine;
 
 import java.io.Closeable;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 
 import javax.security.auth.Destroyable;
@@ -33,6 +34,14 @@ public interface FileContentEncryptor extends Destroyable, Closeable {
 	 */
 	void append(ByteBuffer cleartext) throws InterruptedException;
 
+	/**
+	 * Cancels encryption due to an exception in the thread responsible for appending cleartext.
+	 * The exception will be the root cause of an {@link UncheckedIOException} thrown by {@link #ciphertext()} when retrieving the encrypted result.
+	 * 
+	 * @param cause The exception making it impossible to {@link #append(ByteBuffer)} further cleartext.
+	 */
+	void cancelWithException(Exception cause) throws InterruptedException;
+
 	/**
 	 * Returns the next ciphertext in byte-by-byte FIFO order, meaning in the order cleartext has been appended to this encryptor.
 	 * However the number and size of the ciphertext byte buffers doesn't need to resemble the cleartext buffers.
@@ -40,8 +49,9 @@ public interface FileContentEncryptor extends Destroyable, Closeable {
 	 * This method might block if no ciphertext is available yet.
 	 * 
 	 * @return Encrypted ciphertext of {@link FileContentCryptor#EOF}.
+	 * @throws UncheckedIOException In case of I/O exceptions, e.g. caused by previous {@link #cancelWithException(Exception)}.
 	 */
-	ByteBuffer ciphertext() throws InterruptedException;
+	ByteBuffer ciphertext() throws InterruptedException, UncheckedIOException;
 
 	/**
 	 * Clears file-specific sensitive information.

+ 11 - 0
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java

@@ -11,6 +11,8 @@ package org.cryptomator.crypto.engine.impl;
 import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.CHUNK_SIZE;
 import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.MAC_SIZE;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
@@ -67,6 +69,13 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
 		}
 	}
 
+	@Override
+	public void cancelWithException(Exception cause) throws InterruptedException {
+		dataProcessor.submit(() -> {
+			throw cause;
+		});
+	}
+
 	private void submitCiphertextBufferIfFull() throws InterruptedException {
 		if (!ciphertextBuffer.hasRemaining()) {
 			submitCiphertextBuffer();
@@ -93,6 +102,8 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
 		} catch (ExecutionException e) {
 			if (e.getCause() instanceof AuthenticationFailedException) {
 				throw new AuthenticationFailedException(e);
+			} else if (e.getCause() instanceof IOException || e.getCause() instanceof UncheckedIOException) {
+				throw new UncheckedIOException(new IOException("Decryption failed due to I/O exception during ciphertext supply.", e));
 			} else {
 				throw new RuntimeException(e);
 			}

+ 14 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java

@@ -10,6 +10,8 @@ package org.cryptomator.crypto.engine.impl;
 
 import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.CHUNK_SIZE;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.security.InvalidAlgorithmParameterException;
 import java.security.InvalidKeyException;
@@ -72,6 +74,13 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
 		}
 	}
 
+	@Override
+	public void cancelWithException(Exception cause) throws InterruptedException {
+		dataProcessor.submit(() -> {
+			throw cause;
+		});
+	}
+
 	private void submitCleartextBufferIfFull() throws InterruptedException {
 		if (!cleartextBuffer.hasRemaining()) {
 			submitCleartextBuffer();
@@ -96,7 +105,11 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
 		try {
 			return dataProcessor.processedData();
 		} catch (ExecutionException e) {
-			throw new RuntimeException(e);
+			if (e.getCause() instanceof IOException || e.getCause() instanceof UncheckedIOException) {
+				throw new UncheckedIOException(new IOException("Encryption failed due to I/O exception during cleartext supply.", e));
+			} else {
+				throw new RuntimeException(e);
+			}
 		}
 	}
 

+ 14 - 5
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java

@@ -1,6 +1,7 @@
 package org.cryptomator.filesystem.crypto;
 
 import java.io.InterruptedIOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.util.concurrent.Callable;
 
@@ -24,9 +25,18 @@ class CiphertextReader implements Callable<Void> {
 
 	@Override
 	public Void call() throws InterruptedIOException {
-		file.position(startpos);
-		int bytesRead = -1;
 		try {
+			callInterruptibly();
+		} catch (InterruptedException e) {
+			throw new InterruptedIOException("Task interrupted while waiting for ciphertext");
+		}
+		return null;
+	}
+
+	private void callInterruptibly() throws InterruptedException {
+		try {
+			file.position(startpos);
+			int bytesRead = -1;
 			do {
 				ByteBuffer ciphertext = ByteBuffer.allocate(READ_BUFFER_SIZE);
 				file.read(ciphertext);
@@ -37,10 +47,9 @@ class CiphertextReader implements Callable<Void> {
 				}
 			} while (bytesRead > 0);
 			decryptor.append(FileContentCryptor.EOF);
-		} catch (InterruptedException e) {
-			throw new InterruptedIOException("Task interrupted while waiting for ciphertext");
+		} catch (UncheckedIOException e) {
+			decryptor.cancelWithException(e);
 		}
-		return null;
 	}
 
 }

+ 12 - 3
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java

@@ -1,6 +1,7 @@
 package org.cryptomator.filesystem.crypto;
 
 import java.io.InterruptedIOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.util.concurrent.Callable;
 
@@ -20,15 +21,23 @@ class CiphertextWriter implements Callable<Void> {
 
 	@Override
 	public Void call() throws InterruptedIOException {
+		try {
+			callInterruptibly();
+		} catch (InterruptedException e) {
+			throw new InterruptedIOException("Task interrupted while waiting for ciphertext");
+		}
+		return null;
+	}
+
+	private void callInterruptibly() throws InterruptedException {
 		try {
 			ByteBuffer ciphertext;
 			while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) {
 				file.write(ciphertext);
 			}
-		} catch (InterruptedException e) {
-			throw new InterruptedIOException("Task interrupted while waiting for ciphertext");
+		} catch (UncheckedIOException e) {
+			encryptor.cancelWithException(e);
 		}
-		return null;
 	}
 
 }

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

@@ -8,10 +8,13 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.util.Optional;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
+import java.util.function.Supplier;
 
 class NoFileContentCryptor implements FileContentCryptor {
 
@@ -40,7 +43,7 @@ class NoFileContentCryptor implements FileContentCryptor {
 
 	private class Decryptor implements FileContentDecryptor {
 
-		private final BlockingQueue<ByteBuffer> cleartextQueue = new LinkedBlockingQueue<>();
+		private final BlockingQueue<Supplier<ByteBuffer>> cleartextQueue = new LinkedBlockingQueue<>();
 		private final long contentLength;
 
 		private Decryptor(ByteBuffer header) {
@@ -57,18 +60,25 @@ class NoFileContentCryptor implements FileContentCryptor {
 		public void append(ByteBuffer ciphertext) {
 			try {
 				if (ciphertext == FileContentCryptor.EOF) {
-					cleartextQueue.put(FileContentCryptor.EOF);
+					cleartextQueue.put(() -> FileContentCryptor.EOF);
 				} else {
-					cleartextQueue.put(ciphertext.asReadOnlyBuffer());
+					cleartextQueue.put(ciphertext::asReadOnlyBuffer);
 				}
 			} catch (InterruptedException e) {
 				Thread.currentThread().interrupt();
 			}
 		}
 
+		@Override
+		public void cancelWithException(Exception cause) throws InterruptedException {
+			cleartextQueue.put(() -> {
+				throw new UncheckedIOException(new IOException(cause));
+			});
+		}
+
 		@Override
 		public ByteBuffer cleartext() throws InterruptedException {
-			return cleartextQueue.take();
+			return cleartextQueue.take().get();
 		}
 
 		@Override
@@ -80,7 +90,7 @@ class NoFileContentCryptor implements FileContentCryptor {
 
 	private class Encryptor implements FileContentEncryptor {
 
-		private final BlockingQueue<ByteBuffer> ciphertextQueue = new LinkedBlockingQueue<>();
+		private final BlockingQueue<Supplier<ByteBuffer>> ciphertextQueue = new LinkedBlockingQueue<>();
 		private long numCleartextBytesEncrypted = 0;
 
 		@Override
@@ -94,10 +104,10 @@ class NoFileContentCryptor implements FileContentCryptor {
 		public void append(ByteBuffer cleartext) {
 			try {
 				if (cleartext == FileContentCryptor.EOF) {
-					ciphertextQueue.put(FileContentCryptor.EOF);
+					ciphertextQueue.put(() -> FileContentCryptor.EOF);
 				} else {
 					int cleartextLen = cleartext.remaining();
-					ciphertextQueue.put(cleartext.asReadOnlyBuffer());
+					ciphertextQueue.put(cleartext::asReadOnlyBuffer);
 					numCleartextBytesEncrypted += cleartextLen;
 				}
 			} catch (InterruptedException e) {
@@ -105,9 +115,16 @@ class NoFileContentCryptor implements FileContentCryptor {
 			}
 		}
 
+		@Override
+		public void cancelWithException(Exception cause) throws InterruptedException {
+			ciphertextQueue.put(() -> {
+				throw new UncheckedIOException(new IOException(cause));
+			});
+		}
+
 		@Override
 		public ByteBuffer ciphertext() throws InterruptedException {
-			return ciphertextQueue.take();
+			return ciphertextQueue.take().get();
 		}
 
 		@Override

+ 15 - 0
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java

@@ -8,6 +8,8 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine.impl;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.security.SecureRandom;
 import java.util.Arrays;
@@ -60,6 +62,19 @@ public class FileContentDecryptorImplTest {
 		}
 	}
 
+	@Test(expected = UncheckedIOException.class)
+	public void testPassthroughException() throws InterruptedException {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
+		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)) {
+			decryptor.cancelWithException(new IOException("can not do"));
+			decryptor.cleartext();
+		}
+	}
+
 	@Test(timeout = 2000)
 	public void testPartialDecryption() throws InterruptedException {
 		final byte[] keyBytes = new byte[32];

+ 14 - 0
main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java

@@ -8,6 +8,8 @@
  *******************************************************************************/
 package org.cryptomator.crypto.engine.impl;
 
+import java.io.IOException;
+import java.io.UncheckedIOException;
 import java.nio.ByteBuffer;
 import java.security.SecureRandom;
 import java.util.Arrays;
@@ -59,4 +61,16 @@ public class FileContentEncryptorImplTest {
 		}
 	}
 
+	@Test(expected = UncheckedIOException.class)
+	public void testPassthroughException() throws InterruptedException {
+		final byte[] keyBytes = new byte[32];
+		final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
+		final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+
+		try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK, 0)) {
+			encryptor.cancelWithException(new IOException("can not do"));
+			encryptor.ciphertext();
+		}
+	}
+
 }

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

@@ -0,0 +1,37 @@
+package org.cryptomator.filesystem.crypto;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.nio.ByteBuffer;
+
+import org.cryptomator.crypto.engine.FileContentCryptor;
+import org.cryptomator.crypto.engine.NoCryptor;
+import org.cryptomator.filesystem.ReadableFile;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+public class CryptoReadableFileTest {
+
+	@Test(expected = UncheckedIOException.class)
+	public void testPassthroughExceptions() {
+		FileContentCryptor fileContentCryptor = new NoCryptor().getFileContentCryptor();
+
+		// return a valid header but throw exception on consecutive read attempts:
+		ReadableFile underlyingFile = Mockito.mock(ReadableFile.class);
+		Mockito.when(underlyingFile.read(Mockito.any(ByteBuffer.class))).thenAnswer(new Answer<Integer>() {
+			@Override
+			public Integer answer(InvocationOnMock invocation) throws Throwable {
+				ByteBuffer buf = (ByteBuffer) invocation.getArguments()[0];
+				buf.position(fileContentCryptor.getHeaderSize());
+				return fileContentCryptor.getHeaderSize();
+			}
+		}).thenThrow(new UncheckedIOException(new IOException("failed.")));
+
+		@SuppressWarnings("resource")
+		ReadableFile cryptoReadableFile = new CryptoReadableFile(fileContentCryptor, underlyingFile);
+		cryptoReadableFile.read(ByteBuffer.allocate(1));
+	}
+
+}