started implementation of FileContentEncryptorImpl

This commit is contained in:
Sebastian Stenzel
2015-12-19 18:22:03 +01:00
parent 951a02a9a5
commit 4e0143eb05
18 changed files with 500 additions and 123 deletions

View File

@@ -44,6 +44,12 @@
<version>${bouncycastle.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!-- Commons -->
<dependency>
<groupId>org.apache.commons</groupId>

View File

@@ -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);
}

View File

@@ -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<ByteBuffer> 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();
}

View File

@@ -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<ByteBuffer> 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();
}

View File

@@ -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<BytesWithSequenceNumber> {
private final Future<ByteBuffer> byteBuffer;
private final long sequenceNumber;
public BytesWithSequenceNumber(Future<ByteBuffer> 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);
}
}

View File

@@ -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> 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> T initializeLazily(AtomicReference<T> reference, Supplier<T> 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];

View File

@@ -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<ByteBuffer> header) {
throw new UnsupportedOperationException("Method not implemented");
return new FileContentEncryptorImpl(encryptionKey, macKey, randomSource);
}
}

View File

@@ -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<BytesWithSequenceNumber> ciphertextQueue = new PriorityBlockingQueue<>();
private final ExecutorService executorService = Executors.newFixedThreadPool(NUM_WORKERS);
private final ThreadLocal<Mac> 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<ByteBuffer> result = executorService.submit(new EncryptionJob(cleartextBuffer, myChunkNumber));
ciphertextQueue.offer(new BytesWithSequenceNumber(result, myChunkNumber));
}
private void submitEof() {
Future<ByteBuffer> 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<ByteBuffer> {
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);
}
}
}
}

View File

@@ -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;
}

View File

@@ -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<Cipher> 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();
}
}

View File

@@ -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<Mac> {
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;
}
}

View File

@@ -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();
}
}

View File

@@ -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) {

View File

@@ -54,8 +54,8 @@ class NoFileContentCryptor implements FileContentCryptor {
}
@Override
public BlockingQueue<ByteBuffer> 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<ByteBuffer> 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
}
}
}

View File

@@ -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<FilenameCryptor> receivedByT1 = new AtomicReference<>();
final Thread t1 = new Thread(() -> {
receivedByT1.set(cryptor.getFilenameCryptor());
});
final AtomicReference<FilenameCryptor> 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());
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);