From 084e78a213d7fd5f193d8f545cd7a366988ba996 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 6 Dec 2024 17:25:39 +0100 Subject: [PATCH] added primitives for file name encryption --- pom.xml | 2 +- .../cryptomator/cryptolib/api/Cryptor.java | 29 +++- .../cryptolib/api/FileNameCryptor.java | 16 +- .../cryptolib/api/UVFMasterkey.java | 10 +- .../cryptomator/cryptolib/v1/CryptorImpl.java | 14 +- .../cryptolib/v1/FileNameCryptorImpl.java | 5 +- .../cryptomator/cryptolib/v2/CryptorImpl.java | 14 +- .../cryptolib/v2/FileNameCryptorImpl.java | 5 +- .../cryptomator/cryptolib/v3/CryptorImpl.java | 10 +- .../cryptolib/v3/FileNameCryptorImpl.java | 52 +++---- .../cryptolib/api/UVFMasterkeyTest.java | 9 ++ .../cryptolib/v1/CryptorImplTest.java | 11 +- .../cryptolib/v2/CryptorImplTest.java | 19 +-- .../cryptolib/v3/CryptorImplTest.java | 37 +++-- .../cryptolib/v3/FileNameCryptorImplTest.java | 143 ++++++++++++++++++ 15 files changed, 291 insertions(+), 85 deletions(-) create mode 100644 src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java diff --git a/pom.xml b/pom.xml index 41d9bde..ef49b41 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ 2.11.0 33.2.1-jre - 1.5.2 + 1.6.0 1.78.1 2.0.13 diff --git a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java index 5e79c2c..b040ce4 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/Cryptor.java @@ -1,23 +1,36 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ package org.cryptomator.cryptolib.api; import javax.security.auth.Destroyable; public interface Cryptor extends Destroyable, AutoCloseable { + /** + * Encryption and decryption of file content. + * @return utility for encrypting and decrypting file content + */ FileContentCryptor fileContentCryptor(); + /** + * Encryption and decryption of file headers. + * @return utility for encrypting and decrypting file headers + */ FileHeaderCryptor fileHeaderCryptor(); + /** + * Encryption and decryption of file names in Cryptomator Vault Format. + * @return utility for encrypting and decrypting file names + * @apiNote Only relevant for Cryptomator Vault Format, for Universal Vault Format see {@link #fileNameCryptor(int)} + */ FileNameCryptor fileNameCryptor(); + /** + * Encryption and decryption of file names in Universal Vault Format. + * @param revision The revision of the seed to {@link RevolvingMasterkey#subKey(int, int, byte[], String) derive subkeys}. + * @return utility for encrypting and decrypting file names + * @apiNote Only relevant for Universal Vault Format, for Cryptomator Vault Format see {@link #fileNameCryptor()} + */ + FileNameCryptor fileNameCryptor(int revision); + @Override void destroy(); diff --git a/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java b/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java index e20cd87..5187c69 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java +++ b/src/main/java/org/cryptomator/cryptolib/api/FileNameCryptor.java @@ -10,6 +10,8 @@ import com.google.common.io.BaseEncoding; +import java.nio.charset.StandardCharsets; + /** * Provides deterministic encryption capabilities as filenames must not change on subsequent encryption attempts, * otherwise each change results in major directory structure changes which would be a terrible idea for cloud storage encryption. @@ -18,11 +20,23 @@ */ public interface FileNameCryptor { + /** + * @param cleartextDirectoryIdStr a UTF-8-encoded arbitrary directory id to be passed to one-way hash function + * @return constant length string, that is unlikely to collide with any other name. + * @apiNote Only relevant for Cryptomator Vault Format, not for Universal Vault Format + * @deprecated Use {@link #hashDirectoryId(byte[])} instead + */ + @Deprecated + default String hashDirectoryId(String cleartextDirectoryIdStr) { + return hashDirectoryId(cleartextDirectoryIdStr.getBytes(StandardCharsets.UTF_8)); + } + /** * @param cleartextDirectoryId an arbitrary directory id to be passed to one-way hash function * @return constant length string, that is unlikely to collide with any other name. + * @apiNote Only relevant for Cryptomator Vault Format, not for Universal Vault Format */ - String hashDirectoryId(String cleartextDirectoryId); + String hashDirectoryId(byte[] cleartextDirectoryId); /** * @param encoding Encoding to use to encode the returned ciphertext diff --git a/src/main/java/org/cryptomator/cryptolib/api/UVFMasterkey.java b/src/main/java/org/cryptomator/cryptolib/api/UVFMasterkey.java index c58c7bb..140881c 100644 --- a/src/main/java/org/cryptomator/cryptolib/api/UVFMasterkey.java +++ b/src/main/java/org/cryptomator/cryptolib/api/UVFMasterkey.java @@ -9,9 +9,9 @@ import org.cryptomator.cryptolib.common.HKDFHelper; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; -import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; @@ -21,6 +21,8 @@ */ public class UVFMasterkey implements RevolvingMasterkey { + private static final byte[] ROOT_DIRID_KDF_CONTEXT = "rootDirId".getBytes(StandardCharsets.US_ASCII); + @VisibleForTesting final Map seeds; @VisibleForTesting final byte[] kdfSalt; @VisibleForTesting final int initialSeed; @@ -67,6 +69,10 @@ public int currentRevision() { return latestSeed; } + public byte[] rootDirId() { + return HKDFHelper.hkdfSha512(kdfSalt, seeds.get(initialSeed), ROOT_DIRID_KDF_CONTEXT, 32); + } + @Override public DestroyableSecretKey subKey(int revision, int length, byte[] context, String algorithm) { if (isDestroyed()) { @@ -79,7 +85,7 @@ public DestroyableSecretKey subKey(int revision, int length, byte[] context, Str try { return new DestroyableSecretKey(subkey, algorithm); } finally { - //Arrays.fill(subkey, (byte) 0x00); + Arrays.fill(subkey, (byte) 0x00); } } diff --git a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java index ca4480d..3406862 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/CryptorImpl.java @@ -1,14 +1,7 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ package org.cryptomator.cryptolib.v1; import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.PerpetualMasterkey; @@ -50,6 +43,11 @@ public FileNameCryptorImpl fileNameCryptor() { return fileNameCryptor; } + @Override + public FileNameCryptor fileNameCryptor(int revision) { + throw new UnsupportedOperationException(); + } + @Override public boolean isDestroyed() { return masterkey.isDestroyed(); diff --git a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java index 9f9cfa3..ddf6ba0 100644 --- a/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v1/FileNameCryptorImpl.java @@ -36,12 +36,11 @@ class FileNameCryptorImpl implements FileNameCryptor { } @Override - public String hashDirectoryId(String cleartextDirectoryId) { + public String hashDirectoryId(byte[] cleartextDirectoryId) { try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance(); ObjectPool.Lease siv = AES_SIV.get()) { - byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); - byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes); + byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextDirectoryId); byte[] hashedBytes = sha1.get().digest(encryptedBytes); return BASE32.encode(hashedBytes); } diff --git a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java index 402d595..02e063b 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/CryptorImpl.java @@ -1,14 +1,7 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ package org.cryptomator.cryptolib.v2; import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.PerpetualMasterkey; import org.cryptomator.cryptolib.v1.CryptorProviderImpl; @@ -51,6 +44,11 @@ public FileNameCryptorImpl fileNameCryptor() { return fileNameCryptor; } + @Override + public FileNameCryptor fileNameCryptor(int revision) { + throw new UnsupportedOperationException(); + } + @Override public boolean isDestroyed() { return masterkey.isDestroyed(); diff --git a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java index 286352a..c56d622 100644 --- a/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v2/FileNameCryptorImpl.java @@ -36,12 +36,11 @@ class FileNameCryptorImpl implements FileNameCryptor { } @Override - public String hashDirectoryId(String cleartextDirectoryId) { + public String hashDirectoryId(byte[] cleartextDirectoryId) { try (DestroyableSecretKey ek = masterkey.getEncKey(); DestroyableSecretKey mk = masterkey.getMacKey(); ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance(); ObjectPool.Lease siv = AES_SIV.get()) { - byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); - byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes); + byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextDirectoryId); byte[] hashedBytes = sha1.get().digest(encryptedBytes); return BASE32.encode(hashedBytes); } diff --git a/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java index 5c181c7..95effaf 100644 --- a/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v3/CryptorImpl.java @@ -9,6 +9,7 @@ package org.cryptomator.cryptolib.v3; import org.cryptomator.cryptolib.api.Cryptor; +import org.cryptomator.cryptolib.api.FileNameCryptor; import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.RevolvingMasterkey; import org.cryptomator.cryptolib.v1.CryptorProviderImpl; @@ -20,7 +21,6 @@ class CryptorImpl implements Cryptor { private final RevolvingMasterkey masterkey; private final FileContentCryptorImpl fileContentCryptor; private final FileHeaderCryptorImpl fileHeaderCryptor; - private final FileNameCryptorImpl fileNameCryptor; /** * Package-private constructor. @@ -30,7 +30,6 @@ class CryptorImpl implements Cryptor { this.masterkey = masterkey; this.fileHeaderCryptor = new FileHeaderCryptorImpl(masterkey, random); this.fileContentCryptor = new FileContentCryptorImpl(random); - this.fileNameCryptor = new FileNameCryptorImpl(masterkey); } @Override @@ -47,8 +46,13 @@ public FileHeaderCryptorImpl fileHeaderCryptor() { @Override public FileNameCryptorImpl fileNameCryptor() { + throw new UnsupportedOperationException(); + } + + @Override + public FileNameCryptor fileNameCryptor(int revision) { assertNotDestroyed(); - return fileNameCryptor; + return new FileNameCryptorImpl(masterkey, revision); } @Override diff --git a/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java b/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java index 72147c3..15df644 100644 --- a/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java +++ b/src/main/java/org/cryptomator/cryptolib/v3/FileNameCryptorImpl.java @@ -1,11 +1,3 @@ -/******************************************************************************* - * Copyright (c) 2015, 2016 Sebastian Stenzel and others. - * This file is licensed under the terms of the MIT license. - * See the LICENSE.txt file for more info. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ package org.cryptomator.cryptolib.v3; import com.google.common.io.BaseEncoding; @@ -14,14 +6,17 @@ import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.RevolvingMasterkey; import org.cryptomator.cryptolib.common.DestroyableSecretKey; +import org.cryptomator.cryptolib.common.MacSupplier; import org.cryptomator.cryptolib.common.MessageDigestSupplier; import org.cryptomator.cryptolib.common.ObjectPool; import org.cryptomator.siv.SivMode; import org.cryptomator.siv.UnauthenticCiphertextException; import javax.crypto.IllegalBlockSizeException; +import javax.crypto.Mac; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.util.Arrays; import static java.nio.charset.StandardCharsets.UTF_8; @@ -30,44 +25,43 @@ class FileNameCryptorImpl implements FileNameCryptor { private static final BaseEncoding BASE32 = BaseEncoding.base32(); private static final ObjectPool AES_SIV = new ObjectPool<>(SivMode::new); - private final RevolvingMasterkey masterkey; + private final DestroyableSecretKey sivKey; + private final DestroyableSecretKey hmacKey; - FileNameCryptorImpl(RevolvingMasterkey masterkey) { - this.masterkey = masterkey; - } - - private DestroyableSecretKey todo() { - return masterkey.subKey(0, 64, "TODO".getBytes(StandardCharsets.US_ASCII), "AES"); + /** + * Create a file name encryption/decryption tool for a certain masterkey revision. + * @param masterkey The masterkey from which to derive subkeys + * @param revision Which masterkey revision to use + * @throws IllegalArgumentException If no subkey could be derived for the given revision + */ + FileNameCryptorImpl(RevolvingMasterkey masterkey, int revision) throws IllegalArgumentException { + this.sivKey = masterkey.subKey(revision, 64, "siv".getBytes(StandardCharsets.US_ASCII), "AES"); + this.hmacKey = masterkey.subKey(revision, 32, "hmac".getBytes(StandardCharsets.US_ASCII), "HMAC"); } @Override - public String hashDirectoryId(String cleartextDirectoryId) { - try (DestroyableSecretKey ek = todo(); DestroyableSecretKey mk = todo(); //FIXME - ObjectPool.Lease sha1 = MessageDigestSupplier.SHA1.instance(); - ObjectPool.Lease siv = AES_SIV.get()) { - byte[] cleartextBytes = cleartextDirectoryId.getBytes(UTF_8); - byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes); - byte[] hashedBytes = sha1.get().digest(encryptedBytes); - return BASE32.encode(hashedBytes); + public String hashDirectoryId(byte[] cleartextDirectoryId) { + try (DestroyableSecretKey key = this.hmacKey.copy(); + ObjectPool.Lease hmacSha256 = MacSupplier.HMAC_SHA256.keyed(key)) { + byte[] hash = hmacSha256.get().doFinal(cleartextDirectoryId); + return BASE32.encode(hash, 0, 20); // only use first 160 bits } } @Override public String encryptFilename(BaseEncoding encoding, String cleartextName, byte[]... associatedData) { - try (DestroyableSecretKey ek = todo(); DestroyableSecretKey mk = todo(); //FIXME - ObjectPool.Lease siv = AES_SIV.get()) { + try (DestroyableSecretKey key = this.sivKey.copy(); ObjectPool.Lease siv = AES_SIV.get()) { byte[] cleartextBytes = cleartextName.getBytes(UTF_8); - byte[] encryptedBytes = siv.get().encrypt(ek, mk, cleartextBytes, associatedData); + byte[] encryptedBytes = siv.get().encrypt(key, cleartextBytes, associatedData); return encoding.encode(encryptedBytes); } } @Override public String decryptFilename(BaseEncoding encoding, String ciphertextName, byte[]... associatedData) throws AuthenticationFailedException { - try (DestroyableSecretKey ek = todo(); DestroyableSecretKey mk = todo(); //FIXME - ObjectPool.Lease siv = AES_SIV.get()) { + try (DestroyableSecretKey key = this.sivKey.copy(); ObjectPool.Lease siv = AES_SIV.get()) { byte[] encryptedBytes = encoding.decode(ciphertextName); - byte[] cleartextBytes = siv.get().decrypt(ek, mk, encryptedBytes, associatedData); + byte[] cleartextBytes = siv.get().decrypt(key, encryptedBytes, associatedData); return new String(cleartextBytes, UTF_8); } catch (IllegalArgumentException | UnauthenticCiphertextException | IllegalBlockSizeException e) { throw new AuthenticationFailedException("Invalid Ciphertext.", e); diff --git a/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java b/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java index ca0a4e9..40270a9 100644 --- a/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java +++ b/src/test/java/org/cryptomator/cryptolib/api/UVFMasterkeyTest.java @@ -48,4 +48,13 @@ public void testSubkey() { } } + @Test + public void testRootDirId() { + Map seeds = Collections.singletonMap(-1540072521, Base64.getDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU=")); + byte[] kdfSalt = Base64.getDecoder().decode("HE4OP+2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY="); + try (UVFMasterkey masterkey = new UVFMasterkey(seeds, kdfSalt, -1540072521, -1540072521)) { + Assertions.assertEquals("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc=", Base64.getEncoder().encodeToString(masterkey.rootDirId())); + } + } + } \ No newline at end of file diff --git a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java index a967398..9dd9058 100644 --- a/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v1/CryptorImplTest.java @@ -8,7 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptolib.v1; -import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.PerpetualMasterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.hamcrest.CoreMatchers; @@ -16,6 +15,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import java.security.SecureRandom; @@ -52,6 +53,14 @@ public void testGetFileNameCryptor() { } } + @ParameterizedTest + @ValueSource(ints = {-1, 0, 1, 42, 1337}) + public void testGetFileNameCryptorWithRevisions(int revision) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertThrows(UnsupportedOperationException.class, () -> cryptor.fileNameCryptor(revision)); + } + } + @Test public void testExplicitDestruction() { PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class); diff --git a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java index 56497dd..34b8a0b 100644 --- a/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v2/CryptorImplTest.java @@ -1,14 +1,5 @@ -/******************************************************************************* - * Copyright (c) 2016 Sebastian Stenzel and others. - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE.txt. - * - * Contributors: - * Sebastian Stenzel - initial API and implementation - *******************************************************************************/ package org.cryptomator.cryptolib.v2; -import org.cryptomator.cryptolib.api.Masterkey; import org.cryptomator.cryptolib.api.PerpetualMasterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; import org.hamcrest.CoreMatchers; @@ -16,6 +7,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; import java.security.SecureRandom; @@ -52,6 +45,14 @@ public void testGetFileNameCryptor() { } } + @ParameterizedTest + @ValueSource(ints = {-1, 0, 1, 42, 1337}) + public void testGetFileNameCryptorWithRevisions(int revision) { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertThrows(UnsupportedOperationException.class, () -> cryptor.fileNameCryptor(revision)); + } + } + @Test public void testExplicitDestruction() { PerpetualMasterkey masterkey = Mockito.mock(PerpetualMasterkey.class); diff --git a/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java index e738d9c..6e561f1 100644 --- a/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java +++ b/src/test/java/org/cryptomator/cryptolib/v3/CryptorImplTest.java @@ -2,9 +2,8 @@ import org.cryptomator.cryptolib.api.UVFMasterkey; import org.cryptomator.cryptolib.common.SecureRandomMock; -import org.hamcrest.CoreMatchers; -import org.hamcrest.MatcherAssert; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -18,26 +17,46 @@ public class CryptorImplTest { private static final SecureRandom RANDOM_MOCK = SecureRandomMock.NULL_RANDOM; private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU=")); private static final byte[] KDF_SALT = Base64.getDecoder().decode("HE4OP+2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY="); - private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521); + + private UVFMasterkey masterkey; + + @BeforeEach + public void setup() { + masterkey = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521); + } @Test public void testGetFileContentCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { - MatcherAssert.assertThat(cryptor.fileContentCryptor(), CoreMatchers.instanceOf(FileContentCryptorImpl.class)); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertInstanceOf(FileContentCryptorImpl.class, cryptor.fileContentCryptor()); } } @Test public void testGetFileHeaderCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { - MatcherAssert.assertThat(cryptor.fileHeaderCryptor(), CoreMatchers.instanceOf(FileHeaderCryptorImpl.class)); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertInstanceOf(FileHeaderCryptorImpl.class, cryptor.fileHeaderCryptor()); } } @Test public void testGetFileNameCryptor() { - try (CryptorImpl cryptor = new CryptorImpl(MASTERKEY, RANDOM_MOCK)) { - MatcherAssert.assertThat(cryptor.fileNameCryptor(), CoreMatchers.instanceOf(FileNameCryptorImpl.class)); + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertThrows(UnsupportedOperationException.class, cryptor::fileNameCryptor); + } + } + + @Test + public void testGetFileNameCryptorWithInvalidRevisions() { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertThrows(IllegalArgumentException.class, () -> cryptor.fileNameCryptor(0xBAD5EED)); + } + } + + @Test + public void testGetFileNameCryptorWithCorrectRevisions() { + try (CryptorImpl cryptor = new CryptorImpl(masterkey, RANDOM_MOCK)) { + Assertions.assertInstanceOf(FileNameCryptorImpl.class, cryptor.fileNameCryptor(-1540072521)); } } diff --git a/src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java b/src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java new file mode 100644 index 0000000..1283230 --- /dev/null +++ b/src/test/java/org/cryptomator/cryptolib/v3/FileNameCryptorImplTest.java @@ -0,0 +1,143 @@ +package org.cryptomator.cryptolib.v3; + +import com.google.common.io.BaseEncoding; +import org.cryptomator.cryptolib.api.AuthenticationFailedException; +import org.cryptomator.cryptolib.api.PerpetualMasterkey; +import org.cryptomator.cryptolib.api.UVFMasterkey; +import org.cryptomator.cryptolib.common.HKDFHelper; +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.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static java.nio.charset.StandardCharsets.UTF_8; + + +public class FileNameCryptorImplTest { + + private static final BaseEncoding BASE32 = BaseEncoding.base32(); + private static final Map SEEDS = Collections.singletonMap(-1540072521, Base64.getDecoder().decode("fP4V4oAjsUw5DqackAvLzA0oP1kAQZ0f5YFZQviXSuU=")); + private static final byte[] KDF_SALT = Base64.getDecoder().decode("HE4OP+2vyfLLURicF1XmdIIsWv0Zs6MobLKROUIEhQY="); + private static final UVFMasterkey MASTERKEY = new UVFMasterkey(SEEDS, KDF_SALT, -1540072521, -1540072521); + + private final FileNameCryptorImpl filenameCryptor = new FileNameCryptorImpl(MASTERKEY, -1540072521); + + 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(BASE32, origName); + String encrypted2 = filenameCryptor.encryptFilename(BASE32, origName); + String decrypted = filenameCryptor.decryptFilename(BASE32, 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 + String originalPath3 = "aaaabbbbccccdddd"; // 128 bit ascii + String encryptedPath3a = filenameCryptor.encryptFilename(BASE32, originalPath3); + String encryptedPath3b = filenameCryptor.encryptFilename(BASE32, originalPath3); + String decryptedPath3 = filenameCryptor.decryptFilename(BASE32, encryptedPath3a); + + Assertions.assertEquals(encryptedPath3a, encryptedPath3b); + Assertions.assertEquals(originalPath3, decryptedPath3); + } + + @DisplayName("hash root dir id") + @Test + public void testHashRootDirId() { + final byte[] rootDirId = Base64.getDecoder().decode("24UBEDeGu5taq7U4GqyA0MXUXb9HTYS6p3t9vvHGJAc="); + final String hashedRootDirId = filenameCryptor.hashDirectoryId(rootDirId); + Assertions.assertEquals("CRAX3I7DP4HQHA6TDQDMJQUTDKDJ7QG5", hashedRootDirId); + } + + @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.getBytes(UTF_8)); + final String hashedDirectory2 = filenameCryptor.hashDirectoryId(originalDirectoryId.getBytes(UTF_8)); + Assertions.assertEquals(hashedDirectory1, hashedDirectory2); + } + + @Test + @DisplayName("decrypt non-ciphertext") + public void testDecryptionOfMalformedFilename() { + AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { + filenameCryptor.decryptFilename(BASE32, "lol"); + }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(IllegalArgumentException.class)); + } + + @Test + @DisplayName("decrypt tampered ciphertext") + public void testDecryptionOfManipulatedFilename() { + final byte[] encrypted = filenameCryptor.encryptFilename(BASE32, "test").getBytes(UTF_8); + encrypted[0] ^= (byte) 0x01; // change 1 bit in first byte + String ciphertextName = new String(encrypted, UTF_8); + + AuthenticationFailedException e = Assertions.assertThrows(AuthenticationFailedException.class, () -> { + filenameCryptor.decryptFilename(BASE32, ciphertextName); + }); + MatcherAssert.assertThat(e.getCause(), CoreMatchers.instanceOf(UnauthenticCiphertextException.class)); + } + + @Test + @DisplayName("encrypt with different AD") + public void testEncryptionOfSameFilenamesWithDifferentAssociatedData() { + final String encrypted1 = filenameCryptor.encryptFilename(BASE32, "test", "ad1".getBytes(UTF_8)); + final String encrypted2 = filenameCryptor.encryptFilename(BASE32, "test", "ad2".getBytes(UTF_8)); + Assertions.assertNotEquals(encrypted1, encrypted2); + } + + @Test + @DisplayName("decrypt ciphertext with correct AD") + public void testDeterministicEncryptionOfFilenamesWithAssociatedData() throws AuthenticationFailedException { + final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "ad".getBytes(UTF_8)); + final String decrypted = filenameCryptor.decryptFilename(BASE32, encrypted, "ad".getBytes(UTF_8)); + Assertions.assertEquals("test", decrypted); + } + + @Test + @DisplayName("decrypt ciphertext with incorrect AD") + public void testDeterministicEncryptionOfFilenamesWithWrongAssociatedData() { + final String encrypted = filenameCryptor.encryptFilename(BASE32, "test", "right".getBytes(UTF_8)); + final byte[] ad = "wrong".getBytes(UTF_8); + + Assertions.assertThrows(AuthenticationFailedException.class, () -> { + filenameCryptor.decryptFilename(BASE32, encrypted, ad); + }); + } + +}