diff --git a/main/core/src/test/java/org/cryptomator/webdav/jackrabbit/RangeRequestTest.java b/main/core/src/test/java/org/cryptomator/webdav/jackrabbit/RangeRequestTest.java index 9b46bf300..6165855c9 100644 --- a/main/core/src/test/java/org/cryptomator/webdav/jackrabbit/RangeRequestTest.java +++ b/main/core/src/test/java/org/cryptomator/webdav/jackrabbit/RangeRequestTest.java @@ -1,7 +1,9 @@ package org.cryptomator.webdav.jackrabbit; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -62,6 +64,36 @@ public class RangeRequestTest { FileUtils.deleteQuietly(TMP_VAULT); } + @Test + public void testFullFileDecryption() throws IOException, URISyntaxException { + final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "fullFileDecryptionTestFile.txt"); + final HttpClient client = new HttpClient(); + + // prepare 64MiB test data: + final byte[] plaintextData = new byte[6777216 * Integer.BYTES]; + final ByteBuffer bbIn = ByteBuffer.wrap(plaintextData); + for (int i = 0; i < 6777216; i++) { + bbIn.putInt(i); + } + final InputStream plaintextDataInputStream = new ByteArrayInputStream(plaintextData); + + // put request: + final EntityEnclosingMethod putMethod = new PutMethod(testResourceUrl.toString()); + putMethod.setRequestEntity(new ByteArrayRequestEntity(plaintextData)); + final int putResponse = client.executeMethod(putMethod); + putMethod.releaseConnection(); + Assert.assertEquals(201, putResponse); + + // get request: + final HttpMethod getMethod = new GetMethod(testResourceUrl.toString()); + final int statusCode = client.executeMethod(getMethod); + Assert.assertEquals(200, statusCode); + Assert.assertTrue(IOUtils.contentEquals(plaintextDataInputStream, getMethod.getResponseBodyAsStream())); + getMethod.releaseConnection(); + + System.out.println("hello world"); + } + @Test public void testAsyncRangeRequests() throws IOException, URISyntaxException, InterruptedException { final URL testResourceUrl = new URL(VAULT_BASE_URI.toURL(), "asyncRangeRequestTestFile.txt"); diff --git a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java index 79b568e79..93cd5cf7f 100644 --- a/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java +++ b/main/crypto-aes/src/main/java/org/cryptomator/crypto/aes256/Aes256Cryptor.java @@ -19,7 +19,17 @@ import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; @@ -362,6 +372,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { @Override public Long decryptFile(SeekableByteChannel encryptedFile, OutputStream plaintextFile, boolean authenticate) throws IOException, DecryptFailedException { + long t0 = System.nanoTime(); + // read header: encryptedFile.position(0l); final ByteBuffer headerBuf = ByteBuffer.allocate(104); @@ -369,26 +381,36 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { if (headerBytesRead != headerBuf.capacity()) { throw new IOException("Failed to read file header."); } + System.err.println("read header: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // read iv: final byte[] iv = new byte[AES_BLOCK_LENGTH]; headerBuf.position(0); headerBuf.get(iv); + System.err.println("read iv: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // read nonce: final byte[] nonce = new byte[8]; headerBuf.position(16); headerBuf.get(nonce); + System.err.println("read nonce: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // read sensitive header data: final byte[] encryptedSensitiveHeaderContentBytes = new byte[48]; headerBuf.position(24); headerBuf.get(encryptedSensitiveHeaderContentBytes); + System.err.println("read sensitive header data: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // read header mac: final byte[] storedHeaderMac = new byte[32]; headerBuf.position(72); headerBuf.get(storedHeaderMac); + System.err.println("read header mac: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // calculate mac over first 72 bytes of header: if (authenticate) { @@ -400,6 +422,8 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { throw new MacAuthenticationFailedException("Header MAC authentication failed."); } } + System.err.println("calculate mac over first 72 bytes of header: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // decrypt sensitive header data: final byte[] fileKeyBytes = new byte[32]; @@ -407,53 +431,112 @@ public class Aes256Cryptor implements Cryptor, AesCryptographicConfiguration { final ByteBuffer sensitiveHeaderContentBuf = ByteBuffer.wrap(decryptedSensitiveHeaderContentBytes); final Long fileSize = sensitiveHeaderContentBuf.getLong(); sensitiveHeaderContentBuf.get(fileKeyBytes); - - // append counter to nonce: - final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH); - nonceAndCounterBuf.put(nonce); - nonceAndCounterBuf.putLong(0L); - final byte[] nonceAndCounter = nonceAndCounterBuf.array(); + System.err.println("decrypt sensitive header data: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + t0 = System.nanoTime(); // content decryption: encryptedFile.position(104l); final SecretKey fileKey = new SecretKeySpec(fileKeyBytes, AES_KEY_ALGORITHM); - final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE); - final Mac contentMac = this.hmacSha256(hMacMasterKey); + System.err.println("init DEC: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + + final int numWorkers = 1; + final ExecutorService executorService = Executors.newFixedThreadPool(numWorkers); + final Lock lock = new ReentrantLock(); + final Condition blockCondition = lock.newCondition(); // reading ciphered input and MACs interleaved: - long bytesDecrypted = 0; + final AtomicLong bytesDecrypted = new AtomicLong(); final InputStream in = new SeekableByteChannelInputStream(encryptedFile); - byte[] buffer = new byte[CONTENT_MAC_BLOCK + 32]; + final byte[] buffer = new byte[(CONTENT_MAC_BLOCK + 32) * numWorkers]; int n = 0; - long blockNum = 0; - while ((n = IOUtils.read(in, buffer)) > 0 && bytesDecrypted < fileSize) { - if (n < 32) { - throw new DecryptFailedException("Invalid file content, missing MAC."); + final AtomicLong blockNum = new AtomicLong(); + final AtomicLong numBlocksWritten = new AtomicLong(); + while ((n = IOUtils.read(in, buffer)) > 0 && bytesDecrypted.get() < fileSize) { + t0 = System.nanoTime(); + + final int finalN = n; + final List> tasks = new ArrayList<>(); + for (int i = 0; i < numWorkers; i++) { + final Future task = executorService.submit(() -> { + final long myBlockNum = blockNum.getAndIncrement(); + final int myBufferOffset = (int) ((myBlockNum % numWorkers) * (CONTENT_MAC_BLOCK + 32)); + final int myN = Math.min(finalN - myBufferOffset, CONTENT_MAC_BLOCK + 32); + + if (myN <= 0) { + // EOF + } else if (myN < 32) { + throw new DecryptFailedException("Invalid file content, missing MAC."); + } + + // check MAC of current block: + if (authenticate) { + final Mac contentMac = this.hmacSha256(hMacMasterKey); + contentMac.update(iv); + contentMac.update(longToByteArray(myBlockNum)); + contentMac.update(buffer, myBufferOffset, myN - 32); + final byte[] calculatedMac = contentMac.doFinal(); + final byte[] storedMac = new byte[32]; + System.arraycopy(buffer, myBufferOffset + myN - 32, storedMac, 0, 32); + if (!MessageDigest.isEqual(calculatedMac, storedMac)) { + throw new MacAuthenticationFailedException("Content MAC authentication failed."); + } + } + + // decrypt block: + final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH); + nonceAndCounterBuf.put(nonce); + nonceAndCounterBuf.putLong(myBlockNum * CONTENT_MAC_BLOCK / AES_BLOCK_LENGTH); + final byte[] nonceAndCounter = nonceAndCounterBuf.array(); + final Cipher cipher = this.aesCtrCipher(fileKey, nonceAndCounter, Cipher.DECRYPT_MODE); + final byte[] plaintext = cipher.update(buffer, myBufferOffset, myN - 32); + + // wait for our turn to write the plaintext: + lock.lock(); + try { + while (numBlocksWritten.get() != myBlockNum) { + blockCondition.await(); + } + final int plaintextLengthWithoutPadding = (int) Math.min(plaintext.length, fileSize - bytesDecrypted.get()); // plaintext.length is known to be a 32 bit int + plaintextFile.write(plaintext, 0, plaintextLengthWithoutPadding); + bytesDecrypted.addAndGet(plaintextLengthWithoutPadding); + numBlocksWritten.incrementAndGet(); + blockCondition.signalAll(); + } catch (InterruptedException e) { + + } finally { + lock.unlock(); + } + return true; + }); + tasks.add(task); } - // check MAC of current block: - if (authenticate) { - contentMac.update(iv); - contentMac.update(longToByteArray(blockNum)); - contentMac.update(buffer, 0, n - 32); - final byte[] calculatedMac = contentMac.doFinal(); - final byte[] storedMac = new byte[32]; - System.arraycopy(buffer, n - 32, storedMac, 0, 32); - if (!MessageDigest.isEqual(calculatedMac, storedMac)) { - throw new MacAuthenticationFailedException("Content MAC authentication failed."); + for (Future task : tasks) { + try { + task.get(); + } catch (InterruptedException e) { + // TODO Auto-generated catch block + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof DecryptFailedException) { + throw (DecryptFailedException) cause; + } else if (cause instanceof MacAuthenticationFailedException) { + throw (MacAuthenticationFailedException) cause; + } else if (cause instanceof IOException) { + throw (IOException) cause; + } else { + // TODO loggingpower + } } } - // decrypt block: - final byte[] plaintext = cipher.update(buffer, 0, n - 32); - final int plaintextLengthWithoutPadding = (int) Math.min(plaintext.length, fileSize - bytesDecrypted); // plaintext.length is known to be a 32 bit int - plaintextFile.write(plaintext, 0, plaintextLengthWithoutPadding); - bytesDecrypted += plaintextLengthWithoutPadding; - blockNum++; + // System.err.println("dec " + numWorkers + " blocks: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); } destroyQuietly(fileKey); - return bytesDecrypted; + System.err.println("cleanup: " + (System.nanoTime() - t0) / 1000 / 1000.0 + "ms"); + + return bytesDecrypted.get(); } @Override