File content encryption and decryption (still without padding, no partial support)

This commit is contained in:
Sebastian Stenzel
2015-12-20 00:38:14 +01:00
parent 4e0143eb05
commit 3045805751
16 changed files with 519 additions and 72 deletions

View File

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

View File

@@ -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.
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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