mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-18 10:41:26 +00:00
started implementation of FileContentEncryptorImpl
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user