Explorar o código

- Changed file layout, added MAC (see #17)
- Obfuscates file size (fixes #18)

Sebastian Stenzel %!s(int64=10) %!d(string=hai) anos
pai
achega
e19cf1c942

+ 3 - 3
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFile.java

@@ -29,6 +29,7 @@ import org.apache.jackrabbit.webdav.lock.LockManager;
 import org.apache.jackrabbit.webdav.property.DavPropertyName;
 import org.apache.jackrabbit.webdav.property.DefaultDavProperty;
 import org.cryptomator.crypto.Cryptor;
+import org.cryptomator.crypto.exceptions.DecryptFailedException;
 import org.cryptomator.webdav.exceptions.IORuntimeException;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpHeaderValue;
@@ -78,9 +79,8 @@ public class EncryptedFile extends AbstractEncryptedNode {
 				}
 			} catch (EOFException e) {
 				LOG.warn("Unexpected end of stream (possibly client hung up).");
-			} catch (IOException e) {
-				LOG.error("Error reading file " + path.toString(), e);
-				throw new IORuntimeException(e);
+			} catch (DecryptFailedException e) {
+				throw new IOException("Error decrypting file " + path.toString(), e);
 			} finally {
 				IOUtils.closeQuietly(channel);
 			}

+ 3 - 4
main/core/src/main/java/org/cryptomator/webdav/jackrabbit/resources/EncryptedFilePart.java

@@ -21,7 +21,7 @@ import org.apache.jackrabbit.webdav.DavSession;
 import org.apache.jackrabbit.webdav.io.OutputContext;
 import org.apache.jackrabbit.webdav.lock.LockManager;
 import org.cryptomator.crypto.Cryptor;
-import org.cryptomator.webdav.exceptions.IORuntimeException;
+import org.cryptomator.crypto.exceptions.DecryptFailedException;
 import org.eclipse.jetty.http.HttpHeader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -128,9 +128,8 @@ public class EncryptedFilePart extends EncryptedFile {
 				if (LOG.isDebugEnabled()) {
 					LOG.debug("Unexpected end of stream during delivery of partial content (client hung up).");
 				}
-			} catch (IOException e) {
-				LOG.error("Error reading file " + path.toString(), e);
-				throw new IORuntimeException(e);
+			} catch (DecryptFailedException e) {
+				throw new IOException("Error decrypting file " + path.toString(), e);
 			} finally {
 				IOUtils.closeQuietly(channel);
 			}

+ 113 - 51
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java

@@ -86,8 +86,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
 	 */
 	private SecretKey hMacMasterKey;
 
-	private static final int SIZE_OF_LONG = Long.BYTES;
-
 	static {
 		try {
 			SECURE_PRNG = SecureRandom.getInstance(PRNG_ALGORITHM);
@@ -240,6 +238,31 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
 		}
 	}
 
+	private Cipher aesEcbCipher(SecretKey key, int cipherMode) {
+		try {
+			final Cipher cipher = Cipher.getInstance(AES_ECB_CIPHER);
+			cipher.init(cipherMode, key);
+			return cipher;
+		} catch (InvalidKeyException ex) {
+			throw new IllegalArgumentException("Invalid key.", ex);
+		} catch (NoSuchAlgorithmException | NoSuchPaddingException ex) {
+			throw new AssertionError("Every implementation of the Java platform is required to support AES/ECB/PKCS5Padding.", ex);
+		}
+
+	}
+
+	private Mac hmacSha256(SecretKey key) {
+		try {
+			final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
+			mac.init(key);
+			return mac;
+		} catch (NoSuchAlgorithmException e) {
+			throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
+		} catch (InvalidKeyException e) {
+			throw new IllegalArgumentException("Invalid key", e);
+		}
+	}
+
 	private byte[] randomData(int length) {
 		final byte[] result = new byte[length];
 		SECURE_PRNG.setSeed(SECURE_PRNG.generateSeed(PRNG_SEED_LENGTH));
@@ -269,18 +292,6 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
 		return crc32.getValue();
 	}
 
-	private byte[] hmacSha256(byte[] data) {
-		try {
-			final Mac mac = Mac.getInstance(HMAC_KEY_ALGORITHM);
-			mac.init(hMacMasterKey);
-			return mac.doFinal(data);
-		} catch (NoSuchAlgorithmException e) {
-			throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256.", e);
-		} catch (InvalidKeyException e) {
-			throw new IllegalArgumentException("Invalid key", e);
-		}
-	}
-
 	@Override
 	public String encryptPath(String cleartextPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport) {
 		try {
@@ -312,7 +323,7 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
 	 * {@link FileNamingConventions#LONG_NAME_FILE_EXT}.
 	 */
 	private String encryptPathComponent(final String cleartext, final SecretKey key, CryptorIOSupport ioSupport) throws IllegalBlockSizeException, BadPaddingException, IOException {
-		final byte[] mac = hmacSha256(cleartext.getBytes());
+		final byte[] mac = hmacSha256(hMacMasterKey).doFinal(cleartext.getBytes());
 		final byte[] partialIv = ArrayUtils.subarray(mac, 0, 10);
 		final ByteBuffer iv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
 		iv.put(partialIv);
@@ -390,58 +401,83 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
 		ioSupport.writePathSpecificMetadata(metadataFile, objectMapper.writeValueAsBytes(metadata));
 	}
 
+	@Override
+	public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
+		throw new UnsupportedOperationException("Not yet implemented.");
+	}
+
 	@Override
 	public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
-		final ByteBuffer sizeBuffer = ByteBuffer.allocate(SIZE_OF_LONG);
-		final int read = encryptedFile.read(sizeBuffer);
-		if (read == SIZE_OF_LONG) {
-			return sizeBuffer.getLong(0);
-		} else {
+		// skip 128bit IV + 256 bit MAC:
+		encryptedFile.position(48);
+
+		// read encrypted value:
+		final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
+		final int numFileSizeBytesRead = encryptedFile.read(encryptedFileSizeBuffer);
+
+		// return "unknown" value, if EOF
+		if (numFileSizeBytesRead != encryptedFileSizeBuffer.capacity()) {
 			return null;
 		}
+
+		// decrypt size:
+		try {
+			final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.DECRYPT_MODE);
+			final byte[] decryptedFileSize = sizeCipher.doFinal(encryptedFileSizeBuffer.array());
+			final ByteBuffer fileSizeBuffer = ByteBuffer.wrap(decryptedFileSize);
+			return fileSizeBuffer.getLong();
+		} catch (IllegalBlockSizeException | BadPaddingException e) {
+			throw new IllegalStateException(e);
+		}
 	}
 
 	@Override
 	public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
-		// skip content size:
-		encryptedFile.position(SIZE_OF_LONG);
-
 		// read iv:
+		encryptedFile.position(0);
 		final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
-		final int read = encryptedFile.read(countingIv);
-		if (read != AES_BLOCK_LENGTH) {
-			throw new IOException("Failed to read encrypted file header.");
+		final int numIvBytesRead = encryptedFile.read(countingIv);
+
+		// read file size:
+		final Long fileSize = decryptedContentLength(encryptedFile);
+
+		// check validity of header:
+		if (numIvBytesRead != AES_BLOCK_LENGTH || fileSize == null) {
+			throw new IOException("Failed to read file header.");
 		}
 
+		// go to begin of content:
+		encryptedFile.position(64);
+
 		// generate cipher:
 		final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
 
 		// read content
 		final InputStream in = new SeekableByteChannelInputStream(encryptedFile);
 		final InputStream cipheredIn = new CipherInputStream(in, cipher);
-		return IOUtils.copyLarge(cipheredIn, plaintextFile);
+		return IOUtils.copyLarge(cipheredIn, plaintextFile, 0, fileSize);
 	}
 
 	@Override
 	public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
-		// skip content size:
-		encryptedFile.position(SIZE_OF_LONG);
-
 		// read iv:
+		encryptedFile.position(0);
 		final ByteBuffer countingIv = ByteBuffer.allocate(AES_BLOCK_LENGTH);
-		final int read = encryptedFile.read(countingIv);
-		if (read != AES_BLOCK_LENGTH) {
-			throw new IOException("Failed to read encrypted file header.");
+		final int numIvBytesRead = encryptedFile.read(countingIv);
+
+		// check validity of header:
+		if (numIvBytesRead != AES_BLOCK_LENGTH) {
+			throw new IOException("Failed to read file header.");
 		}
 
 		// seek relevant position and update iv:
 		long firstRelevantBlock = pos / AES_BLOCK_LENGTH; // cut of fraction!
 		long beginOfFirstRelevantBlock = firstRelevantBlock * AES_BLOCK_LENGTH;
 		long offsetInsideFirstRelevantBlock = pos - beginOfFirstRelevantBlock;
-		countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, firstRelevantBlock);
+		countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, firstRelevantBlock);
 
 		// fast forward stream:
-		encryptedFile.position(SIZE_OF_LONG + AES_BLOCK_LENGTH + beginOfFirstRelevantBlock);
+		encryptedFile.position(64 + beginOfFirstRelevantBlock);
 
 		// generate cipher:
 		final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.DECRYPT_MODE);
@@ -459,32 +495,58 @@ public class Aes256Cryptor extends AbstractCryptor implements AesCryptographicCo
 
 		// use an IV, whose last 8 bytes store a long used in counter mode and write initial value to file.
 		final ByteBuffer countingIv = ByteBuffer.wrap(randomData(AES_BLOCK_LENGTH));
-		countingIv.putLong(AES_BLOCK_LENGTH - SIZE_OF_LONG, 0l);
+		countingIv.putLong(AES_BLOCK_LENGTH - Long.BYTES, 0l);
 		countingIv.position(0);
+		encryptedFile.write(countingIv);
 
-		// generate cipher:
+		// init crypto stuff:
+		final Mac mac = this.hmacSha256(hMacMasterKey);
 		final Cipher cipher = this.aesCtrCipher(primaryMasterKey, countingIv.array(), Cipher.ENCRYPT_MODE);
 
-		// 8 bytes (file size: temporarily -1):
-		final ByteBuffer fileSize = ByteBuffer.allocate(SIZE_OF_LONG);
-		fileSize.putLong(-1L);
-		fileSize.position(0);
-		encryptedFile.write(fileSize);
+		// init mac buffer and skip 32 bytes
+		final ByteBuffer macBuffer = ByteBuffer.allocate(mac.getMacLength());
+		encryptedFile.write(macBuffer);
 
-		// 16 bytes (iv):
-		encryptedFile.write(countingIv);
+		// init filesize buffer and skip 16 bytes
+		final ByteBuffer encryptedFileSizeBuffer = ByteBuffer.allocate(AES_BLOCK_LENGTH);
+		encryptedFile.write(encryptedFileSizeBuffer);
 
 		// write content:
 		final OutputStream out = new SeekableByteChannelOutputStream(encryptedFile);
-		final OutputStream cipheredOut = new CipherOutputStream(out, cipher);
+		final OutputStream macOut = new MacOutputStream(out, mac);
+		final OutputStream cipheredOut = new CipherOutputStream(macOut, cipher);
 		final Long actualSize = IOUtils.copyLarge(plaintextFile, cipheredOut);
 
-		// write filesize
-		fileSize.position(0);
-		fileSize.putLong(actualSize);
-		fileSize.position(0);
-		encryptedFile.position(0);
-		encryptedFile.write(fileSize);
+		// append fake content:
+		final int randomContentLength = (int) Math.ceil(Math.random() * actualSize / 10.0);
+		final byte[] emptyBytes = new byte[AES_BLOCK_LENGTH];
+		for (int i = 0; i < randomContentLength; i += AES_BLOCK_LENGTH) {
+			cipheredOut.write(emptyBytes);
+		}
+		cipheredOut.flush();
+
+		// copy MAC:
+		macBuffer.position(0);
+		macBuffer.put(mac.doFinal());
+
+		// encrypt actualSize
+		try {
+			final ByteBuffer fileSizeBuffer = ByteBuffer.allocate(Long.BYTES);
+			fileSizeBuffer.putLong(actualSize);
+			final Cipher sizeCipher = aesEcbCipher(primaryMasterKey, Cipher.ENCRYPT_MODE);
+			final byte[] encryptedFileSize = sizeCipher.doFinal(fileSizeBuffer.array());
+			encryptedFileSizeBuffer.position(0);
+			encryptedFileSizeBuffer.put(encryptedFileSize);
+		} catch (IllegalBlockSizeException | BadPaddingException e) {
+			throw new IllegalStateException(e);
+		}
+
+		// write file header
+		encryptedFile.position(16); // skip already written 128 bit IV
+		macBuffer.position(0);
+		encryptedFile.write(macBuffer); // 256 bit MAC
+		encryptedFileSizeBuffer.position(0);
+		encryptedFile.write(encryptedFileSizeBuffer); // 128 bit encrypted file size
 
 		return actualSize;
 	}

+ 7 - 0
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/AesCryptographicConfiguration.java

@@ -66,6 +66,13 @@ interface AesCryptographicConfiguration {
 	 */
 	String AES_CTR_CIPHER = "AES/CTR/NoPadding";
 
+	/**
+	 * Cipher specs for single block encryption (like file size).
+	 * 
+	 * @see http://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#impl
+	 */
+	String AES_ECB_CIPHER = "AES/ECB/PKCS5Padding";
+
 	/**
 	 * AES block size is 128 bit or 16 bytes.
 	 */

+ 39 - 0
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacInputStream.java

@@ -0,0 +1,39 @@
+package org.cryptomator.crypto.aes256;
+
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.crypto.Mac;
+
+/**
+ * Updates a {@link Mac} with the bytes read from this stream.
+ */
+class MacInputStream extends FilterInputStream {
+
+	private final Mac mac;
+
+	/**
+	 * @param in Stream from which to read contents, which will update the Mac.
+	 * @param mac Mac to be updated during writes.
+	 */
+	public MacInputStream(InputStream in, Mac mac) {
+		super(in);
+		this.mac = mac;
+	}
+
+	@Override
+	public int read() throws IOException {
+		int b = in.read();
+		mac.update((byte) b);
+		return b;
+	}
+
+	@Override
+	public int read(byte[] b, int off, int len) throws IOException {
+		int read = in.read(b, off, len);
+		mac.update(b);
+		return read;
+	}
+
+}

+ 37 - 0
main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/MacOutputStream.java

@@ -0,0 +1,37 @@
+package org.cryptomator.crypto.aes256;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.crypto.Mac;
+
+/**
+ * Updates a {@link Mac} with the bytes written to this stream.
+ */
+class MacOutputStream extends FilterOutputStream {
+
+	private final Mac mac;
+
+	/**
+	 * @param out Stream to redirect contents to after updating the mac.
+	 * @param mac Mac to be updated during writes.
+	 */
+	public MacOutputStream(OutputStream out, Mac mac) {
+		super(out);
+		this.mac = mac;
+	}
+
+	@Override
+	public void write(int b) throws IOException {
+		mac.update((byte) b);
+		out.write(b);
+	}
+
+	@Override
+	public void write(byte[] b, int off, int len) throws IOException {
+		mac.update(b, off, len);
+		out.write(b, off, len);
+	}
+
+}

+ 38 - 4
main/crypto-aes/src/test/java/org/cryptomator/crypto/aes256/Aes256CryptorTest.java

@@ -25,6 +25,7 @@ import org.cryptomator.crypto.exceptions.DecryptFailedException;
 import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException;
 import org.cryptomator.crypto.exceptions.WrongPasswordException;
 import org.junit.Assert;
+import org.junit.Ignore;
 import org.junit.Test;
 
 public class Aes256CryptorTest {
@@ -72,6 +73,31 @@ public class Aes256CryptorTest {
 		}
 	}
 
+	@Ignore
+	@Test
+	public void testIntegrityAuthentication() throws IOException {
+		// our test plaintext data:
+		final byte[] plaintextData = "Hello World".getBytes();
+		final InputStream plaintextIn = new ByteArrayInputStream(plaintextData);
+
+		// init cryptor:
+		final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
+
+		// encrypt:
+		final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
+		final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
+		cryptor.encryptFile(plaintextIn, encryptedOut);
+		IOUtils.closeQuietly(plaintextIn);
+		IOUtils.closeQuietly(encryptedOut);
+
+		encryptedData.position(0);
+
+		// authenticate unmodified content:
+		final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
+		final boolean unmodifiedContent = cryptor.authenticateContent(encryptedIn);
+		Assert.assertTrue(unmodifiedContent);
+	}
+
 	@Test
 	public void testEncryptionAndDecryption() throws IOException, DecryptFailedException, WrongPasswordException, UnsupportedKeyLengthException {
 		// our test plaintext data:
@@ -82,19 +108,25 @@ public class Aes256CryptorTest {
 		final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
 
 		// encrypt:
-		final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
+		final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
 		final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
 		cryptor.encryptFile(plaintextIn, encryptedOut);
 		IOUtils.closeQuietly(plaintextIn);
 		IOUtils.closeQuietly(encryptedOut);
 
-		// decrypt:
+		encryptedData.position(0);
+
+		// decrypt file size:
 		final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
+		final Long filesize = cryptor.decryptedContentLength(encryptedIn);
+		Assert.assertEquals(plaintextData.length, filesize.longValue());
+
+		// decrypt:
 		final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();
 		final Long numDecryptedBytes = cryptor.decryptedFile(encryptedIn, plaintextOut);
 		IOUtils.closeQuietly(encryptedIn);
 		IOUtils.closeQuietly(plaintextOut);
-		Assert.assertTrue(numDecryptedBytes > 0);
+		Assert.assertEquals(filesize.longValue(), numDecryptedBytes.longValue());
 
 		// check decrypted data:
 		final byte[] result = plaintextOut.toByteArray();
@@ -115,12 +147,14 @@ public class Aes256CryptorTest {
 		final Aes256Cryptor cryptor = new Aes256Cryptor(TEST_PRNG);
 
 		// encrypt:
-		final ByteBuffer encryptedData = ByteBuffer.allocate(plaintextData.length + 200);
+		final ByteBuffer encryptedData = ByteBuffer.allocate(64 + plaintextData.length * 4);
 		final SeekableByteChannel encryptedOut = new ByteBufferBackedSeekableChannel(encryptedData);
 		cryptor.encryptFile(plaintextIn, encryptedOut);
 		IOUtils.closeQuietly(plaintextIn);
 		IOUtils.closeQuietly(encryptedOut);
 
+		encryptedData.position(0);
+
 		// decrypt:
 		final SeekableByteChannel encryptedIn = new ByteBufferBackedSeekableChannel(encryptedData);
 		final ByteArrayOutputStream plaintextOut = new ByteArrayOutputStream();

+ 9 - 2
main/crypto-api/src/main/java/org/cryptomator/crypto/Cryptor.java

@@ -68,6 +68,11 @@ public interface Cryptor extends SensitiveDataSwipeListener {
 	 */
 	String decryptPath(String encryptedPath, char encryptedPathSep, char cleartextPathSep, CryptorIOSupport ioSupport);
 
+	/**
+	 * @return <code>true</code> If the integrity of the file can be assured.
+	 */
+	boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException;
+
 	/**
 	 * @param metadataSupport Support object allowing the Cryptor to read and write its own metadata to the location of the encrypted file.
 	 * @return Content length of the decrypted file or <code>null</code> if unknown.
@@ -76,15 +81,17 @@ public interface Cryptor extends SensitiveDataSwipeListener {
 
 	/**
 	 * @return Number of decrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.
+	 * @throws DecryptFailedException If decryption failed
 	 */
-	Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException;
+	Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException;
 
 	/**
 	 * @param pos First byte (inclusive)
 	 * @param length Number of requested bytes beginning at pos.
 	 * @return Number of decrypted bytes. This might not be equal to the number of bytes requested due to potential overheads.
+	 * @throws DecryptFailedException If decryption failed
 	 */
-	Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException;
+	Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException;
 
 	/**
 	 * @return Number of encrypted bytes. This might not be equal to the encrypted file size due to optional metadata written to it.

+ 7 - 2
main/crypto-api/src/main/java/org/cryptomator/crypto/SamplingDecorator.java

@@ -76,19 +76,24 @@ public class SamplingDecorator implements Cryptor, CryptorIOSampling {
 		return cryptor.decryptPath(encryptedPath, encryptedPathSep, cleartextPathSep, ioSupport);
 	}
 
+	@Override
+	public boolean authenticateContent(SeekableByteChannel encryptedFile) throws IOException {
+		return cryptor.authenticateContent(encryptedFile);
+	}
+
 	@Override
 	public Long decryptedContentLength(SeekableByteChannel encryptedFile) throws IOException {
 		return cryptor.decryptedContentLength(encryptedFile);
 	}
 
 	@Override
-	public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException {
+	public Long decryptedFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile) throws IOException, DecryptFailedException {
 		final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
 		return cryptor.decryptedFile(encryptedFile, countingInputStream);
 	}
 
 	@Override
-	public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException {
+	public Long decryptRange(SeekableByteChannel encryptedFile, OutputStream plaintextFile, long pos, long length) throws IOException, DecryptFailedException {
 		final OutputStream countingInputStream = new CountingOutputStream(decryptedBytes, plaintextFile);
 		return cryptor.decryptRange(encryptedFile, countingInputStream, pos, length);
 	}

+ 4 - 0
main/crypto-api/src/main/java/org/cryptomator/crypto/exceptions/DecryptFailedException.java

@@ -6,4 +6,8 @@ public class DecryptFailedException extends StorageCryptingException {
 	public DecryptFailedException(Throwable t) {
 		super("Decryption failed.", t);
 	}
+
+	protected DecryptFailedException(String msg) {
+		super(msg);
+	}
 }