mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-20 19:51:27 +00:00
added file size obfuscation padding
This commit is contained in:
@@ -22,6 +22,7 @@ import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.LongAdder;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
@@ -45,6 +46,8 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
private final ThreadLocal<Mac> hmacSha256;
|
||||
private final FileHeader header;
|
||||
private final boolean authenticate;
|
||||
private final LongAdder cleartextBytesScheduledForDecryption = new LongAdder();
|
||||
private final LongAdder cleartextBytesDecrypted = new LongAdder();
|
||||
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
@@ -63,11 +66,13 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
|
||||
@Override
|
||||
public void append(ByteBuffer ciphertext) throws InterruptedException {
|
||||
if (ciphertext == FileContentCryptor.EOF) {
|
||||
if (cleartextBytesScheduledForDecryption.sum() >= contentLength()) {
|
||||
submitEof();
|
||||
} else if (ciphertext == FileContentCryptor.EOF) {
|
||||
submitCiphertextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
while (ciphertext.hasRemaining()) {
|
||||
while (ciphertext.hasRemaining() && cleartextBytesScheduledForDecryption.sum() < contentLength()) {
|
||||
ByteBuffers.copy(ciphertext, ciphertextBuffer);
|
||||
submitCiphertextBufferIfFull();
|
||||
}
|
||||
@@ -91,6 +96,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
private void submitCiphertextBuffer() throws InterruptedException {
|
||||
ciphertextBuffer.flip();
|
||||
if (ciphertextBuffer.hasRemaining()) {
|
||||
cleartextBytesScheduledForDecryption.add(ciphertextBuffer.remaining() - MAC_SIZE - NONCE_SIZE);
|
||||
Callable<ByteBuffer> encryptionJob = new DecryptionJob(ciphertextBuffer, chunkNumber++);
|
||||
dataProcessor.submit(encryptionJob);
|
||||
}
|
||||
@@ -103,7 +109,15 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
|
||||
@Override
|
||||
public ByteBuffer cleartext() throws InterruptedException {
|
||||
try {
|
||||
return dataProcessor.processedData();
|
||||
final ByteBuffer cleartext = dataProcessor.processedData();
|
||||
long bytesUntilLogicalEof = contentLength() - cleartextBytesDecrypted.sum();
|
||||
if (bytesUntilLogicalEof <= 0) {
|
||||
return FileContentCryptor.EOF;
|
||||
} else if (bytesUntilLogicalEof < cleartext.remaining()) {
|
||||
cleartext.limit((int) bytesUntilLogicalEof);
|
||||
}
|
||||
cleartextBytesDecrypted.add(cleartext.remaining());
|
||||
return cleartext;
|
||||
} catch (ExecutionException e) {
|
||||
if (e.getCause() instanceof AuthenticationFailedException) {
|
||||
throw new AuthenticationFailedException(e);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
*******************************************************************************/
|
||||
package org.cryptomator.crypto.engine.impl;
|
||||
|
||||
import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.NONCE_SIZE;
|
||||
import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.PAYLOAD_SIZE;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -34,8 +35,9 @@ import org.cryptomator.io.ByteBuffers;
|
||||
|
||||
class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
private static final int NONCE_SIZE = 16;
|
||||
private static final String HMAC_SHA256 = "HmacSHA256";
|
||||
private static final int PADDING_LOWER_BOUND = 4 * 1024; // 4k
|
||||
private static final int PADDING_UPPER_BOUND = 16 * 1024 * 1024; // 16M
|
||||
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
|
||||
private static final int READ_AHEAD = 2;
|
||||
private static final ExecutorService SHARED_DECRYPTION_EXECUTOR = Executors.newFixedThreadPool(NUM_THREADS);
|
||||
@@ -45,7 +47,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
private final SecretKey headerKey;
|
||||
private final FileHeader header;
|
||||
private final SecureRandom randomSource;
|
||||
private final LongAdder cleartextBytesEncrypted = new LongAdder();
|
||||
private final LongAdder cleartextBytesScheduledForEncryption = new LongAdder();
|
||||
private ByteBuffer cleartextBuffer = ByteBuffer.allocate(PAYLOAD_SIZE);
|
||||
private long chunkNumber = 0;
|
||||
|
||||
@@ -61,7 +63,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
@Override
|
||||
public ByteBuffer getHeader() {
|
||||
header.getPayload().setFilesize(cleartextBytesEncrypted.sum());
|
||||
header.getPayload().setFilesize(cleartextBytesScheduledForEncryption.sum());
|
||||
return header.toByteBuffer(headerKey, hmacSha256);
|
||||
}
|
||||
|
||||
@@ -72,15 +74,31 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
|
||||
|
||||
@Override
|
||||
public void append(ByteBuffer cleartext) throws InterruptedException {
|
||||
cleartextBytesEncrypted.add(cleartext.remaining());
|
||||
cleartextBytesScheduledForEncryption.add(cleartext.remaining());
|
||||
if (cleartext == FileContentCryptor.EOF) {
|
||||
appendSizeObfuscationPadding(cleartextBytesScheduledForEncryption.sum());
|
||||
submitCleartextBuffer();
|
||||
submitEof();
|
||||
} else {
|
||||
while (cleartext.hasRemaining()) {
|
||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||
submitCleartextBufferIfFull();
|
||||
}
|
||||
appendAllAndSubmitIfFull(cleartext);
|
||||
}
|
||||
}
|
||||
|
||||
private void appendSizeObfuscationPadding(long actualSize) throws InterruptedException {
|
||||
final int maxPaddingLength = (int) Math.min(Math.max(actualSize / 10, PADDING_LOWER_BOUND), PADDING_UPPER_BOUND); // preferably 10%, but at least lower bound and no more than upper bound
|
||||
final int randomPaddingLength = randomSource.nextInt(maxPaddingLength);
|
||||
int remainingPadding = randomPaddingLength;
|
||||
while (remainingPadding > 0) {
|
||||
ByteBuffer buf = ByteBuffer.allocate(Math.min(remainingPadding, PAYLOAD_SIZE));
|
||||
appendAllAndSubmitIfFull(buf);
|
||||
remainingPadding -= buf.capacity();
|
||||
}
|
||||
}
|
||||
|
||||
private void appendAllAndSubmitIfFull(ByteBuffer cleartext) throws InterruptedException {
|
||||
while (cleartext.hasRemaining()) {
|
||||
ByteBuffers.copy(cleartext, cleartextBuffer);
|
||||
submitCleartextBufferIfFull();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ class CryptoWritableFile implements WritableFile {
|
||||
if (file.isOpen()) {
|
||||
terminateAndWaitForWriteTask();
|
||||
writeHeader();
|
||||
// TODO append padding
|
||||
}
|
||||
} finally {
|
||||
executorService.shutdownNow();
|
||||
|
||||
@@ -36,7 +36,19 @@ public class FileContentCryptorImplTest {
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
|
||||
|
||||
private static final long serialVersionUID = 1505563778398085504L;
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return 500;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
@@ -125,6 +137,45 @@ public class FileContentCryptorImplTest {
|
||||
Assert.assertArrayEquals("cleartext message".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEncryptionAndDecryptionWithSizeObfuscationPadding() throws InterruptedException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
|
||||
final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256");
|
||||
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK_2);
|
||||
|
||||
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
|
||||
ByteBuffer ciphertext = ByteBuffer.allocate(16 + 11 + 500 + 32 + 1); // 16 bytes iv + 11 bytes ciphertext + 500 bytes padding + 32 bytes mac + 1.
|
||||
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
|
||||
encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, ciphertext);
|
||||
}
|
||||
ByteBuffers.copy(encryptor.getHeader(), header);
|
||||
}
|
||||
header.flip();
|
||||
ciphertext.flip();
|
||||
|
||||
Assert.assertEquals(16 + 11 + 500 + 32, ciphertext.remaining());
|
||||
|
||||
ByteBuffer plaintext = ByteBuffer.allocate(12); // 11 bytes plaintext + 1
|
||||
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0, true)) {
|
||||
decryptor.append(ciphertext);
|
||||
decryptor.append(FileContentCryptor.EOF);
|
||||
ByteBuffer buf;
|
||||
while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, plaintext);
|
||||
}
|
||||
}
|
||||
plaintext.flip();
|
||||
|
||||
byte[] result = new byte[plaintext.remaining()];
|
||||
plaintext.get(result);
|
||||
Assert.assertArrayEquals("hello world".getBytes(), result);
|
||||
}
|
||||
|
||||
@Test(timeout = 20000) // assuming a minimum speed of 10mb/s during encryption and decryption 20s should be enough
|
||||
public void testEncryptionAndDecryptionSpeed() throws InterruptedException, IOException {
|
||||
final byte[] keyBytes = new byte[32];
|
||||
|
||||
@@ -28,7 +28,19 @@ public class FileContentEncryptorImplTest {
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK = new SecureRandom() {
|
||||
|
||||
private static final long serialVersionUID = 1505563778398085504L;
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
Arrays.fill(bytes, (byte) 0x00);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
private static final SecureRandom RANDOM_MOCK_2 = new SecureRandom() {
|
||||
|
||||
@Override
|
||||
public int nextInt(int bound) {
|
||||
return 42;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void nextBytes(byte[] bytes) {
|
||||
@@ -83,4 +95,24 @@ public class FileContentEncryptorImplTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSizeObfuscation() 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_2, 0)) {
|
||||
encryptor.append(FileContentCryptor.EOF);
|
||||
|
||||
ByteBuffer result = ByteBuffer.allocate(91); // 16 bytes iv + 42 bytes size obfuscation + 32 bytes mac + 1
|
||||
ByteBuffer buf;
|
||||
while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
|
||||
ByteBuffers.copy(buf, result);
|
||||
}
|
||||
result.flip();
|
||||
|
||||
Assert.assertEquals(90, result.remaining());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user