mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-14 08:41:28 +00:00
Preparations for file content encryption (no partial support yet)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ public interface Cryptor extends Destroyable {
|
||||
|
||||
FilenameCryptor getFilenameCryptor();
|
||||
|
||||
FileContentCryptor getFileContentCryptor();
|
||||
|
||||
void randomizeMasterkey();
|
||||
|
||||
boolean readKeysFromMasterkeyFile(byte[] masterkeyFileContents, CharSequence passphrase);
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user