mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 10:11:27 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user