Preparations for file content encryption (no partial support yet)

This commit is contained in:
Sebastian Stenzel
2015-12-18 22:07:12 +01:00
parent 9711314080
commit a879ed2237
22 changed files with 778 additions and 32 deletions

View File

@@ -24,7 +24,7 @@ public interface WritableBytes {
/**
* Writes the data in the given byte buffer to this readable bytes at the
* given position.
* given position, overwriting existing content (not inserting).
*
* @param target
* the byte buffer to use

View File

@@ -0,0 +1,30 @@
package org.cryptomator.io;
import java.nio.ByteBuffer;
/**
* TODO this probably doesn't belong into this maven module, but it is used by various filesystem layers.
*/
public final class ByteBuffers {
private ByteBuffers() {
}
/**
* Copies as many bytes as possible from the given source to the destination buffer.
* The position of both buffers will be incremented by as many bytes as have been copied.
*
* @param source ByteBuffer from which bytes are read
* @param destination ByteBuffer into which bytes are written
* @return number of bytes copied, i.e. {@link ByteBuffer#remaining() source.remaining()} or {@link ByteBuffer#remaining() destination.remaining()}, whatever is less.
*/
public static int copy(ByteBuffer source, ByteBuffer destination) {
final int numBytes = Math.min(source.remaining(), destination.remaining());
final ByteBuffer tmp = source.asReadOnlyBuffer();
tmp.limit(tmp.position() + numBytes);
destination.put(tmp);
source.position(tmp.position());
return numBytes;
}
}

View File

@@ -0,0 +1,73 @@
package org.cryptomator.io;
import java.nio.ByteBuffer;
import org.junit.Assert;
import org.junit.Test;
public class ByteBuffersTest {
@Test
public void testCopyOfEmptySource() {
final ByteBuffer src = ByteBuffer.allocate(0);
final ByteBuffer dst = ByteBuffer.allocate(5);
dst.put(new byte[3]);
Assert.assertEquals(0, src.position());
Assert.assertEquals(0, src.remaining());
Assert.assertEquals(3, dst.position());
Assert.assertEquals(2, dst.remaining());
ByteBuffers.copy(src, dst);
Assert.assertEquals(0, src.position());
Assert.assertEquals(0, src.remaining());
Assert.assertEquals(3, dst.position());
Assert.assertEquals(2, dst.remaining());
}
@Test
public void testCopyToEmptyDestination() {
final ByteBuffer src = ByteBuffer.wrap(new byte[4]);
final ByteBuffer dst = ByteBuffer.allocate(0);
src.put(new byte[2]);
Assert.assertEquals(2, src.position());
Assert.assertEquals(2, src.remaining());
Assert.assertEquals(0, dst.position());
Assert.assertEquals(0, dst.remaining());
ByteBuffers.copy(src, dst);
Assert.assertEquals(2, src.position());
Assert.assertEquals(2, src.remaining());
Assert.assertEquals(0, dst.position());
Assert.assertEquals(0, dst.remaining());
}
@Test
public void testCopyToBiggerDestination() {
final ByteBuffer src = ByteBuffer.wrap(new byte[2]);
final ByteBuffer dst = ByteBuffer.allocate(10);
dst.put(new byte[3]);
Assert.assertEquals(0, src.position());
Assert.assertEquals(2, src.remaining());
Assert.assertEquals(3, dst.position());
Assert.assertEquals(7, dst.remaining());
ByteBuffers.copy(src, dst);
Assert.assertEquals(2, src.position());
Assert.assertEquals(0, src.remaining());
Assert.assertEquals(5, dst.position());
Assert.assertEquals(5, dst.remaining());
}
@Test
public void testCopyToSmallerDestination() {
final ByteBuffer src = ByteBuffer.wrap(new byte[5]);
final ByteBuffer dst = ByteBuffer.allocate(2);
Assert.assertEquals(0, src.position());
Assert.assertEquals(5, src.remaining());
Assert.assertEquals(0, dst.position());
Assert.assertEquals(2, dst.remaining());
ByteBuffers.copy(src, dst);
Assert.assertEquals(2, src.position());
Assert.assertEquals(3, src.remaining());
Assert.assertEquals(2, dst.position());
Assert.assertEquals(0, dst.remaining());
}
}

View File

@@ -0,0 +1,43 @@
package org.cryptomator.crypto.engine;
public class ByteRange {
private final long start;
private final long length;
private ByteRange(long start, long length) {
if (start < 0) {
throw new IllegalArgumentException("start must not be a negative value");
}
if (length < 0) {
throw new IllegalArgumentException("length must not be a negative value");
}
this.start = start;
this.length = length;
}
static ByteRange of(long start, long length) {
return new ByteRange(start, length);
}
/**
* @return Begin of range (inclusive)
*/
public long start() {
return start;
}
/**
* @return End of range (exclusive)
*/
public long end() {
return start + length;
}
/**
* @return Number of bytes between start and end
*/
public long length() {
return length;
}
}

View File

@@ -17,6 +17,8 @@ public interface Cryptor extends Destroyable {
FilenameCryptor getFilenameCryptor();
FileContentCryptor getFileContentCryptor();
void randomizeMasterkey();
boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase);

View File

@@ -0,0 +1,28 @@
package org.cryptomator.crypto.engine;
import java.nio.ByteBuffer;
import java.util.Optional;
import javax.security.auth.Destroyable;
public interface FileContentCryptor extends Destroyable {
/**
* @return The fixed number of bytes of the file header. The header length is implementation-specific.
*/
int getHeaderSize();
/**
* @param header The full fixed-length header of an encrypted file. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
* @return A possibly new FileContentDecryptor instance which is capable of decrypting ciphertexts associated with the given file header.
*/
FileContentDecryptor getFileContentDecryptor(ByteBuffer header);
/**
* @param header The full fixed-length header of an encrypted file or {@link Optional#empty()}. The caller is required to pass the exact amount of bytes returned by {@link #getHeaderSize()}.
* If the header is empty, a new one will be created by the returned encryptor.
* @return A possibly new FileContentEncryptor instance which is capable of encrypting cleartext associated with the given file header.
*/
FileContentEncryptor getFileContentEncryptor(Optional<ByteBuffer> header);
}

View File

@@ -0,0 +1,53 @@
package org.cryptomator.crypto.engine;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
/**
* Not necessarily thread-safe.
*/
public interface FileContentDecryptor {
public static final ByteBuffer EOF = ByteBuffer.allocate(0);
/**
* @return Number of bytes of the decrypted file.
*/
long contentLength();
/**
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
*
* @param cleartext Cleartext data or {@link #EOF} to indicate the end of a ciphertext.
* @see #skipToPosition(long)
*/
void append(ByteBuffer ciphertext);
/**
* Returns a queue containing cleartext in byte-by-byte FIFO order, meaning in the order ciphertext has been appended to this encryptor.
* However the number and size of the ciphertext byte buffers doesn't need to resemble the ciphertext buffers.
*
* The queue returns {@link #EOF}, when all ciphertext has been processed.
*
* @return A queue from which decrypted data can be {@link BlockingQueue#take() taken}.
*/
BlockingQueue<ByteBuffer> cleartext();
/**
* Calculates the ciphertext bytes required to perform a partial decryption of a requested cleartext byte range.
* If this decryptor doesn't support partial decryption the result will be <code>[0, {@link Long#MAX_VALUE}]</code>.
*
* @param cleartextRange The cleartext range the caller is interested in.
* @return The ciphertext range required in order to decrypt the cleartext range.
*/
ByteRange ciphertextRequiredToDecryptRange(ByteRange cleartextRange);
/**
* Informs the decryptor, what the first byte of the next ciphertext block will be. This method needs to be called only for partial decryption.
*
* @param nextCiphertextByte The first byte of the next ciphertext buffer given via {@link #append(ByteBuffer)}.
* @throws IllegalArgumentException If nextCiphertextByte is an invalid starting point. Only start bytes determined by {@link #ciphertextRequiredToDecryptRange(ByteRange)} are supported.
*/
void skipToPosition(long nextCiphertextByte) throws IllegalArgumentException;
}

View File

@@ -0,0 +1,55 @@
package org.cryptomator.crypto.engine;
import java.nio.ByteBuffer;
import java.util.concurrent.BlockingQueue;
/**
* Not necessarily thread-safe.
*/
public interface FileContentEncryptor {
public static final ByteBuffer EOF = ByteBuffer.allocate(0);
/**
* Creates the encrypted file header. This header might depend on the already encrypted data,
* thus the caller should make sure all data is processed before requesting the header.
*
* @return Encrypted file header.
*/
ByteBuffer getHeader();
/**
* Appends further cleartext to this encryptor. This method might block until space becomes available.
*
* @param cleartext Cleartext data or {@link #EOF} to indicate the end of a cleartext.
*/
void append(ByteBuffer cleartext);
/**
* Returns a queue containing ciphertext in byte-by-byte FIFO order, meaning in the order cleartext has been appended to this encryptor.
* However the number and size of the ciphertext byte buffers doesn't need to resemble the cleartext buffers.
*
* The queue returns {@link #EOF}, when all cleartext has been processed.
*
* @return A queue from which encrypted data can be {@link BlockingQueue#take() taken}.
*/
BlockingQueue<ByteBuffer> ciphertext();
/**
* Calculates the cleartext bytes required to perform a partial encryption of a specific cleartext byte range.
* If this decryptor doesn't support partial encryption the result will be <code>[0, {@link Long#MAX_VALUE}]</code>.
*
* @param cleartextRange The cleartext range the caller wants to ecnrypt.
* @return The cleartext range required in order to encrypt the given cleartext range.
*/
ByteRange cleartextRequiredToEncryptRange(ByteRange cleartextRange);
/**
* Informs the encryptor, what the first byte of the next cleartext block will be. This method needs to be called only for partial encryption.
*
* @param nextCleartextByte The first byte of the next cleartext buffer given via {@link #append(ByteBuffer)}.
* @throws IllegalArgumentException If nextCleartextByte is an invalid starting point. Only start bytes determined by {@link #cleartextRequiredToEncryptRange(ByteRange)} are supported.
*/
void skipToPosition(long nextCleartextByte) throws IllegalArgumentException;
}

View File

@@ -20,6 +20,7 @@ import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FilenameCryptor;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -37,6 +38,7 @@ public class CryptorImpl implements Cryptor {
private SecretKey encryptionKey;
private SecretKey macKey;
private final AtomicReference<FilenameCryptor> filenameCryptor = new AtomicReference<>();
private final AtomicReference<FileContentCryptor> fileContentCryptor = new AtomicReference<>();
private final SecureRandom randomSource;
public CryptorImpl(SecureRandom randomSource) {
@@ -61,6 +63,24 @@ public class CryptorImpl implements Cryptor {
}
}
@Override
public FileContentCryptor getFileContentCryptor() {
// lazy initialization pattern as proposed here http://stackoverflow.com/a/30247202/4014509
final FileContentCryptor existingCryptor = fileContentCryptor.get();
if (existingCryptor != null) {
return existingCryptor;
} else {
final FileContentCryptorImpl newCryptor = new FileContentCryptorImpl(encryptionKey, macKey);
if (fileContentCryptor.compareAndSet(null, newCryptor)) {
return newCryptor;
} else {
// CAS failed: other thread set an object
newCryptor.destroy();
return fileContentCryptor.get();
}
}
}
@Override
public void randomizeMasterkey() {
final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES];
@@ -147,11 +167,17 @@ public class CryptorImpl implements Cryptor {
if (filenameCryptor.get() != null) {
TheDestroyer.destroyQuietly(getFilenameCryptor());
}
if (fileContentCryptor.get() != null) {
TheDestroyer.destroyQuietly(getFileContentCryptor());
}
}
@Override
public boolean isDestroyed() {
return encryptionKey.isDestroyed() && macKey.isDestroyed() && (filenameCryptor.get() == null || filenameCryptor.get().isDestroyed());
return encryptionKey.isDestroyed() //
&& macKey.isDestroyed() //
&& (filenameCryptor.get() == null || filenameCryptor.get().isDestroyed()) //
&& (fileContentCryptor.get() == null || fileContentCryptor.get().isDestroyed());
}
}

View File

@@ -0,0 +1,53 @@
package org.cryptomator.crypto.engine.impl;
import java.nio.ByteBuffer;
import java.util.Optional;
import javax.crypto.SecretKey;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
class FileContentCryptorImpl implements FileContentCryptor {
private final SecretKey encryptionKey;
private final SecretKey macKey;
public FileContentCryptorImpl(SecretKey encryptionKey, SecretKey macKey) {
if (encryptionKey == null || macKey == null) {
throw new IllegalArgumentException("Key must not be null");
}
this.encryptionKey = encryptionKey;
this.macKey = macKey;
}
@Override
public int getHeaderSize() {
throw new UnsupportedOperationException("Method not implemented");
}
@Override
public FileContentDecryptor getFileContentDecryptor(ByteBuffer header) {
throw new UnsupportedOperationException("Method not implemented");
}
@Override
public FileContentEncryptor getFileContentEncryptor(Optional<ByteBuffer> header) {
throw new UnsupportedOperationException("Method not implemented");
}
/* ======================= destruction ======================= */
@Override
public void destroy() {
TheDestroyer.destroyQuietly(encryptionKey);
TheDestroyer.destroyQuietly(macKey);
}
@Override
public boolean isDestroyed() {
return encryptionKey.isDestroyed() && macKey.isDestroyed();
}
}

View File

@@ -24,26 +24,24 @@ public class CryptoFile extends CryptoNode implements File {
super(parent, name, cryptor);
}
String encryptedName() {
@Override
protected String encryptedName() {
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
}
@Override
public Instant lastModified() throws UncheckedIOException {
// TODO Auto-generated method stub
return null;
return physicalFile().lastModified();
}
@Override
public ReadableFile openReadable() {
// TODO Auto-generated method stub
return null;
return new CryptoReadableFile(cryptor.getFileContentCryptor(), physicalFile().openReadable());
}
@Override
public WritableFile openWritable() {
// TODO Auto-generated method stub
return null;
return new CryptoWritableFile(cryptor.getFileContentCryptor(), physicalFile().openWritable());
}
@Override

View File

@@ -68,12 +68,12 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
}
@Override
File physicalFile() {
protected File physicalFile() {
return physicalDataRoot().file(ROOT_DIR_FILE);
}
@Override
Folder physicalDataRoot() {
protected Folder physicalDataRoot() {
return physicalRoot.folder(DATA_ROOT_DIR);
}

View File

@@ -35,10 +35,16 @@ class CryptoFolder extends CryptoNode implements Folder {
super(parent, name, cryptor);
}
String encryptedName() {
@Override
protected String encryptedName() {
return cryptor.getFilenameCryptor().encryptFilename(name()) + FILE_EXT;
}
Folder physicalFolder() {
final String encryptedThenHashedDirId = cryptor.getFilenameCryptor().hashDirectoryId(getDirectoryId());
return physicalDataRoot().folder(encryptedThenHashedDirId.substring(0, 2)).folder(encryptedThenHashedDirId.substring(2));
}
protected String getDirectoryId() {
if (directoryId.get() == null) {
File dirFile = physicalFile();
@@ -58,15 +64,6 @@ class CryptoFolder extends CryptoNode implements Folder {
return directoryId.get();
}
File physicalFile() {
return parent.physicalFolder().file(encryptedName());
}
Folder physicalFolder() {
final String encryptedThenHashedDirId = cryptor.getFilenameCryptor().hashDirectoryId(getDirectoryId());
return physicalDataRoot().folder(encryptedThenHashedDirId.substring(0, 2)).folder(encryptedThenHashedDirId.substring(2));
}
@Override
public Instant lastModified() {
return physicalFile().lastModified();

View File

@@ -11,6 +11,7 @@ package org.cryptomator.crypto.fs;
import java.util.Optional;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
@@ -26,10 +27,16 @@ abstract class CryptoNode implements Node {
this.cryptor = cryptor;
}
Folder physicalDataRoot() {
protected Folder physicalDataRoot() {
return parent.physicalDataRoot();
}
protected abstract String encryptedName();
protected File physicalFile() {
return parent.physicalFolder().file(encryptedName());
}
@Override
public Optional<CryptoFolder> parent() {
return Optional.of(parent);

View File

@@ -0,0 +1,101 @@
package org.cryptomator.crypto.fs;
import java.nio.ByteBuffer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.io.ByteBuffers;
class CryptoReadableFile implements ReadableFile {
private static final int READ_BUFFER_SIZE = 32 * 1024 + 32; // aligned with encrypted chunk size + MAC size
private final ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final FileContentDecryptor decryptor;
private final ReadableFile file;
private Future<Void> readAheadTask;
private ByteBuffer bufferedCleartext;
public CryptoReadableFile(FileContentCryptor cryptor, ReadableFile file) {
final ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
file.read(header, 0);
header.flip();
this.decryptor = cryptor.getFileContentDecryptor(header);
this.file = file;
this.prepareReadAtPosition(0);
}
private void prepareReadAtPosition(long pos) {
if (readAheadTask != null) {
readAheadTask.cancel(true);
}
readAheadTask = executorService.submit(new Reader());
}
@Override
public void read(ByteBuffer target) {
try {
while (target.remaining() > 0 && bufferedCleartext != FileContentDecryptor.EOF) {
bufferCleartext();
readFromBufferedCleartext(target);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
executorService.shutdownNow();
}
}
private void bufferCleartext() throws InterruptedException {
if (bufferedCleartext == null || !bufferedCleartext.hasRemaining()) {
bufferedCleartext = decryptor.cleartext().take();
}
}
private void readFromBufferedCleartext(ByteBuffer target) {
assert bufferedCleartext != null;
ByteBuffers.copy(bufferedCleartext, target);
}
@Override
public void read(ByteBuffer target, int position) {
throw new UnsupportedOperationException("Partial read not implemented yet.");
}
@Override
public void copyTo(WritableFile other) {
file.copyTo(other);
}
@Override
public void close() {
file.close();
}
private class Reader implements Callable<Void> {
@Override
public Void call() {
int bytesRead = -1;
do {
ByteBuffer ciphertext = ByteBuffer.allocate(READ_BUFFER_SIZE);
file.read(ciphertext);
ciphertext.flip();
bytesRead = ciphertext.remaining();
if (bytesRead > 0) {
decryptor.append(ciphertext);
}
} while (bytesRead > 0);
decryptor.append(FileContentDecryptor.EOF);
return null;
}
}
}

View File

@@ -0,0 +1,109 @@
package org.cryptomator.crypto.fs;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.io.ByteBuffers;
class CryptoWritableFile implements WritableFile {
private final ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final FileContentEncryptor encryptor;
private final WritableFile file;
private final Future<Void> writeTask;
public CryptoWritableFile(FileContentCryptor cryptor, WritableFile file) {
this.encryptor = cryptor.getFileContentEncryptor(Optional.empty());
this.file = file;
writeHeader();
this.writeTask = executorService.submit(new Writer());
}
private void writeHeader() {
ByteBuffer header = encryptor.getHeader();
header.rewind();
file.write(header, 0);
}
@Override
public void write(ByteBuffer source) {
final ByteBuffer cleartextCopy = ByteBuffer.allocate(source.remaining());
ByteBuffers.copy(source, cleartextCopy);
cleartextCopy.flip();
encryptor.append(cleartextCopy);
file.write(source);
}
@Override
public void write(ByteBuffer source, int position) {
throw new UnsupportedOperationException("Partial write not implemented yet.");
}
@Override
public void moveTo(WritableFile other) {
file.moveTo(other);
}
@Override
public void setLastModified(Instant instant) {
file.setLastModified(instant);
}
@Override
public void delete() {
file.delete();
}
@Override
public void truncate() {
this.write(ByteBuffer.allocate(0), 0);
}
@Override
public void close() {
try {
encryptor.append(FileContentEncryptor.EOF);
writeTask.get();
executorService.shutdown();
writeHeader();
} catch (ExecutionException e) {
if (e.getCause() instanceof UncheckedIOException) {
throw (UncheckedIOException) e.getCause();
} else {
throw new IllegalStateException("Unexpected exception while waiting for encrypted file to be written", e);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
file.close();
}
}
private class Writer implements Callable<Void> {
@Override
public Void call() {
try {
ByteBuffer ciphertext;
while ((ciphertext = encryptor.ciphertext().take()) != FileContentEncryptor.EOF) {
file.write(ciphertext);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
}
}

View File

@@ -11,12 +11,18 @@ package org.cryptomator.crypto.engine;
public class NoCryptor implements Cryptor {
private final FilenameCryptor filenameCryptor = new NoFilenameCryptor();
private final FileContentCryptor fileContentCryptor = new NoFileContentCryptor();
@Override
public FilenameCryptor getFilenameCryptor() {
return filenameCryptor;
}
@Override
public FileContentCryptor getFileContentCryptor() {
return fileContentCryptor;
}
@Override
public void randomizeMasterkey() {
// like this? https://xkcd.com/221/

View File

@@ -0,0 +1,117 @@
package org.cryptomator.crypto.engine;
import java.nio.ByteBuffer;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class NoFileContentCryptor implements FileContentCryptor {
@Override
public int getHeaderSize() {
return Long.BYTES;
}
@Override
public FileContentDecryptor getFileContentDecryptor(ByteBuffer header) {
if (header.remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header size.");
}
return new Decryptor(header);
}
@Override
public FileContentEncryptor getFileContentEncryptor(Optional<ByteBuffer> header) {
return new Encryptor();
}
private class Decryptor implements FileContentDecryptor {
private final BlockingQueue<ByteBuffer> cleartextQueue = new LinkedBlockingQueue<>();
private final long contentLength;
private Decryptor(ByteBuffer header) {
assert header.remaining() == Long.BYTES;
this.contentLength = header.getLong();
}
@Override
public long contentLength() {
return contentLength;
}
@Override
public void append(ByteBuffer ciphertext) {
try {
if (ciphertext == FileContentDecryptor.EOF) {
cleartextQueue.put(FileContentDecryptor.EOF);
} else {
cleartextQueue.put(ciphertext.asReadOnlyBuffer());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public BlockingQueue<ByteBuffer> cleartext() {
return cleartextQueue;
}
@Override
public ByteRange ciphertextRequiredToDecryptRange(ByteRange cleartextRange) {
return cleartextRange;
}
@Override
public void skipToPosition(long nextCiphertextByte) throws IllegalArgumentException {
// no-op
}
}
private class Encryptor implements FileContentEncryptor {
private final BlockingQueue<ByteBuffer> ciphertextQueue = new LinkedBlockingQueue<>();
private long numCleartextBytesEncrypted = 0;
@Override
public ByteBuffer getHeader() {
ByteBuffer buf = ByteBuffer.allocate(Long.BYTES);
buf.putLong(numCleartextBytesEncrypted);
return buf;
}
@Override
public void append(ByteBuffer cleartext) {
try {
if (cleartext == FileContentEncryptor.EOF) {
ciphertextQueue.put(FileContentEncryptor.EOF);
} else {
int cleartextLen = cleartext.remaining();
ciphertextQueue.put(cleartext.asReadOnlyBuffer());
numCleartextBytesEncrypted += cleartextLen;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public BlockingQueue<ByteBuffer> ciphertext() {
return ciphertextQueue;
}
@Override
public ByteRange cleartextRequiredToEncryptRange(ByteRange cleartextRange) {
return cleartextRange;
}
@Override
public void skipToPosition(long nextCleartextByte) throws IllegalArgumentException {
// no-op
}
}
}

View File

@@ -10,7 +10,9 @@ package org.cryptomator.crypto.fs;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import org.cryptomator.crypto.engine.Cryptor;
@@ -19,6 +21,8 @@ import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
import org.junit.Test;
@@ -142,6 +146,32 @@ public class CryptoFileSystemTest {
fooBarFolder.moveTo(fooFolder);
}
@Test
public void testWriteAndReadEncryptedFile() {
// mock stuff and prepare crypto FS:
final Cryptor cryptor = new NoCryptor();
final FileSystem physicalFs = new InMemoryFileSystem();
final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
fs.create(FolderCreateMode.INCLUDING_PARENTS);
// write test content to physical file
try (WritableFile writable = fs.file("test1.txt").openWritable()) {
writable.write(ByteBuffer.wrap("Hello World".getBytes()));
}
// read test content from encrypted file
try (ReadableFile readable = fs.file("test1.txt").openReadable()) {
ByteBuffer buf1 = ByteBuffer.allocate(5);
readable.read(buf1);
buf1.flip();
Assert.assertEquals("Hello", new String(buf1.array(), 0, buf1.remaining()));
ByteBuffer buf2 = ByteBuffer.allocate(10);
readable.read(buf2);
buf2.flip();
Assert.assertArrayEquals(" World".getBytes(), Arrays.copyOfRange(buf2.array(), 0, buf2.remaining()));
}
}
/**
* @return number of folders on second level inside the given dataRoot folder.
*/

View File

@@ -17,6 +17,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.io.ByteBuffers;
class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableFile {
@@ -33,6 +34,7 @@ class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableF
throw new UncheckedIOException(new FileNotFoundException(this.name() + " does not exist"));
}
lock.readLock().lock();
content.rewind();
return this;
}
@@ -51,14 +53,13 @@ class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableF
@Override
public void read(ByteBuffer target) {
this.read(target, 0);
ByteBuffers.copy(content, target);
}
@Override
public void read(ByteBuffer target, int position) {
content.rewind();
content.position(position);
target.put(content);
ByteBuffers.copy(content, target);
}
@Override
@@ -69,16 +70,23 @@ class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableF
@Override
public void write(ByteBuffer source, int position) {
assert content != null;
if (position + source.remaining() > content.remaining()) {
// create bigger buffer
ByteBuffer tmp = ByteBuffer.allocate(Math.max(position, content.capacity()) + source.remaining());
tmp.put(content);
content = tmp;
}
expandContentCapacityIfRequired(position + source.remaining());
content.position(position);
assert content.remaining() >= source.remaining();
content.put(source);
}
private void expandContentCapacityIfRequired(int requiredCapacity) {
if (requiredCapacity > content.capacity()) {
final int currentPos = content.position();
final ByteBuffer tmp = ByteBuffer.allocate(requiredCapacity);
content.rewind();
ByteBuffers.copy(content, tmp);
content = tmp;
content.position(currentPos);
}
}
@Override
public void setLastModified(Instant instant) {
this.lastModified = instant;
@@ -110,7 +118,7 @@ class InMemoryFile extends InMemoryNode implements File, ReadableFile, WritableF
// returning null removes the entry.
return null;
});
assert !this.exists();
assert!this.exists();
}
@Override

View File

@@ -125,6 +125,15 @@ public class InMemoryFileSystemTest {
Assert.assertTrue(bazFile.exists());
// read "hello world" from baz
final ByteBuffer readBuf1 = ByteBuffer.allocate(6);
try (ReadableFile readable = bazFile.openReadable()) {
readable.read(readBuf1);
readBuf1.flip();
Assert.assertEquals("hello ", new String(readBuf1.array(), 0, readBuf1.remaining()));
readable.read(readBuf1);
readBuf1.flip();
Assert.assertEquals("world", new String(readBuf1.array(), 0, readBuf1.remaining()));
}
final ByteBuffer readBuf = ByteBuffer.allocate(5);
try (ReadableFile readable = bazFile.openReadable()) {
readable.read(readBuf, 6);

View File

@@ -137,6 +137,7 @@ public class ShorteningFileSystemTest {
try (ReadableFile file = fs.folder("foo").file("test1.txt").openReadable()) {
ByteBuffer buf = ByteBuffer.allocate(11);
file.read(buf);
buf.flip();
Assert.assertEquals("hello world", new String(buf.array()));
}
Assert.assertTrue(fs.folder("foo").file("test1.txt").lastModified().isAfter(testStart));