diff --git a/main/filesystem-crypto/pom.xml b/main/filesystem-crypto/pom.xml
index 8bed43dfd..fb0baadbe 100644
--- a/main/filesystem-crypto/pom.xml
+++ b/main/filesystem-crypto/pom.xml
@@ -43,12 +43,6 @@
bcprov-jdk15on
${bouncycastle.version}
-
-
-
- com.google.guava
- guava
-
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 c00bcca12..ee42bce7f 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
@@ -8,6 +8,8 @@ import java.util.Optional;
*/
public interface FileContentCryptor {
+ public static final ByteBuffer EOF = ByteBuffer.allocate(0);
+
/**
* @return The fixed number of bytes of the file header. The header length is implementation-specific.
*/
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 343e0b0ae..ca1949c07 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
@@ -1,5 +1,6 @@
package org.cryptomator.crypto.engine;
+import java.io.Closeable;
import java.nio.ByteBuffer;
import javax.security.auth.Destroyable;
@@ -7,9 +8,7 @@ import javax.security.auth.Destroyable;
/**
* Stateful, thus not thread-safe.
*/
-public interface FileContentDecryptor extends Destroyable {
-
- public static final ByteBuffer EOF = ByteBuffer.allocate(0);
+public interface FileContentDecryptor extends Destroyable, Closeable {
/**
* @return Number of bytes of the decrypted file.
@@ -19,7 +18,7 @@ public interface FileContentDecryptor extends Destroyable {
/**
* Appends further ciphertext to this decryptor. This method might block until space becomes available. If so, it is interruptable.
*
- * @param cleartext Cleartext data or {@link #EOF} to indicate the end of a ciphertext.
+ * @param cleartext Cleartext data or {@link FileContentCryptor#EOF} to indicate the end of a ciphertext.
* @see #skipToPosition(long)
*/
void append(ByteBuffer ciphertext);
@@ -30,7 +29,7 @@ public interface FileContentDecryptor extends Destroyable {
*
* This method might block if no cleartext is available yet.
*
- * @return Decrypted cleartext or {@link #EOF}.
+ * @return Decrypted cleartext or {@link FileContentCryptor#EOF}.
*/
ByteBuffer cleartext() throws InterruptedException;
@@ -57,4 +56,9 @@ public interface FileContentDecryptor extends Destroyable {
@Override
void destroy();
+ @Override
+ default void close() {
+ this.destroy();
+ }
+
}
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 3ca09c4a8..bc5804010 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
@@ -1,5 +1,6 @@
package org.cryptomator.crypto.engine;
+import java.io.Closeable;
import java.nio.ByteBuffer;
import javax.security.auth.Destroyable;
@@ -7,9 +8,7 @@ import javax.security.auth.Destroyable;
/**
* Stateful, thus not thread-safe.
*/
-public interface FileContentEncryptor extends Destroyable {
-
- public static final ByteBuffer EOF = ByteBuffer.allocate(0);
+public interface FileContentEncryptor extends Destroyable, Closeable {
/**
* Creates the encrypted file header. This header might depend on the already encrypted data,
@@ -22,7 +21,7 @@ public interface FileContentEncryptor extends Destroyable {
/**
* Appends further cleartext to this encryptor. This method might block until space becomes available.
*
- * @param cleartext Cleartext data or {@link #EOF} to indicate the end of a cleartext.
+ * @param cleartext Cleartext data or {@link FileContentCryptor#EOF} to indicate the end of a cleartext.
*/
void append(ByteBuffer cleartext);
@@ -32,7 +31,7 @@ public interface FileContentEncryptor extends Destroyable {
*
* This method might block if no ciphertext is available yet.
*
- * @return Encrypted ciphertext of {@link #EOF}.
+ * @return Encrypted ciphertext of {@link FileContentCryptor#EOF}.
*/
ByteBuffer ciphertext() throws InterruptedException;
@@ -59,4 +58,9 @@ public interface FileContentEncryptor extends Destroyable {
@Override
void destroy();
+ @Override
+ default void close() {
+ this.destroy();
+ }
+
}
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/AbstractFileContentProcessor.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/AbstractFileContentProcessor.java
new file mode 100644
index 000000000..acb754cd8
--- /dev/null
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/AbstractFileContentProcessor.java
@@ -0,0 +1,60 @@
+package org.cryptomator.crypto.engine.impl;
+
+import java.io.Closeable;
+import java.nio.ByteBuffer;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+import org.apache.commons.lang3.concurrent.ConcurrentUtils;
+
+abstract class AbstractFileContentProcessor implements Closeable {
+
+ private static final int NUM_WORKERS = Runtime.getRuntime().availableProcessors();
+ private static final int READ_AHEAD = 0;
+
+ private final BlockingQueue processedData = new PriorityBlockingQueue<>();
+ private final BlockingQueue workQueue = new ArrayBlockingQueue<>(NUM_WORKERS + READ_AHEAD);
+ private final ExecutorService executorService = new ThreadPoolExecutor(1, NUM_WORKERS, 1, TimeUnit.SECONDS, workQueue);
+ private final AtomicLong jobSequence = new AtomicLong();
+
+ /**
+ * Enqueues a job for execution. The results of multiple submissions can be polled in FIFO order using {@link #processedData()}.
+ *
+ * @param processingJob A ByteBuffer-generating task.
+ */
+ protected void submit(Callable processingJob) {
+ Future result = executorService.submit(processingJob);
+ processedData.offer(new BytesWithSequenceNumber(result, jobSequence.getAndIncrement()));
+ }
+
+ /**
+ * Submits already processed data, that can be polled in FIFO order from {@link #processedData()}.
+ */
+ protected void submitPreprocessed(ByteBuffer preprocessedData) {
+ Future resolvedFuture = ConcurrentUtils.constantFuture(preprocessedData);
+ processedData.offer(new BytesWithSequenceNumber(resolvedFuture, jobSequence.getAndIncrement()));
+ }
+
+ /**
+ * Result of previously {@link #submit(Callable) submitted} jobs in the same order as they have been submitted. Blocks if the job didn't finish yet.
+ *
+ * @return Next job result
+ * @throws InterruptedException If the calling thread was interrupted while waiting for the next result.
+ */
+ protected ByteBuffer processedData() throws InterruptedException {
+ return processedData.take().get();
+ }
+
+ @Override
+ public void close() {
+ executorService.shutdown();
+ }
+
+}
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 2aa46c3ed..ef3842075 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
@@ -32,11 +32,17 @@ class FileContentCryptorImpl implements FileContentCryptor {
@Override
public FileContentDecryptor createFileContentDecryptor(ByteBuffer header) {
- throw new UnsupportedOperationException("Method not implemented");
+ if (header.remaining() != getHeaderSize()) {
+ throw new IllegalArgumentException("Invalid header.");
+ }
+ return new FileContentDecryptorImpl(encryptionKey, macKey, header);
}
@Override
public FileContentEncryptor createFileContentEncryptor(Optional header) {
+ if (header.isPresent() && header.get().remaining() != getHeaderSize()) {
+ throw new IllegalArgumentException("Invalid header.");
+ }
return new FileContentEncryptorImpl(encryptionKey, macKey, randomSource);
}
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
new file mode 100644
index 000000000..cf8ef471b
--- /dev/null
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java
@@ -0,0 +1,213 @@
+package org.cryptomator.crypto.engine.impl;
+
+import java.nio.ByteBuffer;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.concurrent.Callable;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.auth.DestroyFailedException;
+
+import org.cryptomator.crypto.engine.ByteRange;
+import org.cryptomator.crypto.engine.FileContentCryptor;
+import org.cryptomator.crypto.engine.FileContentDecryptor;
+import org.cryptomator.io.ByteBuffers;
+
+class FileContentDecryptorImpl extends AbstractFileContentProcessor implements FileContentDecryptor {
+
+ private static final String AES = "AES";
+ private static final int AES_BLOCK_LENGTH_IN_BYTES = 16;
+ private static final String AES_CBC = "AES/CBC/PKCS5Padding";
+ private static final String HMAC_SHA256 = "HmacSHA256";
+ private static final int CHUNK_SIZE = 32 * 1024;
+ private static final int MAC_SIZE = 32;
+
+ private final ThreadLocal hmacSha256;
+ private final SecretKey contentKey;
+ private final byte[] nonce;
+ private final long cleartextLength;
+ private ByteBuffer ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE + MAC_SIZE);
+ private long chunkNumber = 0;
+
+ public FileContentDecryptorImpl(SecretKey headerKey, SecretKey macKey, ByteBuffer header) {
+ this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256);
+
+ checkHeaderMac(header, hmacSha256.get());
+
+ this.nonce = new byte[8];
+ ByteBuffer nonceBuffer = header.asReadOnlyBuffer();
+ nonceBuffer.position(16).limit(24);
+ nonceBuffer.get(this.nonce);
+
+ byte[] contentKeyBytes = new byte[32];
+ ByteBuffer sensitiveDataBuffer = getCleartextSensitiveHeaderData(header, headerKey);
+ this.cleartextLength = sensitiveDataBuffer.getLong();
+ sensitiveDataBuffer.get(contentKeyBytes);
+ this.contentKey = new SecretKeySpec(contentKeyBytes, AES);
+
+ }
+
+ private static void checkHeaderMac(ByteBuffer header, Mac mac) throws IllegalArgumentException {
+ assert mac.getMacLength() == MAC_SIZE;
+ ByteBuffer headerData = header.asReadOnlyBuffer();
+ headerData.position(0).limit(72);
+ mac.update(headerData);
+ ByteBuffer headerMac = header.asReadOnlyBuffer();
+ headerMac.position(72).limit(72 + MAC_SIZE);
+ byte[] expectedMac = new byte[MAC_SIZE];
+ headerMac.get(expectedMac);
+
+ if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
+ throw new IllegalArgumentException("Corrupt header.");
+ }
+ }
+
+ private static ByteBuffer getCleartextSensitiveHeaderData(ByteBuffer header, SecretKey headerKey) {
+ try {
+ byte[] iv = new byte[16];
+ ByteBuffer ivBuffer = header.asReadOnlyBuffer();
+ ivBuffer.position(0).limit(16);
+ ivBuffer.get(iv);
+
+ ByteBuffer sensitiveHeaderDataBuffer = header.asReadOnlyBuffer();
+ sensitiveHeaderDataBuffer.position(24).limit(72);
+
+ final Cipher cipher = Cipher.getInstance(AES_CBC);
+ cipher.init(Cipher.DECRYPT_MODE, headerKey, new IvParameterSpec(iv));
+ final int cleartextLength = cipher.getOutputSize(sensitiveHeaderDataBuffer.remaining());
+ assert cleartextLength == 48 : "decryption shouldn't need more output than input buffer size.";
+ final ByteBuffer cleartext = ByteBuffer.allocate(cleartextLength);
+ cipher.doFinal(sensitiveHeaderDataBuffer, cleartext);
+ cleartext.flip();
+ return cleartext;
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
+ throw new IllegalStateException("Unable to decrypt header.", e);
+ }
+ }
+
+ @Override
+ public long contentLength() {
+ return cleartextLength;
+ }
+
+ @Override
+ public void append(ByteBuffer ciphertext) {
+ if (ciphertext == FileContentCryptor.EOF) {
+ submitCiphertextBuffer();
+ submitEof();
+ } else {
+ while (ciphertext.hasRemaining()) {
+ ByteBuffers.copy(ciphertext, ciphertextBuffer);
+ submitCiphertextBufferIfFull();
+ }
+ }
+ }
+
+ private void submitCiphertextBufferIfFull() {
+ if (!ciphertextBuffer.hasRemaining()) {
+ submitCiphertextBuffer();
+ ciphertextBuffer = ByteBuffer.allocate(CHUNK_SIZE + MAC_SIZE);
+ }
+ }
+
+ private void submitCiphertextBuffer() {
+ ciphertextBuffer.flip();
+ Callable encryptionJob = new DecryptionJob(ciphertextBuffer, chunkNumber++);
+ submit(encryptionJob);
+ }
+
+ private void submitEof() {
+ submitPreprocessed(FileContentCryptor.EOF);
+ }
+
+ @Override
+ public ByteBuffer cleartext() throws InterruptedException {
+ return 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.");
+ }
+
+ @Override
+ public void destroy() {
+ try {
+ contentKey.destroy();
+ } catch (DestroyFailedException e) {
+ // ignore
+ }
+ }
+
+ @Override
+ public void close() {
+ this.destroy();
+ super.close();
+ }
+
+ private class DecryptionJob implements Callable {
+
+ private final ByteBuffer ciphertextChunk;
+ private final byte[] expectedMac;
+ private final byte[] nonceAndCtr;
+
+ public DecryptionJob(ByteBuffer ciphertextChunk, long chunkNumber) {
+ if (ciphertextChunk.remaining() < MAC_SIZE) {
+ throw new IllegalArgumentException("Chunk must end with a MAC");
+ }
+ this.ciphertextChunk = ciphertextChunk.asReadOnlyBuffer();
+ this.ciphertextChunk.position(0).limit(ciphertextChunk.limit() - MAC_SIZE);
+ this.expectedMac = new byte[MAC_SIZE];
+ ByteBuffer macBuf = ciphertextChunk.asReadOnlyBuffer();
+ macBuf.position(macBuf.limit() - MAC_SIZE);
+ macBuf.get(expectedMac);
+
+ final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH_IN_BYTES);
+ nonceAndCounterBuf.put(nonce);
+ nonceAndCounterBuf.putLong(chunkNumber * CHUNK_SIZE / AES_BLOCK_LENGTH_IN_BYTES);
+ this.nonceAndCtr = nonceAndCounterBuf.array();
+ }
+
+ @Override
+ public ByteBuffer call() {
+ try {
+ Mac mac = hmacSha256.get();
+ mac.update(ciphertextChunk.asReadOnlyBuffer());
+ if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) {
+ // TODO handle invalid MAC properly
+ throw new IllegalArgumentException("Corrupt mac.");
+ }
+
+ Cipher cipher = ThreadLocalAesCtrCipher.get();
+ cipher.init(Cipher.DECRYPT_MODE, contentKey, new IvParameterSpec(nonceAndCtr));
+ ByteBuffer cleartextChunk = ByteBuffer.allocate(cipher.getOutputSize(ciphertextChunk.remaining()));
+ cipher.update(ciphertextChunk, cleartextChunk);
+ cleartextChunk.flip();
+ return cleartextChunk;
+ } catch (InvalidKeyException e) {
+ throw new IllegalStateException("File content key created by current class invalid.", e);
+ } catch (ShortBufferException e) {
+ throw new IllegalStateException("Buffer allocated for reported output size apparently not big enought.", e);
+ } catch (InvalidAlgorithmParameterException e) {
+ throw new IllegalStateException("CTR mode known to accept an IV (aka. nonce).", e);
+ }
+ }
+
+ }
+
+}
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 fe55fa0df..df195a1f2 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
@@ -5,12 +5,8 @@ import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
-import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.PriorityBlockingQueue;
+import java.util.concurrent.atomic.LongAdder;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -24,31 +20,24 @@ import javax.crypto.spec.SecretKeySpec;
import javax.security.auth.DestroyFailedException;
import org.cryptomator.crypto.engine.ByteRange;
+import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
import org.cryptomator.io.ByteBuffers;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import com.google.common.util.concurrent.Futures;
+class FileContentEncryptorImpl extends AbstractFileContentProcessor implements FileContentEncryptor {
-public class FileContentEncryptorImpl implements FileContentEncryptor {
-
- private static final Logger LOG = LoggerFactory.getLogger(FileContentEncryptorImpl.class);
private static final String AES = "AES";
private static final int AES_BLOCK_LENGTH_IN_BYTES = 16;
private static final String AES_CBC = "AES/CBC/PKCS5Padding";
private static final String HMAC_SHA256 = "HmacSHA256";
private static final int CHUNK_SIZE = 32 * 1024;
- private static final int NUM_WORKERS = Runtime.getRuntime().availableProcessors();
- private final BlockingQueue ciphertextQueue = new PriorityBlockingQueue<>();
- private final ExecutorService executorService = Executors.newFixedThreadPool(NUM_WORKERS);
private final ThreadLocal hmacSha256;
- private final long cleartextBytesEncrypted = 0;
private final SecretKey headerKey;
private final SecretKey contentKey;
private final byte[] iv;
private final byte[] nonce;
+ private final LongAdder cleartextBytesEncrypted = new LongAdder();
private ByteBuffer cleartextBuffer = ByteBuffer.allocate(CHUNK_SIZE);
private long chunkNumber = 0;
@@ -66,7 +55,7 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
private ByteBuffer getCleartextSensitiveHeaderData() {
ByteBuffer header = ByteBuffer.allocate(104);
- header.putLong(cleartextBytesEncrypted);
+ header.putLong(cleartextBytesEncrypted.sum());
header.put(contentKey.getEncoded());
header.flip();
return header;
@@ -81,6 +70,7 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
assert ciphertextLength == 48 : "8 byte long and 32 byte file key should fit into 3 blocks";
final ByteBuffer ciphertext = ByteBuffer.allocate(ciphertextLength);
cipher.doFinal(cleartext, ciphertext);
+ ciphertext.flip();
return ciphertext;
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
throw new IllegalStateException("Unable to compute encrypted header.", e);
@@ -109,7 +99,8 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
@Override
public void append(ByteBuffer cleartext) {
- if (cleartext == FileContentEncryptor.EOF) {
+ cleartextBytesEncrypted.add(cleartext.remaining());
+ if (cleartext == FileContentCryptor.EOF) {
submitCleartextBuffer();
submitEof();
} else {
@@ -129,19 +120,17 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
private void submitCleartextBuffer() {
cleartextBuffer.flip();
- long myChunkNumber = chunkNumber++;
- Future result = executorService.submit(new EncryptionJob(cleartextBuffer, myChunkNumber));
- ciphertextQueue.offer(new BytesWithSequenceNumber(result, myChunkNumber));
+ Callable encryptionJob = new EncryptionJob(cleartextBuffer, chunkNumber++);
+ submit(encryptionJob);
}
private void submitEof() {
- Future resolvedFuture = Futures.immediateFuture(FileContentEncryptor.EOF);
- ciphertextQueue.offer(new BytesWithSequenceNumber(resolvedFuture, Long.MAX_VALUE));
+ submitPreprocessed(FileContentCryptor.EOF);
}
@Override
public ByteBuffer ciphertext() throws InterruptedException {
- return ciphertextQueue.take().get();
+ return processedData();
}
@Override
@@ -151,7 +140,7 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
@Override
public void skipToPosition(long nextCleartextByte) throws IllegalArgumentException {
- throw new UnsupportedOperationException("partial encryption not supported.");
+ throw new UnsupportedOperationException("Partial encryption not supported.");
}
@Override
@@ -159,17 +148,23 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
try {
contentKey.destroy();
} catch (DestroyFailedException e) {
- LOG.warn("Could not destroy file-specific key", e);
+ // ignore
}
}
+ @Override
+ public void close() {
+ this.destroy();
+ super.close();
+ }
+
private class EncryptionJob implements Callable {
- private final ByteBuffer cleartextBlock;
+ private final ByteBuffer cleartextChunk;
private final byte[] nonceAndCtr;
- public EncryptionJob(ByteBuffer cleartext, long chunkNumber) {
- this.cleartextBlock = cleartext;
+ public EncryptionJob(ByteBuffer cleartextChunk, long chunkNumber) {
+ this.cleartextChunk = cleartextChunk;
final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH_IN_BYTES);
nonceAndCounterBuf.put(nonce);
@@ -183,15 +178,15 @@ public class FileContentEncryptorImpl implements FileContentEncryptor {
Cipher cipher = ThreadLocalAesCtrCipher.get();
cipher.init(Cipher.ENCRYPT_MODE, contentKey, new IvParameterSpec(nonceAndCtr));
Mac mac = hmacSha256.get();
- ByteBuffer ciphertextBlock = ByteBuffer.allocate(cipher.getOutputSize(cleartextBlock.remaining()) + mac.getMacLength());
- cipher.update(cleartextBlock, ciphertextBlock);
- ByteBuffer ciphertextSoFar = ciphertextBlock.asReadOnlyBuffer();
+ ByteBuffer ciphertextChunk = ByteBuffer.allocate(cipher.getOutputSize(cleartextChunk.remaining()) + mac.getMacLength());
+ cipher.update(cleartextChunk, ciphertextChunk);
+ ByteBuffer ciphertextSoFar = ciphertextChunk.asReadOnlyBuffer();
ciphertextSoFar.flip();
mac.update(ciphertextSoFar);
byte[] authenticationCode = mac.doFinal();
- ciphertextBlock.put(authenticationCode);
- ciphertextBlock.flip();
- return ciphertextBlock;
+ ciphertextChunk.put(authenticationCode);
+ ciphertextChunk.flip();
+ return ciphertextChunk;
} catch (InvalidKeyException e) {
throw new IllegalStateException("File content key created by current class invalid.", e);
} catch (ShortBufferException e) {
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java
index a84c0b227..64b3ee6de 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoReadableFile.java
@@ -44,7 +44,7 @@ class CryptoReadableFile implements ReadableFile {
@Override
public void read(ByteBuffer target) {
try {
- while (target.remaining() > 0 && bufferedCleartext != FileContentDecryptor.EOF) {
+ while (target.remaining() > 0 && bufferedCleartext != FileContentCryptor.EOF) {
bufferCleartext();
readFromBufferedCleartext(target);
}
@@ -101,7 +101,7 @@ class CryptoReadableFile implements ReadableFile {
decryptor.append(ciphertext);
}
} while (bytesRead > 0);
- decryptor.append(FileContentDecryptor.EOF);
+ decryptor.append(FileContentCryptor.EOF);
return null;
}
diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java
index d4c806d47..028d63be5 100644
--- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java
+++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/fs/CryptoWritableFile.java
@@ -72,7 +72,7 @@ class CryptoWritableFile implements WritableFile {
@Override
public void close() {
try {
- encryptor.append(FileContentEncryptor.EOF);
+ encryptor.append(FileContentCryptor.EOF);
writeTask.get();
writeHeader();
} catch (ExecutionException e) {
@@ -95,7 +95,7 @@ class CryptoWritableFile implements WritableFile {
public Void call() {
try {
ByteBuffer ciphertext;
- while ((ciphertext = encryptor.ciphertext()) != FileContentEncryptor.EOF) {
+ while ((ciphertext = encryptor.ciphertext()) != FileContentCryptor.EOF) {
file.write(ciphertext);
}
} catch (InterruptedException e) {
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 149b82333..2e1f0d579 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
@@ -43,8 +43,8 @@ class NoFileContentCryptor implements FileContentCryptor {
@Override
public void append(ByteBuffer ciphertext) {
try {
- if (ciphertext == FileContentDecryptor.EOF) {
- cleartextQueue.put(FileContentDecryptor.EOF);
+ if (ciphertext == FileContentCryptor.EOF) {
+ cleartextQueue.put(FileContentCryptor.EOF);
} else {
cleartextQueue.put(ciphertext.asReadOnlyBuffer());
}
@@ -90,8 +90,8 @@ class NoFileContentCryptor implements FileContentCryptor {
@Override
public void append(ByteBuffer cleartext) {
try {
- if (cleartext == FileContentEncryptor.EOF) {
- ciphertextQueue.put(FileContentEncryptor.EOF);
+ if (cleartext == FileContentCryptor.EOF) {
+ ciphertextQueue.put(FileContentCryptor.EOF);
} else {
int cleartextLen = cleartext.remaining();
ciphertextQueue.put(cleartext.asReadOnlyBuffer());
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
new file mode 100644
index 000000000..1db011456
--- /dev/null
+++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java
@@ -0,0 +1,89 @@
+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;
+
+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 FileContentCryptorTest {
+
+ 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(expected = IllegalArgumentException.class)
+ public void testShortHeaderInDecryptor() throws InterruptedException {
+ final byte[] keyBytes = new byte[32];
+ final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
+ final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+ FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
+
+ ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
+ cryptor.createFileContentDecryptor(tooShortHeader);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testShortHeaderInEncryptor() throws InterruptedException {
+ final byte[] keyBytes = new byte[32];
+ final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
+ final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+ FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
+
+ ByteBuffer tooShortHeader = ByteBuffer.allocate(63);
+ cryptor.createFileContentEncryptor(Optional.of(tooShortHeader));
+ }
+
+ @Test
+ public void testEncryptionAndDecryption() throws InterruptedException {
+ final byte[] keyBytes = new byte[32];
+ final SecretKey encryptionKey = new SecretKeySpec(keyBytes, "AES");
+ final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+ FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK);
+
+ ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize());
+ ByteBuffer ciphertext = ByteBuffer.allocate(100);
+ try (FileContentEncryptor encryptor = cryptor.createFileContentEncryptor(Optional.empty())) {
+ encryptor.append(ByteBuffer.wrap("cleartext message".getBytes()));
+ 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();
+
+ ByteBuffer plaintext = ByteBuffer.allocate(100);
+ try (FileContentDecryptor decryptor = cryptor.createFileContentDecryptor(header)) {
+ decryptor.append(ciphertext);
+ decryptor.append(FileContentCryptor.EOF);
+ ByteBuffer buf;
+ while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
+ ByteBuffers.copy(buf, plaintext);
+ }
+ }
+ plaintext.flip();
+
+ byte[] result = new byte[plaintext.remaining()];
+ plaintext.get(result);
+ Assert.assertArrayEquals("cleartext message".getBytes(), result);
+ }
+}
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
new file mode 100644
index 000000000..754f0b5b2
--- /dev/null
+++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java
@@ -0,0 +1,41 @@
+package org.cryptomator.crypto.engine.impl;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+import javax.crypto.SecretKey;
+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.io.ByteBuffers;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class FileContentDecryptorImplTest {
+
+ @Test
+ public void testDecryption() throws InterruptedException {
+ final byte[] keyBytes = new byte[32];
+ final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
+ final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
+ final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAbQMxxKDDeVNbWcxRPUp3zSKaIl9RDlCco7Aa975ufw/3rL27hDTQEnd3FZNlWh1VHmi5hGO9Cn5n4hrsZARZQ8mJeLxjNKI4DZL72lGQKN4=");
+ final byte[] content = Base64.decode("tPCsFM1g/ubfJMY0O2wdWwEHrRZG0HQPfeaAJxtXs7Xkq3g0idoVCp2BbUc=");
+
+ try (FileContentDecryptor decryptor = new FileContentDecryptorImpl(headerKey, macKey, ByteBuffer.wrap(header))) {
+ decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 0, 10)));
+ decryptor.append(ByteBuffer.wrap(Arrays.copyOfRange(content, 10, 44)));
+ decryptor.append(FileContentCryptor.EOF);
+
+ ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
+ ByteBuffer buf;
+ while ((buf = decryptor.cleartext()) != FileContentCryptor.EOF) {
+ ByteBuffers.copy(buf, result);
+ }
+
+ Assert.assertArrayEquals("hello world".getBytes(), result.array());
+ }
+ }
+
+}
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 dd720d43d..ede56aacf 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
@@ -8,6 +8,7 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.util.encoders.Base64;
+import org.cryptomator.crypto.engine.FileContentCryptor;
import org.cryptomator.crypto.engine.FileContentEncryptor;
import org.cryptomator.io.ByteBuffers;
import org.junit.Assert;
@@ -31,20 +32,21 @@ public class FileContentEncryptorImplTest {
final byte[] keyBytes = new byte[32];
final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES");
final SecretKey macKey = new SecretKeySpec(keyBytes, "AES");
- FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK);
- encryptor.append(ByteBuffer.wrap("hello world".getBytes()));
- encryptor.append(FileContentEncryptor.EOF);
+ try (FileContentEncryptor encryptor = new FileContentEncryptorImpl(headerKey, macKey, RANDOM_MOCK)) {
+ encryptor.append(ByteBuffer.wrap("hello ".getBytes()));
+ encryptor.append(ByteBuffer.wrap("world ".getBytes()));
+ encryptor.append(FileContentCryptor.EOF);
- ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
- ByteBuffer buf;
- while ((buf = encryptor.ciphertext()) != FileContentEncryptor.EOF) {
- ByteBuffers.copy(buf, result);
+ ByteBuffer result = ByteBuffer.allocate(11); // we just care about the first 11 bytes, as this is the ciphertext.
+ ByteBuffer buf;
+ while ((buf = encryptor.ciphertext()) != FileContentCryptor.EOF) {
+ ByteBuffers.copy(buf, result);
+ }
+
+ // echo -n "hello world" | openssl enc -aes-256-ctr -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 | base64
+ Assert.assertArrayEquals(Base64.decode("tPCsFM1g/ubfJMY="), result.array());
}
-
- // echo -n "hello world" | openssl enc -aes-256-ctr -K 0000000000000000000000000000000000000000000000000000000000000000 -iv 00000000000000000000000000000000 | base64
- final String expected = "tPCsFM1g/ubfJMY=";
- Assert.assertArrayEquals(Base64.decode(expected), result.array());
}
}
diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java
index 7042697da..65d06ae22 100644
--- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java
+++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java
@@ -154,12 +154,12 @@ public class CryptoFileSystemTest {
final FileSystem fs = new CryptoFileSystem(physicalFs, cryptor, "foo");
fs.create(FolderCreateMode.INCLUDING_PARENTS);
- // write test content to physical file
+ // write test content to file
try (WritableFile writable = fs.file("test1.txt").openWritable()) {
writable.write(ByteBuffer.wrap("Hello World".getBytes()));
}
- // read test content from encrypted file
+ // read test content from file
try (ReadableFile readable = fs.file("test1.txt").openReadable()) {
ByteBuffer buf1 = ByteBuffer.allocate(5);
readable.read(buf1);
diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java
index 37dd19fc8..956ea22a3 100644
--- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java
+++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java
@@ -1,11 +1,17 @@
package org.cryptomator.crypto.fs;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
import org.cryptomator.crypto.engine.Cryptor;
import org.cryptomator.crypto.engine.impl.TestCryptorImplFactory;
+import org.cryptomator.filesystem.File;
import org.cryptomator.filesystem.FileSystem;
import org.cryptomator.filesystem.Folder;
import org.cryptomator.filesystem.FolderCreateMode;
import org.cryptomator.filesystem.Node;
+import org.cryptomator.filesystem.ReadableFile;
+import org.cryptomator.filesystem.WritableFile;
import org.cryptomator.filesystem.inmem.InMemoryFileSystem;
import org.cryptomator.shortening.ShorteningFileSystem;
import org.junit.Assert;
@@ -45,4 +51,35 @@ public class EncryptAndShortenIntegrationTest {
Assert.assertArrayEquals(new String[] {"normal folder name", "this will be a long filename after encryption"}, fs.folders().map(Node::name).sorted().toArray());
}
+ @Test
+ public void testEncryptionAndDecryptionOfFiles() {
+ final FileSystem physicalFs = new InMemoryFileSystem();
+ final FileSystem shorteningFs = new ShorteningFileSystem(physicalFs, physicalFs.folder("m"), 70);
+ final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl();
+ cryptor.randomizeMasterkey();
+ final FileSystem fs = new CryptoFileSystem(shorteningFs, cryptor, "foo");
+ fs.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);
+
+ // write test content to encrypted file
+ try (WritableFile writable = fs.file("test1.txt").openWritable()) {
+ writable.write(ByteBuffer.wrap("Hello ".getBytes()));
+ writable.write(ByteBuffer.wrap("World".getBytes()));
+ }
+
+ File physicalFile = physicalFs.folder("d").folders().findAny().get().folders().findAny().get().files().findAny().get();
+ Assert.assertTrue(physicalFile.exists());
+
+ // read test content from decrypted file
+ try (ReadableFile readable = fs.file("test1.txt").openReadable()) {
+ ByteBuffer buf1 = ByteBuffer.allocate(5);
+ readable.read(buf1);
+ buf1.flip();
+ Assert.assertEquals("Hello", new String(buf1.array(), 0, buf1.remaining()));
+ ByteBuffer buf2 = ByteBuffer.allocate(10);
+ readable.read(buf2);
+ buf2.flip();
+ Assert.assertArrayEquals(" World".getBytes(), Arrays.copyOfRange(buf2.array(), 0, buf2.remaining()));
+ }
+ }
+
}