Переглянути джерело

added file size obfuscation padding

Sebastian Stenzel 9 роки тому
батько
коміт
c7c4dd4581

+ 17 - 3
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<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);

+ 26 - 8
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();
 		}
 	}
 

+ 0 - 1
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();

+ 52 - 1
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];

+ 33 - 1
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());
+		}
+	}
+
 }