diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java index cf0b95a3c2..5632b76a3d 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java +++ b/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. diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java index 3544ede364..e295a5a72a 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java +++ b/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. diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java index fd5df0e776..f4b652f0ac 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java @@ -11,6 +11,8 @@ 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 @@ public void append(ByteBuffer ciphertext) throws InterruptedException { } } + @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 @@ public ByteBuffer cleartext() throws InterruptedException { } 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); } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java index e1f17538c6..e48692974b 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java @@ -10,6 +10,8 @@ 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 @@ public void append(ByteBuffer cleartext) throws InterruptedException { } } + @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 @@ public ByteBuffer ciphertext() throws InterruptedException { 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); + } } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java index fbba8e9502..b39cc2a3bf 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java +++ b/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 @@ public CiphertextReader(ReadableFile file, FileContentDecryptor decryptor, long @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 @@ public Void call() throws InterruptedIOException { } } 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; } } \ No newline at end of file diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java index 2fe3c320e6..7b2a2e8b6b 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java +++ b/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 @@ public CiphertextWriter(WritableFile file, FileContentEncryptor encryptor) { @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; } } \ No newline at end of file diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java index 6eebffc2fd..76a09ea630 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java +++ b/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 @@ public FileContentEncryptor createFileContentEncryptor(Optional head private class Decryptor implements FileContentDecryptor { - private final BlockingQueue cleartextQueue = new LinkedBlockingQueue<>(); + private final BlockingQueue> cleartextQueue = new LinkedBlockingQueue<>(); private final long contentLength; private Decryptor(ByteBuffer header) { @@ -57,18 +60,25 @@ public long contentLength() { 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 @@ public void destroy() { private class Encryptor implements FileContentEncryptor { - private final BlockingQueue ciphertextQueue = new LinkedBlockingQueue<>(); + private final BlockingQueue> ciphertextQueue = new LinkedBlockingQueue<>(); private long numCleartextBytesEncrypted = 0; @Override @@ -94,10 +104,10 @@ public ByteBuffer getHeader() { 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 @@ public void append(ByteBuffer cleartext) { } } + @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 diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java index d80e3ca468..b89bdca5e2 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java +++ b/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 void testDecryption() throws InterruptedException { } } + @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]; diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java index 6310fcabda..aad5fc7f65 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java +++ b/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 void testEncryption() throws InterruptedException { } } + @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(); + } + } + } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoReadableFileTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/filesystem/crypto/CryptoReadableFileTest.java new file mode 100644 index 0000000000..94ddba0313 --- /dev/null +++ b/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() { + @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)); + } + +}