diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java index ef3842075..e1179b495 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentCryptorImpl.java @@ -12,9 +12,6 @@ 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; @@ -27,7 +24,7 @@ class FileContentCryptorImpl implements FileContentCryptor { @Override public int getHeaderSize() { - return HEADER_SIZE; + return FileHeader.HEADER_SIZE; } @Override diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java index 4706a1691..14b4dac03 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImpl.java @@ -4,19 +4,13 @@ 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; @@ -25,9 +19,7 @@ import org.cryptomator.io.ByteBuffers; class FileContentDecryptorImpl 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; @@ -36,71 +28,19 @@ class FileContentDecryptorImpl implements FileContentDecryptor { private final FifoParallelDataProcessor dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS, NUM_THREADS + READ_AHEAD); private final ThreadLocal hmacSha256; - private final SecretKey contentKey; - private final byte[] nonce; - private final long cleartextLength; + private final FileHeader header; 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); - } + final ThreadLocalMac hmacSha256 = new ThreadLocalMac(macKey, HMAC_SHA256); + this.hmacSha256 = hmacSha256; + this.header = FileHeader.decrypt(headerKey, hmacSha256, header); } @Override public long contentLength() { - return cleartextLength; + return header.getPayload().getFilesize(); } @Override @@ -150,11 +90,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { @Override public void destroy() { - try { - contentKey.destroy(); - } catch (DestroyFailedException e) { - // ignore - } + header.destroy(); } private class DecryptionJob implements Callable { @@ -175,7 +111,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { macBuf.get(expectedMac); final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH_IN_BYTES); - nonceAndCounterBuf.put(nonce); + nonceAndCounterBuf.put(header.getNonce()); nonceAndCounterBuf.putLong(chunkNumber * CHUNK_SIZE / AES_BLOCK_LENGTH_IN_BYTES); this.nonceAndCtr = nonceAndCounterBuf.array(); } @@ -191,7 +127,7 @@ class FileContentDecryptorImpl implements FileContentDecryptor { } Cipher cipher = ThreadLocalAesCtrCipher.get(); - cipher.init(Cipher.DECRYPT_MODE, contentKey, new IvParameterSpec(nonceAndCtr)); + cipher.init(Cipher.DECRYPT_MODE, header.getPayload().getContentKey(), new IvParameterSpec(nonceAndCtr)); ByteBuffer cleartextChunk = ByteBuffer.allocate(cipher.getOutputSize(ciphertextChunk.remaining())); cipher.update(ciphertextChunk, cleartextChunk); cleartextChunk.flip(); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java index 4d4de84be..0ca24f397 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileContentEncryptorImpl.java @@ -3,21 +3,15 @@ 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.Callable; import java.util.concurrent.atomic.LongAdder; -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; @@ -26,20 +20,16 @@ import org.cryptomator.io.ByteBuffers; class FileContentEncryptorImpl implements FileContentEncryptor { - 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_THREADS = Runtime.getRuntime().availableProcessors(); private static final int READ_AHEAD = 2; private final FifoParallelDataProcessor dataProcessor = new FifoParallelDataProcessor<>(NUM_THREADS, NUM_THREADS + READ_AHEAD); - private final ThreadLocal hmacSha256; + private final ThreadLocalMac hmacSha256; + private final FileHeader header; 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; @@ -47,57 +37,13 @@ class FileContentEncryptorImpl implements FileContentEncryptor { 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.sum()); - 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); - ciphertext.flip(); - 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(); + this.header = new FileHeader(randomSource); } @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; + header.getPayload().setFilesize(cleartextBytesEncrypted.sum()); + return header.toByteBuffer(headerKey, hmacSha256); } @Override @@ -148,11 +94,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor { @Override public void destroy() { - try { - contentKey.destroy(); - } catch (DestroyFailedException e) { - // ignore - } + header.destroy(); } private class EncryptionJob implements Callable { @@ -164,7 +106,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor { this.cleartextChunk = cleartextChunk; final ByteBuffer nonceAndCounterBuf = ByteBuffer.allocate(AES_BLOCK_LENGTH_IN_BYTES); - nonceAndCounterBuf.put(nonce); + nonceAndCounterBuf.put(header.getNonce()); nonceAndCounterBuf.putLong(chunkNumber * CHUNK_SIZE / AES_BLOCK_LENGTH_IN_BYTES); this.nonceAndCtr = nonceAndCounterBuf.array(); } @@ -173,7 +115,7 @@ class FileContentEncryptorImpl implements FileContentEncryptor { public ByteBuffer call() { try { Cipher cipher = ThreadLocalAesCtrCipher.get(); - cipher.init(Cipher.ENCRYPT_MODE, contentKey, new IvParameterSpec(nonceAndCtr)); + cipher.init(Cipher.ENCRYPT_MODE, header.getPayload().getContentKey(), new IvParameterSpec(nonceAndCtr)); Mac mac = hmacSha256.get(); ByteBuffer ciphertextChunk = ByteBuffer.allocate(cipher.getOutputSize(cleartextChunk.remaining()) + mac.getMacLength()); cipher.update(cleartextChunk, ciphertextChunk); diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileHeader.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileHeader.java new file mode 100644 index 000000000..93fa0b08e --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileHeader.java @@ -0,0 +1,124 @@ +package org.cryptomator.crypto.engine.impl; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.SecureRandom; +import java.util.function.Supplier; + +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.security.auth.Destroyable; + +class FileHeader implements Destroyable { + + static final int HEADER_SIZE = 104; + + private static final int IV_POS = 0; + private static final int IV_LEN = 16; + private static final int NONCE_POS = 16; + private static final int NONCE_LEN = 8; + private static final int PAYLOAD_POS = 24; + private static final int PAYLOAD_LEN = 48; + private static final int MAC_POS = 72; + private static final int MAC_LEN = 32; + + private final byte[] iv; + private final byte[] nonce; + private final FileHeaderPayload payload; + + public FileHeader(SecureRandom randomSource) { + this.iv = new byte[IV_LEN]; + this.nonce = new byte[NONCE_LEN]; + this.payload = new FileHeaderPayload(randomSource); + randomSource.nextBytes(iv); + randomSource.nextBytes(nonce); + } + + private FileHeader(byte[] iv, byte[] nonce, FileHeaderPayload payload) { + this.iv = iv; + this.nonce = nonce; + this.payload = payload; + } + + public byte[] getIv() { + return iv; + } + + public byte[] getNonce() { + return nonce; + } + + public FileHeaderPayload getPayload() { + return payload; + } + + public ByteBuffer toByteBuffer(SecretKey headerKey, Supplier hmacSha256Factory) { + ByteBuffer result = ByteBuffer.allocate(HEADER_SIZE); + result.position(IV_POS).limit(IV_POS + IV_LEN); + result.put(iv); + result.position(NONCE_POS).limit(NONCE_POS + NONCE_LEN); + result.put(nonce); + result.position(PAYLOAD_POS).limit(PAYLOAD_POS + PAYLOAD_LEN); + result.put(payload.toCiphertextByteBuffer(headerKey, iv)); + ByteBuffer resultSoFar = result.asReadOnlyBuffer(); + resultSoFar.flip(); + Mac mac = hmacSha256Factory.get(); + assert mac.getMacLength() == MAC_LEN; + mac.update(resultSoFar); + result.position(MAC_POS).limit(MAC_POS + MAC_LEN); + result.put(mac.doFinal()); + result.flip(); + return result; + } + + @Override + public boolean isDestroyed() { + return payload.isDestroyed(); + } + + @Override + public void destroy() { + payload.destroy(); + } + + public static FileHeader decrypt(SecretKey headerKey, Supplier hmacSha256Factory, ByteBuffer header) throws IllegalArgumentException { + if (header.remaining() != HEADER_SIZE) { + throw new IllegalArgumentException("Invalid header size."); + } + + checkHeaderMac(header, hmacSha256Factory.get()); + + final byte[] iv = new byte[IV_LEN]; + final ByteBuffer ivBuf = header.asReadOnlyBuffer(); + ivBuf.position(IV_POS).limit(IV_POS + IV_LEN); + ivBuf.get(iv); + + final byte[] nonce = new byte[NONCE_LEN]; + final ByteBuffer nonceBuf = header.asReadOnlyBuffer(); + nonceBuf.position(NONCE_POS).limit(NONCE_POS + NONCE_LEN); + nonceBuf.get(nonce); + + final ByteBuffer payloadBuf = header.asReadOnlyBuffer(); + payloadBuf.position(PAYLOAD_POS).limit(PAYLOAD_POS + PAYLOAD_LEN); + + final FileHeaderPayload payload = FileHeaderPayload.fromCiphertextByteBuffer(payloadBuf, headerKey, iv); + + return new FileHeader(iv, nonce, payload); + } + + private static void checkHeaderMac(ByteBuffer header, Mac mac) throws IllegalArgumentException { + assert mac.getMacLength() == MAC_LEN; + ByteBuffer headerData = header.asReadOnlyBuffer(); + headerData.position(0).limit(MAC_POS); + mac.update(headerData); + ByteBuffer headerMac = header.asReadOnlyBuffer(); + headerMac.position(MAC_POS).limit(MAC_POS + MAC_LEN); + byte[] expectedMac = new byte[MAC_LEN]; + headerMac.get(expectedMac); + + if (!MessageDigest.isEqual(expectedMac, mac.doFinal())) { + throw new IllegalArgumentException("Corrupt header."); + } + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileHeaderPayload.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileHeaderPayload.java new file mode 100644 index 000000000..dd5320f79 --- /dev/null +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/FileHeaderPayload.java @@ -0,0 +1,142 @@ +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.Arrays; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +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 javax.security.auth.Destroyable; + +class FileHeaderPayload implements Destroyable { + + private static final int FILESIZE_POS = 0; + private static final int FILESIZE_LEN = Long.BYTES; + private static final int CONTENT_KEY_POS = 8; + private static final int CONTENT_KEY_LEN = 32; + private static final String AES = "AES"; + private static final String AES_CBC = "AES/CBC/PKCS5Padding"; + + private long filesize; + private final SecretKey contentKey; + + public FileHeaderPayload(SecureRandom randomSource) { + filesize = 0; + final byte[] contentKey = new byte[CONTENT_KEY_LEN]; + try { + randomSource.nextBytes(contentKey); + this.contentKey = new SecretKeySpec(contentKey, AES); + } finally { + Arrays.fill(contentKey, (byte) 0x00); + } + } + + private FileHeaderPayload(long filesize, SecretKey contentKey) { + this.filesize = filesize; + this.contentKey = contentKey; + } + + public long getFilesize() { + return filesize; + } + + public void setFilesize(long filesize) { + this.filesize = filesize; + } + + public SecretKey getContentKey() { + return contentKey; + } + + @Override + public boolean isDestroyed() { + return contentKey.isDestroyed(); + } + + @Override + public void destroy() { + try { + contentKey.destroy(); + } catch (DestroyFailedException e) { + // no-op + } + } + + private ByteBuffer toCleartextByteBuffer() { + ByteBuffer cleartext = ByteBuffer.allocate(FILESIZE_LEN + CONTENT_KEY_LEN); + cleartext.position(FILESIZE_POS).limit(FILESIZE_POS + FILESIZE_LEN); + cleartext.putLong(filesize); + cleartext.position(CONTENT_KEY_POS).limit(CONTENT_KEY_POS + CONTENT_KEY_LEN); + cleartext.put(contentKey.getEncoded()); + cleartext.flip(); + return cleartext; + } + + public ByteBuffer toCiphertextByteBuffer(SecretKey headerKey, byte[] iv) { + final ByteBuffer cleartext = toCleartextByteBuffer(); + 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); + ciphertext.flip(); + return ciphertext; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalStateException("Unable to compute encrypted header.", e); + } finally { + Arrays.fill(cleartext.array(), (byte) 0x00); + } + } + + public static FileHeaderPayload fromCiphertextByteBuffer(ByteBuffer ciphertextPayload, SecretKey headerKey, byte[] iv) { + final ByteBuffer cleartext = decryptPayload(ciphertextPayload, headerKey, iv); + try { + return fromCleartextByteBuffer(cleartext); + } finally { + // destroy evidence: + Arrays.fill(cleartext.array(), (byte) 0x00); + } + } + + private static FileHeaderPayload fromCleartextByteBuffer(ByteBuffer cleartext) { + final byte[] contentKey = new byte[CONTENT_KEY_LEN]; + try { + cleartext.position(FILESIZE_POS).limit(FILESIZE_POS + FILESIZE_LEN); + final long filesize = cleartext.getLong(); + cleartext.position(CONTENT_KEY_POS).limit(CONTENT_KEY_POS + CONTENT_KEY_LEN); + cleartext.get(contentKey); + return new FileHeaderPayload(filesize, new SecretKeySpec(contentKey, AES)); + } finally { + // destroy evidence: + Arrays.fill(contentKey, (byte) 0x00); + } + } + + private static ByteBuffer decryptPayload(ByteBuffer ciphertext, SecretKey headerKey, byte[] iv) { + try { + final Cipher cipher = Cipher.getInstance(AES_CBC); + cipher.init(Cipher.DECRYPT_MODE, headerKey, new IvParameterSpec(iv)); + final int cleartextLength = cipher.getOutputSize(ciphertext.remaining()); + assert cleartextLength == ciphertext.remaining() : "decryption shouldn't need more output than input buffer size."; + final ByteBuffer cleartext = ByteBuffer.allocate(cleartextLength); + cipher.doFinal(ciphertext, cleartext); + cleartext.flip(); + return cleartext; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | ShortBufferException | IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalStateException("Unable to decrypt header.", e); + } + } + +} diff --git a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java index 7402146b7..72e432fc7 100644 --- a/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java +++ b/main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/ThreadLocalMac.java @@ -2,11 +2,12 @@ package org.cryptomator.crypto.engine.impl; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.function.Supplier; import javax.crypto.Mac; import javax.crypto.SecretKey; -class ThreadLocalMac extends ThreadLocal { +class ThreadLocalMac extends ThreadLocalimplements Supplier { private final SecretKey macKey; private final String macAlgorithm; diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java index 1db011456..5a6ebf8ab 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentCryptorTest.java @@ -32,7 +32,7 @@ public class FileContentCryptorTest { 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"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK); ByteBuffer tooShortHeader = ByteBuffer.allocate(63); @@ -43,7 +43,7 @@ public class FileContentCryptorTest { 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"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK); ByteBuffer tooShortHeader = ByteBuffer.allocate(63); @@ -54,7 +54,7 @@ public class FileContentCryptorTest { 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"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); FileContentCryptor cryptor = new FileContentCryptorImpl(encryptionKey, macKey, RANDOM_MOCK); ByteBuffer header = ByteBuffer.allocate(cryptor.getHeaderSize()); diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java index 01e4a181c..100a8e0b6 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileContentDecryptorImplTest.java @@ -19,7 +19,7 @@ public class FileContentDecryptorImplTest { 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 SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); final byte[] header = Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwN74OFIGKQKgsI7bakfCYm1VXJZiKFLyhZkQCz0Ye/il0PmdZOYsSYEH9h6S00RsdHL3wLtB1FJsb9QLTtP00H8M2theZaZdlKTmjhXsmbc="); final byte[] content = Base64.decode("tPCsFM1g/ubfJMa+AocdPh/WPHfXMFRJdIz6PkLuRijSIIXvxn7IUwVzHQ=="); diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderPayloadTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderPayloadTest.java new file mode 100644 index 000000000..6059bd3fc --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderPayloadTest.java @@ -0,0 +1,50 @@ +package org.cryptomator.crypto.engine.impl; + +import java.nio.ByteBuffer; +import java.security.SecureRandom; +import java.util.Arrays; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import org.bouncycastle.util.encoders.Base64; +import org.junit.Assert; +import org.junit.Test; + +public class FileHeaderPayloadTest { + + 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() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final FileHeaderPayload header = new FileHeaderPayload(RANDOM_MOCK); + header.setFilesize(42); + final ByteBuffer encrypted = header.toCiphertextByteBuffer(headerKey, new byte[16]); + + // echo -n "AAAAAAAAACoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" | base64 --decode | openssl enc -aes-256-cbc -K 0000000000000000000000000000000000000000000000000000000000000000 -iv + // 00000000000000000000000000000000 | base64 + Assert.assertArrayEquals(Base64.decode("S+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5"), Arrays.copyOfRange(encrypted.array(), 0, encrypted.remaining())); + } + + @Test + public void testDecryption() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final ByteBuffer ciphertextBuf = ByteBuffer.wrap(Base64.decode("S+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5")); + final FileHeaderPayload header = FileHeaderPayload.fromCiphertextByteBuffer(ciphertextBuf, headerKey, new byte[16]); + Assert.assertEquals(42, header.getFilesize()); + Assert.assertArrayEquals(new byte[32], header.getContentKey().getEncoded()); + } + +} diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java new file mode 100644 index 000000000..b49507afb --- /dev/null +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/engine/impl/FileHeaderTest.java @@ -0,0 +1,78 @@ +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.junit.Assert; +import org.junit.Test; + +public class FileHeaderTest { + + 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() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); + final FileHeader header = new FileHeader(RANDOM_MOCK); + header.getPayload().setFilesize(42); + Assert.assertArrayEquals(new byte[16], header.getIv()); + Assert.assertArrayEquals(new byte[8], header.getNonce()); + Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded()); + final ByteBuffer headerAsByteBuffer = header.toByteBuffer(headerKey, new ThreadLocalMac(macKey, "HmacSHA256")); + + // 24 bytes 0x00 + // + 48 bytes encrypted payload (see FileHeaderPayloadTest) + // + 32 bytes HMAC of both (openssl dgst -sha256 -mac HMAC -macopt hexkey:0000000000000000000000000000000000000000000000000000000000000000 -binary) + final String expected = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iQ="; + Assert.assertArrayEquals(Base64.decode(expected), Arrays.copyOf(headerAsByteBuffer.array(), headerAsByteBuffer.remaining())); + } + + @Test + public void testDecryption() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); + final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iQ=")); + final FileHeader header = FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf); + + Assert.assertEquals(42, header.getPayload().getFilesize()); + Assert.assertArrayEquals(new byte[16], header.getIv()); + Assert.assertArrayEquals(new byte[8], header.getNonce()); + Assert.assertArrayEquals(new byte[32], header.getPayload().getContentKey().getEncoded()); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecryptionWithInvalidMac1() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); + final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iq=")); + FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf); + } + + @Test(expected = IllegalArgumentException.class) + public void testDecryptionWithInvalidMac2() { + final byte[] keyBytes = new byte[32]; + final SecretKey headerKey = new SecretKeySpec(keyBytes, "AES"); + final SecretKey macKey = new SecretKeySpec(keyBytes, "HmacSHA256"); + final ByteBuffer headerBuf = ByteBuffer.wrap(Base64.decode("aAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAS+uR3CoV6Mp/PWStVf2upywdYw2W84hMLWfINiTodqKaCopvSvdY6sqRYcnQF9J5ZVoITcmvp7VPXI4Tzdc87/cBHxjkBbY0QkRa0iow+iQ=")); + FileHeader.decrypt(headerKey, new ThreadLocalMac(macKey, "HmacSHA256"), headerBuf); + } + +} diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java index 65d06ae22..4606213e6 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/CryptoFileSystemTest.java @@ -26,13 +26,9 @@ import org.cryptomator.filesystem.WritableFile; import org.cryptomator.filesystem.inmem.InMemoryFileSystem; import org.junit.Assert; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class CryptoFileSystemTest { - private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemTest.class); - @Test(timeout = 1000) public void testVaultStructureInitialization() throws UncheckedIOException, IOException { // mock cryptor: @@ -56,8 +52,6 @@ public class CryptoFileSystemTest { Assert.assertEquals(3, physicalFs.children().count()); // d + masterkey.cryptomator + masterkey.cryptomator.bkup Assert.assertEquals(1, physicalDataRoot.files().count()); // ROOT file Assert.assertEquals(1, physicalDataRoot.folders().count()); // ROOT directory - - LOG.debug(DirectoryPrinter.print(physicalFs)); } @Test(timeout = 1000) @@ -104,8 +98,6 @@ public class CryptoFileSystemTest { Assert.assertTrue(fooFolder.exists()); Assert.assertTrue(fooBarFolder.exists()); Assert.assertEquals(3, countDataFolders(physicalDataRoot)); // parent + foo + bar - - LOG.debug(DirectoryPrinter.print(fs)); } @Test(timeout = 1000) diff --git a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java index 956ea22a3..21bbaa9c4 100644 --- a/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java +++ b/main/filesystem-crypto/src/test/java/org/cryptomator/crypto/fs/EncryptAndShortenIntegrationTest.java @@ -16,12 +16,10 @@ import org.cryptomator.filesystem.inmem.InMemoryFileSystem; import org.cryptomator.shortening.ShorteningFileSystem; import org.junit.Assert; import org.junit.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class EncryptAndShortenIntegrationTest { - private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class); + // private static final Logger LOG = LoggerFactory.getLogger(EncryptAndShortenIntegrationTest.class); @Test public void testEncryptionOfLongFolderNames() { @@ -37,17 +35,17 @@ public class EncryptAndShortenIntegrationTest { longFolder.create(FolderCreateMode.FAIL_IF_PARENT_IS_MISSING); // the long name will produce a metadata file on the physical layer: - LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs)); + // LOG.debug("Physical file system:\n" + DirectoryPrinter.print(physicalFs)); Assert.assertEquals(1, physicalFs.folder("m").folders().count()); // on the second layer all .lng files are resolved to their actual names: - LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs)); + // LOG.debug("Unlimited filename length:\n" + DirectoryPrinter.print(shorteningFs)); DirectoryWalker.walk(shorteningFs, node -> { Assert.assertFalse(node.name().endsWith(".lng")); }); // on the third (cleartext layer) we have cleartext names on the root level: - LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs)); + // LOG.debug("Cleartext files:\n" + DirectoryPrinter.print(fs)); Assert.assertArrayEquals(new String[] {"normal folder name", "this will be a long filename after encryption"}, fs.folders().map(Node::name).sorted().toArray()); }