diff --git a/CHANGELOG.md b/CHANGELOG.md index 52721643..139a7d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ Changes to prior versions can be found on the [GitHub release page](https://gith ## [Unreleased](https://github.com/cryptomator/cryptofs/compare/2.10.0...HEAD) +### Added +* Property `vaultId` in `CryptoFileSystemProperties` to set the vault id during vault initialization ([#358](https://github.com/cryptomator/cryptofs/pull/358)) +* Getter `UnverifiedVaultConfig::allegedVaultId` to get the vault id of an unverified vault config ([#358](https://github.com/cryptomator/cryptofs/pull/358)) + ### Changed * Updated dependencies ([#357](https://github.com/cryptomator/cryptofs/pull/357)): * `com.auth0:java-jwt` from 4.5.1 to 4.6.1 diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java index 377829a4..1540315d 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java @@ -8,7 +8,6 @@ *******************************************************************************/ package org.cryptomator.cryptofs; -import com.google.common.base.Strings; import org.cryptomator.cryptofs.event.FilesystemEvent; import org.cryptomator.cryptolib.api.CryptorProvider; import org.cryptomator.cryptolib.api.MasterkeyLoader; @@ -19,7 +18,9 @@ import java.util.AbstractMap; import java.util.Collection; import java.util.EnumSet; +import java.util.HashSet; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Supplier; @@ -116,6 +117,13 @@ public enum FileSystemFlags { static final CryptorProvider.Scheme DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_GCM; + /** + * Key identifying the id of a vault. Only meaningful during vault initialization. If absent, a random id is generated. + * + * @since 2.11.0 + */ + public static final String PROPERTY_VAULT_ID = "vaultId"; + /** * Key identifying the filesystem owner supply method. * @@ -128,7 +136,7 @@ public enum FileSystemFlags { private final Set> entries; private CryptoFileSystemProperties(Builder builder) { - this.entries = Set.of( // + var entries = new HashSet>(Set.of( // Map.entry(PROPERTY_KEYLOADER, builder.keyLoader), // Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), // Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), // @@ -138,7 +146,11 @@ private CryptoFileSystemProperties(Builder builder) { Map.entry(PROPERTY_SHORTENING_THRESHOLD, builder.shorteningThreshold), // Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo), // Map.entry(PROPERTY_OWNER_GETTER, builder.ownerGetter) // - ); + )); + if (builder.vaultId != null) { + entries.add(Map.entry(PROPERTY_VAULT_ID, builder.vaultId)); + } + this.entries = Set.copyOf(entries); } MasterkeyLoader keyLoader() { @@ -149,6 +161,14 @@ public CryptorProvider.Scheme cipherCombo() { return (CryptorProvider.Scheme) get(PROPERTY_CIPHER_COMBO); } + /** + * @return the vault id to use during vault initialization, or empty if a random id should be generated + * @since 2.11.0 + */ + public Optional vaultId() { + return Optional.ofNullable((String) get(PROPERTY_VAULT_ID)); + } + @SuppressWarnings("unchecked") public Set flags() { return (Set) get(PROPERTY_FILESYSTEM_FLAGS); @@ -233,6 +253,7 @@ public static CryptoFileSystemProperties wrap(Map properties) { public static class Builder { public CryptorProvider.Scheme cipherCombo = DEFAULT_CIPHER_COMBO; + private String vaultId = null; private MasterkeyLoader keyLoader = null; private final Set flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS); private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME; @@ -253,6 +274,7 @@ private Builder(Map properties) { checkedSet(Integer.class, PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, properties, this::withMaxCleartextNameLength); checkedSet(Integer.class, PROPERTY_SHORTENING_THRESHOLD, properties, this::withShorteningThreshold); checkedSet(CryptorProvider.Scheme.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo); + checkedSet(String.class, PROPERTY_VAULT_ID, properties, this::withVaultId); checkedSet(Consumer.class, PROPERTY_EVENT_CONSUMER, properties, this::withFilesystemEventConsumer); checkedSet(Supplier.class, PROPERTY_OWNER_GETTER, properties, this::withOwnerGetter); } @@ -307,6 +329,21 @@ public Builder withCipherCombo(CryptorProvider.Scheme cipherCombo) { return this; } + /** + * Sets the vault id used during vault initialization. If not set, a random id is generated. + * + * @param vaultId The id of the vault + * @return this + * @since 2.11.0 + */ + public Builder withVaultId(String vaultId) { + if (vaultId == null || vaultId.isBlank()) { + throw new IllegalArgumentException("Parameter vaultId must not be null or blank"); + } + this.vaultId = vaultId; + return this; + } + /** * Sets the keyloader for a CryptoFileSystem. * @@ -417,7 +454,7 @@ private void validate() { if (keyLoader == null) { throw new IllegalStateException("keyLoader is required"); } - if (Strings.nullToEmpty(masterkeyFilename).trim().isEmpty()) { + if (masterkeyFilename == null || masterkeyFilename.isBlank()) { throw new IllegalStateException("masterkeyFilename is required"); } } diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java index 6d4abd45..2abbc66b 100644 --- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java +++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java @@ -142,7 +142,11 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope throw new NotDirectoryException(pathToVault.toString()); } byte[] rawKey = new byte[0]; - var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(properties.shorteningThreshold()).build(); + var configBuilder = VaultConfig.createNew() // + .cipherCombo(properties.cipherCombo()) // + .shorteningThreshold(properties.shorteningThreshold()); + properties.vaultId().ifPresent(configBuilder::vaultId); + var config = configBuilder.build(); try (Masterkey key = properties.keyLoader().loadKey(keyId); // Cryptor cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key, strongSecureRandom())) { rawKey = key.getEncoded(); diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java index 4a1392ac..d221dcef 100644 --- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java +++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java @@ -171,6 +171,13 @@ public int allegedShorteningThreshold() { return unverifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt(); } + /** + * @return The unverified vault id (signature not verified) + */ + public String allegedVaultId() { + return unverifiedConfig.getId(); + } + private Algorithm initAlgorithm(byte[] rawKey) throws VaultConfigLoadException { var algo = unverifiedConfig.getAlgorithm(); return switch (algo) { @@ -211,11 +218,16 @@ public VaultConfig verify(byte[] rawKey, int expectedVaultVersion) throws VaultK public static class VaultConfigBuilder { - private final String id = UUID.randomUUID().toString(); private final int vaultVersion = Constants.VAULT_VERSION; + private String id = UUID.randomUUID().toString(); private CryptorProvider.Scheme cipherCombo; private int shorteningThreshold; + public VaultConfigBuilder vaultId(String vaultId) { + this.id = vaultId; + return this; + } + public VaultConfigBuilder cipherCombo(CryptorProvider.Scheme cipherCombo) { this.cipherCombo = cipherCombo; return this; diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java index d360ac87..a37c2f36 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java @@ -15,10 +15,12 @@ import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Optional; import static org.cryptomator.cryptofs.CryptoFileSystemProperties.*; import static org.hamcrest.CoreMatchers.sameInstance; import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; public class CryptoFileSystemPropertiesTest { @@ -86,6 +88,46 @@ public void testFromMap() { ); } + @Test + public void testVaultIdAbsentByDefault() { + CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoader(keyLoader).build(); + + Assertions.assertTrue(inTest.vaultId().isEmpty()); + Assertions.assertFalse(inTest.containsKey(PROPERTY_VAULT_ID)); + } + + @Test + public void testSetVaultId() { + CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoader(keyLoader).withVaultId("my-vault-id").build(); + + Assertions.assertEquals(Optional.of("my-vault-id"), inTest.vaultId()); + MatcherAssert.assertThat(inTest.entrySet(), hasItem(anEntry(PROPERTY_VAULT_ID, "my-vault-id"))); + } + + @Test + public void testVaultIdFromMap() { + Map map = new HashMap<>(); + map.put(PROPERTY_KEYLOADER, keyLoader); + map.put(PROPERTY_VAULT_ID, "my-vault-id"); + CryptoFileSystemProperties inTest = cryptoFileSystemPropertiesFrom(map).build(); + + Assertions.assertEquals(Optional.of("my-vault-id"), inTest.vaultId()); + } + + @Test + public void testBlankVaultIdThrowsIAE() { + Assertions.assertThrows(IllegalArgumentException.class, () -> cryptoFileSystemProperties().withVaultId(" ")); + } + + @Test + public void testWrapMapWithInvalidVaultId() { + Map map = new HashMap<>(); + map.put(PROPERTY_KEYLOADER, keyLoader); + map.put(PROPERTY_VAULT_ID, 42); + + Assertions.assertThrows(IllegalArgumentException.class, () -> CryptoFileSystemProperties.wrap(map)); + } + @Test public void testWrapMapWithTrueReadonly() { Map map = new HashMap<>(); diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java index 348bf046..6bde1c47 100644 --- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java +++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java @@ -201,6 +201,20 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException Assertions.assertEquals(Constants.DIR_ID_BACKUP_FILE_NAME, dirIdBackup.get().getFileName().toString()); } + @Test + public void testInitializeWithVaultId() throws IOException, MasterkeyLoadingFailedException { + FileSystem fs = Jimfs.newFileSystem(Configuration.unix()); + Path pathToVault = fs.getPath("/vaultDir"); + Path vaultConfigFile = pathToVault.resolve("vault.cryptomator"); + var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).withVaultId("my-vault-id").build(); + + Files.createDirectory(pathToVault); + CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key")); + + var config = VaultConfig.decode(Files.readString(vaultConfigFile)); + Assertions.assertEquals("my-vault-id", config.allegedVaultId()); + } + @Test public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedException { Path pathToVault = Path.of("/vaultDir"); diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java index 0f6068d2..7379f49c 100644 --- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java +++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java @@ -71,7 +71,8 @@ public void testToToken() { @DisplayName("Using valid tokens...") public class WithValidToken { - private static final String TOKEN_NONE = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6Im5vbmUifQ.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0."; + private static final String VAULT_ID = "f4b29f3a-47d6-469f-964c-f4c2daee8eb6"; + private static final String TOKEN_NONE ="eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6Im5vbmUifQ.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0."; private static final String TOKEN_HS256 = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.V7pqSXX1tBRgmntL1sXovnhNR4Z1_7z3Jzrq7NMqPO8"; private static final String TOKEN_HS384 = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6IkhTMzg0In0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.rx03sCVAyrCmT6halPaFU46lu-DOd03iwDgvdw362hfgJj782q6xPXjAxdKeVKxG"; private static final String TOKEN_HS512 = "eyJraWQiOiJURVNUX0tFWSIsInR5cCI6IkpXVCIsImFsZyI6IkhTNTEyIn0.eyJmb3JtYXQiOjgsInNob3J0ZW5pbmdUaHJlc2hvbGQiOjIyMCwianRpIjoiZjRiMjlmM2EtNDdkNi00NjlmLTk2NGMtZjRjMmRhZWU4ZWI2IiwiY2lwaGVyQ29tYm8iOiJTSVZfQ1RSTUFDIn0.fzkVI34Ou3z7RaFarS9VPCaA0NX9z7My14gAISTXJGKGNSID7xEcoaY56SBdWbU7Ta17KhxcHhbXffxk3Mzing"; @@ -95,6 +96,15 @@ public void testSuccessfulLoad(String algo, String token) throws VaultConfigLoad Assertions.assertEquals(8, loaded.getVaultVersion()); Assertions.assertEquals(CryptorProvider.Scheme.SIV_CTRMAC, loaded.getCipherCombo()); Assertions.assertEquals(220, loaded.getShorteningThreshold()); + Assertions.assertEquals(VAULT_ID, loaded.getId()); + } + + @Test + @DisplayName("allegedVaultId() reads jti without verification") + public void testAllegedVaultId() throws VaultConfigLoadException { + var unverified = VaultConfig.decode(TOKEN_HS256); + + Assertions.assertEquals(VAULT_ID, unverified.allegedVaultId()); } @DisplayName("load using key with...") @@ -121,6 +131,14 @@ public void testCreateNew() { Assertions.assertEquals(220, config.getShorteningThreshold()); } + @Test + @DisplayName("test VaultConfig.createNew().vaultId(...)") + public void testCreateNewWithVaultId() { + var config = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).vaultId("my-vault-id").build(); + + Assertions.assertEquals("my-vault-id", config.getId()); + } + @Test @DisplayName("test VaultConfig.load(...)") public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoadingFailedException {