added file size obfuscation padding

This commit is contained in:
Sebastian Stenzel
2016-02-21 00:20:57 +01:00
parent c93e4e462b
commit c7c4dd4581
5 changed files with 128 additions and 14 deletions

View File

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

View File

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

View File

@@ -115,7 +115,6 @@ class CryptoWritableFile implements WritableFile {
if (file.isOpen()) {
terminateAndWaitForWriteTask();
writeHeader();
// TODO append padding
}
} finally {
executorService.shutdownNow();

View File

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

View File

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