From 7f313772e52db636df5d38db5da4547e9c6e1fe3 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 20 Feb 2016 14:10:46 +0100 Subject: [PATCH] fixed random access positioning --- .../CryptoFileSystemIntegrationTest.java | 126 +++++++++++++++--- .../engine/AuthenticationFailedException.java | 4 + .../crypto/engine/CryptoException.java | 4 + .../engine/impl/FileContentCryptorImpl.java | 5 +- .../engine/impl/FileContentDecryptorImpl.java | 13 +- 5 files changed, 127 insertions(+), 25 deletions(-) diff --git a/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java b/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java index 577e98a32..de1f2a6d1 100644 --- a/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java +++ b/main/filesystem-crypto-integration-tests/src/test/java/org/cryptomator/filesystem/crypto/CryptoFileSystemIntegrationTest.java @@ -11,7 +11,13 @@ package org.cryptomator.filesystem.crypto; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.ByteBuffer; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.ForkJoinTask; +import java.util.concurrent.Future; import org.cryptomator.filesystem.File; import org.cryptomator.filesystem.FileSystem; @@ -175,32 +181,118 @@ public class CryptoFileSystemIntegrationTest { } @Test - public void testRandomAccess() { - File cleartextFile = cleartextFs.file("test"); - try (WritableFile writable = cleartextFile.openWritable()) { - ByteBuffer buf = ByteBuffer.allocate(25000); - for (int i = 0; i < 40; i++) { // 40 * 25k = 1M - buf.clear(); - Arrays.fill(buf.array(), (byte) i); - writable.write(buf); - } + public void testRandomAccessOnLastBlock() { + // prepare test data: + ByteBuffer testData = ByteBuffer.allocate(16000 * Integer.BYTES); // < 64kb + for (int i = 0; i < 16000; i++) { + testData.putInt(i); } - Folder ciphertextRootFolder = ciphertextFs.folder("d").folders().findAny().get().folders().findAny().get(); - Assert.assertTrue(ciphertextRootFolder.exists()); - File ciphertextFile = ciphertextRootFolder.files().findAny().get(); - Assert.assertTrue(ciphertextFile.exists()); + // write test data to file: + File cleartextFile = cleartextFs.file("test"); + try (WritableFile writable = cleartextFile.openWritable()) { + testData.flip(); + writable.write(testData); + } + // read last block: try (ReadableFile readable = cleartextFile.openReadable()) { - ByteBuffer buf = ByteBuffer.allocate(1); - for (int i = 0; i < 40; i++) { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + buf.clear(); + readable.position(15999 * Integer.BYTES); + readable.read(buf); + buf.flip(); + Assert.assertEquals(15999, buf.getInt()); + } + } + + @Test + public void testSequentialRandomAccess() { + // prepare test data: + ByteBuffer testData = ByteBuffer.allocate(1_000_000 * Integer.BYTES); // = 4MB + for (int i = 0; i < 1000000; i++) { + testData.putInt(i); + } + + // write test data to file: + File cleartextFile = cleartextFs.file("test"); + try (WritableFile writable = cleartextFile.openWritable()) { + testData.flip(); + writable.write(testData); + } + + // shuffle our test positions: + List nums = new ArrayList<>(); + for (int i = 0; i < 1_000_000; i++) { + nums.add(i); + } + Collections.shuffle(nums); + + // read parts from positions: + try (ReadableFile readable = cleartextFile.openReadable()) { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + for (int i = 0; i < 1000; i++) { + int num = nums.get(i); buf.clear(); - readable.position(i * 25000 + (long) Math.random() * 24999); // "random access", told you so. + readable.position(num * Integer.BYTES); readable.read(buf); buf.flip(); - Assert.assertEquals(i, buf.get()); + Assert.assertEquals(num, buf.getInt()); } } } + @Test + public void testParallelRandomAccess() { + // prepare test data: + ByteBuffer testData = ByteBuffer.allocate(1_000_000 * Integer.BYTES); // = 4MB + for (int i = 0; i < 1000000; i++) { + testData.putInt(i); + } + + // write test data to file: + final File cleartextFile = cleartextFs.file("test"); + try (WritableFile writable = cleartextFile.openWritable()) { + testData.flip(); + writable.write(testData); + } + + // shuffle our test positions: + List nums = new ArrayList<>(); + for (int i = 0; i < 1_000_000; i++) { + nums.add(i); + } + Collections.shuffle(nums); + + // read parts from positions in parallel: + final ForkJoinPool pool = new ForkJoinPool(10); + final List> tasks = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + final int num = nums.get(i); + final ForkJoinTask task = ForkJoinTask.adapt(() -> { + try (ReadableFile readable = cleartextFile.openReadable()) { + ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES); + buf.clear(); + readable.position(num * Integer.BYTES); + readable.read(buf); + buf.flip(); + int numRead = buf.getInt(); + return num == numRead; + } + }); + pool.execute(task); + tasks.add(task); + } + + // Wait for tasks to finish and check results + Assert.assertTrue(tasks.stream().allMatch(task -> { + try { + return task.get(); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + })); + } + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java index 76d26eb42..0b350cc67 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java @@ -14,6 +14,10 @@ public class AuthenticationFailedException extends CryptoException { super(); } + public AuthenticationFailedException(String message) { + super(message); + } + public AuthenticationFailedException(Throwable cause) { super(cause); } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java index 1c3ad964e..5b9e81ade 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java @@ -14,6 +14,10 @@ abstract class CryptoException extends RuntimeException { super(); } + public CryptoException(String message) { + super(message); + } + public CryptoException(Throwable cause) { super(cause); } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java index 727cacdea..1aadd94e6 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java @@ -23,6 +23,7 @@ public class FileContentCryptorImpl implements FileContentCryptor { public static final int PAYLOAD_SIZE = 32 * 1024; public static final int NONCE_SIZE = 16; public static final int MAC_SIZE = 32; + public static final int CHUNK_SIZE = NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE; private final SecretKey encryptionKey; private final SecretKey macKey; @@ -46,7 +47,7 @@ public class FileContentCryptorImpl implements FileContentCryptor { assert cleartextChunkStart <= cleartextPos; long chunkInternalDiff = cleartextPos - cleartextChunkStart; assert chunkInternalDiff >= 0 && chunkInternalDiff < PAYLOAD_SIZE; - long ciphertextChunkStart = chunkNum * (NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE); + long ciphertextChunkStart = chunkNum * CHUNK_SIZE; return ciphertextChunkStart + chunkInternalDiff; } @@ -55,7 +56,7 @@ public class FileContentCryptorImpl implements FileContentCryptor { if (header.remaining() != getHeaderSize()) { throw new IllegalArgumentException("Invalid header."); } - if (firstCiphertextByte % (NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE) != 0) { + if (firstCiphertextByte % CHUNK_SIZE != 0) { throw new IllegalArgumentException("Invalid starting point for decryption."); } return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte, authenticate); 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 0af4c1597..2453c1ce8 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 @@ -8,8 +8,9 @@ *******************************************************************************/ package org.cryptomator.crypto.engine.impl; +import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.CHUNK_SIZE; import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.MAC_SIZE; -import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.PAYLOAD_SIZE; +import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.NONCE_SIZE; import java.io.IOException; import java.io.UncheckedIOException; @@ -35,7 +36,6 @@ import org.cryptomator.io.ByteBuffers; class FileContentDecryptorImpl implements FileContentDecryptor { - private static final int NONCE_SIZE = 16; private static final String HMAC_SHA256 = "HmacSHA256"; private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors(); private static final int READ_AHEAD = 2; @@ -45,7 +45,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { private final ThreadLocal hmacSha256; private final FileHeader header; private final boolean authenticate; - private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE); + private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE); private long chunkNumber = 0; public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte, boolean authenticate) { @@ -53,7 +53,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { this.hmacSha256 = hmacSha256; this.header = FileHeader.decrypt(headerKey, hmacSha256, header); this.authenticate = authenticate; - this.chunkNumber = firstCiphertextByte / PAYLOAD_SIZE; // floor() by int-truncation + this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation } @Override @@ -84,7 +84,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { private void submitCiphertextBufferIfFull() throws InterruptedException { if (!ciphertextBuffer.hasRemaining()) { submitCiphertextBuffer(); - ciphertextBuffer = ByteBuffer.allocate(NONCE_SIZE + PAYLOAD_SIZE + MAC_SIZE); + ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE); } } @@ -155,7 +155,8 @@ class FileContentDecryptorImpl implements FileContentDecryptor { mac.update(nonce); mac.update(inBuf.asReadOnlyBuffer()); if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) { - throw new AuthenticationFailedException(); + chunkNumberBigEndian.rewind(); + throw new AuthenticationFailedException("Auth error in chunk " + chunkNumberBigEndian.getLong()); } }