From 30458057513359f884f077bcf44bb1f1268840aa Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 20 Dec 2015 00:38:14 +0100 Subject: [PATCH] File content encryption and decryption (still without padding, no partial support) --- main/filesystem-crypto/pom.xml | 6 - .../crypto/engine/FileContentCryptor.java | 2 + .../crypto/engine/FileContentDecryptor.java | 14 +- .../crypto/engine/FileContentEncryptor.java | 14 +- .../impl/AbstractFileContentProcessor.java | 60 +++++ .../engine/impl/FileContentCryptorImpl.java | 8 +- .../engine/impl/FileContentDecryptorImpl.java | 213 ++++++++++++++++++ .../engine/impl/FileContentEncryptorImpl.java | 63 +++--- .../crypto/fs/CryptoReadableFile.java | 4 +- .../crypto/fs/CryptoWritableFile.java | 4 +- .../crypto/engine/NoFileContentCryptor.java | 8 +- .../engine/impl/FileContentCryptorTest.java | 89 ++++++++ .../impl/FileContentDecryptorImplTest.java | 41 ++++ .../impl/FileContentEncryptorImplTest.java | 24 +- .../crypto/fs/CryptoFileSystemTest.java | 4 +- .../fs/EncryptAndShortenIntegrationTest.java | 37 +++ 16 files changed, 519 insertions(+), 72 deletions(-) create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/AbstractFileContentProcessor.java create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java create mode 100644 main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java create mode 100644 main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java 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())); + } + } + }