Random Access Decryption

This commit is contained in:
Sebastian Stenzel
2016-01-04 20:31:49 +01:00
parent f46a79fa63
commit ae55874709
34 changed files with 514 additions and 293 deletions

View File

@@ -64,6 +64,17 @@
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- DI -->
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<scope>provided</scope>
</dependency>
<!-- Tests -->
<dependency>
<groupId>org.cryptomator</groupId>

View File

@@ -0,0 +1,15 @@
package org.cryptomator.crypto;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.impl.CryptoModule;
import dagger.Component;
@Singleton
@Component(modules = CryptoModule.class)
interface CryptoComponent {
CryptoFileSystemFactory cryptoFileSystemFactory();
}

View File

@@ -0,0 +1,27 @@
package org.cryptomator.crypto;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.impl.FileContentCryptorImpl;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.blockaligned.BlockAlignedFileSystem;
import org.cryptomator.filesystem.crypto.CryptoFileSystem;
@Singleton
public class CryptoFileSystemFactory {
private final Provider<Cryptor> cryptorProvider;
@Inject
public CryptoFileSystemFactory(Provider<Cryptor> cryptorProvider) {
this.cryptorProvider = cryptorProvider;
}
public FileSystem get(Folder root, CharSequence passphrase) {
return new BlockAlignedFileSystem(new CryptoFileSystem(root, cryptorProvider.get(), passphrase), FileContentCryptorImpl.CHUNK_SIZE);
}
}

View File

@@ -10,6 +10,14 @@ package org.cryptomator.crypto.engine;
public class AuthenticationFailedException extends CryptoException {
public AuthenticationFailedException() {
super();
}
public AuthenticationFailedException(Throwable cause) {
super(cause);
}
public AuthenticationFailedException(String message, Throwable cause) {
super(message, cause);
}

View File

@@ -1,51 +0,0 @@
/*******************************************************************************
* Copyright (c) 2015 Sebastian Stenzel and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Sebastian Stenzel - initial API and implementation
*******************************************************************************/
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;
}
public 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

@@ -10,6 +10,14 @@ package org.cryptomator.crypto.engine;
abstract class CryptoException extends RuntimeException {
public CryptoException() {
super();
}
public CryptoException(Throwable cause) {
super(cause);
}
public CryptoException(String message, Throwable cause) {
super(message, cause);
}

View File

@@ -23,17 +23,26 @@ public interface FileContentCryptor {
*/
int getHeaderSize();
/**
* @return The ciphertext position that correlates to the cleartext position.
*/
long toCiphertextPos(long cleartextPos);
/**
* @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()}.
* @param firstCiphertextByte Position of the first ciphertext byte passed to the decryptor. If the decryptor can not fast-forward to the requested byte, an exception is thrown.
* If firstCiphertextByte is an invalid starting point, i.e. doesn't align with the decryptors internal block size, an IllegalArgumentException will be thrown.
* @return A possibly new FileContentDecryptor instance which is capable of decrypting ciphertexts associated with the given file header.
*/
FileContentDecryptor createFileContentDecryptor(ByteBuffer header);
FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) throws IllegalArgumentException;
/**
* @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.
* @param firstCleartextByte Position of the first cleartext byte passed to the encryptor. If the encryptor can not fast-forward to the requested byte, an exception is thrown.
* If firstCiphertextByte is an invalid starting point, i.e. doesn't align with the encryptors internal block size, an IllegalArgumentException will be thrown.
* @return A possibly new FileContentEncryptor instance which is capable of encrypting cleartext associated with the given file header.
*/
FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header);
FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header, long firstCleartextByte) throws IllegalArgumentException;
}

View File

@@ -39,24 +39,7 @@ public interface FileContentDecryptor extends Destroyable, Closeable {
*
* @return Decrypted cleartext or {@link FileContentCryptor#EOF}.
*/
ByteBuffer cleartext() throws InterruptedException;
/**
* 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;
ByteBuffer cleartext() throws InterruptedException, AuthenticationFailedException;
/**
* Clears file-specific sensitive information.

View File

@@ -43,23 +43,6 @@ public interface FileContentEncryptor extends Destroyable, Closeable {
*/
ByteBuffer ciphertext() throws InterruptedException;
/**
* 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;
/**
* Clears file-specific sensitive information.
*/

View File

@@ -31,5 +31,5 @@ public interface FilenameCryptor {
* @param ciphertextName Ciphertext only, with any additional strings like file extensions stripped first.
* @return cleartext filename, probably including its cleartext file extension.
*/
String decryptFilename(String ciphertextName);
String decryptFilename(String ciphertextName) throws AuthenticationFailedException;
}

View File

@@ -0,0 +1,9 @@
package org.cryptomator.crypto.engine;
public class InvalidPassphraseException extends CryptoException {
public InvalidPassphraseException() {
super();
}
}

View File

@@ -0,0 +1,28 @@
package org.cryptomator.crypto.engine.impl;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import org.cryptomator.crypto.engine.Cryptor;
import dagger.Module;
import dagger.Provides;
@Module
public class CryptoModule {
@Provides
Cryptor provideCryptor(SecureRandom secureRandom) {
return new CryptorImpl(secureRandom);
}
@Provides
SecureRandom provideSecureRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No strong PRNGs available.", e);
}
}
}

View File

@@ -43,27 +43,10 @@ public class CryptorImpl implements Cryptor {
private final AtomicReference<FileContentCryptor> fileContentCryptor = new AtomicReference<>();
private final SecureRandom randomSource;
/**
* Designated constructor.
*
* Package-visible for testing only, use secondary constructors otherwise to ensure a proper PRNG.
*/
CryptorImpl(SecureRandom randomSource) {
this.randomSource = randomSource;
}
public CryptorImpl() {
this(getStrongSecureRandom());
}
private static SecureRandom getStrongSecureRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No strong PRNGs available.", e);
}
}
@Override
public FilenameCryptor getFilenameCryptor() {
assertKeysExist();

View File

@@ -64,15 +64,18 @@ class FifoParallelDataProcessor<T> {
* @return Next job result
* @throws InterruptedException If the calling thread was interrupted while waiting for the next result.
*/
T processedData() throws InterruptedException {
try {
return processedData.take().get();
} catch (ExecutionException e) {
if (e.getCause() instanceof RuntimeException) {
throw (RuntimeException) e.getCause();
} else {
throw new RuntimeException(e);
}
T processedData() throws InterruptedException, ExecutionException {
return processedData.take().get();
}
/**
* Stops work in progress immediatley.
*/
@Deprecated
void cancelAllPendingJobs() {
Future<T> job;
while ((job = processedData.poll()) != null) {
job.cancel(true);
}
}

View File

@@ -18,13 +18,16 @@ import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
class FileContentCryptorImpl implements FileContentCryptor {
public class FileContentCryptorImpl implements FileContentCryptor {
public static final int CHUNK_SIZE = 32 * 1024;
static final int MAC_SIZE = 32;
private final SecretKey encryptionKey;
private final SecretKey macKey;
private final SecureRandom randomSource;
public FileContentCryptorImpl(SecretKey encryptionKey, SecretKey macKey, SecureRandom randomSource) {
FileContentCryptorImpl(SecretKey encryptionKey, SecretKey macKey, SecureRandom randomSource) {
this.encryptionKey = encryptionKey;
this.macKey = macKey;
this.randomSource = randomSource;
@@ -36,19 +39,36 @@ class FileContentCryptorImpl implements FileContentCryptor {
}
@Override
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header) {
if (header.remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header.");
}
return new FileContentDecryptorImpl(encryptionKey, macKey, header);
public long toCiphertextPos(long cleartextPos) {
long chunkNum = cleartextPos / CHUNK_SIZE;
long cleartextChunkStart = chunkNum * CHUNK_SIZE;
assert cleartextChunkStart <= cleartextPos;
long chunkInternalDiff = cleartextPos - cleartextChunkStart;
assert chunkInternalDiff >= 0 && chunkInternalDiff < CHUNK_SIZE;
long ciphertextChunkStart = chunkNum * (CHUNK_SIZE + MAC_SIZE);
return ciphertextChunkStart + chunkInternalDiff;
}
@Override
public FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header) {
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) {
if (header.remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header.");
}
if (firstCiphertextByte % (CHUNK_SIZE + MAC_SIZE) != 0) {
throw new IllegalArgumentException("Invalid starting point for decryption.");
}
return new FileContentDecryptorImpl(encryptionKey, macKey, header, firstCiphertextByte);
}
@Override
public FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header, long firstCleartextByte) {
if (header.isPresent() && header.get().remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header.");
}
return new FileContentEncryptorImpl(encryptionKey, macKey, randomSource);
if (firstCleartextByte % CHUNK_SIZE != 0) {
throw new IllegalArgumentException("Invalid starting point for encryption.");
}
return new FileContentEncryptorImpl(encryptionKey, macKey, randomSource, firstCleartextByte);
}
}

View File

@@ -8,11 +8,15 @@
*******************************************************************************/
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 java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import javax.crypto.Cipher;
import javax.crypto.Mac;
@@ -20,7 +24,7 @@ import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import org.cryptomator.crypto.engine.ByteRange;
import org.cryptomator.crypto.engine.AuthenticationFailedException;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.io.ByteBuffers;
@@ -29,8 +33,6 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
private static final int AES_BLOCK_LENGTH_IN_BYTES = 16;
private static final String HMAC_SHA256 = "HmacSHA256";
private static final int CHUNK_SIZE = 32 * 1024;
private static final int MAC_SIZE = 32;
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
private static final int READ_AHEAD = 2;
@@ -40,10 +42,11 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE + MAC_SIZE);
private long chunkNumber = 0;
public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header) {
public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header, long firstCiphertextByte) {
final ThreadLocalMac hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
this.hmacSha256 = hmacSha256;
this.header = FileHeader.decrypt(headerKey, hmacSha256, header);
this.chunkNumber = firstCiphertextByte / CHUNK_SIZE; // floor() by int-truncation
}
@Override
@@ -73,8 +76,10 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
private void submitCiphertextBuffer() throws InterruptedException {
ciphertextBuffer.flip();
Callable<ByteBuffer> encryptionJob = new DecryptionJob(ciphertextBuffer, chunkNumber++);
dataProcessor.submit(encryptionJob);
if (ciphertextBuffer.hasRemaining()) {
Callable<ByteBuffer> encryptionJob = new DecryptionJob(ciphertextBuffer, chunkNumber++);
dataProcessor.submit(encryptionJob);
}
}
private void submitEof() throws InterruptedException {
@@ -83,17 +88,15 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
@Override
public ByteBuffer cleartext() throws InterruptedException {
return dataProcessor.processedData();
}
@Override
public ByteRange ciphertextRequiredToDecryptRange(ByteRange cleartextRange) {
return ByteRange.of(0, Long.MAX_VALUE);
}
@Override
public void skipToPosition(long nextCiphertextByte) throws IllegalArgumentException {
throw new UnsupportedOperationException("Partial decryption not supported.");
try {
return dataProcessor.processedData();
} catch (ExecutionException e) {
if (e.getCause() instanceof AuthenticationFailedException) {
throw new AuthenticationFailedException(e);
} else {
throw new RuntimeException(e);
}
}
}
@Override
@@ -130,8 +133,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor {
Mac mac = hmacSha256.get();
mac.update(ciphertextChunk.asReadOnlyBuffer());
if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
// TODO handle invalid MAC properly
throw new IllegalArgumentException("Corrupt mac.");
throw new AuthenticationFailedException();
}
Cipher cipher = ThreadLocalAesCtrCipher.get();

View File

@@ -8,11 +8,14 @@
*******************************************************************************/
package org.cryptomator.crypto.engine.impl;
import static org.cryptomator.crypto.engine.impl.FileContentCryptorImpl.CHUNK_SIZE;
import java.nio.ByteBuffer;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.LongAdder;
import javax.crypto.Cipher;
@@ -21,7 +24,6 @@ import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import org.cryptomator.crypto.engine.ByteRange;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
import org.cryptomator.io.ByteBuffers;
@@ -30,7 +32,6 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
private static final int AES_BLOCK_LENGTH_IN_BYTES = 16;
private static final String HMAC_SHA256 = "HmacSHA256";
private static final int CHUNK_SIZE = 32 * 1024;
private static final int NUM_THREADS = Runtime.getRuntime().availableProcessors();
private static final int READ_AHEAD = 2;
@@ -42,7 +43,10 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
private ByteBuffer cleartextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
private long chunkNumber = 0;
public FileContentEncryptorImpl(SecretKey headerKey, SecretKey macKey, SecureRandom randomSource) {
public FileContentEncryptorImpl(SecretKey headerKey, SecretKey macKey, SecureRandom randomSource, long firstCleartextByte) {
if (firstCleartextByte != 0) {
throw new UnsupportedOperationException("Partial encryption not supported.");
}
this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
this.headerKey = headerKey;
this.header = new FileHeader(randomSource);
@@ -87,17 +91,11 @@ class FileContentEncryptorImpl implements FileContentEncryptor {
@Override
public ByteBuffer ciphertext() throws InterruptedException {
return dataProcessor.processedData();
}
@Override
public ByteRange cleartextRequiredToEncryptRange(ByteRange cleartextRange) {
return ByteRange.of(0, Long.MAX_VALUE);
}
@Override
public void skipToPosition(long nextCleartextByte) throws IllegalArgumentException {
throw new UnsupportedOperationException("Partial encryption not supported.");
try {
return dataProcessor.processedData();
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
@Override

View File

@@ -51,7 +51,7 @@ class FilenameCryptorImpl implements FilenameCryptor {
}
@Override
public String decryptFilename(String ciphertextName) {
public String decryptFilename(String ciphertextName) throws AuthenticationFailedException {
final byte[] encryptedBytes = BASE32.decode(ciphertextName);
try {
final byte[] cleartextBytes = AES_SIV.decrypt(encryptionKey, macKey, encryptedBytes);

View File

@@ -35,11 +35,14 @@ class BlockAlignedReadableFile extends DelegatingReadableFile {
public void position(long logicalPosition) throws UncheckedIOException {
long blockNumber = logicalPosition / blockSize;
long physicalPosition = blockNumber * blockSize;
assert physicalPosition <= logicalPosition;
int diff = (int) (logicalPosition - physicalPosition);
assert diff >= 0;
assert diff < blockSize;
super.position(physicalPosition);
eofReached = false;
readCurrentBlock();
int advance = (int) (logicalPosition - physicalPosition);
currentBlockBuffer.position(advance);
currentBlockBuffer.position(diff);
}
@Override

View File

@@ -44,7 +44,6 @@ class BlockAlignedWritableFile extends DelegatingWritableFile {
public int write(ByteBuffer source) throws UncheckedIOException {
int written = 0;
while (source.hasRemaining()) {
currentBlockBuffer.limit(Math.max(currentBlockBuffer.limit(), Math.min(currentBlockBuffer.position() + source.remaining(), currentBlockBuffer.capacity())));
written += ByteBuffers.copy(source, currentBlockBuffer);
writeCurrentBlockIfNeeded();
}
@@ -53,6 +52,7 @@ class BlockAlignedWritableFile extends DelegatingWritableFile {
@Override
public void close() throws UncheckedIOException {
currentBlockBuffer.flip();
writeCurrentBlock();
readableFile.close();
super.close();
@@ -73,7 +73,7 @@ class BlockAlignedWritableFile extends DelegatingWritableFile {
private void readCurrentBlock() {
currentBlockBuffer.clear();
readableFile.read(currentBlockBuffer);
currentBlockBuffer.flip();
currentBlockBuffer.rewind();
}
}

View File

@@ -0,0 +1,45 @@
package org.cryptomator.filesystem.crypto;
import java.nio.ByteBuffer;
import java.util.concurrent.Callable;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.filesystem.ReadableFile;
class CiphertextReader implements Callable<Void> {
private static final int READ_BUFFER_SIZE = 32 * 1024 + 32; // aligned with encrypted chunk size + MAC size
private final ReadableFile file;
private final FileContentDecryptor decryptor;
private final long startpos;
public CiphertextReader(ReadableFile file, FileContentDecryptor decryptor, long startpos) {
this.file = file;
this.decryptor = decryptor;
this.startpos = startpos;
}
@Override
public Void call() {
file.position(startpos);
int bytesRead = -1;
try {
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(FileContentCryptor.EOF);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
package org.cryptomator.filesystem.crypto;
import java.nio.ByteBuffer;
import java.util.concurrent.Callable;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
import org.cryptomator.filesystem.WritableFile;
class CiphertextWriter implements Callable<Void> {
private final WritableFile file;
private final FileContentEncryptor encryptor;
public CiphertextWriter(WritableFile file, FileContentEncryptor encryptor) {
this.file = file;
this.encryptor = encryptor;
}
@Override
public Void call() {
try {
ByteBuffer ciphertext;
while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) {
file.write(ciphertext);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
}

View File

@@ -12,6 +12,7 @@ import java.nio.ByteBuffer;
import java.util.Optional;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.InvalidPassphraseException;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
@@ -27,17 +28,17 @@ public class CryptoFileSystem extends CryptoFolder implements FileSystem {
private final Folder physicalRoot;
public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CharSequence passphrase) {
public CryptoFileSystem(Folder physicalRoot, Cryptor cryptor, CharSequence passphrase) throws InvalidPassphraseException {
super(null, "", cryptor);
this.physicalRoot = physicalRoot;
final File masterkeyFile = physicalRoot.file(MASTERKEY_FILENAME);
if (masterkeyFile.exists()) {
final boolean unlocked = decryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
if (!unlocked) {
// TODO new InvalidPassphraseException() ?
throw new IllegalArgumentException("Wrong passphrase.");
throw new InvalidPassphraseException();
}
} else {
cryptor.randomizeMasterkey();
encryptMasterKeyFile(cryptor, masterkeyFile, passphrase);
}
assert masterkeyFile.exists() : "A CryptoFileSystem can not exist without a masterkey file.";

View File

@@ -8,18 +8,23 @@
*******************************************************************************/
package org.cryptomator.filesystem.crypto;
import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Stream;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.Node;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
class CryptoFolder extends CryptoNode implements Folder {
@@ -46,13 +51,10 @@ class CryptoFolder extends CryptoNode implements Folder {
if (directoryId.get() == null) {
File dirFile = physicalFile();
if (dirFile.exists()) {
try (ReadableFile readable = dirFile.openReadable()) {
final ByteBuffer buf = ByteBuffer.allocate(64);
readable.read(buf);
buf.flip();
byte[] bytes = new byte[buf.remaining()];
buf.get(bytes);
directoryId.set(new String(bytes));
try (Reader reader = Channels.newReader(dirFile.openReadable(), StandardCharsets.UTF_8.newDecoder(), -1)) {
directoryId.set(IOUtils.toString(reader));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} else {
directoryId.compareAndSet(null, UUID.randomUUID().toString());

View File

@@ -10,7 +10,6 @@ package org.cryptomator.filesystem.crypto;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -24,35 +23,24 @@ 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 static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);
private final ExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
private final FileContentDecryptor decryptor;
private final ByteBuffer header;
private final FileContentCryptor cryptor;
private final ReadableFile file;
private FileContentDecryptor decryptor;
private Future<Void> readAheadTask;
private ByteBuffer bufferedCleartext = EMPTY_BUFFER;
public CryptoReadableFile(FileContentCryptor cryptor, ReadableFile file) {
final int headerSize = cryptor.getHeaderSize();
final ByteBuffer header = ByteBuffer.allocate(headerSize);
this.header = ByteBuffer.allocate(cryptor.getHeaderSize());
this.cryptor = cryptor;
this.file = file;
file.position(0);
file.read(header);
header.flip();
this.decryptor = cryptor.createFileContentDecryptor(header);
this.file = file;
this.prepareReadAtPhysicalPosition(headerSize + 0);
}
private void prepareReadAtPhysicalPosition(long pos) {
if (readAheadTask != null) {
readAheadTask.cancel(true);
bufferedCleartext = EMPTY_BUFFER;
}
readAheadTask = executorService.submit(new Reader(pos));
this.position(0);
}
@Override
@@ -75,7 +63,13 @@ class CryptoReadableFile implements ReadableFile {
@Override
public void position(long position) throws UncheckedIOException {
throw new UnsupportedOperationException("Partial read unsupported");
if (readAheadTask != null) {
readAheadTask.cancel(true);
bufferedCleartext = EMPTY_BUFFER;
}
long ciphertextPos = cryptor.toCiphertextPos(position);
decryptor = cryptor.createFileContentDecryptor(header.asReadOnlyBuffer(), ciphertextPos);
readAheadTask = executorService.submit(new CiphertextReader(file, decryptor, header.remaining() + ciphertextPos));
}
private void bufferCleartext() throws InterruptedException {
@@ -110,35 +104,4 @@ class CryptoReadableFile implements ReadableFile {
file.close();
}
private class Reader implements Callable<Void> {
private final long startpos;
public Reader(long startpos) {
this.startpos = startpos;
}
@Override
public Void call() {
file.position(startpos);
int bytesRead = -1;
try {
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(FileContentCryptor.EOF);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
}
}

View File

@@ -12,7 +12,6 @@ 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;
@@ -33,9 +32,9 @@ class CryptoWritableFile implements WritableFile {
public CryptoWritableFile(FileContentCryptor cryptor, WritableFile file) {
this.file = file;
this.encryptor = cryptor.createFileContentEncryptor(Optional.empty());
this.encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0);
writeHeader();
this.writeTask = executorService.submit(new Writer());
this.writeTask = executorService.submit(new CiphertextWriter(file, encryptor));
}
private void writeHeader() {
@@ -119,21 +118,4 @@ class CryptoWritableFile implements WritableFile {
}
}
private class Writer implements Callable<Void> {
@Override
public Void call() {
try {
ByteBuffer ciphertext;
while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) {
file.write(ciphertext);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return null;
}
}
}

View File

@@ -0,0 +1,60 @@
package org.cryptomator.crypto;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.ReadableFile;
import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class CryptoFileSystemIntegrationTest {
private FileSystem ciphertextFs;
private FileSystem cleartextFs;
@Before
public void setupFileSystems() {
ciphertextFs = new InMemoryFileSystem();
// final Predicate<Node> isMetadataFolder = (Node node) -> node.equals(physicalFs.folder("m"));
// final FileSystem metadataHidingFs = new BlacklistingFileSystem(physicalFs, isMetadataFolder);
// final FileSystem shorteningFs = new ShorteningFileSystem(metadataHidingFs, physicalFs.folder("m"), 70);
cleartextFs = DaggerCryptoTestComponent.create().cryptoFileSystemFactory().get(ciphertextFs, "TopSecret");
cleartextFs.create();
}
@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);
}
}
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());
try (ReadableFile readable = cleartextFile.openReadable()) {
ByteBuffer buf = ByteBuffer.allocate(1);
for (int i = 0; i < 40; i++) {
buf.clear();
readable.position(i * 25000 + (long) Math.random() * 24999);
readable.read(buf);
buf.flip();
Assert.assertEquals(i, buf.get());
}
}
}
}

View File

@@ -0,0 +1,15 @@
package org.cryptomator.crypto;
import javax.inject.Singleton;
import org.cryptomator.crypto.engine.impl.CryptoTestModule;
import dagger.Component;
@Singleton
@Component(modules = CryptoTestModule.class)
interface CryptoTestComponent {
CryptoFileSystemFactory cryptoFileSystemFactory();
}

View File

@@ -21,7 +21,12 @@ class NoFileContentCryptor implements FileContentCryptor {
}
@Override
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header) {
public long toCiphertextPos(long cleartextPos) {
return cleartextPos;
}
@Override
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header, long firstCiphertextByte) {
if (header.remaining() != getHeaderSize()) {
throw new IllegalArgumentException("Invalid header size.");
}
@@ -29,7 +34,7 @@ class NoFileContentCryptor implements FileContentCryptor {
}
@Override
public FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header) {
public FileContentEncryptor createFileContentEncryptor(Optional<ByteBuffer> header, long firstCleartextByte) {
return new Encryptor();
}
@@ -66,16 +71,6 @@ class NoFileContentCryptor implements FileContentCryptor {
return cleartextQueue.take();
}
@Override
public ByteRange ciphertextRequiredToDecryptRange(ByteRange cleartextRange) {
return cleartextRange;
}
@Override
public void skipToPosition(long nextCiphertextByte) throws IllegalArgumentException {
// no-op
}
@Override
public void destroy() {
// no-op
@@ -115,16 +110,6 @@ class NoFileContentCryptor implements FileContentCryptor {
return ciphertextQueue.take();
}
@Override
public ByteRange cleartextRequiredToEncryptRange(ByteRange cleartextRange) {
return cleartextRange;
}
@Override
public void skipToPosition(long nextCleartextByte) throws IllegalArgumentException {
// no-op
}
@Override
public void destroy() {
// no-op

View File

@@ -0,0 +1,31 @@
package org.cryptomator.crypto.engine.impl;
import java.security.SecureRandom;
import java.util.Arrays;
import org.cryptomator.crypto.engine.Cryptor;
import dagger.Module;
import dagger.Provides;
@Module
public class CryptoTestModule {
@Provides
Cryptor provideCryptor(SecureRandom secureRandom) {
return new CryptorImpl(secureRandom);
}
@Provides
SecureRandom provideSecureRandom() {
return new SecureRandom() {
@Override
public void nextBytes(byte[] bytes) {
Arrays.fill(bytes, (byte) 0x00);
}
};
}
}

View File

@@ -10,6 +10,7 @@ package org.cryptomator.crypto.engine.impl;
import java.lang.reflect.Field;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -19,25 +20,12 @@ import org.junit.Test;
public class FifoParallelDataProcessorTest {
@Test(expected = IllegalStateException.class)
public void testRethrowsException() throws InterruptedException {
@Test(expected = ExecutionException.class)
public void testRethrowsExceptionAsExecutionException() throws InterruptedException, ExecutionException {
FifoParallelDataProcessor<Object> processor = new FifoParallelDataProcessor<>(1, 1);
try {
processor.submit(() -> {
throw new IllegalStateException("will be rethrown during 'processedData()'");
});
} catch (Exception e) {
Assert.fail("Exception must not yet be thrown.");
}
processor.processedData();
}
@Test(expected = RuntimeException.class)
public void testRethrowsCheckedExceptionAsRuntimeExceptions() throws InterruptedException {
FifoParallelDataProcessor<Object> processor = new FifoParallelDataProcessor<>(1, 1);
try {
processor.submit(() -> {
throw new Exception("will be wrapped in a RuntimeException during 'processedData()'");
throw new Exception("will be wrapped in a ExecutionException during 'processedData()'");
});
} catch (Exception e) {
Assert.fail("Exception must not yet be thrown.");
@@ -56,7 +44,7 @@ public class FifoParallelDataProcessorTest {
}
@Test
public void testStrictFifoOrder() throws InterruptedException {
public void testStrictFifoOrder() throws InterruptedException, ExecutionException {
FifoParallelDataProcessor<Integer> processor = new FifoParallelDataProcessor<>(4, 10);
processor.submit(new IntegerJob(100, 1));
processor.submit(new IntegerJob(50, 2));
@@ -74,7 +62,7 @@ public class FifoParallelDataProcessorTest {
}
@Test
public void testBlockingBehaviour() throws InterruptedException {
public void testBlockingBehaviour() throws InterruptedException, ExecutionException {
FifoParallelDataProcessor<Integer> processor = new FifoParallelDataProcessor<>(1, 1);
processor.submitPreprocessed(1); // #1 in queue
@@ -95,7 +83,7 @@ public class FifoParallelDataProcessorTest {
}
@Test
public void testInterruptionDuringSubmission() throws InterruptedException {
public void testInterruptionDuringSubmission() throws InterruptedException, ExecutionException {
FifoParallelDataProcessor<Integer> processor = new FifoParallelDataProcessor<>(1, 1);
processor.submitPreprocessed(1); // #1 in queue

View File

@@ -53,7 +53,7 @@ public class FileContentCryptorTest {
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
cryptor.createFileContentDecryptor(tooShortHeader);
cryptor.createFileContentDecryptor(tooShortHeader, 0);
}
@Test(expected = IllegalArgumentException.class)
@@ -64,7 +64,29 @@ public class FileContentCryptorTest {
FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
cryptor.createFileContentEncryptor(Optional.of(tooShortHeader));
cryptor.createFileContentEncryptor(Optional.of(tooShortHeader), 0);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidStartingPointInDecryptor() 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);
ByteBuffer tooShortHeader = ByteBuffer.allocate(64);
cryptor.createFileContentDecryptor(tooShortHeader, 3);
}
@Test(expected = IllegalArgumentException.class)
public void testInvalidStartingPointEncryptor() 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);
ByteBuffer tooShortHeader = ByteBuffer.allocate(64);
cryptor.createFileContentEncryptor(Optional.of(tooShortHeader), 3);
}
@Test
@@ -76,7 +98,7 @@ public class FileContentCryptorTest {
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
ByteBuffer ciphertext = ByteBuffer.allocate(100);
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty())) {
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
encryptor.append(ByteBuffer.wrap("cleartext message".getBytes()));
encryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
@@ -89,7 +111,7 @@ public class FileContentCryptorTest {
ciphertext.flip();
ByteBuffer plaintext = ByteBuffer.allocate(100);
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header)) {
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0)) {
decryptor.append(ciphertext);
decryptor.append(FileContentCryptor.EOF);
ByteBuffer buf;
@@ -115,7 +137,7 @@ public class FileContentCryptorTest {
final Thread fileWriter;
final ByteBuffer header;
final long encStart = System.nanoTime();
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty())) {
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
fileWriter = new Thread(() -> {
try (FileChannel fc = FileChannel.open(tmpFile, StandardOpenOption.WRITE)) {
ByteBuffer ciphertext;
@@ -142,7 +164,7 @@ public class FileContentCryptorTest {
final Thread fileReader;
final long decStart = System.nanoTime();
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header)) {
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, 0)) {
fileReader = new Thread(() -> {
try (FileChannel fc = FileChannel.open(tmpFile, StandardOpenOption.READ)) {
ByteBuffer ciphertext = ByteBuffer.allocate(654321);

View File

@@ -9,7 +9,9 @@
package org.cryptomator.crypto.engine.impl;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Optional;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@@ -17,12 +19,24 @@ import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.util.encoders.Base64;
import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentDecryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
import org.cryptomator.io.ByteBuffers;
import org.junit.Assert;
import org.junit.Test;
public class FileContentDecryptorImplTest {
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);
}
};
@Test
public void testDecryption() throws InterruptedException {
final byte[] keyBytes = new byte[32];
@@ -31,7 +45,7 @@ public class FileContentDecryptorImplTest {
final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc=");
final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHQ==");
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header))) {
try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header), 0)) {
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 15)));
decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 15, 43)));
decryptor.append(FileContentCryptor.EOF);
@@ -46,4 +60,45 @@ public class FileContentDecryptorImplTest {
}
}
@Test
public void testPartialDecryption() 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);
ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
ByteBuffer ciphertext = ByteBuffer.allocate(131200); // 4 * (32k + 32)
try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty(), 0)) {
ByteBuffer intBuf = ByteBuffer.allocate(32768);
for (int i = 0; i < 4; i++) {
intBuf.clear();
intBuf.putInt(i);
intBuf.rewind();
encryptor.append(intBuf);
}
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();
for (int i = 3; i >= 0; i--) {
int ciphertextPos = (int) cryptor.toCiphertextPos(i * 32768);
try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header, ciphertextPos)) {
ByteBuffer intBuf = ByteBuffer.allocate(32768);
intBuf.clear();
ciphertext.position(ciphertextPos);
decryptor.append(ciphertext);
ByteBuffer decrypted = decryptor.cleartext();
Assert.assertEquals(i, decrypted.getInt());
}
}
}
}

View File

@@ -41,7 +41,7 @@ public class FileContentEncryptorImplTest {
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK)) {
try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK, 0)) {
encryptor.append(ByteBuffer.wrap("hello ".getBytes()));
encryptor.append(ByteBuffer.wrap("world".getBytes()));
encryptor.append(FileContentCryptor.EOF);