diff --git a/main/filesystem-crypto/pom.xml b/main/filesystem-crypto/pom.xml index a04425df9..8c21359b3 100644 --- a/main/filesystem-crypto/pom.xml +++ b/main/filesystem-crypto/pom.xml @@ -64,6 +64,17 @@ jackson-databind + + + com.google.dagger + dagger + + + com.google.dagger + dagger-compiler + provided + + org.cryptomator diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/CryptoComponent.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/CryptoComponent.java new file mode 100644 index 000000000..927a0a6ab --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/CryptoComponent.java @@ -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(); + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/CryptoFileSystemFactory.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/CryptoFileSystemFactory.java new file mode 100644 index 000000000..bca2ce86d --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/CryptoFileSystemFactory.java @@ -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 cryptorProvider; + + @Inject + public CryptoFileSystemFactory(Provider cryptorProvider) { + this.cryptorProvider = cryptorProvider; + } + + public FileSystem get(Folder root, CharSequence passphrase) { + return new BlockAlignedFileSystem(new CryptoFileSystem(root, cryptorProvider.get(), passphrase), FileContentCryptorImpl.CHUNK_SIZE); + } +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java index e2ddaaf4f..76d26eb42 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/AuthenticationFailedException.java @@ -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); } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java deleted file mode 100644 index ee7f42cb9..000000000 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java +++ /dev/null @@ -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; - } -} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java index 5f8c97c05..1c3ad964e 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/CryptoException.java @@ -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); } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentCryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentCryptor.java index 1850fb3b9..0363c4558 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentCryptor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentCryptor.java @@ -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 header); + FileContentEncryptor createFileContentEncryptor(Optional header, long firstCleartextByte) throws IllegalArgumentException; } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java index dc5d90c01..cf0b95a3c 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentDecryptor.java @@ -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 [0, {@link Long#MAX_VALUE}]. - * - * @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. diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java index 300a64669..3544ede36 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FileContentEncryptor.java @@ -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 [0, {@link Long#MAX_VALUE}]. - * - * @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. */ diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java index 595ebe1e4..67a89af67 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/FilenameCryptor.java @@ -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; } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/InvalidPassphraseException.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/InvalidPassphraseException.java new file mode 100644 index 000000000..049c915da --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/InvalidPassphraseException.java @@ -0,0 +1,9 @@ +package org.cryptomator.crypto.engine; + +public class InvalidPassphraseException extends CryptoException { + + public InvalidPassphraseException() { + super(); + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptoModule.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptoModule.java new file mode 100644 index 000000000..4be4e6621 --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptoModule.java @@ -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); + } + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java index 22298a9ec..17b0a721e 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java @@ -43,27 +43,10 @@ public class CryptorImpl implements Cryptor { private final AtomicReference 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(); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessor.java index bec284230..3a502ca30 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessor.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessor.java @@ -64,15 +64,18 @@ class FifoParallelDataProcessor { * @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 job; + while ((job = processedData.poll()) != null) { + job.cancel(true); } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java index 364dbb2e1..144d1555c 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java @@ -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 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 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); } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java index 6d426e10d..fd5df0e77 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java @@ -8,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 encryptionJob = new DecryptionJob(ciphertextBuffer, chunkNumber++); - dataProcessor.submit(encryptionJob); + if (ciphertextBuffer.hasRemaining()) { + Callable 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(); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java index 31b9be1e5..12244501e 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java @@ -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 diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java index 693419269..8ed616306 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java @@ -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); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedReadableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedReadableFile.java index 0f875d3bc..dd4215fbc 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedReadableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedReadableFile.java @@ -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 diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedWritableFile.java index efc40bf8c..489334b6f 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedWritableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/blockaligned/BlockAlignedWritableFile.java @@ -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(); } } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java new file mode 100644 index 000000000..491b954e7 --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextReader.java @@ -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 { + + 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; + } + +} \ No newline at end of file diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java new file mode 100644 index 000000000..7a01d1d8a --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CiphertextWriter.java @@ -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 { + + 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; + } + +} \ No newline at end of file diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java index 6482505b6..b19555e20 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFileSystem.java @@ -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."; diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java index 0ab451682..a07105b30 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoFolder.java @@ -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()); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java index 570c30da5..82d72be56 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoReadableFile.java @@ -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 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 { - - 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; - } - - } - } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java index d9e34625a..bd2bc5faa 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/CryptoWritableFile.java @@ -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 { - - @Override - public Void call() { - try { - ByteBuffer ciphertext; - while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) { - file.write(ciphertext); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - return null; - } - - } - } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/CryptoFileSystemIntegrationTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/CryptoFileSystemIntegrationTest.java new file mode 100644 index 000000000..e7ff9c454 --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/CryptoFileSystemIntegrationTest.java @@ -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 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()); + } + } + } + +} diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/CryptoTestComponent.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/CryptoTestComponent.java new file mode 100644 index 000000000..d98995407 --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/CryptoTestComponent.java @@ -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(); + +} diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java index b40d03ffb..6eebffc2f 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/NoFileContentCryptor.java @@ -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 header) { + public FileContentEncryptor createFileContentEncryptor(Optional 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 diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptoTestModule.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptoTestModule.java new file mode 100644 index 000000000..5f9c31165 --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptoTestModule.java @@ -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); + } + + }; + } + +} diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessorTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessorTest.java index 0314ef7ae..56d0a4c87 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessorTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FifoParallelDataProcessorTest.java @@ -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 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 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 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 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 processor = new FifoParallelDataProcessor<>(1, 1); processor.submitPreprocessed(1); // #1 in queue diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java index ce815dffd..3102bf8e4 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java @@ -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); diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java index c03c19431..bce1ef2ac 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java @@ -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()); + } + } + + } + } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java index 5d941d18a..6310fcabd 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java @@ -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);