mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-08-17 12:46:01 +00:00
first multithreaded decryption test (PoC quality)
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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<Future<Boolean>> tasks = new ArrayList<>();
|
||||
for (int i = 0; i < numWorkers; i++) {
|
||||
final Future<Boolean> 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<Boolean> 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
|
||||
|
||||
Reference in New Issue
Block a user