From 4e0143eb057ae96499e59ecf9d1645422660e532 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 19 Dec 2015 18:22:03 +0100 Subject: [PATCH] started implementation of FileContentEncryptorImpl --- main/filesystem-crypto/pom.xml | 6 + .../cryptomator/crypto/engine/ByteRange.java | 2 +- .../crypto/engine/FileContentDecryptor.java | 21 +- .../crypto/engine/FileContentEncryptor.java | 19 +- .../engine/impl/BytesWithSequenceNumber.java | 31 +++ .../crypto/engine/impl/CryptorImpl.java | 74 +++++-- .../engine/impl/FileContentCryptorImpl.java | 15 +- .../engine/impl/FileContentEncryptorImpl.java | 206 ++++++++++++++++++ .../engine/impl/FilenameCryptorImpl.java | 3 - .../engine/impl/ThreadLocalAesCtrCipher.java | 28 +++ .../crypto/engine/impl/ThreadLocalMac.java | 37 ++++ .../crypto/fs/CryptoReadableFile.java | 3 +- .../crypto/fs/CryptoWritableFile.java | 2 +- .../crypto/engine/NoFileContentCryptor.java | 18 +- .../crypto/engine/impl/CryptorImplTest.java | 64 +----- .../impl/FileContentEncryptorImplTest.java | 50 +++++ .../engine/impl/TestCryptorImplFactory.java | 26 +++ .../fs/EncryptAndShortenIntegrationTest.java | 18 +- 18 files changed, 500 insertions(+), 123 deletions(-) create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/BytesWithSequenceNumber.java create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalAesCtrCipher.java create mode 100644 main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java create mode 100644 main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java create mode 100644 main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/TestCryptorImplFactory.java diff --git a/main/filesystem-crypto/pom.xml b/main/filesystem-crypto/pom.xml index 77656a929..8bed43dfd 100644 --- a/main/filesystem-crypto/pom.xml +++ b/main/filesystem-crypto/pom.xml @@ -44,6 +44,12 @@ ${bouncycastle.version} + + + com.google.guava + guava + + org.apache.commons diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java index 642e34e20..362048854 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/ByteRange.java @@ -16,7 +16,7 @@ public class ByteRange { this.length = length; } - static ByteRange of(long start, long length) { + public static ByteRange of(long start, long length) { return new ByteRange(start, length); } 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 dc334cc44..343e0b0ae 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,12 +1,13 @@ package org.cryptomator.crypto.engine; import java.nio.ByteBuffer; -import java.util.concurrent.BlockingQueue; + +import javax.security.auth.Destroyable; /** * Stateful, thus not thread-safe. */ -public interface FileContentDecryptor { +public interface FileContentDecryptor extends Destroyable { public static final ByteBuffer EOF = ByteBuffer.allocate(0); @@ -24,14 +25,14 @@ public interface FileContentDecryptor { void append(ByteBuffer ciphertext); /** - * Returns a queue containing cleartext in byte-by-byte FIFO order, meaning in the order ciphertext has been appended to this encryptor. - * However the number and size of the ciphertext byte buffers doesn't need to resemble the ciphertext buffers. + * Returns the next decrypted cleartext in byte-by-byte FIFO order, meaning in the order ciphertext has been appended to this encryptor. + * However the number and size of the cleartext byte buffers doesn't need to resemble the ciphertext buffers. * - * The queue returns {@link #EOF}, when all ciphertext has been processed. + * This method might block if no cleartext is available yet. * - * @return A queue from which decrypted data can be {@link BlockingQueue#take() taken}. + * @return Decrypted cleartext or {@link #EOF}. */ - BlockingQueue cleartext(); + ByteBuffer cleartext() throws InterruptedException; /** * Calculates the ciphertext bytes required to perform a partial decryption of a requested cleartext byte range. @@ -50,4 +51,10 @@ public interface FileContentDecryptor { */ void skipToPosition(long nextCiphertextByte) throws IllegalArgumentException; + /** + * Clears file-specific sensitive information. + */ + @Override + void 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 76928c4b6..3ca09c4a8 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,12 +1,13 @@ package org.cryptomator.crypto.engine; import java.nio.ByteBuffer; -import java.util.concurrent.BlockingQueue; + +import javax.security.auth.Destroyable; /** * Stateful, thus not thread-safe. */ -public interface FileContentEncryptor { +public interface FileContentEncryptor extends Destroyable { public static final ByteBuffer EOF = ByteBuffer.allocate(0); @@ -26,14 +27,14 @@ public interface FileContentEncryptor { void append(ByteBuffer cleartext); /** - * Returns a queue containing ciphertext in byte-by-byte FIFO order, meaning in the order cleartext has been appended to this encryptor. + * Returns the next ciphertext in byte-by-byte FIFO order, meaning in the order cleartext has been appended to this encryptor. * However the number and size of the ciphertext byte buffers doesn't need to resemble the cleartext buffers. * - * The queue returns {@link #EOF}, when all cleartext has been processed. + * This method might block if no ciphertext is available yet. * - * @return A queue from which encrypted data can be {@link BlockingQueue#take() taken}. + * @return Encrypted ciphertext of {@link #EOF}. */ - BlockingQueue ciphertext(); + ByteBuffer ciphertext() throws InterruptedException; /** * Calculates the cleartext bytes required to perform a partial encryption of a specific cleartext byte range. @@ -52,4 +53,10 @@ public interface FileContentEncryptor { */ void skipToPosition(long nextCleartextByte) throws IllegalArgumentException; + /** + * Clears file-specific sensitive information. + */ + @Override + void destroy(); + } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/BytesWithSequenceNumber.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/BytesWithSequenceNumber.java new file mode 100644 index 000000000..b835c475f --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/BytesWithSequenceNumber.java @@ -0,0 +1,31 @@ +package org.cryptomator.crypto.engine.impl; + +import java.nio.ByteBuffer; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +class BytesWithSequenceNumber implements Comparable { + + private final Future byteBuffer; + private final long sequenceNumber; + + public BytesWithSequenceNumber(Future byteBuffer, long sequenceNumber) { + this.byteBuffer = byteBuffer; + this.sequenceNumber = sequenceNumber; + } + + public ByteBuffer get() throws InterruptedException { + try { + return byteBuffer.get(); + } catch (ExecutionException e) { + assert e.getCause() instanceof RuntimeException; + throw (RuntimeException) e.getCause(); + } + } + + @Override + public int compareTo(BytesWithSequenceNumber other) { + return Long.compare(this.sequenceNumber, other.sequenceNumber); + } + +} \ No newline at end of file diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java index e5cd54f20..0b4c7bdba 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java @@ -14,6 +14,7 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.Arrays; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; @@ -42,44 +43,69 @@ public class CryptorImpl implements Cryptor { private final AtomicReference fileContentCryptor = new AtomicReference<>(); private final SecureRandom randomSource; - public CryptorImpl(SecureRandom randomSource) { + /** + * Designated constructor. + * + * Package-visible for testing only, use secondary constructors otherwise to ensure a proper PRNG. + */ + CryptorImpl(SecureRandom randomSource) { this.randomSource = randomSource; } - @Override - public FilenameCryptor getFilenameCryptor() { - // lazy initialization pattern as proposed here http://stackoverflow.com/a/30247202/4014509 - final FilenameCryptor existingCryptor = filenameCryptor.get(); - if (existingCryptor != null) { - return existingCryptor; - } else { - final FilenameCryptor newCryptor = new FilenameCryptorImpl(encryptionKey, macKey); - if (filenameCryptor.compareAndSet(null, newCryptor)) { - return newCryptor; - } else { - // CAS failed: other thread set an object - return filenameCryptor.get(); - } + 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(); + return initializeLazily(filenameCryptor, () -> { + return new FilenameCryptorImpl(encryptionKey, macKey); + }); + } + @Override public FileContentCryptor getFileContentCryptor() { - // lazy initialization pattern as proposed here http://stackoverflow.com/a/30247202/4014509 - final FileContentCryptor existingCryptor = fileContentCryptor.get(); - if (existingCryptor != null) { - return existingCryptor; + assertKeysExist(); + return initializeLazily(fileContentCryptor, () -> { + return new FileContentCryptorImpl(encryptionKey, macKey, randomSource); + }); + } + + /** + * threadsafe lazy initialization pattern as proposed on http://stackoverflow.com/a/30247202/4014509 + */ + private T initializeLazily(AtomicReference reference, Supplier factory) { + final T existingInstance = reference.get(); + if (existingInstance != null) { + return existingInstance; } else { - final FileContentCryptor newCryptor = new FileContentCryptorImpl(encryptionKey, macKey); - if (fileContentCryptor.compareAndSet(null, newCryptor)) { - return newCryptor; + final T newInstance = factory.get(); + if (reference.compareAndSet(null, newInstance)) { + return newInstance; } else { - // CAS failed: other thread set an object - return fileContentCryptor.get(); + return reference.get(); } } } + private void assertKeysExist() { + if (encryptionKey == null || encryptionKey.isDestroyed()) { + throw new IllegalStateException("No or invalid encryptionKey."); + } + if (macKey == null || macKey.isDestroyed()) { + throw new IllegalStateException("No or invalid MAC key."); + } + } + @Override public void randomizeMasterkey() { final byte[] randomBytes = new byte[KEYLENGTH_IN_BYTES]; 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 3448c3427..2aa46c3ed 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 @@ -1,6 +1,7 @@ package org.cryptomator.crypto.engine.impl; import java.nio.ByteBuffer; +import java.security.SecureRandom; import java.util.Optional; import javax.crypto.SecretKey; @@ -11,20 +12,22 @@ import org.cryptomator.crypto.engine.FileContentEncryptor; class FileContentCryptorImpl implements FileContentCryptor { + // 16 header IV, 8 content nonce, 48 sensitive header data, 32 headerMac + static final int HEADER_SIZE = 104; + private final SecretKey encryptionKey; private final SecretKey macKey; + private final SecureRandom randomSource; - public FileContentCryptorImpl(SecretKey encryptionKey, SecretKey macKey) { - if (encryptionKey == null || macKey == null) { - throw new IllegalArgumentException("Key must not be null"); - } + public FileContentCryptorImpl(SecretKey encryptionKey, SecretKey macKey, SecureRandom randomSource) { this.encryptionKey = encryptionKey; this.macKey = macKey; + this.randomSource = randomSource; } @Override public int getHeaderSize() { - throw new UnsupportedOperationException("Method not implemented"); + return HEADER_SIZE; } @Override @@ -34,7 +37,7 @@ class FileContentCryptorImpl implements FileContentCryptor { @Override public FileContentEncryptor createFileContentEncryptor(Optional header) { - throw new UnsupportedOperationException("Method not implemented"); + return new FileContentEncryptorImpl(encryptionKey, macKey, randomSource); } } 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 new file mode 100644 index 000000000..fe55fa0df --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java @@ -0,0 +1,206 @@ +package org.cryptomator.crypto.engine.impl; + +import java.nio.ByteBuffer; +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 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.FileContentEncryptor; +import org.cryptomator.io.ByteBuffers; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.Futures; + +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 ByteBuffer cleartextBuffer = ByteBuffer.allocate(CHUNK_SIZE); + private long chunkNumber = 0; + + public FileContentEncryptorImpl(SecretKey headerKey, SecretKey macKey, SecureRandom randomSource) { + this.hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256); + this.headerKey = headerKey; + this.iv = new byte[16]; + this.nonce = new byte[8]; + final byte[] contentKeyBytes = new byte[32]; + randomSource.nextBytes(iv); + randomSource.nextBytes(nonce); + randomSource.nextBytes(contentKeyBytes); + this.contentKey = new SecretKeySpec(contentKeyBytes, AES); + } + + private ByteBuffer getCleartextSensitiveHeaderData() { + ByteBuffer header = ByteBuffer.allocate(104); + header.putLong(cleartextBytesEncrypted); + header.put(contentKey.getEncoded()); + header.flip(); + return header; + } + + private ByteBuffer getCiphertextSensitiveHeaderData() { + final ByteBuffer cleartext = getCleartextSensitiveHeaderData(); + try { + final Cipher cipher = Cipher.getInstance(AES_CBC); + cipher.init(Cipher.ENCRYPT_MODE, headerKey, new IvParameterSpec(iv)); + final int ciphertextLength = cipher.getOutputSize(cleartext.remaining()); + 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); + return ciphertext; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalStateException("Unable to compute encrypted header.", e); + } + } + + private byte[] mac(ByteBuffer what) { + Mac mac = hmacSha256.get(); + mac.update(what); + return mac.doFinal(); + } + + @Override + public ByteBuffer getHeader() { + final ByteBuffer header = ByteBuffer.allocate(FileContentCryptorImpl.HEADER_SIZE); + header.put(iv); + header.put(nonce); + final ByteBuffer sensitiveHeaderData = getCiphertextSensitiveHeaderData(); + header.put(sensitiveHeaderData); + final ByteBuffer headerSoFar = header.asReadOnlyBuffer(); + headerSoFar.flip(); + header.put(mac(headerSoFar)); + header.flip(); + return header; + } + + @Override + public void append(ByteBuffer cleartext) { + if (cleartext == FileContentEncryptor.EOF) { + submitCleartextBuffer(); + submitEof(); + } else { + while (cleartext.hasRemaining()) { + ByteBuffers.copy(cleartext, cleartextBuffer); + submitCleartextBufferIfFull(); + } + } + } + + private void submitCleartextBufferIfFull() { + if (!cleartextBuffer.hasRemaining()) { + submitCleartextBuffer(); + cleartextBuffer = ByteBuffer.allocate(CHUNK_SIZE); + } + } + + private void submitCleartextBuffer() { + cleartextBuffer.flip(); + long myChunkNumber = chunkNumber++; + Future result = executorService.submit(new EncryptionJob(cleartextBuffer, myChunkNumber)); + ciphertextQueue.offer(new BytesWithSequenceNumber(result, myChunkNumber)); + } + + private void submitEof() { + Future resolvedFuture = Futures.immediateFuture(FileContentEncryptor.EOF); + ciphertextQueue.offer(new BytesWithSequenceNumber(resolvedFuture, Long.MAX_VALUE)); + } + + @Override + public ByteBuffer ciphertext() throws InterruptedException { + return ciphertextQueue.take().get(); + } + + @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."); + } + + @Override + public void destroy() { + try { + contentKey.destroy(); + } catch (DestroyFailedException e) { + LOG.warn("Could not destroy file-specific key", e); + } + } + + private class EncryptionJob implements Callable { + + private final ByteBuffer cleartextBlock; + private final byte[] nonceAndCtr; + + public EncryptionJob(ByteBuffer cleartext, long chunkNumber) { + this.cleartextBlock = cleartext; + + 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 { + 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(); + ciphertextSoFar.flip(); + mac.update(ciphertextSoFar); + byte[] authenticationCode = mac.doFinal(); + ciphertextBlock.put(authenticationCode); + ciphertextBlock.flip(); + return ciphertextBlock; + } 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/FilenameCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java index cd6695b3d..fdf573413 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FilenameCryptorImpl.java @@ -32,9 +32,6 @@ class FilenameCryptorImpl implements FilenameCryptor { private final SecretKey macKey; FilenameCryptorImpl(SecretKey encryptionKey, SecretKey macKey) { - if (encryptionKey == null || macKey == null) { - throw new IllegalArgumentException("Key must not be null"); - } this.encryptionKey = encryptionKey; this.macKey = macKey; } diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalAesCtrCipher.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalAesCtrCipher.java new file mode 100644 index 000000000..7c805955f --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalAesCtrCipher.java @@ -0,0 +1,28 @@ +package org.cryptomator.crypto.engine.impl; + +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; + +final class ThreadLocalAesCtrCipher { + + private ThreadLocalAesCtrCipher() { + } + + private static final String AES_CTR = "AES/CTR/NoPadding"; + private static final ThreadLocal THREAD_LOCAL_CIPHER = ThreadLocal.withInitial(ThreadLocalAesCtrCipher::newCipherInstance); + + private static Cipher newCipherInstance() { + try { + return Cipher.getInstance(AES_CTR); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException("Could not create MAC.", e); + } + } + + public static Cipher get() { + return THREAD_LOCAL_CIPHER.get(); + } + +} \ No newline at end of file diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java new file mode 100644 index 000000000..7402146b7 --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java @@ -0,0 +1,37 @@ +package org.cryptomator.crypto.engine.impl; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; + +class ThreadLocalMac extends ThreadLocal { + + private final SecretKey macKey; + private final String macAlgorithm; + + ThreadLocalMac(SecretKey macKey, String macAlgorithm) { + this.macKey = macKey; + this.macAlgorithm = macAlgorithm; + } + + @Override + protected Mac initialValue() { + try { + Mac mac = Mac.getInstance(macAlgorithm); + mac.init(macKey); + return mac; + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException("Could not create MAC.", e); + } + } + + @Override + public Mac get() { + Mac mac = super.get(); + mac.reset(); + return mac; + } + +} \ No newline at end of file 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 45e2b1d9e..a84c0b227 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 @@ -37,7 +37,6 @@ class CryptoReadableFile implements ReadableFile { if (readAheadTask != null) { readAheadTask.cancel(true); bufferedCleartext = EMPTY_BUFFER; - decryptor.cleartext().clear(); } readAheadTask = executorService.submit(new Reader(pos)); } @@ -56,7 +55,7 @@ class CryptoReadableFile implements ReadableFile { private void bufferCleartext() throws InterruptedException { if (!bufferedCleartext.hasRemaining()) { - bufferedCleartext = decryptor.cleartext().take(); + bufferedCleartext = decryptor.cleartext(); } } 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 591d29f1d..d4c806d47 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 @@ -95,7 +95,7 @@ class CryptoWritableFile implements WritableFile { public Void call() { try { ByteBuffer ciphertext; - while ((ciphertext = encryptor.ciphertext().take()) != FileContentEncryptor.EOF) { + while ((ciphertext = encryptor.ciphertext()) != FileContentEncryptor.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 2e28ec236..149b82333 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 @@ -54,8 +54,8 @@ class NoFileContentCryptor implements FileContentCryptor { } @Override - public BlockingQueue cleartext() { - return cleartextQueue; + public ByteBuffer cleartext() throws InterruptedException { + return cleartextQueue.take(); } @Override @@ -68,6 +68,11 @@ class NoFileContentCryptor implements FileContentCryptor { // no-op } + @Override + public void destroy() { + // no-op + } + } private class Encryptor implements FileContentEncryptor { @@ -98,8 +103,8 @@ class NoFileContentCryptor implements FileContentCryptor { } @Override - public BlockingQueue ciphertext() { - return ciphertextQueue; + public ByteBuffer ciphertext() throws InterruptedException { + return ciphertextQueue.take(); } @Override @@ -112,6 +117,11 @@ class NoFileContentCryptor implements FileContentCryptor { // no-op } + @Override + public void destroy() { + // no-op + } + } } diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java index f4127a45f..d7370fcdb 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/CryptorImplTest.java @@ -9,34 +9,19 @@ package org.cryptomator.crypto.engine.impl; import java.io.IOException; -import java.security.SecureRandom; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicReference; import org.cryptomator.crypto.engine.Cryptor; -import org.cryptomator.crypto.engine.FilenameCryptor; import org.junit.Assert; import org.junit.Test; public class CryptorImplTest { - 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 testMasterkeyDecryption() throws IOException { final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}"; - final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); + final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); Assert.assertFalse(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "qwe")); Assert.assertTrue(cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd")); } @@ -46,52 +31,25 @@ public class CryptorImplTest { final String expectedMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":16384,\"scryptBlockSize\":8," // + "\"primaryMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"," // + "\"hmacMasterKey\":\"BJPIq5pvhN24iDtPJLMFPLaVJWdGog9k4n0P03j4ru+ivbWY9OaRGQ==\"}"; - final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); + final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); cryptor.randomizeMasterkey(); final byte[] masterkeyFile = cryptor.writeKeysToMasterkeyFile("asd"); Assert.assertArrayEquals(expectedMasterKey.getBytes(), masterkeyFile); } @Test - public void testGetFilenameCryptorAfterUnlocking() { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}"; - final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); - cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd"); - Assert.assertNotNull(cryptor.getFilenameCryptor()); + public void testGetFilenameAndFileContentCryptor() throws InterruptedException { + final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); + cryptor.randomizeMasterkey(); + + Assert.assertSame(cryptor.getFilenameCryptor(), cryptor.getFilenameCryptor()); + Assert.assertSame(cryptor.getFileContentCryptor(), cryptor.getFileContentCryptor()); } - @Test(expected = RuntimeException.class) - public void testGetFilenameCryptorBeforeUnlocking() { - final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); + @Test(expected = IllegalStateException.class) + public void testGetFilenameAndFileContentCryptorWithoutKeys() throws InterruptedException { + final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); cryptor.getFilenameCryptor(); } - @Test - public void testConcurrentGetFilenameCryptor() throws InterruptedException { - final String testMasterKey = "{\"version\":3,\"scryptSalt\":\"AAAAAAAAAAA=\",\"scryptCostParam\":2,\"scryptBlockSize\":8," // - + "\"primaryMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"," // - + "\"hmacMasterKey\":\"mM+qoQ+o0qvPTiDAZYt+flaC3WbpNAx1sTXaUzxwpy0M9Ctj6Tih/Q==\"}"; - final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); - cryptor.readKeysFromMasterkeyFile(testMasterKey.getBytes(), "asd"); - - final AtomicReference receivedByT1 = new AtomicReference<>(); - final Thread t1 = new Thread(() -> { - receivedByT1.set(cryptor.getFilenameCryptor()); - }); - - final AtomicReference receivedByT2 = new AtomicReference<>(); - final Thread t2 = new Thread(() -> { - receivedByT2.set(cryptor.getFilenameCryptor()); - }); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - // It is not guaranteed, both threads will enter getFilenameCryptor() exactly simultaneously. (But logging shows it is very likely) - // In any case both threads should receive the same FilenameCryptor - Assert.assertSame(receivedByT1.get(), receivedByT2.get()); - } - } 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 new file mode 100644 index 000000000..dd720d43d --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImplTest.java @@ -0,0 +1,50 @@ +package org.cryptomator.crypto.engine.impl; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.bouncycastle.util.encoders.Base64; +import org.cryptomator.crypto.engine.FileContentEncryptor; +import org.cryptomator.io.ByteBuffers; +import org.junit.Assert; +import org.junit.Test; + +public class FileContentEncryptorImplTest { + + 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 testEncryption() throws InterruptedException { + 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); + + 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); + } + + // 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/engine/impl/TestCryptorImplFactory.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/TestCryptorImplFactory.java new file mode 100644 index 000000000..efba2a189 --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/TestCryptorImplFactory.java @@ -0,0 +1,26 @@ +package org.cryptomator.crypto.engine.impl; + +import java.security.SecureRandom; +import java.util.Arrays; + +public class TestCryptorImplFactory { + + 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); + } + + }; + + /** + * @return A CryptorImpl with a mocked PRNG, that can be used during tests without the need of "real" random numbers. + */ + public static CryptorImpl insecureCryptorImpl() { + return new CryptorImpl(RANDOM_MOCK); + } + +} 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 ee05654e3..37dd19fc8 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,10 +1,7 @@ package org.cryptomator.crypto.fs; -import java.security.SecureRandom; -import java.util.Arrays; - import org.cryptomator.crypto.engine.Cryptor; -import org.cryptomator.crypto.engine.impl.CryptorImpl; +import org.cryptomator.crypto.engine.impl.TestCryptorImplFactory; import org.cryptomator.filesystem.FileSystem; import org.cryptomator.filesystem.Folder; import org.cryptomator.filesystem.FolderCreateMode; @@ -20,22 +17,11 @@ public class EncryptAndShortenIntegrationTest { private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class); - 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 testEncryptionOfLongFolderNames() { final FileSystem physicalFs = new InMemoryFileSystem(); final FileSystem shorteningFs = new ShorteningFileSystem(physicalFs, physicalFs.folder("m"), 70); - final Cryptor cryptor = new CryptorImpl(RANDOM_MOCK); + final Cryptor cryptor = TestCryptorImplFactory.insecureCryptorImpl(); cryptor.randomizeMasterkey(); final FileSystem fs = new CryptoFileSystem(shorteningFs, cryptor, "foo"); fs.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING);