mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-17 10:11:27 +00:00
fixed random access positioning
This commit is contained in:
@@ -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<Integer> 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<Integer> 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<Future<Boolean>> tasks = new ArrayList<>();
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
final int num = nums.get(i);
|
||||
final ForkJoinTask<Boolean> 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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ public class AuthenticationFailedException extends CryptoException {
|
||||
super();
|
||||
}
|
||||
|
||||
public AuthenticationFailedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AuthenticationFailedException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ abstract class CryptoException extends RuntimeException {
|
||||
super();
|
||||
}
|
||||
|
||||
public CryptoException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public CryptoException(Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Mac> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user