Skip to content

Commit

Permalink
pass I/O exceptions on producer side to the consumer, so that decrypt…
Browse files Browse the repository at this point in the history
…ion fails, if reading the decrypted file fails.
  • Loading branch information
overheadhunter committed Jan 17, 2016
1 parent d5c43f6 commit cd72dae
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 19 deletions.
Expand Up @@ -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;
Expand All @@ -31,15 +32,25 @@ 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.
*
* 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.
Expand Down
Expand Up @@ -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;
Expand All @@ -33,15 +34,24 @@ 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.
*
* 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.
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
}
}

Expand Down
@@ -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;

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

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

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

}
Expand Up @@ -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 {

Expand Down Expand Up @@ -40,7 +43,7 @@ public FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> head

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) {
Expand All @@ -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
Expand All @@ -80,7 +90,7 @@ public void destroy() {

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
Expand All @@ -94,20 +104,27 @@ 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) {
Thread.currentThread().interrupt();
}
}

@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
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}

}

0 comments on commit cd72dae

Please sign in to comment.