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