mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-21 20:21:27 +00:00
Random Access Decryption
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.cryptomator.crypto.engine;
|
||||
|
||||
public class InvalidPassphraseException extends CryptoException {
|
||||
|
||||
public InvalidPassphraseException() {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.";
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user