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

This commit is contained in:
Sebastian Stenzel
2016-01-17 21:44:47 +01:00
parent d5c43f625f
commit cd72dae0d7
10 changed files with 166 additions and 20 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
@@ -21,14 +22,22 @@ class CiphertextWriter implements Callable<Void> {
@Override
public Void call() throws InterruptedIOException {
try {
ByteBuffer ciphertext;
while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) {
file.write(ciphertext);
}
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 (UncheckedIOException e) {
encryptor.cancelWithException(e);
}
}
}

View File

@@ -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

View File

@@ -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];

View File

@@ -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();
}
}
}

View File

@@ -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));
}
}