From c7c4dd4581f59c7323e70f9ce52c9f6dc39ea652 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 21 Feb 2016 00:20:57 +0100 Subject: [PATCH] added file size obfuscation padding --- .../engine/impl/FileContentDecryptorImpl.java | 20 +++++-- .../engine/impl/FileContentEncryptorImpl.java | 34 +++++++++--- .../filesystem/crypto/CryptoWritableFile.java | 1 - .../impl/FileContentCryptorImplTest.java | 53 ++++++++++++++++++- .../impl/FileContentEncryptorImplTest.java | 34 +++++++++++- 5 files changed, 128 insertions(+), 14 deletions(-) diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java index 2453c1ce8..7afc299a9 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java @@ -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 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 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); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java index ab7d39893..6310a8b4d 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java @@ -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(); } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java index 7eb2a8b9d..5f4ecc177 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java @@ -115,7 +115,6 @@ class CryptoWritableFile implements WritableFile { if (file.isOpen()) { terminateAndWaitForWriteTask(); writeHeader(); - // TODO append padding } } finally { executorService.shutdownNow(); diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java index d8fce4283..71ed1b825 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImplTest.java @@ -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]; diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java index 8653e1f47..b99964a77 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java @@ -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()); + } + } + }