diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java index 1ba2ab5..fa8bd68 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java @@ -27,9 +27,9 @@ class CryptorImpl implements Cryptor { */ CryptorImpl(Masterkey masterkey, SecureRandom random) { this.masterkey = masterkey; - this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey.getEncKey(), random); + this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random); this.fileContentCryptor = new FileContentCryptorImpl(random); - this.fileNameCryptor = new FileNameCryptorImpl(masterkey.getEncKey(), masterkey.getMacKey()); + this.fileNameCryptor = new FileNameCryptorImpl(masterkey); } @Override diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java index 729a627..3c05f48 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImpl.java @@ -11,6 +11,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; import org.cryptomator.cryptolib.api.FileHeaderCryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; @@ -28,11 +29,11 @@ class FileHeaderCryptorImpl implements FileHeaderCryptor { - private final DestroyableSecretKey headerKey; + private final Masterkey masterkey; private final SecureRandom random; - FileHeaderCryptorImpl(DestroyableSecretKey headerKey, SecureRandom random) { - this.headerKey = headerKey; + FileHeaderCryptorImpl(Masterkey masterkey, SecureRandom random) { + this.masterkey = masterkey; this.random = random; } @@ -57,12 +58,12 @@ public ByteBuffer encryptHeader(FileHeader header) { payloadCleartextBuf.putLong(-1l); payloadCleartextBuf.put(headerImpl.getPayload().getContentKeyBytes()); payloadCleartextBuf.flip(); - try (DestroyableSecretKey hk = headerKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey()) { ByteBuffer result = ByteBuffer.allocate(FileHeaderImpl.SIZE); result.put(headerImpl.getNonce()); // encrypt payload: - Cipher cipher = CipherSupplier.AES_GCM.forEncryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce())); + Cipher cipher = CipherSupplier.AES_GCM.forEncryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, headerImpl.getNonce())); int encrypted = cipher.doFinal(payloadCleartextBuf, result); assert encrypted == FileHeaderImpl.PAYLOAD_LEN + FileHeaderImpl.TAG_LEN; @@ -91,9 +92,9 @@ public FileHeader decryptHeader(ByteBuffer ciphertextHeaderBuf) throws Authentic buf.get(ciphertextAndTag); ByteBuffer payloadCleartextBuf = ByteBuffer.allocate(FileHeaderImpl.Payload.SIZE); - try (DestroyableSecretKey hk = headerKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey()) { // decrypt payload: - Cipher cipher = CipherSupplier.AES_GCM.forDecryption(hk, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + Cipher cipher = CipherSupplier.AES_GCM.forDecryption(ek, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); int decrypted = cipher.doFinal(ByteBuffer.wrap(ciphertextAndTag), payloadCleartextBuf); assert decrypted == FileHeaderImpl.Payload.SIZE; diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java index 6410d5e..27756f6 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java @@ -11,6 +11,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileNameCryptor; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.siv.SivMode; @@ -30,17 +31,15 @@ protected SivMode initialValue() { }; }; - private final DestroyableSecretKey encryptionKey; - private final DestroyableSecretKey macKey; + private final Masterkey masterkey; - FileNameCryptorImpl(DestroyableSecretKey encryptionKey, DestroyableSecretKey macKey) { - this.encryptionKey = encryptionKey; - this.macKey = macKey; + FileNameCryptorImpl(Masterkey masterkey) { + this.masterkey = masterkey; } @Override public String hashDirectoryId(String cleartextDirectoryId) { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes); byte[] hashedBytes = MessageDigestSupplier.SHA1.get().digest(encryptedBytes); @@ -55,7 +54,7 @@ public String encryptFilename(String cleartextName, byte[]... associatedData) { @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] cleartextBytes = cleartextName.getBytes(UTF_8); byte[] encryptedBytes = AES_SIV.get().encrypt(ek, mk, cleartextBytes, associatedData); return encoding.encode(encryptedBytes); @@ -69,11 +68,11 @@ public String decryptFilename(String ciphertextName, byte[]... associatedData) t @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - try (DestroyableSecretKey ek = encryptionKey.clone(); DestroyableSecretKey mk = macKey.clone()) { + try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey()) { byte[] encryptedBytes = encoding.decode(ciphertextName); byte[] cleartextBytes = AES_SIV.get().decrypt(ek, mk, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); - } catch (UnauthenticCiphertextException | IllegalBlockSizeException e) { + } catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) { throw new AuthenticationFailedException("Invalid Ciphertext.", e); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java index 2a6d93f..566375e 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileContentCryptorImplTest.java @@ -14,6 +14,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.Cryptor; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.cryptomator.cryptolib.common.SeekableByteChannelMock; @@ -56,9 +57,9 @@ public class FileContentCryptorImplTest { @BeforeEach public void setup() { - DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); + Masterkey masterkey = new Masterkey(new byte[64]); header = new FileHeaderImpl(new byte[12], new byte[32]); - headerCryptor = new FileHeaderCryptorImpl(encKey, CSPRNG); + headerCryptor = new FileHeaderCryptorImpl(masterkey, CSPRNG); fileContentCryptor = new FileContentCryptorImpl(CSPRNG); cryptor = Mockito.mock(Cryptor.class); Mockito.when(cryptor.fileContentCryptor()).thenReturn(fileContentCryptor); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java index 84e2267..1d2cd4b 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorBenchmark.java @@ -10,6 +10,7 @@ import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.openjdk.jmh.annotations.Benchmark; @@ -38,8 +39,8 @@ public class FileHeaderCryptorBenchmark { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.PRNG_RANDOM; - private static final DestroyableSecretKey ENC_KEY = new DestroyableSecretKey(new byte[16], "AES"); - private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(ENC_KEY, RANDOM_MOCK); + private static final Masterkey MASTERKEY = new Masterkey(new byte[64]); + private static final FileHeaderCryptorImpl HEADER_CRYPTOR = new FileHeaderCryptorImpl(MASTERKEY, RANDOM_MOCK); private ByteBuffer validHeaderCiphertextBuf; private FileHeader header; diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java index 2af9cc3..394de95 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileHeaderCryptorImplTest.java @@ -11,6 +11,7 @@ import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; import org.cryptomator.cryptolib.api.FileHeader; +import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.common.CipherSupplier; import org.cryptomator.cryptolib.common.DestroyableSecretKey; import org.cryptomator.cryptolib.common.SecureRandomMock; @@ -35,14 +36,14 @@ public class FileHeaderCryptorImplTest { @BeforeEach public void setup() { - DestroyableSecretKey encKey = new DestroyableSecretKey(new byte[32], "AES"); - headerCryptor = new FileHeaderCryptorImpl(encKey, RANDOM_MOCK); + Masterkey masterkey = new Masterkey(new byte[64]); + headerCryptor = new FileHeaderCryptorImpl(masterkey, RANDOM_MOCK); // create new (unused) cipher, just to cipher.init() internally. This is an attempt to avoid // InvalidAlgorithmParameterExceptions due to IV-reuse, when the actual unit tests use constant IVs byte[] nonce = new byte[GCM_NONCE_SIZE]; ANTI_REUSE_PRNG.nextBytes(nonce); - Cipher cipher = CipherSupplier.AES_GCM.forEncryption(encKey, new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); + Cipher cipher = CipherSupplier.AES_GCM.forEncryption(masterkey.getEncKey(), new GCMParameterSpec(GCM_TAG_SIZE * Byte.SIZE, nonce)); Assertions.assertNotNull(cipher); } diff --git a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java index b94c858..b9a3a34 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/FileNameCryptorImplTest.java @@ -8,109 +8,124 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v2; +import com.google.common.io.BaseEncoding; import org.cryptomator.cryptolib.api.AuthenticationFailedException; -import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.siv.UnauthenticCiphertextException; +import org.hamcrest.CoreMatchers; +import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; -import java.io.IOException; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.UUID; +import java.util.stream.Stream; public class FileNameCryptorImplTest { private static final Charset UTF_8 = StandardCharsets.UTF_8; - @Test - public void testDeterministicEncryptionOfFilenames() throws AuthenticationFailedException { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - - // some random - for (int i = 0; i < 2000; i++) { - final String origName = UUID.randomUUID().toString(); - final String encrypted1 = filenameCryptor.encryptFilename(origName); - final String encrypted2 = filenameCryptor.encryptFilename(origName); - Assertions.assertEquals(encrypted1, encrypted2); - final String decrypted = filenameCryptor.decryptFilename(encrypted1); - Assertions.assertEquals(origName, decrypted); - } + private final Masterkey masterkey = new Masterkey(new byte[64]); + private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(masterkey); + + private static Stream filenameGenerator() { + return Stream.generate(UUID::randomUUID).map(UUID::toString).limit(100); + } + + @DisplayName("encrypt and decrypt file names") + @ParameterizedTest(name = "decrypt(encrypt({0}))") + @MethodSource("filenameGenerator") + public void testDeterministicEncryptionOfFilenames(String origName) throws AuthenticationFailedException { + String encrypted1 = filenameCryptor.encryptFilename(origName); + String encrypted2 = filenameCryptor.encryptFilename(origName); + String decrypted = filenameCryptor.decryptFilename(encrypted1); + + Assertions.assertEquals(encrypted1, encrypted2); + Assertions.assertEquals(origName, decrypted); + } + + @DisplayName("encrypt and decrypt file names with AD and custom encoding") + @ParameterizedTest(name = "decrypt(encrypt({0}))") + @MethodSource("filenameGenerator") + public void testDeterministicEncryptionOfFilenamesWithCustomEncodingAndAssociatedData(String origName) throws AuthenticationFailedException { + byte[] associdatedData = new byte[10]; + String encrypted1 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData); + String encrypted2 = filenameCryptor.encryptFilename(BaseEncoding.base64Url(), origName, associdatedData); + String decrypted = filenameCryptor.decryptFilename(BaseEncoding.base64Url(), encrypted1, associdatedData); + + Assertions.assertEquals(encrypted1, encrypted2); + Assertions.assertEquals(origName, decrypted); + } + @Test + @DisplayName("encrypt and decrypt 128 bit filename") + public void testDeterministicEncryptionOf128bitFilename() throws AuthenticationFailedException { // block size length file names - final String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii - final String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); - final String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3); + String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii + String encryptedPath3a = filenameCryptor.encryptFilename(originalPath3); + String encryptedPath3b = filenameCryptor.encryptFilename(originalPath3); + String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a); + Assertions.assertEquals(encryptedPath3a, encryptedPath3b); - final String decryptedPath3 = filenameCryptor.decryptFilename(encryptedPath3a); Assertions.assertEquals(originalPath3, decryptedPath3); } + @DisplayName("hash directory id for random directory ids") + @ParameterizedTest(name = "hashDirectoryId({0})") + @MethodSource("filenameGenerator") + public void testDeterministicHashingOfDirectoryIds(String originalDirectoryId) { + final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId); + final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId); + Assertions.assertEquals(hashedDirectory1, hashedDirectory2); + } + @Test - public void testDeterministicHashingOfDirectoryIds() throws IOException { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - - // some random - for (int i = 0; i < 2000; i++) { - final String originalDirectoryId = UUID.randomUUID().toString(); - final String hashedDirectory1 = filenameCryptor.hashDirectoryId(originalDirectoryId); - final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId); - Assertions.assertEquals(hashedDirectory1, hashedDirectory2); - } + @DisplayName("decrypt non-ciphertext") + public void testDecryptionOfMalformedFilename() { + AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { + filenameCryptor.decryptFilename("lol"); + }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class)); } @Test + @DisplayName("decrypt tampered ciphertext") public void testDecryptionOfManipulatedFilename() { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final byte[] encrypted = filenameCryptor.encryptFilename("test").getBytes(UTF_8); encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte - Assertions.assertThrows(AuthenticationFailedException.class, () -> { + + AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { filenameCryptor.decryptFilename(new String(encrypted, UTF_8)); }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class)); } @Test + @DisplayName("encrypt with different AD") public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final String encrypted1 = filenameCryptor.encryptFilename("test", "ad1".getBytes(UTF_8)); final String encrypted2 = filenameCryptor.encryptFilename("test", "ad2".getBytes(UTF_8)); Assertions.assertNotEquals(encrypted1, encrypted2); } @Test + @DisplayName("decrypt ciphertext with correct AD") public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final String encrypted = filenameCryptor.encryptFilename("test", "ad".getBytes(UTF_8)); final String decrypted = filenameCryptor.decryptFilename(encrypted, "ad".getBytes(UTF_8)); Assertions.assertEquals("test", decrypted); } @Test + @DisplayName("decrypt ciphertext with incorrect AD") public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() { - final byte[] keyBytes = new byte[32]; - final DestroyableSecretKey encryptionKey = new DestroyableSecretKey(keyBytes, "AES"); - final DestroyableSecretKey macKey = new DestroyableSecretKey(keyBytes, "AES"); - final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(encryptionKey, macKey); - final String encrypted = filenameCryptor.encryptFilename("test", "right".getBytes(UTF_8)); + Assertions.assertThrows(AuthenticationFailedException.class, () -> { filenameCryptor.decryptFilename(encrypted, "wrong".getBytes(UTF_8)); });