From e719b8e5043ff1c972b3481ff1677db583289844 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 20 Nov 2020 15:12:41 +0100
Subject: [PATCH 01/70] Added vault version 8 migrator (references #95)

---
 pom.xml                                       | 20 +++--
 .../migration/v8/Version8Migrator.java        | 84 +++++++++++++++++++
 .../migration/v8/Version8MigratorTest.java    | 67 +++++++++++++++
 3 files changed, 162 insertions(+), 9 deletions(-)
 create mode 100644 src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
 create mode 100644 src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java

diff --git a/pom.xml b/pom.xml
index ab1a2afe..e89b0c79 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,7 @@
 
 		<!-- dependencies -->
 		<cryptolib.version>1.4.0</cryptolib.version>
+		<jwt.version>3.11.0</jwt.version>
 		<dagger.version>2.29.1</dagger.version>
 		<guava.version>30.0-jre</guava.version>
 		<slf4j.version>1.7.30</slf4j.version>
@@ -67,26 +68,27 @@
 			<artifactId>cryptolib</artifactId>
 			<version>${cryptolib.version}</version>
 		</dependency>
+		<dependency>
+			<groupId>com.auth0</groupId>
+			<artifactId>java-jwt</artifactId>
+			<version>${jwt.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.google.dagger</groupId>
+			<artifactId>dagger</artifactId>
+			<version>${dagger.version}</version>
+		</dependency>
 		<dependency>
 			<groupId>com.google.guava</groupId>
 			<artifactId>guava</artifactId>
 			<version>${guava.version}</version>
 		</dependency>
-
-		<!-- Logging -->
 		<dependency>
 			<groupId>org.slf4j</groupId>
 			<artifactId>slf4j-api</artifactId>
 			<version>${slf4j.version}</version>
 		</dependency>
 
-		<!-- DI -->
-		<dependency>
-			<groupId>com.google.dagger</groupId>
-			<artifactId>dagger</artifactId>
-			<version>${dagger.version}</version>
-		</dependency>
-
 		<!-- Test -->
 		<dependency>
 			<groupId>org.junit.jupiter</groupId>
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
new file mode 100644
index 00000000..83fdd9f4
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the accompanying LICENSE file.
+ *******************************************************************************/
+package org.cryptomator.cryptofs.migration.v8;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
+import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
+import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
+import org.cryptomator.cryptofs.migration.api.Migrator;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.KeyFile;
+import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+import java.util.Arrays;
+import java.util.UUID;
+
+public class Version8Migrator implements Migrator {
+
+	private static final String CONFIG_FILE_NAME = "vaultconfig.cryptomator";
+	private static final Logger LOG = LoggerFactory.getLogger(Version8Migrator.class);
+
+	private final CryptorProvider cryptorProvider;
+
+	@Inject
+	public Version8Migrator(CryptorProvider cryptorProvider) {
+		this.cryptorProvider = cryptorProvider;
+	}
+
+	@Override
+	public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+		LOG.info("Upgrading {} from version 7 to version 8.", vaultRoot);
+		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
+		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
+		Path vaultConfigFile = vaultRoot.resolve(CONFIG_FILE_NAME);
+		byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
+		byte[] rawKey = new byte[0];
+		KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
+		try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 7)) {
+			// create backup, as soon as we know the password was correct:
+			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
+			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
+
+			// create vaultconfig.cryptomator
+			rawKey = cryptor.getRawKey();
+			Algorithm algorithm = Algorithm.HMAC256(rawKey);
+			var config = JWT.create() //
+					.withJWTId(UUID.randomUUID().toString()) //
+					.withClaim("format", 8) //
+					.withClaim("keysource", "MASTERKEY_FILE") //
+					.withClaim("ciphermode", "SIV_CTRMAC") //
+					.withClaim("maxFileNameLen", 220) //
+					.sign(algorithm);
+			Files.writeString(vaultConfigFile, config, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
+			LOG.info("Wrote vault config to {}.", vaultConfigFile);
+
+			progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
+
+			// rewrite masterkey file with normalized passphrase:
+			byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(passphrase, 999).serialize();
+			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
+			LOG.info("Updated masterkey.");
+		} finally {
+			Arrays.fill(rawKey, (byte) 0x00);
+		}
+		LOG.info("Upgraded {} from version 7 to version 8.", vaultRoot);
+	}
+
+}
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
new file mode 100644
index 00000000..5dcac963
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -0,0 +1,67 @@
+package org.cryptomator.cryptofs.migration.v8;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.google.common.jimfs.Configuration;
+import com.google.common.jimfs.Jimfs;
+import org.cryptomator.cryptofs.migration.api.Migrator;
+import org.cryptomator.cryptofs.mocks.NullSecureRandom;
+import org.cryptomator.cryptolib.Cryptors;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.KeyFile;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.file.FileSystem;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+public class Version8MigratorTest {
+
+	private FileSystem fs;
+	private Path pathToVault;
+	private Path masterkeyFile;
+	private Path vaultConfigFile;
+	private CryptorProvider cryptorProvider;
+
+	@BeforeEach
+	public void setup() throws IOException {
+		cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE);
+		fs = Jimfs.newFileSystem(Configuration.unix());
+		pathToVault = fs.getPath("/vaultDir");
+		masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
+		vaultConfigFile = pathToVault.resolve("vaultconfig.cryptomator");
+		Files.createDirectory(pathToVault);
+	}
+
+	@AfterEach
+	public void teardown() throws IOException {
+		fs.close();
+	}
+
+	@Test
+	public void testMigrate() throws IOException {
+		KeyFile beforeMigration = cryptorProvider.createNew().writeKeysToMasterkeyFile("topsecret", 7);
+		Assumptions.assumeTrue(beforeMigration.getVersion() == 7);
+		Assumptions.assumeFalse(Files.exists(vaultConfigFile));
+		Files.write(masterkeyFile, beforeMigration.serialize());
+
+		Migrator migrator = new Version8Migrator(cryptorProvider);
+		migrator.migrate(pathToVault, masterkeyFile.getFileName().toString(), "topsecret");
+
+		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
+		Assertions.assertEquals(999, afterMigration.getVersion());
+		Assertions.assertTrue(Files.exists(vaultConfigFile));
+		DecodedJWT token = JWT.decode(Files.readString(vaultConfigFile));
+		Assertions.assertNotNull(token.getId());
+		Assertions.assertEquals(8, token.getClaim("format").asInt());
+		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("ciphermode").asString());
+		Assertions.assertEquals("MASTERKEY_FILE", token.getClaim("keysource").asString());
+		Assertions.assertEquals(220, token.getClaim("maxFileNameLen").asInt());
+	}
+
+}
\ No newline at end of file

From 0d6050000fcb6efa7c4c922effb5ed7437afbc2e Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 23 Nov 2020 10:08:05 +0100
Subject: [PATCH 02/70] use kid field to signify which key to use

---
 .../org/cryptomator/cryptofs/migration/v8/Version8Migrator.java | 2 +-
 .../cryptomator/cryptofs/migration/v8/Version8MigratorTest.java | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 83fdd9f4..9b224da9 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -61,8 +61,8 @@ public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passp
 			Algorithm algorithm = Algorithm.HMAC256(rawKey);
 			var config = JWT.create() //
 					.withJWTId(UUID.randomUUID().toString()) //
+					.withKeyId("MASTERKEY_FILE") //
 					.withClaim("format", 8) //
-					.withClaim("keysource", "MASTERKEY_FILE") //
 					.withClaim("ciphermode", "SIV_CTRMAC") //
 					.withClaim("maxFileNameLen", 220) //
 					.sign(algorithm);
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 5dcac963..0b786df3 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -58,9 +58,9 @@ public void testMigrate() throws IOException {
 		Assertions.assertTrue(Files.exists(vaultConfigFile));
 		DecodedJWT token = JWT.decode(Files.readString(vaultConfigFile));
 		Assertions.assertNotNull(token.getId());
+		Assertions.assertEquals("MASTERKEY_FILE", token.getKeyId());
 		Assertions.assertEquals(8, token.getClaim("format").asInt());
 		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("ciphermode").asString());
-		Assertions.assertEquals("MASTERKEY_FILE", token.getClaim("keysource").asString());
 		Assertions.assertEquals(220, token.getClaim("maxFileNameLen").asInt());
 	}
 

From 797fbc430bde59b28b1fe1c046f8574ee6dca778 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 24 Nov 2020 08:21:43 +0100
Subject: [PATCH 03/70] Updated migrator API

---
 .../cryptofs/CryptoFileSystemProperties.java  | 16 ++++++++++++
 .../cryptofs/CryptoFileSystemProvider.java    |  2 +-
 .../cryptofs/migration/Migrators.java         |  9 ++++---
 .../cryptofs/migration/api/Migrator.java      | 13 ++++++----
 .../migration/v6/Version6Migrator.java        |  7 +++++-
 .../migration/v7/Version7Migrator.java        | 17 ++++++++++++-
 .../migration/v8/Version8Migrator.java        | 13 +++++++---
 .../CryptoFileSystemPropertiesTest.java       | 20 ++++++---------
 .../cryptofs/migration/MigratorsTest.java     | 25 +++++++------------
 .../migration/v6/Version6MigratorTest.java    |  2 +-
 .../migration/v7/Version7MigratorTest.java    | 12 ++++-----
 .../migration/v8/Version8MigratorTest.java    |  4 +--
 12 files changed, 88 insertions(+), 52 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 7329773d..a9efb4fa 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -70,6 +70,16 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 
 	static final byte[] DEFAULT_PEPPER = new byte[0];
 
+
+	/**
+	 * Key identifying the name of the vault config file located inside the vault directory.
+	 *
+	 * @since 2.0.0
+	 */
+	public static final String PROPERTY_VAULTCONFIG_FILENAME = "vaultConfigFilename";
+
+	static final String DEFAULT_VAULTCONFIG_FILENAME = "vault.cryptomator";
+
 	/**
 	 * Key identifying the name of the masterkey file located inside the vault directory.
 	 * 
@@ -132,6 +142,7 @@ private CryptoFileSystemProperties(Builder builder) {
 				entry(PROPERTY_PASSPHRASE, builder.passphrase), //
 				entry(PROPERTY_PEPPER, builder.pepper), //
 				entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
+				entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
 				entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
 				entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), //
 				entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength) //
@@ -163,6 +174,10 @@ boolean initializeImplicitly() {
 		return flags().contains(FileSystemFlags.INIT_IMPLICITLY);
 	}
 
+	String vaultConfigFilename() {
+		return (String) get(PROPERTY_VAULTCONFIG_FILENAME);
+	}
+
 	String masterkeyFilename() {
 		return (String) get(PROPERTY_MASTERKEY_FILENAME);
 	}
@@ -257,6 +272,7 @@ public static class Builder {
 		private CharSequence passphrase;
 		public byte[] pepper = DEFAULT_PEPPER;
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
+		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
 		private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
 		private int maxPathLength = DEFAULT_MAX_PATH_LENGTH;
 		private int maxNameLength = DEFAULT_MAX_NAME_LENGTH;
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 9ae7236e..d25102c3 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -305,7 +305,7 @@ public CryptoFileSystem newFileSystem(URI uri, Map<String, ?> rawProperties) thr
 	private void migrateFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException {
 		if (Migrators.get().needsMigration(parsedUri.pathToVault(), properties.masterkeyFilename())) {
 			if (properties.migrateImplicitly()) {
-				Migrators.get().migrate(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase(), (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
+				Migrators.get().migrate(parsedUri.pathToVault(), properties.vaultConfigFilename(), properties.masterkeyFilename(), properties.passphrase(), (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
 			} else {
 				throw new FileSystemNeedsMigrationException(parsedUri.pathToVault());
 			}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
index a0456fb0..899da75d 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
@@ -34,7 +34,7 @@
  * <pre>
  * <code>
  * if (Migrators.get().{@link #needsMigration(Path, String) needsMigration(pathToVault, masterkeyFileName)}) {
- * 	Migrators.get().{@link #migrate(Path, String, CharSequence, MigrationProgressListener, MigrationContinuationListener) migrate(pathToVault, masterkeyFileName, passphrase, progressListener, continuationListener)};
+ * 	Migrators.get().{@link #migrate(Path, String, String, CharSequence, MigrationProgressListener, MigrationContinuationListener) migrate(pathToVault, masterkeyFileName, passphrase, progressListener, continuationListener)};
  * }
  * </code>
  * </pre>
@@ -91,7 +91,8 @@ public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws
 	 * Performs the actual migration. This task may take a while and this method will block.
 	 * 
 	 * @param pathToVault Path to the vault's root
-	 * @param masterkeyFilename Name of the masterkey file located in the vault
+	 * @param vaultConfigFilename Name of the vault config file located inside <code>pathToVault</code>
+	 * @param masterkeyFilename Name of the masterkey file located inside <code>pathToVault</code>
 	 * @param passphrase The passphrase needed to unlock the vault
 	 * @param progressListener Listener that will get notified of progress updates
 	 * @param continuationListener Listener that will get asked if there are events that require feedback
@@ -100,7 +101,7 @@ public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws
 	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
 	 * @throws IOException if an I/O error occurs migrating the vault
 	 */
-	public void migrate(Path pathToVault, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
+	public void migrate(Path pathToVault, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
 		fsCapabilityChecker.assertAllCapabilities(pathToVault);
 		
 		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
@@ -109,7 +110,7 @@ public void migrate(Path pathToVault, String masterkeyFilename, CharSequence pas
 
 		try {
 			Migrator migrator = findApplicableMigrator(keyFile.getVersion()).orElseThrow(NoApplicableMigratorException::new);
-			migrator.migrate(pathToVault, masterkeyFilename, passphrase, progressListener, continuationListener);
+			migrator.migrate(pathToVault, vaultConfigFilename, masterkeyFilename, passphrase, progressListener, continuationListener);
 		} catch (UnsupportedVaultFormatException e) {
 			// might be a tampered masterkey file, as this exception is also thrown if the vault version MAC is not authentic.
 			throw new IllegalStateException("Vault version checked beforehand but not supported by migrator.");
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
index 6973e8ac..d1bc9ba8 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
@@ -20,20 +20,22 @@ public interface Migrator {
 	 * Performs the migration this migrator is built for.
 	 * 
 	 * @param vaultRoot
+	 * @param vaultConfigFilename
 	 * @param masterkeyFilename
 	 * @param passphrase
 	 * @throws InvalidPassphraseException
 	 * @throws UnsupportedVaultFormatException
 	 * @throws IOException
 	 */
-	default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
-		migrate(vaultRoot, masterkeyFilename, passphrase, (state, progress) -> {});
+	default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+		migrate(vaultRoot, vaultConfigFilename, masterkeyFilename, passphrase, (state, progress) -> {});
 	}
 
 	/**
 	 * Performs the migration this migrator is built for.
 	 *
 	 * @param vaultRoot
+	 * @param vaultConfigFilename
 	 * @param masterkeyFilename
 	 * @param passphrase
 	 * @param progressListener 
@@ -41,14 +43,15 @@ default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence pass
 	 * @throws UnsupportedVaultFormatException
 	 * @throws IOException
 	 */
-	default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
-		migrate(vaultRoot, masterkeyFilename, passphrase, progressListener, (event) -> MigrationContinuationListener.ContinuationResult.CANCEL);
+	default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+		migrate(vaultRoot, vaultConfigFilename, masterkeyFilename, passphrase, progressListener, (event) -> MigrationContinuationListener.ContinuationResult.CANCEL);
 	}
 
 	/**
 	 * Performs the migration this migrator is built for.
 	 *
 	 * @param vaultRoot
+	 * @param vaultConfigFilename
 	 * @param masterkeyFilename
 	 * @param passphrase
 	 * @param progressListener
@@ -57,6 +60,6 @@ default void migrate(Path vaultRoot, String masterkeyFilename, CharSequence pass
 	 * @throws UnsupportedVaultFormatException
 	 * @throws IOException
 	 */
-	void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException;
+	void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException;
 
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
index 8c39a1c0..9d12fb4b 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
@@ -26,6 +26,11 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+/**
+ * Updates masterkey.cryptomator:
+ *
+ * Version 6 encodes the passphrase in Unicode NFC.
+ */
 public class Version6Migrator implements Migrator {
 
 	private static final Logger LOG = LoggerFactory.getLogger(Version6Migrator.class);
@@ -38,7 +43,7 @@ public Version6Migrator(CryptorProvider cryptorProvider) {
 	}
 
 	@Override
-	public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
 		LOG.info("Upgrading {} from version 5 to version 6.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index b4f5bc5b..61b6f0b9 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -30,6 +30,21 @@
 import java.nio.file.StandardOpenOption;
 import java.util.EnumSet;
 
+/**
+ * Renames ciphertext names:
+ *
+ * <ul>
+ *     <li>Files: BASE32== -> base64==.c9r</li>
+ *     <li>Dirs: 0BASE32== -> base64==.c9r/dir.c9r</li>
+ *     <li>Symlinks: 1SBASE32== -> base64.c9r/symlink.c9r</li>
+ * </ul>
+ *
+ * Shortened names:
+ * <ul>
+ *     <li>shortened.lng -> shortened.c9s</li>
+ *     <li>m/shortened.lng -> shortened.c9s/contents.c9r</li>
+ * </ul>
+ */
 public class Version7Migrator implements Migrator {
 
 	private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class);
@@ -42,7 +57,7 @@ public Version7Migrator(CryptorProvider cryptorProvider) {
 	}
 
 	@Override
-	public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
 		LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 9b224da9..5695e161 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -30,9 +30,16 @@
 import java.util.Arrays;
 import java.util.UUID;
 
+/**
+ * Splits up <code>masterkey.cryptomator</code>:
+ *
+ * <ul>
+ *     <li><code>vault.cryptomator</code> contains vault version and vault-specific metadata</li>
+ *     <li><code>masterkey.cryptomator</code> contains KDF params and may become obsolete when other key sources are supported</li>
+ * </ul>
+ */
 public class Version8Migrator implements Migrator {
 
-	private static final String CONFIG_FILE_NAME = "vaultconfig.cryptomator";
 	private static final Logger LOG = LoggerFactory.getLogger(Version8Migrator.class);
 
 	private final CryptorProvider cryptorProvider;
@@ -43,11 +50,11 @@ public Version8Migrator(CryptorProvider cryptorProvider) {
 	}
 
 	@Override
-	public void migrate(Path vaultRoot, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
 		LOG.info("Upgrading {} from version 7 to version 8.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
-		Path vaultConfigFile = vaultRoot.resolve(CONFIG_FILE_NAME);
+		Path vaultConfigFile = vaultRoot.resolve(vaultConfigFilename);
 		byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
 		byte[] rawKey = new byte[0];
 		KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index c556af23..38d0eafc 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -14,18 +14,7 @@
 import java.util.Map;
 import java.util.Map.Entry;
 
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MASTERKEY_FILENAME;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MAX_NAME_LENGTH;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_MAX_PATH_LENGTH;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.DEFAULT_PEPPER;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_FILESYSTEM_FLAGS;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MASTERKEY_FILENAME;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MAX_NAME_LENGTH;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_MAX_PATH_LENGTH;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_PASSPHRASE;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.PROPERTY_PEPPER;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemPropertiesFrom;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.*;
 import static org.hamcrest.CoreMatchers.sameInstance;
 import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.is;
@@ -56,6 +45,7 @@ public void testSetOnlyPassphrase() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
@@ -80,6 +70,7 @@ public void testSetPassphraseAndReadonlyFlag() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
@@ -106,6 +97,7 @@ public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
@@ -137,6 +129,7 @@ public void testFromMap() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, 255), //
@@ -164,6 +157,7 @@ public void testWrapMapWithTrueReadonly() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
@@ -191,6 +185,7 @@ public void testWrapMapWithFalseReadonly() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
@@ -252,6 +247,7 @@ public void testWrapMapWithoutReadonly() {
 				containsInAnyOrder( //
 						anEntry(PROPERTY_PASSPHRASE, passphrase), //
 						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
index e4391c22..32130047 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -27,6 +27,7 @@
 import java.nio.file.spi.FileSystemProvider;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Map;
 
 public class MigratorsTest {
 
@@ -79,10 +80,10 @@ public void testNeedsNoMigration() throws IOException {
 	}
 
 	@Test
-	public void testMigrateWithoutMigrators() throws IOException {
+	public void testMigrateWithoutMigrators() {
 		Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
 		Assertions.assertThrows(NoApplicableMigratorException.class, () -> {
-			migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
+			migrators.migrate(pathToVault, "vault.cryptomator","masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
 		});
 	}
 	
@@ -92,27 +93,19 @@ public void testMigrate() throws NoApplicableMigratorException, InvalidPassphras
 		MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class);
 		MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class);
 		Migrator migrator = Mockito.mock(Migrator.class);
-		Migrators migrators = new Migrators(new HashMap<Migration, Migrator>() {
-			{
-				put(Migration.ZERO_TO_ONE, migrator);
-			}
-		}, fsCapabilityChecker);
-		migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", progressListener, continuationListener);
-		Mockito.verify(migrator).migrate(pathToVault, "masterkey.cryptomator", "secret", progressListener, continuationListener);
+		Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
+		migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
+		Mockito.verify(migrator).migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
 	}
 
 	@Test
 	@SuppressWarnings("deprecation")
 	public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
 		Migrator migrator = Mockito.mock(Migrator.class);
-		Migrators migrators = new Migrators(new HashMap<Migration, Migrator>() {
-			{
-				put(Migration.ZERO_TO_ONE, migrator);
-			}
-		}, fsCapabilityChecker);
-		Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any());
+		Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
+		Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any());
 		Assertions.assertThrows(IllegalStateException.class, () -> {
-			migrators.migrate(pathToVault, "masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
+			migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
 		});
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
index 1d3620a9..5bc3105e 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
@@ -56,7 +56,7 @@ public void testMigrate() throws IOException {
 		Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupHelper.generateFileIdSuffix(beforeMigration.serialize()) + Constants.MASTERKEY_BACKUP_SUFFIX);
 
 		Migrator migrator = new Version6Migrator(cryptorProvider);
-		migrator.migrate(pathToVault, "masterkey.cryptomator", oldPassword);
+		migrator.migrate(pathToVault, null, "masterkey.cryptomator", oldPassword);
 
 		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
 		Assertions.assertEquals(6, afterMigration.getVersion());
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
index f7fbeaae..2ebcfa25 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
@@ -55,7 +55,7 @@ public void testKeyfileGetsUpdates() throws IOException {
 		Assertions.assertEquals(6, beforeMigration.getVersion());
 
 		Migrator migrator = new Version7Migrator(cryptorProvider);
-		migrator.migrate(vaultRoot, "masterkey.cryptomator", "test");
+		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
 		Assertions.assertEquals(7, afterMigration.getVersion());
@@ -64,7 +64,7 @@ public void testKeyfileGetsUpdates() throws IOException {
 	@Test
 	public void testMDirectoryGetsDeleted() throws IOException {
 		Migrator migrator = new Version7Migrator(cryptorProvider);
-		migrator.migrate(vaultRoot, "masterkey.cryptomator", "test");
+		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 		
 		Assertions.assertFalse(Files.exists(metaDir));
 	}
@@ -79,7 +79,7 @@ public void testMigrationFailsIfEncounteringUnsyncediCloudContent() throws IOExc
 		Migrator migrator = new Version7Migrator(cryptorProvider);
 		
 		IOException e = Assertions.assertThrows(PreMigrationVisitor.PreMigrationChecksFailedException.class, () -> {
-			migrator.migrate(vaultRoot, "masterkey.cryptomator", "test");
+			migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 		});
 		Assertions.assertTrue(e.getMessage().contains("MZUWYZLOMFWWK===.icloud"));
 	}
@@ -93,7 +93,7 @@ public void testMigrationOfNormalFile() throws IOException {
 		Files.createFile(fileBeforeMigration);
 
 		Migrator migrator = new Version7Migrator(cryptorProvider);
-		migrator.migrate(vaultRoot, "masterkey.cryptomator", "test");
+		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		Assertions.assertFalse(Files.exists(fileBeforeMigration));
 		Assertions.assertTrue(Files.exists(fileAfterMigration));
@@ -108,7 +108,7 @@ public void testMigrationOfNormalDirectory() throws IOException {
 		Files.createFile(fileBeforeMigration);
 
 		Migrator migrator = new Version7Migrator(cryptorProvider);
-		migrator.migrate(vaultRoot, "masterkey.cryptomator", "test");
+		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		Assertions.assertFalse(Files.exists(fileBeforeMigration));
 		Assertions.assertTrue(Files.exists(fileAfterMigration));
@@ -123,7 +123,7 @@ public void testMigrationOfNormalSymlink() throws IOException {
 		Files.createFile(fileBeforeMigration);
 
 		Migrator migrator = new Version7Migrator(cryptorProvider);
-		migrator.migrate(vaultRoot, "masterkey.cryptomator", "test");
+		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		Assertions.assertFalse(Files.exists(fileBeforeMigration));
 		Assertions.assertTrue(Files.exists(fileAfterMigration));
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 0b786df3..3b0acbac 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -34,7 +34,7 @@ public void setup() throws IOException {
 		fs = Jimfs.newFileSystem(Configuration.unix());
 		pathToVault = fs.getPath("/vaultDir");
 		masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
-		vaultConfigFile = pathToVault.resolve("vaultconfig.cryptomator");
+		vaultConfigFile = pathToVault.resolve("vault.cryptomator");
 		Files.createDirectory(pathToVault);
 	}
 
@@ -51,7 +51,7 @@ public void testMigrate() throws IOException {
 		Files.write(masterkeyFile, beforeMigration.serialize());
 
 		Migrator migrator = new Version8Migrator(cryptorProvider);
-		migrator.migrate(pathToVault, masterkeyFile.getFileName().toString(), "topsecret");
+		migrator.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "topsecret");
 
 		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
 		Assertions.assertEquals(999, afterMigration.getVersion());

From a968253fb35fb31f24d86549f0e81bee9c9db697 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Wed, 25 Nov 2020 13:39:50 +0100
Subject: [PATCH 04/70] Allow mocking of final fields, therefore removing
 workarounds using reflection

---
 .../cryptofs/CryptoFileSystemImplTest.java    |  5 -
 .../cryptofs/DirectoryIdLoaderTest.java       | 36 ++-----
 .../ch/AsyncDelegatingFileChannelTest.java    | 95 +++++++------------
 .../cryptofs/fh/OpenCryptoFilesTest.java      |  5 -
 .../org.mockito.plugins.MockMaker             |  1 +
 5 files changed, 40 insertions(+), 102 deletions(-)
 create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker

diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 670694d3..6cc3a56e 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -26,10 +26,8 @@
 import org.mockito.Mockito;
 
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
-import java.nio.channels.spi.AbstractInterruptibleChannel;
 import java.nio.file.AccessDeniedException;
 import java.nio.file.AccessMode;
 import java.nio.file.AtomicMoveNotSupportedException;
@@ -699,9 +697,6 @@ public void setup() throws IOException, ReflectiveOperationException {
 				when(cryptoPathMapper.getCiphertextDir(cleartextTargetParent)).thenReturn(new CiphertextDirectory("41", ciphertextTargetParent));
 				when(cryptoPathMapper.getCiphertextDir(cleartextDestination)).thenReturn(new CiphertextDirectory("42", ciphertextDestinationDir));
 				when(physicalFsProv.newFileChannel(Mockito.same(ciphertextDestinationDirFile), Mockito.anySet(), Mockito.any())).thenReturn(ciphertextTargetDirFileChannel);
-				Field closeLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock");
-				closeLockField.setAccessible(true);
-				closeLockField.set(ciphertextTargetDirFileChannel, new Object());
 			}
 
 			@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java
index 036f2412..3943207a 100644
--- a/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DirectoryIdLoaderTest.java
@@ -7,10 +7,8 @@
 import org.mockito.Mockito;
 
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
-import java.nio.channels.spi.AbstractInterruptibleChannel;
 import java.nio.file.FileSystem;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -62,10 +60,10 @@ public void testDirectoryIdForNonExistingFileIsNotEmpty() throws IOException {
 	}
 
 	@Test
-	public void testDirectoryIdIsReadFromExistingFile() throws IOException, ReflectiveOperationException {
+	public void testDirectoryIdIsReadFromExistingFile() throws IOException {
 		String expectedId = "asdüßT°z¬╚‗";
 		byte[] expectedIdBytes = expectedId.getBytes(UTF_8);
-		FileChannel channel = createFileChannelMock();
+		FileChannel channel = Mockito.mock(FileChannel.class);
 		when(provider.newFileChannel(eq(dirFilePath), any())).thenReturn(channel);
 		when(channel.size()).thenReturn((long) expectedIdBytes.length);
 		when(channel.read(any(ByteBuffer.class))).then(invocation -> {
@@ -80,8 +78,8 @@ public void testDirectoryIdIsReadFromExistingFile() throws IOException, Reflecti
 	}
 
 	@Test
-	public void testIOExceptionWhenExistingFileIsEmpty() throws IOException, ReflectiveOperationException {
-		FileChannel channel = createFileChannelMock();
+	public void testIOExceptionWhenExistingFileIsEmpty() throws IOException {
+		FileChannel channel = Mockito.mock(FileChannel.class);
 		when(provider.newFileChannel(eq(dirFilePath), any())).thenReturn(channel);
 		when(channel.size()).thenReturn(0l);
 
@@ -92,8 +90,8 @@ public void testIOExceptionWhenExistingFileIsEmpty() throws IOException, Reflect
 	}
 
 	@Test
-	public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException, ReflectiveOperationException {
-		FileChannel channel = createFileChannelMock();
+	public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException {
+		FileChannel channel = Mockito.mock(FileChannel.class);
 		when(provider.newFileChannel(eq(dirFilePath), any())).thenReturn(channel);
 		when(channel.size()).thenReturn((long) Integer.MAX_VALUE);
 
@@ -103,26 +101,4 @@ public void testIOExceptionWhenExistingFileIsTooLarge() throws IOException, Refl
 		MatcherAssert.assertThat(e.getMessage(), containsString("Unexpectedly large directory file"));
 	}
 
-	private FileChannel createFileChannelMock() throws ReflectiveOperationException {
-		FileChannel channel = Mockito.mock(FileChannel.class);
-		try {
-			Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open");
-			channelOpenField.setAccessible(true);
-			channelOpenField.set(channel, true);
-		} catch (NoSuchFieldException e) {
-			// field only declared in jdk8
-		}
-		try {
-			Field channelClosedField = AbstractInterruptibleChannel.class.getDeclaredField("closed");
-			channelClosedField.setAccessible(true);
-			channelClosedField.set(channel, false);
-		} catch (NoSuchFieldException e) {
-			// field only declared in jdk 9
-		}
-		Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock");
-		channelCloseLockField.setAccessible(true);
-		channelCloseLockField.set(channel, new Object());
-		return channel;
-	}
-
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java
index ce452646..9d10cea3 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/AsyncDelegatingFileChannelTest.java
@@ -8,7 +8,6 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.ch;
 
-import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
@@ -16,17 +15,13 @@
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
 
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
 import java.nio.channels.ClosedChannelException;
 import java.nio.channels.CompletionHandler;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
-import java.nio.channels.spi.AbstractInterruptibleChannel;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -44,38 +39,22 @@ public class AsyncDelegatingFileChannelTest {
 	@BeforeEach
 	public void setup() throws ReflectiveOperationException {
 		channel = Mockito.mock(FileChannel.class);
-		try {
-			Field channelOpenField = AbstractInterruptibleChannel.class.getDeclaredField("open");
-			channelOpenField.setAccessible(true);
-			channelOpenField.set(channel, true);
-		} catch (NoSuchFieldException e) {
-			// field only declared in jdk8
-		}
-		try {
-			Field channelClosedField = AbstractInterruptibleChannel.class.getDeclaredField("closed");
-			channelClosedField.setAccessible(true);
-			channelClosedField.set(channel, false);
-		} catch (NoSuchFieldException e) {
-			// field only declared in jdk 9
-		}
-		Field channelCloseLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock");
-		channelCloseLockField.setAccessible(true);
-		channelCloseLockField.set(channel, new Object());
 		asyncChannel = new AsyncDelegatingFileChannel(channel, executor);
 	}
 
 	@Test
-	public void testIsOpen() throws IOException {
+	public void testIsOpen() {
+		Mockito.when(channel.isOpen()).thenReturn(true);
 		Assertions.assertTrue(asyncChannel.isOpen());
-		channel.close();
+
+		Mockito.when(channel.isOpen()).thenReturn(false);
 		Assertions.assertFalse(asyncChannel.isOpen());
 	}
 
 	@Test
 	public void testClose() throws IOException {
-		Assertions.assertTrue(asyncChannel.isOpen());
 		asyncChannel.close();
-		Assertions.assertFalse(asyncChannel.isOpen());
+		Mockito.verify(channel).close();
 	}
 
 	@Test
@@ -112,21 +91,19 @@ public void testTryLock() throws IOException {
 	public class LockTest {
 
 		@Test
-		public void testSuccess() throws IOException, InterruptedException, ExecutionException {
+		public void testSuccess() throws IOException, InterruptedException {
+			Mockito.when(channel.isOpen()).thenReturn(true);
 			final FileLock lock = Mockito.mock(FileLock.class);
-			Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(new Answer<FileLock>() {
-				@Override
-				public FileLock answer(InvocationOnMock invocation) throws Throwable {
-					Thread.sleep(100);
-					return lock;
-				}
+			Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(invocation -> {
+				Thread.sleep(100);
+				return lock;
 			});
 
 			CountDownLatch cdl = new CountDownLatch(1);
 			AtomicReference<FileLock> result = new AtomicReference<>();
 			AtomicReference<String> attachment = new AtomicReference<>();
 			AtomicReference<Throwable> exception = new AtomicReference<>();
-			asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler<FileLock, String>() {
+			asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler<>() {
 
 				@Override
 				public void completed(FileLock r, String a) {
@@ -162,18 +139,16 @@ public void testClosed() throws Throwable {
 
 		@Test
 		public void testExecutionException() throws Throwable {
-			Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(new Answer<FileLock>() {
-				@Override
-				public FileLock answer(InvocationOnMock invocation) throws Throwable {
-					throw new java.lang.ArithmeticException("fail");
-				}
+			Mockito.when(channel.isOpen()).thenReturn(true);
+			Mockito.when(channel.lock(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyBoolean())).thenAnswer(invocation -> {
+				throw new ArithmeticException("fail");
 			});
 
 			CountDownLatch cdl = new CountDownLatch(1);
 			AtomicReference<FileLock> result = new AtomicReference<>();
 			AtomicReference<String> attachment = new AtomicReference<>();
 			AtomicReference<Throwable> exception = new AtomicReference<>();
-			asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler<FileLock, String>() {
+			asyncChannel.lock(123l, 234l, true, "bam", new CompletionHandler<>() {
 
 				@Override
 				public void completed(FileLock r, String a) {
@@ -204,16 +179,14 @@ public void failed(Throwable e, String a) {
 	public class ReadTest {
 
 		@Test
-		public void testSuccess() throws IOException, InterruptedException, ExecutionException {
-			Mockito.when(channel.read(Mockito.any(), Mockito.anyLong())).thenAnswer(new Answer<Integer>() {
-				@Override
-				public Integer answer(InvocationOnMock invocation) throws Throwable {
-					ByteBuffer dst = invocation.getArgument(0);
-					Thread.sleep(100);
-					int read = dst.remaining();
-					dst.position(dst.position() + read);
-					return read;
-				}
+		public void testSuccess() throws IOException, InterruptedException {
+			Mockito.when(channel.isOpen()).thenReturn(true);
+			Mockito.when(channel.read(Mockito.any(), Mockito.anyLong())).thenAnswer(invocation -> {
+				ByteBuffer dst = invocation.getArgument(0);
+				Thread.sleep(100);
+				int read = dst.remaining();
+				dst.position(dst.position() + read);
+				return read;
 			});
 
 			CountDownLatch cdl = new CountDownLatch(1);
@@ -221,7 +194,7 @@ public Integer answer(InvocationOnMock invocation) throws Throwable {
 			AtomicReference<String> attachment = new AtomicReference<>();
 			AtomicReference<Throwable> exception = new AtomicReference<>();
 			ByteBuffer buf = ByteBuffer.allocate(42);
-			asyncChannel.read(buf, 0l, "bam", new CompletionHandler<Integer, String>() {
+			asyncChannel.read(buf, 0l, "bam", new CompletionHandler<>() {
 
 				@Override
 				public void completed(Integer r, String a) {
@@ -261,16 +234,14 @@ public void testClosed() throws Throwable {
 	public class WriteTest {
 
 		@Test
-		public void testSuccess() throws IOException, InterruptedException, ExecutionException {
-			Mockito.when(channel.write(Mockito.any(), Mockito.anyLong())).thenAnswer(new Answer<Integer>() {
-				@Override
-				public Integer answer(InvocationOnMock invocation) throws Throwable {
-					ByteBuffer dst = invocation.getArgument(0);
-					Thread.sleep(100);
-					int read = dst.remaining();
-					dst.position(dst.position() + read);
-					return read;
-				}
+		public void testSuccess() throws IOException, InterruptedException {
+			Mockito.when(channel.isOpen()).thenReturn(true);
+			Mockito.when(channel.write(Mockito.any(), Mockito.anyLong())).thenAnswer(invocation -> {
+				ByteBuffer dst = invocation.getArgument(0);
+				Thread.sleep(100);
+				int read = dst.remaining();
+				dst.position(dst.position() + read);
+				return read;
 			});
 
 			CountDownLatch cdl = new CountDownLatch(1);
@@ -278,7 +249,7 @@ public Integer answer(InvocationOnMock invocation) throws Throwable {
 			AtomicReference<String> attachment = new AtomicReference<>();
 			AtomicReference<Throwable> exception = new AtomicReference<>();
 			ByteBuffer buf = ByteBuffer.allocate(42);
-			asyncChannel.write(buf, 0l, "bam", new CompletionHandler<Integer, String>() {
+			asyncChannel.write(buf, 0l, "bam", new CompletionHandler<>() {
 
 				@Override
 				public void completed(Integer r, String a) {
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java
index c49de017..a60e65af 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFilesTest.java
@@ -9,10 +9,8 @@
 
 import javax.inject.Provider;
 import java.io.IOException;
-import java.lang.reflect.Field;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
-import java.nio.channels.spi.AbstractInterruptibleChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.Path;
@@ -40,9 +38,6 @@ public void setup() throws IOException, ReflectiveOperationException {
 		Mockito.when(openCryptoFileComponentBuilder.build()).thenReturn(subComponent);
 
 		Mockito.when(file.newFileChannel(Mockito.any())).thenReturn(ciphertextFileChannel);
-		Field closeLockField = AbstractInterruptibleChannel.class.getDeclaredField("closeLock");
-		closeLockField.setAccessible(true);
-		closeLockField.set(ciphertextFileChannel, new Object());
 
 		inTest = new OpenCryptoFiles(openCryptoFileComponentBuilderProvider);
 	}
diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 00000000..ca6ee9ce
--- /dev/null
+++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
\ No newline at end of file

From a041c5e6480611e1e4d21e9a88444507467cde97 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Wed, 25 Nov 2020 14:31:26 +0100
Subject: [PATCH 05/70] * Read `vault.cryptomator` instead of
 `masterkey.cryptomator` * Remove methods from CryptoFileSystemProvider that
 deal with password handling or key derivation (will be added to CryptoLib) *
 Added generic `KeyLoader` interface to allow usage of keys from other sources
 than just password-based * Added `VaultConfiguration` to deal with
 `vault.cryptomator` files

Related to #7, #95 and #94
---
 .../cryptofs/CryptoFileSystemComponent.java   |  13 +-
 .../cryptofs/CryptoFileSystemModule.java      |  17 --
 .../cryptofs/CryptoFileSystemProperties.java  | 200 +++++-----------
 .../cryptofs/CryptoFileSystemProvider.java    | 213 +++---------------
 .../CryptoFileSystemProviderComponent.java    |   8 -
 .../CryptoFileSystemProviderModule.java       |  15 +-
 .../cryptofs/CryptoFileSystems.java           |  72 ++++--
 ...leSystemInitializationFailedException.java |  14 ++
 .../org/cryptomator/cryptofs/KeyLoader.java   |  17 ++
 .../cryptofs/KeyLoadingFailedException.java   |  15 ++
 .../cryptomator/cryptofs/VaultCipherMode.java |  33 +++
 .../org/cryptomator/cryptofs/VaultConfig.java | 188 ++++++++++++++++
 .../cryptofs/VaultConfigLoadException.java    |  12 +
 .../cryptofs/VaultKeyInvalidException.java    |  12 +
 .../VaultVersionMismatchException.java        |   9 +
 .../cryptofs/common/Constants.java            |   3 +-
 .../common/FileSystemCapabilityChecker.java   |   9 +-
 .../migration/v8/Version8Migrator.java        |   4 +-
 ...toFileChannelWriteReadIntegrationTest.java |  14 +-
 .../CryptoFileSystemPropertiesTest.java       | 107 ++-------
 ...yptoFileSystemProviderIntegrationTest.java | 103 +++------
 .../CryptoFileSystemProviderTest.java         | 106 +--------
 .../cryptofs/CryptoFileSystemUriTest.java     |   3 +-
 .../cryptofs/CryptoFileSystemsTest.java       |  90 ++++++--
 ...ptyCiphertextDirectoryIntegrationTest.java |   3 +-
 .../cryptofs/ReadmeCodeSamplesTest.java       |   6 +-
 .../RealFileSystemIntegrationTest.java        |   3 +-
 .../cryptomator/cryptofs/VaultConfigTest.java | 105 +++++++++
 ...iteFileWhileReadonlyChannelIsOpenTest.java |   3 +-
 .../attr/FileAttributeIntegrationTest.java    |   3 +-
 .../migration/v8/Version8MigratorTest.java    |   2 +-
 31 files changed, 744 insertions(+), 658 deletions(-)
 create mode 100644 src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/KeyLoader.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/VaultConfig.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java
 create mode 100644 src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java
 create mode 100644 src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
index 28ab66f4..da268113 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
@@ -1,14 +1,11 @@
 package org.cryptomator.cryptofs;
 
+import com.auth0.jwt.interfaces.DecodedJWT;
 import dagger.BindsInstance;
 import dagger.Subcomponent;
-import org.cryptomator.cryptofs.attr.AttributeViewComponent;
-import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
-import org.cryptomator.cryptofs.dir.DirectoryStreamComponent;
-import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent;
+import org.cryptomator.cryptolib.api.Cryptor;
 
 import java.nio.file.Path;
-import java.util.Set;
 
 @CryptoFileSystemScoped
 @Subcomponent(modules = {CryptoFileSystemModule.class})
@@ -19,6 +16,12 @@ public interface CryptoFileSystemComponent {
 	@Subcomponent.Builder
 	interface Builder {
 
+		@BindsInstance
+		Builder cryptor(Cryptor cryptor);
+
+		@BindsInstance
+		Builder vaultConfig(VaultConfig vaultConfig);
+
 		@BindsInstance
 		Builder provider(CryptoFileSystemProvider provider);
 
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
index 2ccb7f48..0276e590 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
@@ -31,23 +31,6 @@ class CryptoFileSystemModule {
 
 	private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystemModule.class);
 
-	@Provides
-	@CryptoFileSystemScoped
-	public Cryptor provideCryptor(CryptorProvider cryptorProvider, @PathToVault Path pathToVault, CryptoFileSystemProperties properties, ReadonlyFlag readonlyFlag) {
-		try {
-			Path masterKeyPath = pathToVault.resolve(properties.masterkeyFilename());
-			assert Files.exists(masterKeyPath); // since 1.3.0 a file system can only be created for existing vaults. initialization is done before.
-			byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
-			Cryptor cryptor = cryptorProvider.createFromKeyFile(KeyFile.parse(keyFileContents), properties.passphrase(), properties.pepper(), Constants.VAULT_VERSION);
-			if (!readonlyFlag.isSet()) {
-				MasterkeyBackupHelper.attemptMasterKeyBackup(masterKeyPath);
-			}
-			return cryptor;
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
-		}
-	}
-
 	@Provides
 	@CryptoFileSystemScoped
 	public Optional<FileStore> provideNativeFileStore(@PathToVault Path pathToVault) {
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index a9efb4fa..b24475e5 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -8,24 +8,20 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs;
 
-import static java.util.Arrays.asList;
-import static java.util.Collections.unmodifiableSet;
+import com.google.common.base.Strings;
+import org.cryptomator.cryptofs.common.Constants;
 
 import java.net.URI;
 import java.nio.file.FileSystems;
 import java.nio.file.Path;
-import java.text.Normalizer;
-import java.text.Normalizer.Form;
 import java.util.AbstractMap;
 import java.util.Collection;
 import java.util.EnumSet;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 
-import com.google.common.base.Strings;
-import org.cryptomator.cryptofs.common.Constants;
+import static java.util.Arrays.asList;
 
 /**
  * Properties to pass to
@@ -33,16 +29,11 @@
  * <li>{@link FileSystems#newFileSystem(URI, Map)} or
  * <li>{@link CryptoFileSystemProvider#newFileSystem(Path, CryptoFileSystemProperties)}.
  * </ul>
- * 
+ *
  * @author Markus Kreusch
  */
 public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 
-	/**
-	 * Key identifying the passphrase for an encrypted vault.
-	 */
-	public static final String PROPERTY_PASSPHRASE = "passphrase";
-
 	/**
 	 * Maximum ciphertext path length.
 	 *
@@ -62,13 +53,13 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 	static final int DEFAULT_MAX_NAME_LENGTH = Constants.MAX_CIPHERTEXT_NAME_LENGTH;
 
 	/**
-	 * Key identifying the pepper used during key derivation.
-	 * 
-	 * @since 1.3.2
+	 * Key identifying the key loader used during initialization.
+	 *
+	 * @since 2.0.0
 	 */
-	public static final String PROPERTY_PEPPER = "pepper";
+	public static final String PROPERTY_KEYLOADER = "keyLoader";
 
-	static final byte[] DEFAULT_PEPPER = new byte[0];
+	static final KeyLoader DEFAULT_KEYLOADER = null;
 
 
 	/**
@@ -82,7 +73,7 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 
 	/**
 	 * Key identifying the name of the masterkey file located inside the vault directory.
-	 * 
+	 *
 	 * @since 1.1.0
 	 */
 	public static final String PROPERTY_MASTERKEY_FILENAME = "masterkeyFilename";
@@ -91,70 +82,44 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 
 	/**
 	 * Key identifying the filesystem flags.
-	 * 
+	 *
 	 * @since 1.3.0
 	 */
 	public static final String PROPERTY_FILESYSTEM_FLAGS = "flags";
 
-	static final Set<FileSystemFlags> DEFAULT_FILESYSTEM_FLAGS = unmodifiableSet(EnumSet.of(FileSystemFlags.MIGRATE_IMPLICITLY, FileSystemFlags.INIT_IMPLICITLY));
+	static final Set<FileSystemFlags> DEFAULT_FILESYSTEM_FLAGS = EnumSet.noneOf(FileSystemFlags.class);
 
 	public enum FileSystemFlags {
 		/**
 		 * If present, the vault is opened in read-only mode.
-		 * <p>
-		 * This flag can not be set together with {@link #INIT_IMPLICITLY} or {@link #MIGRATE_IMPLICITLY}.
 		 */
 		READONLY,
 
-		/**
-		 * If present, the vault gets automatically migrated during file system creation, which might become significantly slower.
-		 * If absent, a {@link FileSystemNeedsMigrationException} will get thrown during the attempt to open a vault that needs migration.
-		 * <p>
-		 * This flag can not be set together with {@link #READONLY}.
-		 * 
-		 * @since 1.4.0
-		 */
-		MIGRATE_IMPLICITLY,
-
-		/**
-		 * If present, the vault structure will implicitly get initialized upon filesystem creation.
-		 * <p>
-		 * This flag can not be set together with {@link #READONLY}.
-		 * 
-		 * @deprecated Will get removed in version 2.0.0. Use {@link CryptoFileSystemProvider#initialize(Path, String, CharSequence)} explicitly.
-		 */
-		@Deprecated INIT_IMPLICITLY,
-
 		/**
 		 * If present, the maximum ciphertext path length (beginning from the root of the vault directory).
 		 * <p>
 		 * If exceeding the limit during a file operation, an exception is thrown.
-		 * 
+		 *
 		 * @since 1.9.8
 		 */
 		MAX_PATH_LENGTH,
-	};
+	}
 
 	private final Set<Entry<String, Object>> entries;
 
 	private CryptoFileSystemProperties(Builder builder) {
-		this.entries = unmodifiableSet(new HashSet<>(asList( //
-				entry(PROPERTY_PASSPHRASE, builder.passphrase), //
-				entry(PROPERTY_PEPPER, builder.pepper), //
-				entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
-				entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
-				entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
-				entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), //
-				entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength) //
-		)));
-	}
-
-	CharSequence passphrase() {
-		return (CharSequence) get(PROPERTY_PASSPHRASE);
+		this.entries = Set.of( //
+				Map.entry(PROPERTY_KEYLOADER, builder.keyLoader), //
+				Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
+				Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
+				Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
+				Map.entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), //
+				Map.entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength) //
+		);
 	}
 
-	byte[] pepper() {
-		return (byte[]) get(PROPERTY_PEPPER);
+	KeyLoader keyLoader() {
+		return (KeyLoader) get(PROPERTY_KEYLOADER);
 	}
 
 	@SuppressWarnings("unchecked")
@@ -166,14 +131,6 @@ public boolean readonly() {
 		return flags().contains(FileSystemFlags.READONLY);
 	}
 
-	boolean migrateImplicitly() {
-		return flags().contains(FileSystemFlags.MIGRATE_IMPLICITLY);
-	}
-
-	boolean initializeImplicitly() {
-		return flags().contains(FileSystemFlags.INIT_IMPLICITLY);
-	}
-
 	String vaultConfigFilename() {
 		return (String) get(PROPERTY_VAULTCONFIG_FILENAME);
 	}
@@ -185,7 +142,7 @@ String masterkeyFilename() {
 	int maxPathLength() {
 		return (int) get(PROPERTY_MAX_PATH_LENGTH);
 	}
-	
+
 	int maxNameLength() {
 		return (int) get(PROPERTY_MAX_NAME_LENGTH);
 	}
@@ -195,49 +152,18 @@ public Set<Entry<String, Object>> entrySet() {
 		return entries;
 	}
 
-	private static Entry<String, Object> entry(String key, Object value) {
-		return new Entry<String, Object>() {
-			@Override
-			public String getKey() {
-				return key;
-			}
-
-			@Override
-			public Object getValue() {
-				return value;
-			}
-
-			@Override
-			public Object setValue(Object value) {
-				throw new UnsupportedOperationException();
-			}
-		};
-	}
-
 	/**
 	 * Starts construction of {@code CryptoFileSystemProperties}
-	 * 
+	 *
 	 * @return a {@link Builder} which can be used to construct {@code CryptoFileSystemProperties}
 	 */
 	public static Builder cryptoFileSystemProperties() {
 		return new Builder();
 	}
 
-	/**
-	 * Starts construction of {@code CryptoFileSystemProperties}.
-	 * Convenience function for <code>cryptoFileSystemProperties().withPassphrase(passphrase)</code>.
-	 * 
-	 * @param passphrase the passphrase to use
-	 * @return a {@link Builder} which can be used to construct {@code CryptoFileSystemProperties}
-	 * @since 1.4.0
-	 */
-	public static Builder withPassphrase(CharSequence passphrase) {
-		return new Builder().withPassphrase(passphrase);
-	}
-
 	/**
 	 * Starts construction of {@code CryptoFileSystemProperties}
-	 * 
+	 *
 	 * @param properties a {@link Map} containing properties used to initialize the builder
 	 * @return a {@link Builder} which can be used to construct {@code CryptoFileSystemProperties} and has been initialized with the values from properties
 	 */
@@ -247,7 +173,7 @@ public static Builder cryptoFileSystemPropertiesFrom(Map<String, ?> properties)
 
 	/**
 	 * Constructs {@code CryptoFileSystemProperties} from a {@link Map}.
-	 * 
+	 *
 	 * @param properties the {@code Map} to convert
 	 * @return the passed in {@code Map} if already of type {@code CryptoFileSystemProperties} or a new {@code CryptoFileSystemProperties} instance holding the values from the {@code Map}
 	 * @throws IllegalArgumentException if a value in the {@code Map} does not have the expected type or if a required value is missing
@@ -269,8 +195,7 @@ public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
 	 */
 	public static class Builder {
 
-		private CharSequence passphrase;
-		public byte[] pepper = DEFAULT_PEPPER;
+		private KeyLoader keyLoader = DEFAULT_KEYLOADER;
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
 		private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
@@ -281,8 +206,8 @@ private Builder() {
 		}
 
 		private Builder(Map<String, ?> properties) {
-			checkedSet(CharSequence.class, PROPERTY_PASSPHRASE, properties, this::withPassphrase);
-			checkedSet(byte[].class, PROPERTY_PEPPER, properties, this::withPepper);
+			checkedSet(KeyLoader.class, PROPERTY_KEYLOADER, properties, this::withKeyLoader);
+			checkedSet(String.class, PROPERTY_VAULTCONFIG_FILENAME, properties, this::withVaultConfigFilename);
 			checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
 			checkedSet(Integer.class, PROPERTY_MAX_PATH_LENGTH, properties, this::withMaxPathLength);
@@ -300,16 +225,6 @@ private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties
 			}
 		}
 
-		/**
-		 * Sets the passphrase to use for a CryptoFileSystem.
-		 * 
-		 * @param passphrase the passphrase to use
-		 * @return this
-		 */
-		public Builder withPassphrase(CharSequence passphrase) {
-			this.passphrase = Normalizer.normalize(passphrase, Form.NFC);
-			return this;
-		}
 
 		/**
 		 * Sets the maximum ciphertext path length for a CryptoFileSystem.
@@ -336,20 +251,20 @@ public Builder withMaxNameLength(int maxNameLength) {
 		}
 
 		/**
-		 * Sets the pepper for a CryptoFileSystem.
-		 * 
-		 * @param pepper A pepper used during key derivation
+		 * Sets the keyLoader for a CryptoFileSystem.
+		 *
+		 * @param keyLoader A keyLoader used during initialization
 		 * @return this
-		 * @since 1.3.2
+		 * @since 2.0.0
 		 */
-		public Builder withPepper(byte[] pepper) {
-			this.pepper = pepper;
+		public Builder withKeyLoader(KeyLoader keyLoader) {
+			this.keyLoader = keyLoader;
 			return this;
 		}
 
 		/**
 		 * Sets the flags for a CryptoFileSystem.
-		 * 
+		 *
 		 * @param flags File system flags
 		 * @return this
 		 * @since 1.3.1
@@ -360,46 +275,45 @@ public Builder withFlags(FileSystemFlags... flags) {
 
 		/**
 		 * Sets the flags for a CryptoFileSystem.
-		 * 
+		 *
 		 * @param flags collection of file system flags
 		 * @return this
 		 * @since 1.3.0
 		 */
 		public Builder withFlags(Collection<FileSystemFlags> flags) {
-			validate(flags);
 			this.flags.clear();
 			this.flags.addAll(flags);
 			return this;
 		}
 
-		private void validate(Collection<FileSystemFlags> flags) {
-			if (flags.contains(FileSystemFlags.READONLY)) {
-				if (flags.contains(FileSystemFlags.INIT_IMPLICITLY)) {
-					throw new IllegalStateException("Can not set flag INIT_IMPLICITLY in conjunction with flag READONLY.");
-				}
-				if (flags.contains(FileSystemFlags.MIGRATE_IMPLICITLY)) {
-					throw new IllegalStateException("Can not set flag MIGRATE_IMPLICITLY in conjunction with flag READONLY.");
-				}
-			}
-		}
-
 		/**
 		 * Sets the readonly flag for a CryptoFileSystem.
-		 * 
+		 *
 		 * @return this
 		 * @deprecated Will be removed in 2.0.0. Use {@link #withFlags(FileSystemFlags...) withFlags(FileSystemFlags.READONLY)}
 		 */
 		@Deprecated
 		public Builder withReadonlyFlag() {
 			flags.add(FileSystemFlags.READONLY);
-			flags.remove(FileSystemFlags.INIT_IMPLICITLY);
-			flags.remove(FileSystemFlags.MIGRATE_IMPLICITLY);
+			return this;
+		}
+
+
+		/**
+		 * Sets the name of the vault config file located inside the vault directory.
+		 *
+		 * @param vaultConfigFilename the filename of the jwt file containing the vault configuration
+		 * @return this
+		 * @since 2.0.0
+		 */
+		public Builder withVaultConfigFilename(String vaultConfigFilename) {
+			this.vaultConfigFilename = vaultConfigFilename;
 			return this;
 		}
 
 		/**
 		 * Sets the name of the masterkey file located inside the vault directory.
-		 * 
+		 *
 		 * @param masterkeyFilename the filename of the json file containing configuration to decrypt the masterkey
 		 * @return this
 		 * @since 1.1.0
@@ -411,7 +325,7 @@ public Builder withMasterkeyFilename(String masterkeyFilename) {
 
 		/**
 		 * Validates the values and creates new {@link CryptoFileSystemProperties}.
-		 * 
+		 *
 		 * @return a new {@code CryptoFileSystemProperties} with the values from this builder
 		 * @throws IllegalStateException if a required value was not set on this {@code Builder}
 		 */
@@ -421,8 +335,8 @@ public CryptoFileSystemProperties build() {
 		}
 
 		private void validate() {
-			if (passphrase == null) {
-				throw new IllegalStateException("passphrase is required");
+			if (keyLoader == null) {
+				throw new IllegalStateException("keyloader is required");
 			}
 			if (Strings.nullToEmpty(masterkeyFilename).trim().isEmpty()) {
 				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 d25102c3..aff67f11 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -11,19 +11,13 @@
 import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
-import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
-import org.cryptomator.cryptofs.migration.Migrators;
-import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
-import org.cryptomator.cryptolib.Cryptors;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 
 import java.io.IOException;
 import java.net.URI;
 import java.nio.channels.AsynchronousFileChannel;
 import java.nio.channels.FileChannel;
 import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.AccessMode;
 import java.nio.file.CopyOption;
 import java.nio.file.DirectoryStream;
@@ -33,7 +27,6 @@
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
 import java.nio.file.NotDirectoryException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
@@ -43,16 +36,11 @@
 import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.FileAttributeView;
 import java.nio.file.spi.FileSystemProvider;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.text.Normalizer;
-import java.text.Normalizer.Form;
+import java.util.Arrays;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 
-import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
-import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 import static java.nio.file.StandardOpenOption.CREATE_NEW;
 import static java.nio.file.StandardOpenOption.WRITE;
 
@@ -92,14 +80,12 @@
  */
 public class CryptoFileSystemProvider extends FileSystemProvider {
 
-	private static final CryptorProvider CRYPTOR_PROVIDER = Cryptors.version1(strongSecureRandom());
-
 	private final CryptoFileSystems fileSystems;
 	private final MoveOperation moveOperation;
 	private final CopyOperation copyOperation;
 
 	public CryptoFileSystemProvider() {
-		this(DaggerCryptoFileSystemProviderComponent.builder().cryptorProvider(CRYPTOR_PROVIDER).build());
+		this(DaggerCryptoFileSystemProviderComponent.create());
 	}
 
 	/**
@@ -111,23 +97,15 @@ public CryptoFileSystemProvider() {
 		this.copyOperation = component.copyOperation();
 	}
 
-	private static SecureRandom strongSecureRandom() {
-		try {
-			return SecureRandom.getInstanceStrong();
-		} catch (NoSuchAlgorithmException e) {
-			throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
-		}
-	}
-
 	/**
 	 * Typesafe alternative to {@link FileSystems#newFileSystem(URI, Map)}. Default way to retrieve a CryptoFS instance.
 	 *
 	 * @param pathToVault Path to this vault's storage location
-	 * @param properties Parameters used during initialization of the file system
+	 * @param properties  Parameters used during initialization of the file system
 	 * @return a new file system
-	 * @throws FileSystemNeedsMigrationException if the vault format needs to get updated and <code>properties</code> did not contain a flag for implicit migration.
+	 * @throws FileSystemNeedsMigrationException                      if the vault format needs to get updated and <code>properties</code> did not contain a flag for implicit migration.
 	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
-	 * @throws IOException if an I/O error occurs creating the file system
+	 * @throws IOException                                            if an I/O error occurs creating the file system
 	 */
 	public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException {
 		URI uri = CryptoFileSystemUri.create(pathToVault.toAbsolutePath());
@@ -137,143 +115,47 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP
 	/**
 	 * Creates a new vault at the given directory path.
 	 *
-	 * @param pathToVault Path to an existing directory
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @param passphrase Passphrase that should be used to unlock the vault
-	 * @throws NotDirectoryException If the given path is not an existing directory.
-	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
-	 * @throws IOException If the vault structure could not be initialized due to I/O errors
-	 * @since 1.3.0
-	 */
-	public static void initialize(Path pathToVault, String masterkeyFilename, CharSequence passphrase) throws NotDirectoryException, IOException {
-		initialize(pathToVault, masterkeyFilename, new byte[0], passphrase);
-	}
-
-	/**
-	 * Creates a new vault at the given directory path.
-	 *
-	 * @param pathToVault Path to an existing directory
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @param pepper Application-specific pepper used during key derivation
-	 * @param passphrase Passphrase that should be used to unlock the vault
-	 * @throws NotDirectoryException If the given path is not an existing directory.
+	 * @param pathToVault         Path to an existing directory
+	 * @param vaultConfigFilename Name of the vault config file
+	 * @param keyId               The ID of the key to associate with this vault
+	 * @param keyLoader           A key loader providing the masterkey for this new vault
+	 * @throws NotDirectoryException                                  If the given path is not an existing directory.
 	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
-	 * @throws IOException If the vault structure could not be initialized due to I/O errors
-	 * @since 1.3.2
+	 * @throws IOException                                            If the vault structure could not be initialized due to I/O errors
+	 * @since 2.0.0
 	 */
-	public static void initialize(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence passphrase) throws NotDirectoryException, IOException {
+	public static void initialize(Path pathToVault, String vaultConfigFilename, String keyId, KeyLoader keyLoader) throws NotDirectoryException, IOException {
 		if (!Files.isDirectory(pathToVault)) {
 			throw new NotDirectoryException(pathToVault.toString());
 		}
-		try (Cryptor cryptor = CRYPTOR_PROVIDER.createNew()) {
-			// save masterkey file:
-			Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
-			byte[] keyFileContents = cryptor.writeKeysToMasterkeyFile(Normalizer.normalize(passphrase, Form.NFC), pepper, Constants.VAULT_VERSION).serialize();
-			Files.write(masterKeyPath, keyFileContents, CREATE_NEW, WRITE);
-			// create "d/RO/OTDIRECTORY":
-			String rootDirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
-			Path rootDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(rootDirHash.substring(0, 2)).resolve(rootDirHash.substring(2));
-			Files.createDirectories(rootDirPath);
+		byte[] rawKey = keyLoader.loadKey(keyId);
+		try {
+			// save vault config:
+			Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
+			var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(Constants.MAX_CIPHERTEXT_NAME_LENGTH).build();
+			var token = config.toToken(keyId, rawKey);
+			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
+			// create "d" dir:
+			Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
+			Files.createDirectories(dataDirPath);
+		} finally {
+			Arrays.fill(rawKey, (byte) 0x00);
 		}
-		assert containsVault(pathToVault, masterkeyFilename);
+		assert containsVault(pathToVault, vaultConfigFilename);
 	}
 
 	/**
 	 * Checks if the folder represented by the given path exists and contains a valid vault structure.
 	 *
-	 * @param pathToVault A directory path
-	 * @param masterkeyFilename Name of the masterkey file
+	 * @param pathToVault         A directory path
+	 * @param vaultConfigFilename Name of the vault config file
 	 * @return <code>true</code> if the directory seems to contain a vault.
-	 * @since 1.1.0
+	 * @since 2.0.0
 	 */
-	public static boolean containsVault(Path pathToVault, String masterkeyFilename) {
-		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
+	public static boolean containsVault(Path pathToVault, String vaultConfigFilename) {
+		Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
 		Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
-		return Files.isReadable(masterKeyPath) && Files.isDirectory(dataDirPath);
-	}
-
-	/**
-	 * Changes the passphrase of a vault at the given path.
-	 *
-	 * @param pathToVault Vault directory
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @param oldPassphrase Current passphrase
-	 * @param newPassphrase Future passphrase
-	 * @throws InvalidPassphraseException If <code>oldPassphrase</code> can not be used to unlock the vault.
-	 * @throws FileSystemNeedsMigrationException if the vault format needs to get updated.
-	 * @throws IOException If the masterkey could not be read or written.
-	 * @see #changePassphrase(Path, String, byte[], CharSequence, CharSequence)
-	 * @since 1.1.0
-	 */
-	public static void changePassphrase(Path pathToVault, String masterkeyFilename, CharSequence oldPassphrase, CharSequence newPassphrase)
-			throws InvalidPassphraseException, FileSystemNeedsMigrationException, IOException {
-		changePassphrase(pathToVault, masterkeyFilename, new byte[0], oldPassphrase, newPassphrase);
-	}
-
-	/**
-	 * Changes the passphrase of a vault at the given path.
-	 *
-	 * @param pathToVault Vault directory
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @param pepper An application-specific pepper added to the salt during key-derivation (if applicable)
-	 * @param oldPassphrase Current passphrase
-	 * @param newPassphrase Future passphrase
-	 * @throws InvalidPassphraseException If <code>oldPassphrase</code> can not be used to unlock the vault.
-	 * @throws FileSystemNeedsMigrationException if the vault format needs to get updated.
-	 * @throws IOException If the masterkey could not be read or written.
-	 * @since 1.4.0
-	 */
-	public static void changePassphrase(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence oldPassphrase, CharSequence newPassphrase)
-			throws InvalidPassphraseException, FileSystemNeedsMigrationException, IOException {
-		if (Migrators.get().needsMigration(pathToVault, masterkeyFilename)) {
-			throw new FileSystemNeedsMigrationException(pathToVault);
-		}
-		String normalizedOldPassphrase = Normalizer.normalize(oldPassphrase, Form.NFC);
-		String normalizedNewPassphrase = Normalizer.normalize(newPassphrase, Form.NFC);
-		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
-		byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath);
-		byte[] newMasterkeyBytes = Cryptors.changePassphrase(CRYPTOR_PROVIDER, oldMasterkeyBytes, pepper, normalizedOldPassphrase, normalizedNewPassphrase);
-		Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX);
-		Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE);
-		Files.write(masterKeyPath, newMasterkeyBytes, CREATE_NEW, WRITE);
-	}
-
-	/**
-	 * Exports the raw key for backup purposes or external key management.
-	 *
-	 * @param pathToVault       Vault directory
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @param pepper            An application-specific pepper added to the salt during key-derivation (if applicable)
-	 * @param passphrase        Current passphrase
-	 * @return A 64 byte array consisting of 32 byte aes key and 32 byte mac key
-	 * @since 1.9.0
-	 */
-	public static byte[] exportRawKey(Path pathToVault, String masterkeyFilename, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException {
-		String normalizedPassphrase = Normalizer.normalize(passphrase, Form.NFC);
-		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
-		byte[] masterKeyBytes = Files.readAllBytes(masterKeyPath);
-		return Cryptors.exportRawKey(CRYPTOR_PROVIDER, masterKeyBytes, pepper, normalizedPassphrase);
-	}
-
-	/**
-	 * Imports a raw key from backup or external key management.
-	 *
-	 * @param pathToVault       Vault directory
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @param pepper            An application-specific pepper added to the salt during key-derivation (if applicable)
-	 * @param passphrase        Future passphrase
-	 * @since 1.9.0
-	 */
-	public static void restoreRawKey(Path pathToVault, String masterkeyFilename, byte[] rawKey, byte[] pepper, CharSequence passphrase) throws InvalidPassphraseException, IOException {
-		String normalizedPassphrase = Normalizer.normalize(passphrase, Form.NFC);
-		byte[] masterKeyBytes = Cryptors.restoreRawKey(CRYPTOR_PROVIDER, rawKey, pepper, normalizedPassphrase, Constants.VAULT_VERSION);
-		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
-		if (Files.exists(masterKeyPath)) {
-			byte[] oldMasterkeyBytes = Files.readAllBytes(masterKeyPath);
-			Path backupKeyPath = pathToVault.resolve(masterkeyFilename + MasterkeyBackupHelper.generateFileIdSuffix(oldMasterkeyBytes) + Constants.MASTERKEY_BACKUP_SUFFIX);
-			Files.move(masterKeyPath, backupKeyPath, REPLACE_EXISTING, ATOMIC_MOVE);
-		}
-		Files.write(masterKeyPath, masterKeyBytes, CREATE_NEW, WRITE);
+		return Files.isReadable(vaultConfigPath) && Files.isDirectory(dataDirPath);
 	}
 
 	/**
@@ -293,36 +175,9 @@ public String getScheme() {
 	public CryptoFileSystem newFileSystem(URI uri, Map<String, ?> rawProperties) throws IOException {
 		CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
-
-		// TODO remove implicit initialization in 2.0.0
-		initializeFileSystemIfRequired(parsedUri, properties);
-		migrateFileSystemIfRequired(parsedUri, properties);
-
 		return fileSystems.create(this, parsedUri.pathToVault(), properties);
 	}
 
-	@Deprecated
-	private void migrateFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException {
-		if (Migrators.get().needsMigration(parsedUri.pathToVault(), properties.masterkeyFilename())) {
-			if (properties.migrateImplicitly()) {
-				Migrators.get().migrate(parsedUri.pathToVault(), properties.vaultConfigFilename(), properties.masterkeyFilename(), properties.passphrase(), (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
-			} else {
-				throw new FileSystemNeedsMigrationException(parsedUri.pathToVault());
-			}
-		}
-	}
-
-	@Deprecated
-	private void initializeFileSystemIfRequired(CryptoFileSystemUri parsedUri, CryptoFileSystemProperties properties) throws NotDirectoryException, IOException, NoSuchFileException {
-		if (!CryptoFileSystemProvider.containsVault(parsedUri.pathToVault(), properties.masterkeyFilename())) {
-			if (properties.initializeImplicitly()) {
-				CryptoFileSystemProvider.initialize(parsedUri.pathToVault(), properties.masterkeyFilename(), properties.passphrase());
-			} else {
-				throw new NoSuchFileException(parsedUri.pathToVault().toString(), null, "Vault not initialized.");
-			}
-		}
-	}
-
 	@Override
 	public CryptoFileSystem getFileSystem(URI uri) {
 		CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
@@ -394,7 +249,7 @@ public boolean isHidden(Path cleartextPath) throws IOException {
 	}
 
 	@Override
-	public FileStore getFileStore(Path cleartextPath) throws IOException {
+	public FileStore getFileStore(Path cleartextPath) {
 		return fileSystem(cleartextPath).getFileStore();
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
index ce52f6e6..dfd2e4bc 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
@@ -16,12 +16,4 @@ interface CryptoFileSystemProviderComponent {
 
 	CopyOperation copyOperation();
 
-	@Component.Builder
-	interface Builder {
-		@BindsInstance
-		Builder cryptorProvider(CryptorProvider cryptorProvider);
-
-		CryptoFileSystemProviderComponent build();
-	}
-
 }
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
index 9bd7b33e..17c13a17 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
@@ -2,17 +2,22 @@
 
 import dagger.Module;
 import dagger.Provides;
-import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 
 import javax.inject.Singleton;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 
 @Module(subcomponents = {CryptoFileSystemComponent.class})
 public class CryptoFileSystemProviderModule {
-	
+
 	@Provides
 	@Singleton
-	public FileSystemCapabilityChecker provideFileSystemCapabilityChecker() {
-		return new FileSystemCapabilityChecker();
+	public SecureRandom provideCSPRNG() {
+		try {
+			return SecureRandom.getInstanceStrong();
+		} catch (NoSuchAlgorithmException e) {
+			throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
+		}
 	}
-	
+
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index d2332dab..085ba9c3 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -1,16 +1,22 @@
 package org.cryptomator.cryptofs;
 
+import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+import org.cryptomator.cryptolib.api.Cryptor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.io.IOException;
-import java.io.UncheckedIOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystemAlreadyExistsException;
 import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -26,31 +32,69 @@ class CryptoFileSystems {
 	private final ConcurrentMap<Path, CryptoFileSystemImpl> fileSystems = new ConcurrentHashMap<>();
 	private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder; // sharing reusable builder via synchronized
 	private final FileSystemCapabilityChecker capabilityChecker;
+	private final SecureRandom csprng;
 
 	@Inject
-	public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder, FileSystemCapabilityChecker capabilityChecker) {
+	public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder, FileSystemCapabilityChecker capabilityChecker, SecureRandom csprng) {
 		this.cryptoFileSystemComponentBuilder = cryptoFileSystemComponentBuilder;
 		this.capabilityChecker = capabilityChecker;
+		this.csprng = csprng;
 	}
 
-	public synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException {
+	public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException {
+		Path normalizedPathToVault = pathToVault.normalize();
+		var token = readVaultConfigFile(normalizedPathToVault, properties);
+
+		var configLoader = VaultConfig.decode(token);
+		byte[] rawKey = properties.keyLoader().loadKey(configLoader.getKeyId());
 		try {
-			Path normalizedPathToVault = pathToVault.normalize();
-			CryptoFileSystemProperties adjustedProperties = adjustForCapabilities(normalizedPathToVault, properties);
+			var config = configLoader.load(rawKey, Constants.VAULT_VERSION);
+			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
 			return fileSystems.compute(normalizedPathToVault, (key, value) -> {
 				if (value == null) {
-					return cryptoFileSystemComponentBuilder //
-							.pathToVault(key) //
-							.properties(adjustedProperties) //
-							.provider(provider) //
-							.build() //
-							.cryptoFileSystem();
+					return create(provider, normalizedPathToVault, adjustedProperties, rawKey, config);
 				} else {
 					throw new FileSystemAlreadyExistsException();
 				}
 			});
-		} catch (UncheckedIOException e) {
-			throw new IOException("Error during file system creation.", e);
+		} finally {
+			Arrays.fill(rawKey, (byte) 0x00);
+		}
+	}
+
+	// synchronized access to non-threadsafe cryptoFileSystemComponentBuilder required
+	private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, byte[] rawKey, VaultConfig config) {
+		Cryptor cryptor = config.getCiphermode().getCryptorProvider(csprng).createFromRawKey(rawKey);
+		return cryptoFileSystemComponentBuilder //
+				.cryptor(cryptor) //
+				.vaultConfig(config) //
+				.pathToVault(pathToVault) //
+				.properties(properties) //
+				.provider(provider) //
+				.build() //
+				.cryptoFileSystem();
+	}
+
+	/**
+	 * Attempts to read a vault config file
+	 * @param pathToVault path to the vault's root
+	 * @param properties properties used when attempting to construct a fs for this vault
+	 * @return The contents of the file decoded in ASCII
+	 * @throws IOException If the file could not be read
+	 * @throws FileSystemNeedsMigrationException If the file doesn't exists, but a legacy masterkey file was found instead
+	 */
+	private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException {
+		Path vaultConfigFile = pathToVault.resolve(properties.vaultConfigFilename());
+		try {
+			return Files.readString(vaultConfigFile, StandardCharsets.US_ASCII);
+		} catch (NoSuchFileException e) {
+			Path masterkeyPath = pathToVault.resolve(properties.masterkeyFilename());
+			if (Files.exists(masterkeyPath)) {
+				LOG.warn("Failed to read {}, but found {}}", vaultConfigFile, masterkeyPath);
+				throw new FileSystemNeedsMigrationException(pathToVault);
+			} else {
+				throw e;
+			}
 		}
 	}
 	
@@ -83,7 +127,7 @@ public CryptoFileSystemImpl get(Path pathToVault) {
 		Path normalizedPathToVault = pathToVault.normalize();
 		CryptoFileSystemImpl fs = fileSystems.get(normalizedPathToVault);
 		if (fs == null) {
-			throw new FileSystemNotFoundException(format("CryptoFileSystem at %s not initialized", pathToVault));
+			throw new FileSystemNotFoundException(format("CryptoFileSystem at %s not initialized", normalizedPathToVault));
 		} else {
 			return fs;
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java b/src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java
new file mode 100644
index 00000000..442c7eb3
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/FileSystemInitializationFailedException.java
@@ -0,0 +1,14 @@
+package org.cryptomator.cryptofs;
+
+import java.io.IOException;
+
+public class FileSystemInitializationFailedException extends IOException {
+
+	public FileSystemInitializationFailedException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	public FileSystemInitializationFailedException(String message) {
+		super(message);
+	}
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/KeyLoader.java b/src/main/java/org/cryptomator/cryptofs/KeyLoader.java
new file mode 100644
index 00000000..9e2e5d16
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/KeyLoader.java
@@ -0,0 +1,17 @@
+package org.cryptomator.cryptofs;
+
+@FunctionalInterface
+public interface KeyLoader {
+
+	/**
+	 * Loads a key required to unlock a vault.
+	 * <p>
+	 * This might be a long-running operation, as it may require user input or expensive computations.
+	 *
+	 * @param keyId a string uniquely identifying the source of the key and its identity, if multiple keys can be obtained from the same source
+	 * @return The raw key bytes. Must not be null
+	 * @throws KeyLoadingFailedException Thrown when it is impossible to fulfill the request
+	 */
+	byte[] loadKey(String keyId) throws KeyLoadingFailedException;
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java b/src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java
new file mode 100644
index 00000000..9532ea33
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java
@@ -0,0 +1,15 @@
+package org.cryptomator.cryptofs;
+
+import java.io.IOException;
+
+/**
+ * Thrown by a {@link KeyLoader} when loading a key required to unlock a vault failed.
+ * <p>
+ * Possible reasons for this exception are: Unsupported key type, key for given id not found, user cancelled key entry, ...
+ */
+public class KeyLoadingFailedException extends FileSystemInitializationFailedException {
+
+	public KeyLoadingFailedException(String message, Throwable cause) {
+		super(message, cause);
+	}
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java b/src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java
new file mode 100644
index 00000000..c752a00b
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java
@@ -0,0 +1,33 @@
+package org.cryptomator.cryptofs;
+
+import org.cryptomator.cryptolib.Cryptors;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
+
+import java.security.SecureRandom;
+import java.util.function.Function;
+
+public enum VaultCipherMode {
+	/**
+	 * AES-SIV for file name encryption
+	 * AES-CTR + HMAC for content encryption
+	 */
+	SIV_CTRMAC(Cryptors::version1);
+
+// TODO enable eventually (issue 94):
+//	/**
+//	 * AES-SIV for file name encryption
+//	 * AES-GCM for content encryption
+//	 */
+//	SIV_GCM(Cryptors::version2);
+
+	private final Function<SecureRandom, CryptorProvider> cryptorProvider;
+
+	VaultCipherMode(Function<SecureRandom, CryptorProvider> cryptorProvider) {
+		this.cryptorProvider = cryptorProvider;
+	}
+
+	public CryptorProvider getCryptorProvider(SecureRandom csprng) {
+		return cryptorProvider.apply(csprng);
+	}
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
new file mode 100644
index 00000000..8c660fd7
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -0,0 +1,188 @@
+package org.cryptomator.cryptofs;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.exceptions.InvalidClaimException;
+import com.auth0.jwt.exceptions.JWTDecodeException;
+import com.auth0.jwt.exceptions.JWTVerificationException;
+import com.auth0.jwt.exceptions.SignatureVerificationException;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import org.cryptomator.cryptofs.common.Constants;
+
+import java.util.Arrays;
+import java.util.UUID;
+
+/**
+ * Typesafe representation of vault configuration files.
+ *
+ * To prevent config tampering, such as downgrade attacks, vault configurations are cryptographically signed using HMAC-256
+ * with the vault's 64 byte master key.
+ *
+ * If the signature could be successfully verified, the configuration can be assumed valid and the masterkey can be assumed
+ * eligible for the vault.
+ *
+ * When {@link #load(String, KeyLoader, int) loading} a vault configuration, a key must be provided and the signature is checked.
+ * It is impossible to create an instance of this class from an existing configuration without signature verification.
+ */
+public class VaultConfig {
+
+	private static final String JSON_KEY_VAULTVERSION = "format";
+	private static final String JSON_KEY_CIPHERCONFIG = "ciphermode";
+	private static final String JSON_KEY_MAXFILENAMELEN = "maxFilenameLen";
+
+	private final String id;
+	private final int vaultVersion;
+	private final VaultCipherMode ciphermode;
+	private final int maxFilenameLength;
+
+	private VaultConfig(DecodedJWT verifiedConfig) {
+		this.id = verifiedConfig.getId();
+		this.vaultVersion = verifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt();
+		this.ciphermode = VaultCipherMode.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString());
+		this.maxFilenameLength = verifiedConfig.getClaim(JSON_KEY_MAXFILENAMELEN).asInt();
+	}
+
+	private VaultConfig(VaultConfigBuilder builder) {
+		this.id = builder.id;
+		this.vaultVersion = builder.vaultVersion;
+		this.ciphermode = builder.ciphermode;
+		this.maxFilenameLength = builder.maxFilenameLength;
+	}
+
+	public String getId() {
+		return id;
+	}
+
+	public int getVaultVersion() {
+		return vaultVersion;
+	}
+
+	public VaultCipherMode getCiphermode() {
+		return ciphermode;
+	}
+
+	public int getMaxFilenameLength() {
+		return maxFilenameLength;
+	}
+
+	public String toToken(String keyId, byte[] rawKey) {
+		return JWT.create() //
+				.withKeyId(keyId) //
+				.withJWTId(id) //
+				.withClaim(JSON_KEY_VAULTVERSION, vaultVersion) //
+				.withClaim(JSON_KEY_CIPHERCONFIG, ciphermode.name()) //
+				.withClaim(JSON_KEY_MAXFILENAMELEN, maxFilenameLength) //
+				.sign(Algorithm.HMAC256(rawKey));
+	}
+
+	/**
+	 * Convenience wrapper for {@link #decode(String)} and {@link VaultConfigLoader#load(byte[], int)}
+	 *
+	 * @param token                The token
+	 * @param keyLoader            A key loader capable of providing a key for this token
+	 * @param expectedVaultVersion The vault version this token should contain
+	 * @return The decoded configuration
+	 * @throws KeyLoadingFailedException     If the key loader was unable to provide a key for this vault configuration
+	 * @throws VaultConfigLoadException When loading the configuration fails (see {@link VaultConfigLoader#load(String, KeyLoader, int)} for details
+	 */
+	public static VaultConfig load(String token, KeyLoader keyLoader, int expectedVaultVersion) throws KeyLoadingFailedException, VaultConfigLoadException {
+		byte[] rawKey = new byte[0];
+		try {
+			var configLoader = decode(token);
+			rawKey = keyLoader.loadKey(configLoader.getKeyId());
+			return configLoader.load(rawKey, expectedVaultVersion);
+		} finally {
+			Arrays.fill(rawKey, (byte) 0x00);
+		}
+	}
+
+	/**
+	 * Decodes a vault configuration stored in JWT format to load it
+	 *
+	 * @param token                The token
+	 * @return A loader object that allows loading the configuration (if providing the required key)
+	 * @throws VaultConfigLoadException When parsing the token failed
+	 */
+	public static VaultConfigLoader decode(String token) throws VaultConfigLoadException {
+		try {
+			return new VaultConfigLoader(JWT.decode(token));
+		} catch (JWTDecodeException e) {
+			throw new VaultConfigLoadException("Failed to parse config: " + token);
+		}
+	}
+
+	/**
+	 * Create a new configuration object for a new vault.
+	 *
+	 * @return A new configuration builder
+	 */
+	public static VaultConfigBuilder createNew() {
+		return new VaultConfigBuilder();
+	}
+
+	public static class VaultConfigLoader {
+
+		private final DecodedJWT unverifiedConfig;
+
+		private VaultConfigLoader(DecodedJWT unverifiedConfig) {
+			this.unverifiedConfig = unverifiedConfig;
+		}
+
+		/**
+		 * @return The ID of the key required to {@link #load(byte[], int) load} this config.
+		 */
+		public String getKeyId() {
+			return unverifiedConfig.getKeyId();
+		}
+
+		/**
+		 * Decodes a vault configuration stored in JWT format.
+		 *
+		 * @param rawKey               The key matching the id in {@link #getKeyId()}
+		 * @param expectedVaultVersion The vault version this token should contain
+		 * @return The decoded configuration
+		 * @throws VaultKeyInvalidException      If the provided key was invalid
+		 * @throws VaultVersionMismatchException If the token did not match the expected vault version
+		 * @throws VaultConfigLoadException      Generic parse error
+		 */
+		public VaultConfig load(byte[] rawKey, int expectedVaultVersion) throws VaultKeyInvalidException, VaultVersionMismatchException, VaultConfigLoadException {
+			try {
+				var verifier = JWT.require(Algorithm.HMAC256(rawKey)) //
+						.withClaim(JSON_KEY_VAULTVERSION, expectedVaultVersion) //
+						.build();
+				var verifiedConfig = verifier.verify(unverifiedConfig);
+				return new VaultConfig(verifiedConfig);
+			} catch (SignatureVerificationException e) {
+				throw new VaultKeyInvalidException();
+			} catch (InvalidClaimException e) {
+				throw new VaultVersionMismatchException("Vault config not for version " + expectedVaultVersion);
+			} catch (JWTVerificationException e) {
+				throw new VaultConfigLoadException("Failed to verify vault config: " + unverifiedConfig.getToken());
+			}
+		}
+	}
+
+	public static class VaultConfigBuilder {
+
+		private final String id = UUID.randomUUID().toString();
+		private final int vaultVersion = Constants.VAULT_VERSION;
+		private VaultCipherMode ciphermode;
+		private int maxFilenameLength;
+
+		public VaultConfigBuilder cipherMode(VaultCipherMode ciphermode) {
+			this.ciphermode = ciphermode;
+			return this;
+		}
+
+		public VaultConfigBuilder maxFilenameLength(int maxFilenameLength) {
+			this.maxFilenameLength = maxFilenameLength;
+			return this;
+		}
+
+		public VaultConfig build() {
+			return new VaultConfig(this);
+		}
+
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java b/src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java
new file mode 100644
index 00000000..ac0f6bf8
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfigLoadException.java
@@ -0,0 +1,12 @@
+package org.cryptomator.cryptofs;
+
+/**
+ * Failed to parse or verify vault config token.
+ */
+public class VaultConfigLoadException extends FileSystemInitializationFailedException {
+
+	public VaultConfigLoadException(String message) {
+		super(message);
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java b/src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java
new file mode 100644
index 00000000..ba6396e6
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/VaultKeyInvalidException.java
@@ -0,0 +1,12 @@
+package org.cryptomator.cryptofs;
+
+/**
+ * An attempt was made to verify the signature of a vault config token using an invalid key.
+ */
+public class VaultKeyInvalidException extends VaultConfigLoadException {
+
+	public VaultKeyInvalidException() {
+		super("Failed to verify vault config signature using the provided key.");
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java b/src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java
new file mode 100644
index 00000000..6c429063
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/VaultVersionMismatchException.java
@@ -0,0 +1,9 @@
+package org.cryptomator.cryptofs;
+
+public class VaultVersionMismatchException extends VaultConfigLoadException {
+
+	public VaultVersionMismatchException(String message) {
+		super(message);
+	}
+
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
index 0ad683f8..84a7bff4 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
@@ -10,7 +10,7 @@
 
 public final class Constants {
 
-	public static final int VAULT_VERSION = 7;
+	public static final int VAULT_VERSION = 8;
 	public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup";
 	public static final String DATA_DIR_NAME = "d";
 	public static final String ROOT_DIR_ID = "";
@@ -30,5 +30,4 @@ public final class Constants {
 	public static final int MAX_DIR_FILE_LENGTH = 36; // UUIDv4: hex-encoded 16 byte int + 4 hyphens = 36 ASCII chars
 
 	public static final String SEPARATOR = "/";
-
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
index 30ff3f6c..b122e4bc 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
@@ -7,6 +7,8 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.inject.Inject;
+import javax.inject.Singleton;
 import java.io.IOException;
 import java.nio.file.DirectoryIteratorException;
 import java.nio.file.DirectoryStream;
@@ -15,6 +17,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 
+@Singleton
 public class FileSystemCapabilityChecker {
 
 	private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class);
@@ -35,6 +38,10 @@ public enum Capability {
 		WRITE_ACCESS,
 	}
 
+	@Inject
+	public FileSystemCapabilityChecker() {
+	}
+
 	/**
 	 * Checks whether the underlying filesystem has all required capabilities.
 	 *
@@ -85,7 +92,7 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio
 
 	/**
 	 * Determinse the number of chars a ciphertext filename (including its extension) is allowed to have inside a vault's <code>d/XX/YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/</code> directory.
-	 * 
+	 *
 	 * @param pathToVault Path to the vault
 	 * @return Number of chars a .c9r file is allowed to have
 	 * @throws IOException If unable to perform this check
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 5695e161..4f6980d7 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -25,8 +25,6 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
-import java.text.Normalizer;
-import java.text.Normalizer.Form;
 import java.util.Arrays;
 import java.util.UUID;
 
@@ -71,7 +69,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 					.withKeyId("MASTERKEY_FILE") //
 					.withClaim("format", 8) //
 					.withClaim("ciphermode", "SIV_CTRMAC") //
-					.withClaim("maxFileNameLen", 220) //
+					.withClaim("maxFilenameLen", 220) //
 					.sign(algorithm);
 			Files.writeString(vaultConfigFile, config, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
 			LOG.info("Wrote vault config to {}.", vaultConfigFile);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 538d488f..627e2b54 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -60,7 +60,7 @@ public class Windows {
 
 		@BeforeAll
 		public void setupClass(@TempDir Path tmpDir) throws IOException {
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withPassphrase("asd").build());
+			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 		}
 
 		// tests https://github.com/cryptomator/cryptofs/issues/69
@@ -89,21 +89,21 @@ public void testLastModifiedIsPreservedOverSeveralOperations() throws IOExceptio
 
 			try (FileChannel ch = FileChannel.open(file, CREATE_NEW, WRITE)) {
 				t1 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
-				Thread.currentThread().sleep(50);
+				Thread.sleep(50);
 
 				ch.write(data);
 				ch.force(true);
-				Thread.currentThread().sleep(50);
+				Thread.sleep(50);
 				t2 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
 
 				Files.setLastModifiedTime(file, FileTime.from(t0));
 				ch.force(true);
-				Thread.currentThread().sleep(50);
+				Thread.sleep(50);
 				t3 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.MILLIS);
 
 				ch.write(data);
 				ch.force(true);
-				Thread.currentThread().sleep(1000);
+				Thread.sleep(1000);
 				t4 = Files.getLastModifiedTime(file).toInstant().truncatedTo(ChronoUnit.SECONDS);
 
 			}
@@ -131,8 +131,8 @@ public void beforeAll() throws IOException {
 			inMemoryFs = Jimfs.newFileSystem();
 			Path vaultPath = inMemoryFs.getPath("vault");
 			Files.createDirectories(vaultPath);
-			CryptoFileSystemProvider.initialize(vaultPath, "masterkey.cryptomator", "asd");
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, cryptoFileSystemProperties().withPassphrase("asd").withFlags().build());
+			CryptoFileSystemProvider.initialize(vaultPath, "vault.cryptomator", "MASTERKEY_FILE", ignored -> new byte[64]);
+			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).withFlags().build());
 			file = fileSystem.getPath("/test.txt");
 		}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 38d0eafc..6c273b56 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -7,6 +7,7 @@
 import org.hamcrest.TypeSafeDiagnosingMatcher;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.nio.charset.StandardCharsets;
 import java.util.EnumSet;
@@ -21,6 +22,8 @@
 
 public class CryptoFileSystemPropertiesTest {
 
+	private final KeyLoader keyLoader = Mockito.mock(KeyLoader.class);
+
 	@Test
 	public void testSetNoPassphrase() {
 		Assertions.assertThrows(IllegalStateException.class, () -> {
@@ -30,46 +33,17 @@ public void testSetNoPassphrase() {
 
 	@Test
 	@SuppressWarnings({"unchecked", "deprecation"})
-	public void testSetOnlyPassphrase() {
-		String passphrase = "aPassphrase";
-		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withPassphrase(passphrase) //
-				.build();
-
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
-		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
-		MatcherAssert.assertThat(inTest.readonly(), is(false));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(true));
-		MatcherAssert.assertThat(inTest.migrateImplicitly(), is(true));
-		MatcherAssert.assertThat(inTest.entrySet(),
-				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
-						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
-						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
-						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
-	}
-
-	@Test
-	@SuppressWarnings({"unchecked", "deprecation"})
-	public void testSetPassphraseAndReadonlyFlag() {
-		String passphrase = "aPassphrase";
+	public void testSetReadonlyFlag() {
 		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withPassphrase(passphrase) //
+				.withKeyLoader(keyLoader) //
 				.withReadonlyFlag() //
 				.build();
 
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false));
-		MatcherAssert.assertThat(inTest.migrateImplicitly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -79,24 +53,19 @@ public void testSetPassphraseAndReadonlyFlag() {
 
 	@Test
 	@SuppressWarnings({"unchecked", "deprecation"})
-	public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() {
-		String passphrase = "aPassphrase";
+	public void testSetMasterkeyFilenameAndReadonlyFlag() {
 		String masterkeyFilename = "aMasterkeyFilename";
 		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withPassphrase(passphrase) //
+				.withKeyLoader(keyLoader) //
 				.withMasterkeyFilename(masterkeyFilename) //
 				.withReadonlyFlag() //
 				.build();
 
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false));
-		MatcherAssert.assertThat(inTest.migrateImplicitly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, DEFAULT_PEPPER), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -108,27 +77,21 @@ public void testSetPassphraseAndMasterkeyFilenameAndReadonlyFlag() {
 	@SuppressWarnings({"unchecked"})
 	public void testFromMap() {
 		Map<String, Object> map = new HashMap<>();
-		String passphrase = "aPassphrase";
-		byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII);
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_PASSPHRASE, passphrase);
-		map.put(PROPERTY_PEPPER, pepper);
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_MAX_PATH_LENGTH, 1000);
 		map.put(PROPERTY_MAX_NAME_LENGTH, 255);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
 		CryptoFileSystemProperties inTest = cryptoFileSystemPropertiesFrom(map).build();
 
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false));
 		MatcherAssert.assertThat(inTest.maxPathLength(), is(1000));
 		MatcherAssert.assertThat(inTest.maxNameLength(), is(255));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), //
@@ -140,23 +103,17 @@ public void testFromMap() {
 	@SuppressWarnings("unchecked")
 	public void testWrapMapWithTrueReadonly() {
 		Map<String, Object> map = new HashMap<>();
-		String passphrase = "aPassphrase";
-		byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII);
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_PASSPHRASE, passphrase);
-		map.put(PROPERTY_PEPPER, pepper);
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
 
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -168,23 +125,17 @@ public void testWrapMapWithTrueReadonly() {
 	@SuppressWarnings("unchecked")
 	public void testWrapMapWithFalseReadonly() {
 		Map<String, Object> map = new HashMap<>();
-		String passphrase = "aPassphrase";
-		byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII);
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_PASSPHRASE, passphrase);
-		map.put(PROPERTY_PEPPER, pepper);
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
 
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
 		MatcherAssert.assertThat(inTest.readonly(), is(false));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -195,7 +146,6 @@ public void testWrapMapWithFalseReadonly() {
 	@Test
 	public void testWrapMapWithInvalidFilesystemFlags() {
 		Map<String, Object> map = new HashMap<>();
-		map.put(PROPERTY_PASSPHRASE, "any");
 		map.put(PROPERTY_MASTERKEY_FILENAME, "any");
 		map.put(PROPERTY_FILESYSTEM_FLAGS, "invalidType");
 
@@ -207,7 +157,6 @@ public void testWrapMapWithInvalidFilesystemFlags() {
 	@Test
 	public void testWrapMapWithInvalidMasterkeyFilename() {
 		Map<String, Object> map = new HashMap<>();
-		map.put(PROPERTY_PASSPHRASE, "any");
 		map.put(PROPERTY_MASTERKEY_FILENAME, "");
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class));
 
@@ -219,7 +168,6 @@ public void testWrapMapWithInvalidMasterkeyFilename() {
 	@Test
 	public void testWrapMapWithInvalidPassphrase() {
 		Map<String, Object> map = new HashMap<>();
-		map.put(PROPERTY_PASSPHRASE, new Object());
 		map.put(PROPERTY_MASTERKEY_FILENAME, "any");
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class));
 
@@ -232,26 +180,21 @@ public void testWrapMapWithInvalidPassphrase() {
 	@SuppressWarnings({"unchecked", "deprecation"})
 	public void testWrapMapWithoutReadonly() {
 		Map<String, Object> map = new HashMap<>();
-		String passphrase = "aPassphrase";
-		byte[] pepper = "aPepper".getBytes(StandardCharsets.US_ASCII);
-		map.put(PROPERTY_PASSPHRASE, passphrase);
-		map.put(PROPERTY_PEPPER, pepper);
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
 
-		MatcherAssert.assertThat(inTest.passphrase(), is(passphrase));
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
 		MatcherAssert.assertThat(inTest.readonly(), is(false));
-		MatcherAssert.assertThat(inTest.initializeImplicitly(), is(true));
-		MatcherAssert.assertThat(inTest.migrateImplicitly(), is(true));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_PASSPHRASE, passphrase), //
-						anEntry(PROPERTY_PEPPER, pepper), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
-						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.INIT_IMPLICITLY, FileSystemFlags.MIGRATE_IMPLICITLY))));
+						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class))
+				)
+		);
 	}
 
 	@Test
@@ -263,9 +206,7 @@ public void testWrapMapWithoutPassphrase() {
 
 	@Test
 	public void testWrapCryptoFileSystemProperties() {
-		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withPassphrase("any") //
-				.build();
+		CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 
 		MatcherAssert.assertThat(CryptoFileSystemProperties.wrap(inTest), is(sameInstance(inTest)));
 	}
@@ -274,7 +215,7 @@ public void testWrapCryptoFileSystemProperties() {
 	public void testMapIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withPassphrase("irrelevant") //
+					.withKeyLoader(keyLoader) //
 					.build() //
 					.put("test", "test");
 		});
@@ -284,7 +225,7 @@ public void testMapIsImmutable() {
 	public void testEntrySetIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withPassphrase("irrelevant") //
+					.withKeyLoader(keyLoader) //
 					.build() //
 					.entrySet() //
 					.add(null);
@@ -295,7 +236,7 @@ public void testEntrySetIsImmutable() {
 	public void testEntryIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withPassphrase("irrelevant") //
+					.withKeyLoader(keyLoader) //
 					.build() //
 					.entrySet() //
 					.iterator().next() //
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 571bcdff..c64d062d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -48,18 +48,17 @@
 import java.nio.channels.WritableByteChannel;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.AccessDeniedException;
-import java.nio.file.CopyOption;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
 import java.nio.file.StandardOpenOption;
 import java.nio.file.attribute.BasicFileAttributeView;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.nio.file.attribute.DosFileAttributeView;
+import java.util.Arrays;
 import java.util.EnumSet;
 
 import static java.nio.file.Files.readAllBytes;
@@ -74,6 +73,7 @@ public class CryptoFileSystemProviderIntegrationTest {
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	class WithLimitedPaths {
 
+		private KeyLoader keyLoader = ignored -> new byte[64];
 		private CryptoFileSystem fs;
 		private Path shortFilePath;
 		private Path shortSymlinkPath;
@@ -81,11 +81,11 @@ class WithLimitedPaths {
 
 		@BeforeAll
 		public void setup(@TempDir Path tmpDir) throws IOException {
-			CryptoFileSystemProvider.initialize(tmpDir, "masterkey.cryptomator", "asd");
+			CryptoFileSystemProvider.initialize(tmpDir, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 					.withFlags() //
 					.withMasterkeyFilename("masterkey.cryptomator") //
-					.withPassphrase("asd") //
+					.withKeyLoader(keyLoader) //
 					.withMaxPathLength(100)
 					.build();
 			fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
@@ -169,22 +169,30 @@ public void testCopyExceedingPathLengthLimit(String path) {
 	class InMemory {
 
 		private FileSystem tmpFs;
+		private KeyLoader keyLoader1;
+		private KeyLoader keyLoader2;
 		private Path pathToVault1;
 		private Path pathToVault2;
-		private Path masterkeyFile1;
-		private Path masterkeyFile2;
+		private Path vaultConfigFile1;
+		private Path vaultConfigFile2;
 		private FileSystem fs1;
 		private FileSystem fs2;
 
 		@BeforeAll
 		public void setup() throws IOException {
 			tmpFs = Jimfs.newFileSystem(Configuration.unix());
+			byte[] key1 = new byte[64];
+			byte[] key2 = new byte[64];
+			Arrays.fill(key1, (byte) 0x55);
+			Arrays.fill(key2, (byte) 0x77);
+			keyLoader1 = ignored -> Arrays.copyOf(key1, 64);
+			keyLoader2 = ignored -> Arrays.copyOf(key2, 64);
 			pathToVault1 = tmpFs.getPath("/vaultDir1");
 			pathToVault2 = tmpFs.getPath("/vaultDir2");
 			Files.createDirectory(pathToVault1);
 			Files.createDirectory(pathToVault2);
-			masterkeyFile1 = pathToVault1.resolve("masterkey.cryptomator");
-			masterkeyFile2 = pathToVault2.resolve("masterkey.cryptomator");
+			vaultConfigFile1 = pathToVault1.resolve("vault.cryptomator");
+			vaultConfigFile2 = pathToVault2.resolve("vault.cryptomator");
 		}
 
 		@AfterAll
@@ -198,14 +206,13 @@ public void teardown() throws IOException {
 		public void initializeVaults() {
 			Assertions.assertAll(
 					() -> {
-						CryptoFileSystemProvider.initialize(pathToVault1, "masterkey.cryptomator", "asd");
+						CryptoFileSystemProvider.initialize(pathToVault1, "vault.cryptomator", "MASTERKEY_FILE", keyLoader1);
 						Assertions.assertTrue(Files.isDirectory(pathToVault1.resolve("d")));
-						Assertions.assertTrue(Files.isRegularFile(masterkeyFile1));
+						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile1));
 					}, () -> {
-						byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII);
-						CryptoFileSystemProvider.initialize(pathToVault2, "masterkey.cryptomator", pepper, "asd");
+						CryptoFileSystemProvider.initialize(pathToVault2, "vault.cryptomator", "MASTERKEY_FILE", keyLoader2);
 						Assertions.assertTrue(Files.isDirectory(pathToVault2.resolve("d")));
-						Assertions.assertTrue(Files.isRegularFile(masterkeyFile2));
+						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile2));
 					});
 		}
 
@@ -213,91 +220,51 @@ public void initializeVaults() {
 		@Order(2)
 		@DisplayName("get filesystem with incorrect credentials")
 		public void testGetFsWithWrongCredentials() {
-			Assumptions.assumeTrue(Files.exists(masterkeyFile1));
-			Assumptions.assumeTrue(Files.exists(masterkeyFile2));
+			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault1, "vault.cryptomator"));
+			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault2, "vault.cryptomator"));
 			Assertions.assertAll(
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault1);
 						CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 								.withFlags() //
 								.withMasterkeyFilename("masterkey.cryptomator") //
-								.withPassphrase("qwe") //
+								.withKeyLoader(keyLoader2) //
 								.build();
-						Assertions.assertThrows(InvalidPassphraseException.class, () -> {
+						Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 							FileSystems.newFileSystem(fsUri, properties);
 						});
 					},
 					() -> {
-						byte[] pepper = "salt".getBytes(StandardCharsets.US_ASCII);
 						URI fsUri = CryptoFileSystemUri.create(pathToVault2);
 						CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 								.withFlags() //
 								.withMasterkeyFilename("masterkey.cryptomator") //
-								.withPassphrase("qwe") //
-								.withPepper(pepper)
+								.withKeyLoader(keyLoader1) //
 								.build();
-						Assertions.assertThrows(InvalidPassphraseException.class, () -> {
+						Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 							FileSystems.newFileSystem(fsUri, properties);
 						});
 					});
 		}
 
-		@Test
-		@Order(3)
-		@DisplayName("change password")
-		public void testChangePassword() {
-			Assumptions.assumeTrue(Files.exists(masterkeyFile1));
-			Assumptions.assumeTrue(Files.exists(masterkeyFile2));
-			Assertions.assertAll(
-					() -> {
-						Path pathToVault = tmpFs.getPath("/tmpVault");
-						Files.createDirectory(pathToVault);
-						Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
-						Files.write(masterkeyFile, "{\"version\": 0}".getBytes(StandardCharsets.US_ASCII));
-						Assertions.assertThrows(FileSystemNeedsMigrationException.class, () -> {
-							CryptoFileSystemProvider.changePassphrase(pathToVault, "masterkey.cryptomator", "asd", "qwe");
-						});
-					},
-					() -> {
-						Assertions.assertThrows(InvalidPassphraseException.class, () -> {
-							CryptoFileSystemProvider.changePassphrase(pathToVault1, "masterkey.cryptomator", "WRONG", "qwe");
-						});
-					},
-					() -> {
-						CryptoFileSystemProvider.changePassphrase(pathToVault1, "masterkey.cryptomator", "asd", "qwe");
-					},
-					() -> {
-						byte[] pepper = "salt".getBytes(StandardCharsets.US_ASCII);
-						Assertions.assertThrows(InvalidPassphraseException.class, () -> {
-							CryptoFileSystemProvider.changePassphrase(pathToVault2, "masterkey.cryptomator", pepper, "asd", "qwe");
-						});
-					},
-					() -> {
-						byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII);
-						CryptoFileSystemProvider.changePassphrase(pathToVault2, "masterkey.cryptomator", pepper, "asd", "qwe");
-					}
-			);
-		}
-
 		@Test
 		@Order(4)
 		@DisplayName("get filesystem with correct credentials")
 		public void testGetFsViaNioApi() {
-			Assumptions.assumeTrue(Files.exists(masterkeyFile1));
-			Assumptions.assumeTrue(Files.exists(masterkeyFile2));
+			Assumptions.assumeTrue(Files.exists(vaultConfigFile1));
+			Assumptions.assumeTrue(Files.exists(vaultConfigFile2));
 			Assertions.assertAll(
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault1);
-						fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withPassphrase("qwe").build());
+						fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader1).build());
 						Assertions.assertTrue(fs1 instanceof CryptoFileSystemImpl);
 
 						FileSystem sameFs = FileSystems.getFileSystem(fsUri);
 						Assertions.assertSame(fs1, sameFs);
 					},
 					() -> {
-						byte[] pepper = "pepper".getBytes(StandardCharsets.US_ASCII);
 						URI fsUri = CryptoFileSystemUri.create(pathToVault2);
-						fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withPassphrase("qwe").withPepper(pepper).build());
+						fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader2).build());
 						Assertions.assertTrue(fs2 instanceof CryptoFileSystemImpl);
 
 						FileSystem sameFs = FileSystems.getFileSystem(fsUri);
@@ -556,8 +523,9 @@ class PosixTests {
 		public void setup(@TempDir Path tmpDir) throws IOException {
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
-			CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd");
-			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build());
+			KeyLoader keyLoader = ignored -> new byte[64];
+			CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
+			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 		}
 
 		@Nested
@@ -645,8 +613,9 @@ class WindowsTests {
 		public void setup(@TempDir Path tmpDir) throws IOException {
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
-			CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd");
-			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withPassphrase("asd").build());
+			KeyLoader keyLoader = ignored -> new byte[64];
+			CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
+			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 		}
 
 		@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 4ba4731e..fd088071 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -11,6 +11,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.net.URI;
@@ -167,7 +168,7 @@ public void testInitializeFailWithNotDirectoryException() {
 		Path pathToVault = fs.getPath("/vaultDir");
 
 		Assertions.assertThrows(NotDirectoryException.class, () -> {
-			CryptoFileSystemProvider.initialize(pathToVault, "irrelevant.txt", "asd");
+			CryptoFileSystemProvider.initialize(pathToVault, "irrelevant.txt", "irrelevant", ignored -> new byte[0]);
 		});
 	}
 
@@ -175,63 +176,28 @@ public void testInitializeFailWithNotDirectoryException() {
 	public void testInitialize() throws IOException {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 		Path pathToVault = fs.getPath("/vaultDir");
-		Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
+		Path vaultConfigFile = pathToVault.resolve("vault.cryptomator");
 		Path dataDir = pathToVault.resolve("d");
 
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "masterkey.cryptomator", "asd");
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", ignored -> new byte[64]);
 
 		Assertions.assertTrue(Files.isDirectory(dataDir));
-		Assertions.assertTrue(Files.isRegularFile(masterkeyFile));
+		Assertions.assertTrue(Files.isRegularFile(vaultConfigFile));
 	}
 
 	@Test
-	public void testNoImplicitInitialization() throws IOException {
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-		Path pathToVault = fs.getPath("/vaultDir");
-		Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
-		Path dataDir = pathToVault.resolve("d");
-
-		Files.createDirectory(pathToVault);
+	public void testNewFileSystem() throws IOException {
+		Path pathToVault = Path.of("/vaultDir");
 		URI uri = CryptoFileSystemUri.create(pathToVault);
-
 		CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 				.withFlags() //
-				.withMasterkeyFilename("masterkey.cryptomator") //
-				.withPassphrase("asd") //
+				.withKeyLoader(ignored -> new byte[64]) //
 				.build();
 
-		NoSuchFileException e = Assertions.assertThrows(NoSuchFileException.class, () -> {
-			inTest.newFileSystem(uri, properties);
-		});
-		MatcherAssert.assertThat(e.getMessage(), containsString("Vault not initialized"));
-		Assertions.assertTrue(Files.notExists(dataDir));
-		Assertions.assertTrue(Files.notExists(masterkeyFile));
-	}
-
-	@Test
-	@SuppressWarnings("deprecation")
-	public void testImplicitInitialization() throws IOException {
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-		Path pathToVault = fs.getPath("/vaultDir");
-		Path masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
-		Path dataDir = pathToVault.resolve("d");
-
-		Files.createDirectory(pathToVault);
-		URI uri = CryptoFileSystemUri.create(pathToVault);
-
-		CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
-				.withFlags(FileSystemFlags.INIT_IMPLICITLY) //
-				.withMasterkeyFilename("masterkey.cryptomator") //
-				.withPassphrase("asd") //
-				.build();
-		when(fileSystems.create(eq(inTest), eq(pathToVault), eq(properties))).thenReturn(cryptoFileSystem);
-		FileSystem result = inTest.newFileSystem(uri, properties);
-		verify(fileSystems).create(eq(inTest), eq(pathToVault), eq(properties));
+		inTest.newFileSystem(uri, properties);
 
-		Assertions.assertSame(cryptoFileSystem, result);
-		Assertions.assertTrue(Files.isDirectory(dataDir));
-		Assertions.assertTrue(Files.isRegularFile(masterkeyFile));
+		Mockito.verify(fileSystems).create(Mockito.same(inTest), Mockito.eq(pathToVault), Mockito.eq(properties));
 	}
 
 	@Test
@@ -276,58 +242,6 @@ public void testContainsVaultReturnsFalseIfDirectoryContainsMasterkeyFileButNoDa
 		Assertions.assertFalse(containsVault(pathToVault, masterkeyFilename));
 	}
 
-	@Test
-	public void testVaultWithChangedPassphraseCanBeOpenedWithNewPassphrase() throws IOException {
-		String oldPassphrase = "oldPassphrase838283";
-		String newPassphrase = "newPassphrase954810921";
-		String masterkeyFilename = "masterkey.foo.baz";
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-		Path pathToVault = fs.getPath("/vaultDir");
-		Files.createDirectory(pathToVault);
-		newFileSystem( //
-				pathToVault, //
-				cryptoFileSystemProperties() //
-						.withMasterkeyFilename(masterkeyFilename) //
-						.withPassphrase(oldPassphrase) //
-						.build()).close();
-
-		CryptoFileSystemProvider.changePassphrase(pathToVault, masterkeyFilename, oldPassphrase, newPassphrase);
-
-		newFileSystem( //
-				pathToVault, //
-				cryptoFileSystemProperties() //
-						.withMasterkeyFilename(masterkeyFilename) //
-						.withPassphrase(newPassphrase) //
-						.build()).close();
-	}
-
-	@Test
-	public void testVaultWithChangedPassphraseCanNotBeOpenedWithOldPassphrase() throws IOException {
-		String oldPassphrase = "oldPassphrase838283";
-		String newPassphrase = "newPassphrase954810921";
-		String masterkeyFilename = "masterkey.foo.baz";
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-		Path pathToVault = fs.getPath("/vaultDir");
-		Files.createDirectory(pathToVault);
-		newFileSystem( //
-				pathToVault, //
-				cryptoFileSystemProperties() //
-						.withMasterkeyFilename(masterkeyFilename) //
-						.withPassphrase(oldPassphrase) //
-						.build()).close();
-
-		CryptoFileSystemProvider.changePassphrase(pathToVault, masterkeyFilename, oldPassphrase, newPassphrase);
-
-		Assertions.assertThrows(InvalidPassphraseException.class, () -> {
-			newFileSystem( //
-					pathToVault, //
-					cryptoFileSystemProperties() //
-							.withMasterkeyFilename(masterkeyFilename) //
-							.withPassphrase(oldPassphrase) //
-							.build());
-		});
-	}
-
 	@Test
 	public void testGetFileSystemInvokesFileSystemsGetWithPathToVaultFromUri() {
 		Path pathToVault = get("a").toAbsolutePath();
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
index fa7cbd71..5452bf4c 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
@@ -71,7 +71,8 @@ public void testCreateWithPathComponents() throws URISyntaxException {
 	public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException {
 		Path tempDir = createTempDirectory("CryptoFileSystemUrisTest").toAbsolutePath();
 		try {
-			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, cryptoFileSystemProperties().withPassphrase("asd").build());
+			CryptoFileSystemProvider.initialize(tempDir, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 			Path absolutePathToVault = fileSystem.getPath("a").toAbsolutePath();
 
 			URI uri = CryptoFileSystemUri.create(absolutePathToVault, "a", "b");
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 56687d7b..9542102c 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -1,15 +1,24 @@
 package org.cryptomator.cryptofs;
 
+import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystemAlreadyExistsException;
 import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 
 import static org.hamcrest.Matchers.containsString;
 import static org.mockito.ArgumentMatchers.any;
@@ -19,26 +28,61 @@
 
 public class CryptoFileSystemsTest {
 
-	private final Path path = mock(Path.class);
-	private final Path normalizedPath = mock(Path.class);
+	private final Path pathToVault = mock(Path.class, "vaultPath");
+	private final Path normalizedPathToVault = mock(Path.class, "normalizedVaultPath");
+	private final Path configFilePath = mock(Path.class, "normalizedVaultPath/vault.cryptomator");
+	private final FileSystemCapabilityChecker capabilityChecker = mock(FileSystemCapabilityChecker.class);
 	private final CryptoFileSystemProvider provider = mock(CryptoFileSystemProvider.class);
 	private final CryptoFileSystemProperties properties = mock(CryptoFileSystemProperties.class);
 	private final CryptoFileSystemComponent cryptoFileSystemComponent = mock(CryptoFileSystemComponent.class);
 	private final CryptoFileSystemImpl cryptoFileSystem = mock(CryptoFileSystemImpl.class);
-
+	private final VaultConfig.VaultConfigLoader configLoader = mock(VaultConfig.VaultConfigLoader.class);
+	private final byte[] rawKey = new byte[64];
+	private final KeyLoader keyLoader = mock(KeyLoader.class);
+	private final VaultConfig vaultConfig = mock(VaultConfig.class);
+	private final VaultCipherMode cipherMode = mock(VaultCipherMode.class);
+	private final SecureRandom csprng = Mockito.mock(SecureRandom.class);
+	private final CryptorProvider cryptorProvider = mock(CryptorProvider.class);
+	private final Cryptor cryptor = mock(Cryptor.class);
 	private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder = mock(CryptoFileSystemComponent.Builder.class);
-	private final FileSystemCapabilityChecker capabilityChecker = mock(FileSystemCapabilityChecker.class);
 
-	private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker);
+
+	private MockedStatic<VaultConfig> vaultConficClass;
+	private MockedStatic<Files> filesClass;
+
+	private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker, csprng);
 
 	@BeforeEach
-	public void setup() {
-		when(cryptoFileSystemComponentBuilder.provider(any())).thenReturn(cryptoFileSystemComponentBuilder);
+	public void setup() throws IOException {
+		filesClass = Mockito.mockStatic(Files.class);
+		vaultConficClass = Mockito.mockStatic(VaultConfig.class);
+
+		when(pathToVault.normalize()).thenReturn(normalizedPathToVault);
+		when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath);
+		when(properties.vaultConfigFilename()).thenReturn("vault.cryptomator");
+		when(properties.keyLoader()).thenReturn(keyLoader);
+		filesClass.when(() -> Files.readString(configFilePath, StandardCharsets.US_ASCII)).thenReturn("jwt-vault-config");
+		vaultConficClass.when(() -> VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
+		when(VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
+		when(configLoader.getKeyId()).thenReturn("key-id");
+		when(keyLoader.loadKey("key-id")).thenReturn(rawKey);
+		when(configLoader.load(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
+		when(vaultConfig.getCiphermode()).thenReturn(cipherMode);
+		when(cipherMode.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
+		when(cryptorProvider.createFromRawKey(rawKey)).thenReturn(cryptor);
+		when(cryptoFileSystemComponentBuilder.cryptor(any())).thenReturn(cryptoFileSystemComponentBuilder);
+		when(cryptoFileSystemComponentBuilder.vaultConfig(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.pathToVault(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.properties(any())).thenReturn(cryptoFileSystemComponentBuilder);
+		when(cryptoFileSystemComponentBuilder.provider(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.build()).thenReturn(cryptoFileSystemComponent);
 		when(cryptoFileSystemComponent.cryptoFileSystem()).thenReturn(cryptoFileSystem);
-		when(path.normalize()).thenReturn(normalizedPath);
+	}
+
+	@AfterEach
+	public void tearDown() {
+		vaultConficClass.close();
+		filesClass.close();
 	}
 
 	@Test
@@ -48,46 +92,52 @@ public void testContainsReturnsFalseForNonContainedFileSystem() {
 
 	@Test
 	public void testContainsReturnsTrueForContainedFileSystem() throws IOException {
-		CryptoFileSystemImpl impl = inTest.create(provider, path, properties);
+		CryptoFileSystemImpl impl = inTest.create(provider, pathToVault, properties);
 
 		Assertions.assertSame(cryptoFileSystem, impl);
 		Assertions.assertTrue(inTest.contains(cryptoFileSystem));
-		verify(cryptoFileSystemComponentBuilder).provider(provider);
+		verify(cryptoFileSystemComponentBuilder).cryptor(cryptor);
+		verify(cryptoFileSystemComponentBuilder).vaultConfig(vaultConfig);
+		verify(cryptoFileSystemComponentBuilder).pathToVault(normalizedPathToVault);
 		verify(cryptoFileSystemComponentBuilder).properties(properties);
-		verify(cryptoFileSystemComponentBuilder).pathToVault(normalizedPath);
+		verify(cryptoFileSystemComponentBuilder).provider(provider);
 		verify(cryptoFileSystemComponentBuilder).build();
 	}
 
 	@Test
 	public void testCreateThrowsFileSystemAlreadyExistsExceptionIfInvokedWithSamePathTwice() throws IOException {
-		inTest.create(provider, path, properties);
+		inTest.create(provider, pathToVault, properties);
 
 		Assertions.assertThrows(FileSystemAlreadyExistsException.class, () -> {
-			inTest.create(provider, path, properties);
+			inTest.create(provider, pathToVault, properties);
 		});
 	}
 
 	@Test
 	public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIsRemovedBefore() throws IOException {
-		CryptoFileSystemImpl fileSystem = inTest.create(provider, path, properties);
-		inTest.remove(fileSystem);
+		CryptoFileSystemImpl fileSystem1 = inTest.create(provider, pathToVault, properties);
+		Assertions.assertTrue(inTest.contains(fileSystem1));
+		inTest.remove(fileSystem1);
+		Assertions.assertFalse(inTest.contains(fileSystem1));
 
-		inTest.create(provider, path, properties);
+		CryptoFileSystemImpl fileSystem2 = inTest.create(provider, pathToVault, properties);
+		Assertions.assertTrue(inTest.contains(fileSystem2));
 	}
 
 	@Test
 	public void testGetReturnsFileSystemForPathIfItExists() throws IOException {
-		inTest.create(provider, path, properties);
+		CryptoFileSystemImpl fileSystem = inTest.create(provider, pathToVault, properties);
 
-		Assertions.assertSame(cryptoFileSystem, inTest.get(path));
+		Assertions.assertTrue(inTest.contains(fileSystem));
+		Assertions.assertSame(cryptoFileSystem, inTest.get(pathToVault));
 	}
 
 	@Test
 	public void testThrowsFileSystemNotFoundExceptionIfItDoesNotExists() {
 		FileSystemNotFoundException e = Assertions.assertThrows(FileSystemNotFoundException.class, () -> {
-			inTest.get(path);
+			inTest.get(pathToVault);
 		});
-		MatcherAssert.assertThat(e.getMessage(), containsString(path.toString()));
+		MatcherAssert.assertThat(e.getMessage(), containsString(normalizedPathToVault.toString()));
 	}
 
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index d8d47c54..94f299dd 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -44,7 +44,8 @@ public class DeleteNonEmptyCiphertextDirectoryIntegrationTest {
 	public static void setupClass(@TempDir Path tmpDir) throws IOException {
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build());
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
index 9b7cf232..6e634c73 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
@@ -26,7 +26,8 @@ public class ReadmeCodeSamplesTest {
 
 	@Test
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException {
-		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, CryptoFileSystemProperties.cryptoFileSystemProperties().withPassphrase("password").build());
+		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 
 		runCodeSample(fileSystem);
 	}
@@ -34,7 +35,8 @@ public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path
 	@Test
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException {
 		URI uri = CryptoFileSystemUri.create(storageLocation);
-		FileSystem fileSystem = FileSystems.newFileSystem(uri, CryptoFileSystemProperties.cryptoFileSystemProperties().withPassphrase("password").build());
+		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+		FileSystem fileSystem = FileSystems.newFileSystem(uri, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 
 		runCodeSample(fileSystem);
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
index 8cd282b1..742c07a0 100644
--- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
@@ -32,7 +32,8 @@ public class RealFileSystemIntegrationTest {
 	public static void setupClass(@TempDir Path tmpDir) throws IOException {
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build());
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
new file mode 100644
index 00000000..3eec8355
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
@@ -0,0 +1,105 @@
+package org.cryptomator.cryptofs;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.JWTVerifier;
+import com.auth0.jwt.interfaces.Claim;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.Verification;
+import org.cryptomator.cryptofs.common.Constants;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+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.util.Arrays;
+
+public class VaultConfigTest {
+
+	@Test
+	public void testLoadMalformedToken() {
+		Assertions.assertThrows(VaultConfigLoadException.class, () -> {
+			VaultConfig.load("hello world", ignored -> new byte[64], 42);
+		});
+	}
+
+	@Nested
+	public class WithValidToken {
+
+		private byte[] key = new byte[64];
+		private VaultConfig originalConfig;
+		private String token;
+
+
+		@BeforeEach
+		public void setup() {
+			Arrays.fill(key, (byte) 0x55);
+			originalConfig = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(220).build();
+			token = originalConfig.toToken("TEST_KEY", key);
+		}
+
+		@Test
+		public void testSuccessfulLoad() throws VaultConfigLoadException, KeyLoadingFailedException {
+			var loaded = VaultConfig.load(token, ignored -> key, originalConfig.getVaultVersion());
+
+			Assertions.assertEquals(originalConfig.getId(), loaded.getId());
+			Assertions.assertEquals(originalConfig.getVaultVersion(), loaded.getVaultVersion());
+			Assertions.assertEquals(originalConfig.getCiphermode(), loaded.getCiphermode());
+			Assertions.assertEquals(originalConfig.getMaxFilenameLength(), loaded.getMaxFilenameLength());
+		}
+
+		@ParameterizedTest
+		@ValueSource(ints = {0, 1, 2, 3, 10, 20, 30, 63})
+		public void testLoadWithInvalidKey(int pos) {
+			key[pos] = (byte) 0x77;
+
+			Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
+				VaultConfig.load(token, ignored -> key, originalConfig.getVaultVersion());
+			});
+		}
+
+	}
+
+	@Test
+	public void testCreateNew() {
+		var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(220).build();
+
+		Assertions.assertNotNull(config.getId());
+		Assertions.assertEquals(Constants.VAULT_VERSION, config.getVaultVersion());
+		Assertions.assertEquals(VaultCipherMode.SIV_CTRMAC, config.getCiphermode());
+		Assertions.assertEquals(220, config.getMaxFilenameLength());
+	}
+
+	@Test
+	public void testLoadExisting() throws KeyLoadingFailedException, VaultConfigLoadException {
+		var decodedJwt = Mockito.mock(DecodedJWT.class);
+		var formatClaim = Mockito.mock(Claim.class);
+		var ciphermodeClaim = Mockito.mock(Claim.class);
+		var maxFilenameLenClaim = Mockito.mock(Claim.class);
+		var keyLoader = Mockito.mock(KeyLoader.class);
+		var verification = Mockito.mock(Verification.class);
+		var verifier = Mockito.mock(JWTVerifier.class);
+		Mockito.when(decodedJwt.getKeyId()).thenReturn("key-id");
+		Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim);
+		Mockito.when(decodedJwt.getClaim("ciphermode")).thenReturn(ciphermodeClaim);
+		Mockito.when(decodedJwt.getClaim("maxFilenameLen")).thenReturn(maxFilenameLenClaim);
+		Mockito.when(keyLoader.loadKey("key-id")).thenReturn(new byte[64]);
+		Mockito.when(verification.withClaim("format", 42)).thenReturn(verification);
+		Mockito.when(verification.build()).thenReturn(verifier);
+		Mockito.when(verifier.verify(decodedJwt)).thenReturn(decodedJwt);
+		Mockito.when(formatClaim.asInt()).thenReturn(42);
+		Mockito.when(ciphermodeClaim.asString()).thenReturn("SIV_CTRMAC");
+		Mockito.when(maxFilenameLenClaim.asInt()).thenReturn(220);
+		try (var jwtMock = Mockito.mockStatic(JWT.class)) {
+			jwtMock.when(() -> JWT.decode("jwt-vault-config")).thenReturn(decodedJwt);
+			jwtMock.when(() -> JWT.require(Mockito.any())).thenReturn(verification);
+
+			var config = VaultConfig.load("jwt-vault-config", keyLoader, 42);
+			Assertions.assertNotNull(config);
+			Assertions.assertEquals(42, config.getVaultVersion());
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
index ec60d06f..3a46c54f 100644
--- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
@@ -31,7 +31,8 @@ public void setup() throws IOException {
 		inMemoryFs = Jimfs.newFileSystem();
 		Path pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build());
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 		root = fileSystem.getPath("/");
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index 70eb5834..bd51fa80 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -56,7 +56,8 @@ public static void setupClass() throws IOException {
 		inMemoryFs = Jimfs.newFileSystem();
 		pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withPassphrase("asd").build());
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
 	}
 
 	@AfterAll
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 3b0acbac..dedd2af7 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -61,7 +61,7 @@ public void testMigrate() throws IOException {
 		Assertions.assertEquals("MASTERKEY_FILE", token.getKeyId());
 		Assertions.assertEquals(8, token.getClaim("format").asInt());
 		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("ciphermode").asString());
-		Assertions.assertEquals(220, token.getClaim("maxFileNameLen").asInt());
+		Assertions.assertEquals(220, token.getClaim("maxFilenameLen").asInt());
 	}
 
 }
\ No newline at end of file

From 7d2bd9891b6b90b73c0c8edadb629a5a106cd080 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 4 Dec 2020 12:16:27 +0100
Subject: [PATCH 06/70] Adjusted to new cryptolib API

---
 pom.xml                                       |   2 +-
 .../cryptofs/CryptoFileSystemModule.java      |   6 -
 .../cryptofs/CryptoFileSystemProperties.java  |  13 +-
 .../cryptofs/CryptoFileSystemProvider.java    |  18 +-
 .../cryptofs/CryptoFileSystems.java           |  30 +-
 .../org/cryptomator/cryptofs/KeyLoader.java   |  17 --
 .../cryptofs/KeyLoadingFailedException.java   |  15 -
 .../org/cryptomator/cryptofs/VaultConfig.java |  48 ++--
 .../cryptomator/cryptofs/fh/ChunkCache.java   |  29 +-
 .../cryptomator/cryptofs/fh/ChunkLoader.java  |   3 +-
 .../cryptofs/migration/Migration.java         |   9 +-
 .../migration/MigrationComponent.java         |  12 +
 .../cryptofs/migration/MigrationModule.java   |  30 +-
 .../cryptofs/migration/Migrators.java         |  93 ++++---
 .../api/MigrationContinuationListener.java    |   2 +
 .../api/MigrationProgressListener.java        |   2 +
 .../cryptofs/migration/api/Migrator.java      |  10 +-
 .../migration/v6/Version6Migrator.java        |  41 ++-
 .../migration/v7/Version7Migrator.java        |  29 +-
 .../migration/v8/Version8Migrator.java        |  27 +-
 .../cryptofs/CopyOperationTest.java           |   3 +-
 ...toFileChannelWriteReadIntegrationTest.java |  13 +-
 .../cryptofs/CryptoFileSystemImplTest.java    |   5 +-
 .../CryptoFileSystemPropertiesTest.java       |   3 +-
 ...yptoFileSystemProviderIntegrationTest.java |  31 ++-
 .../CryptoFileSystemProviderTest.java         |  24 +-
 .../cryptofs/CryptoFileSystemUriTest.java     |  10 +-
 .../cryptofs/CryptoFileSystemsTest.java       |  25 +-
 .../cryptomator/cryptofs/CryptoPathTest.java  |   3 +-
 ...ptyCiphertextDirectoryIntegrationTest.java |  10 +-
 .../cryptofs/MoveOperationTest.java           |   3 +-
 .../cryptofs/ReadmeCodeSamplesTest.java       |  17 +-
 .../RealFileSystemIntegrationTest.java        |  10 +-
 .../RootDirectoryInitializerTest.java         |   3 +-
 .../cryptomator/cryptofs/VaultConfigTest.java |  26 +-
 ...iteFileWhileReadonlyChannelIsOpenTest.java |  10 +-
 .../attr/FileAttributeIntegrationTest.java    |  10 +-
 .../cryptofs/dir/C9SInflatorTest.java         |   4 +-
 .../cryptofs/dir/C9rDecryptorTest.java        |   6 +-
 .../cryptofs/fh/ChunkCacheTest.java           |  27 +-
 .../cryptofs/fh/ChunkLoaderTest.java          |   7 +-
 .../cryptofs/fh/ChunkSaverTest.java           |   5 +-
 .../cryptofs/fh/FileHeaderHolderTest.java     |   7 +-
 .../migration/MigrationComponentTest.java     |  19 --
 .../cryptofs/migration/MigratorsTest.java     | 263 +++++++++++++-----
 .../migration/TestMigrationComponent.java     |  14 -
 .../migration/v6/Version6MigratorTest.java    |  45 +--
 .../migration/v7/Version7MigratorTest.java    |  55 ++--
 .../migration/v8/Version8MigratorTest.java    |  25 +-
 49 files changed, 641 insertions(+), 478 deletions(-)
 delete mode 100644 src/main/java/org/cryptomator/cryptofs/KeyLoader.java
 delete mode 100644 src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java
 delete mode 100644 src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java
 delete mode 100644 src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java

diff --git a/pom.xml b/pom.xml
index e89b0c79..956bd1ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependencies -->
-		<cryptolib.version>1.4.0</cryptolib.version>
+		<cryptolib.version>2.0.0-beta2</cryptolib.version>
 		<jwt.version>3.11.0</jwt.version>
 		<dagger.version>2.29.1</dagger.version>
 		<guava.version>30.0-jre</guava.version>
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
index 0276e590..eacc1972 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemModule.java
@@ -9,18 +9,12 @@
 import dagger.Provides;
 import org.cryptomator.cryptofs.attr.AttributeComponent;
 import org.cryptomator.cryptofs.attr.AttributeViewComponent;
-import org.cryptomator.cryptofs.common.Constants;
-import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
 import org.cryptomator.cryptofs.dir.DirectoryStreamComponent;
 import org.cryptomator.cryptofs.fh.OpenCryptoFileComponent;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.KeyFile;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.UncheckedIOException;
 import java.nio.file.FileStore;
 import java.nio.file.Files;
 import java.nio.file.Path;
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index b24475e5..05259575 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -10,6 +10,7 @@
 
 import com.google.common.base.Strings;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
 
 import java.net.URI;
 import java.nio.file.FileSystems;
@@ -59,7 +60,7 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 	 */
 	public static final String PROPERTY_KEYLOADER = "keyLoader";
 
-	static final KeyLoader DEFAULT_KEYLOADER = null;
+	static final MasterkeyLoader DEFAULT_KEYLOADER = null;
 
 
 	/**
@@ -118,8 +119,8 @@ private CryptoFileSystemProperties(Builder builder) {
 		);
 	}
 
-	KeyLoader keyLoader() {
-		return (KeyLoader) get(PROPERTY_KEYLOADER);
+	MasterkeyLoader keyLoader() {
+		return (MasterkeyLoader) get(PROPERTY_KEYLOADER);
 	}
 
 	@SuppressWarnings("unchecked")
@@ -195,7 +196,7 @@ public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
 	 */
 	public static class Builder {
 
-		private KeyLoader keyLoader = DEFAULT_KEYLOADER;
+		private MasterkeyLoader keyLoader = DEFAULT_KEYLOADER;
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
 		private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
@@ -206,7 +207,7 @@ private Builder() {
 		}
 
 		private Builder(Map<String, ?> properties) {
-			checkedSet(KeyLoader.class, PROPERTY_KEYLOADER, properties, this::withKeyLoader);
+			checkedSet(MasterkeyLoader.class, PROPERTY_KEYLOADER, properties, this::withKeyLoader);
 			checkedSet(String.class, PROPERTY_VAULTCONFIG_FILENAME, properties, this::withVaultConfigFilename);
 			checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
@@ -257,7 +258,7 @@ public Builder withMaxNameLength(int maxNameLength) {
 		 * @return this
 		 * @since 2.0.0
 		 */
-		public Builder withKeyLoader(KeyLoader keyLoader) {
+		public Builder withKeyLoader(MasterkeyLoader keyLoader) {
 			this.keyLoader = keyLoader;
 			return this;
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index aff67f11..18056194 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -11,6 +11,9 @@
 import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
 import java.io.IOException;
 import java.net.URI;
@@ -120,16 +123,17 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP
 	 * @param keyId               The ID of the key to associate with this vault
 	 * @param keyLoader           A key loader providing the masterkey for this new vault
 	 * @throws NotDirectoryException                                  If the given path is not an existing directory.
-	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
 	 * @throws IOException                                            If the vault structure could not be initialized due to I/O errors
+	 * @throws MasterkeyLoadingFailedException
 	 * @since 2.0.0
 	 */
-	public static void initialize(Path pathToVault, String vaultConfigFilename, String keyId, KeyLoader keyLoader) throws NotDirectoryException, IOException {
+	public static void initialize(Path pathToVault, String vaultConfigFilename, String keyId, MasterkeyLoader keyLoader) throws NotDirectoryException, IOException, MasterkeyLoadingFailedException {
 		if (!Files.isDirectory(pathToVault)) {
 			throw new NotDirectoryException(pathToVault.toString());
 		}
-		byte[] rawKey = keyLoader.loadKey(keyId);
-		try {
+		byte[] rawKey = new byte[0];
+		try (Masterkey key = keyLoader.loadKey(keyId)) {
+			rawKey = key.getEncoded();
 			// save vault config:
 			Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
 			var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(Constants.MAX_CIPHERTEXT_NAME_LENGTH).build();
@@ -175,7 +179,11 @@ public String getScheme() {
 	public CryptoFileSystem newFileSystem(URI uri, Map<String, ?> rawProperties) throws IOException {
 		CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
-		return fileSystems.create(this, parsedUri.pathToVault(), properties);
+		try {
+			return fileSystems.create(this, parsedUri.pathToVault(), properties);
+		} catch (MasterkeyLoadingFailedException e) {
+			throw new IOException("Used invalid key to init filesystem.", e);
+		}
 	}
 
 	@Override
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 085ba9c3..12e90ecc 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -3,6 +3,8 @@
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -26,7 +28,7 @@
 
 @Singleton
 class CryptoFileSystems {
-	
+
 	private static final Logger LOG = LoggerFactory.getLogger(CryptoFileSystems.class);
 
 	private final ConcurrentMap<Path, CryptoFileSystemImpl> fileSystems = new ConcurrentHashMap<>();
@@ -41,18 +43,19 @@ public CryptoFileSystems(CryptoFileSystemComponent.Builder cryptoFileSystemCompo
 		this.csprng = csprng;
 	}
 
-	public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException {
+	public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties) throws IOException, MasterkeyLoadingFailedException {
 		Path normalizedPathToVault = pathToVault.normalize();
 		var token = readVaultConfigFile(normalizedPathToVault, properties);
 
 		var configLoader = VaultConfig.decode(token);
-		byte[] rawKey = properties.keyLoader().loadKey(configLoader.getKeyId());
-		try {
-			var config = configLoader.load(rawKey, Constants.VAULT_VERSION);
+		byte[] rawKey = new byte[0];
+		try (Masterkey key = properties.keyLoader().loadKey(configLoader.getKeyId())) {
+			rawKey = key.getEncoded();
+			var config = configLoader.verify(rawKey, Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
-			return fileSystems.compute(normalizedPathToVault, (key, value) -> {
-				if (value == null) {
-					return create(provider, normalizedPathToVault, adjustedProperties, rawKey, config);
+			return fileSystems.compute(normalizedPathToVault, (path, fs) -> {
+				if (fs == null) {
+					return create(provider, normalizedPathToVault, adjustedProperties, key, config);
 				} else {
 					throw new FileSystemAlreadyExistsException();
 				}
@@ -63,8 +66,8 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 	}
 
 	// synchronized access to non-threadsafe cryptoFileSystemComponentBuilder required
-	private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, byte[] rawKey, VaultConfig config) {
-		Cryptor cryptor = config.getCiphermode().getCryptorProvider(csprng).createFromRawKey(rawKey);
+	private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, Masterkey masterkey, VaultConfig config) {
+		Cryptor cryptor = config.getCiphermode().getCryptorProvider(csprng).withKey(masterkey);
 		return cryptoFileSystemComponentBuilder //
 				.cryptor(cryptor) //
 				.vaultConfig(config) //
@@ -77,10 +80,11 @@ private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provid
 
 	/**
 	 * Attempts to read a vault config file
+	 *
 	 * @param pathToVault path to the vault's root
-	 * @param properties properties used when attempting to construct a fs for this vault
+	 * @param properties  properties used when attempting to construct a fs for this vault
 	 * @return The contents of the file decoded in ASCII
-	 * @throws IOException If the file could not be read
+	 * @throws IOException                       If the file could not be read
 	 * @throws FileSystemNeedsMigrationException If the file doesn't exists, but a legacy masterkey file was found instead
 	 */
 	private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException {
@@ -97,7 +101,7 @@ private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties
 			}
 		}
 	}
-	
+
 	private CryptoFileSystemProperties adjustForCapabilities(Path pathToVault, CryptoFileSystemProperties originalProperties) throws FileSystemCapabilityChecker.MissingCapabilityException {
 		if (!originalProperties.readonly()) {
 			try {
diff --git a/src/main/java/org/cryptomator/cryptofs/KeyLoader.java b/src/main/java/org/cryptomator/cryptofs/KeyLoader.java
deleted file mode 100644
index 9e2e5d16..00000000
--- a/src/main/java/org/cryptomator/cryptofs/KeyLoader.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package org.cryptomator.cryptofs;
-
-@FunctionalInterface
-public interface KeyLoader {
-
-	/**
-	 * Loads a key required to unlock a vault.
-	 * <p>
-	 * This might be a long-running operation, as it may require user input or expensive computations.
-	 *
-	 * @param keyId a string uniquely identifying the source of the key and its identity, if multiple keys can be obtained from the same source
-	 * @return The raw key bytes. Must not be null
-	 * @throws KeyLoadingFailedException Thrown when it is impossible to fulfill the request
-	 */
-	byte[] loadKey(String keyId) throws KeyLoadingFailedException;
-
-}
diff --git a/src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java b/src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java
deleted file mode 100644
index 9532ea33..00000000
--- a/src/main/java/org/cryptomator/cryptofs/KeyLoadingFailedException.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.cryptomator.cryptofs;
-
-import java.io.IOException;
-
-/**
- * Thrown by a {@link KeyLoader} when loading a key required to unlock a vault failed.
- * <p>
- * Possible reasons for this exception are: Unsupported key type, key for given id not found, user cancelled key entry, ...
- */
-public class KeyLoadingFailedException extends FileSystemInitializationFailedException {
-
-	public KeyLoadingFailedException(String message, Throwable cause) {
-		super(message, cause);
-	}
-}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
index 8c660fd7..be4c342d 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -8,20 +8,23 @@
 import com.auth0.jwt.exceptions.SignatureVerificationException;
 import com.auth0.jwt.interfaces.DecodedJWT;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
 import java.util.Arrays;
 import java.util.UUID;
 
 /**
  * Typesafe representation of vault configuration files.
- *
+ * <p>
  * To prevent config tampering, such as downgrade attacks, vault configurations are cryptographically signed using HMAC-256
  * with the vault's 64 byte master key.
- *
+ * <p>
  * If the signature could be successfully verified, the configuration can be assumed valid and the masterkey can be assumed
  * eligible for the vault.
- *
- * When {@link #load(String, KeyLoader, int) loading} a vault configuration, a key must be provided and the signature is checked.
+ * <p>
+ * When {@link #load(String, MasterkeyLoader, int) loading} a vault configuration, a key must be provided and the signature is checked.
  * It is impossible to create an instance of this class from an existing configuration without signature verification.
  */
 public class VaultConfig {
@@ -76,21 +79,21 @@ public String toToken(String keyId, byte[] rawKey) {
 	}
 
 	/**
-	 * Convenience wrapper for {@link #decode(String)} and {@link VaultConfigLoader#load(byte[], int)}
+	 * Convenience wrapper for {@link #decode(String)} and {@link UnverifiedVaultConfig#verify(byte[], int)}
 	 *
 	 * @param token                The token
 	 * @param keyLoader            A key loader capable of providing a key for this token
 	 * @param expectedVaultVersion The vault version this token should contain
 	 * @return The decoded configuration
-	 * @throws KeyLoadingFailedException     If the key loader was unable to provide a key for this vault configuration
-	 * @throws VaultConfigLoadException When loading the configuration fails (see {@link VaultConfigLoader#load(String, KeyLoader, int)} for details
+	 * @throws MasterkeyLoadingFailedException If the key loader was unable to provide a key for this vault configuration
+	 * @throws VaultConfigLoadException        When loading the configuration fails
 	 */
-	public static VaultConfig load(String token, KeyLoader keyLoader, int expectedVaultVersion) throws KeyLoadingFailedException, VaultConfigLoadException {
+	public static VaultConfig load(String token, MasterkeyLoader keyLoader, int expectedVaultVersion) throws MasterkeyLoadingFailedException, VaultConfigLoadException {
+		var configLoader = decode(token);
 		byte[] rawKey = new byte[0];
-		try {
-			var configLoader = decode(token);
-			rawKey = keyLoader.loadKey(configLoader.getKeyId());
-			return configLoader.load(rawKey, expectedVaultVersion);
+		try (Masterkey key = keyLoader.loadKey(configLoader.getKeyId())) {
+			rawKey = key.getEncoded();
+			return configLoader.verify(rawKey, expectedVaultVersion);
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}
@@ -99,13 +102,13 @@ public static VaultConfig load(String token, KeyLoader keyLoader, int expectedVa
 	/**
 	 * Decodes a vault configuration stored in JWT format to load it
 	 *
-	 * @param token                The token
+	 * @param token The token
 	 * @return A loader object that allows loading the configuration (if providing the required key)
 	 * @throws VaultConfigLoadException When parsing the token failed
 	 */
-	public static VaultConfigLoader decode(String token) throws VaultConfigLoadException {
+	public static UnverifiedVaultConfig decode(String token) throws VaultConfigLoadException {
 		try {
-			return new VaultConfigLoader(JWT.decode(token));
+			return new UnverifiedVaultConfig(JWT.decode(token));
 		} catch (JWTDecodeException e) {
 			throw new VaultConfigLoadException("Failed to parse config: " + token);
 		}
@@ -120,21 +123,28 @@ public static VaultConfigBuilder createNew() {
 		return new VaultConfigBuilder();
 	}
 
-	public static class VaultConfigLoader {
+	public static class UnverifiedVaultConfig {
 
 		private final DecodedJWT unverifiedConfig;
 
-		private VaultConfigLoader(DecodedJWT unverifiedConfig) {
+		private UnverifiedVaultConfig(DecodedJWT unverifiedConfig) {
 			this.unverifiedConfig = unverifiedConfig;
 		}
 
 		/**
-		 * @return The ID of the key required to {@link #load(byte[], int) load} this config.
+		 * @return The ID of the key required to {@link #verify(byte[], int) load} this config
 		 */
 		public String getKeyId() {
 			return unverifiedConfig.getKeyId();
 		}
 
+		/**
+		 * @return The unverified vault version (JWT signature not verified)
+		 */
+		public int allegedVaultVersion() {
+			return unverifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt();
+		}
+
 		/**
 		 * Decodes a vault configuration stored in JWT format.
 		 *
@@ -145,7 +155,7 @@ public String getKeyId() {
 		 * @throws VaultVersionMismatchException If the token did not match the expected vault version
 		 * @throws VaultConfigLoadException      Generic parse error
 		 */
-		public VaultConfig load(byte[] rawKey, int expectedVaultVersion) throws VaultKeyInvalidException, VaultVersionMismatchException, VaultConfigLoadException {
+		public VaultConfig verify(byte[] rawKey, int expectedVaultVersion) throws VaultKeyInvalidException, VaultVersionMismatchException, VaultConfigLoadException {
 			try {
 				var verifier = JWT.require(Algorithm.HMAC256(rawKey)) //
 						.withClaim(JSON_KEY_VAULTVERSION, expectedVaultVersion) //
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java b/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java
index 67f6419e..2963af85 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/ChunkCache.java
@@ -1,5 +1,7 @@
 package org.cryptomator.cryptofs.fh;
 
+import com.google.common.base.Throwables;
+import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
@@ -21,7 +23,7 @@ public class ChunkCache {
 	private final ChunkLoader chunkLoader;
 	private final ChunkSaver chunkSaver;
 	private final CryptoFileSystemStats stats;
-	private final LoadingCache<Long, ChunkData> chunks;
+	private final Cache<Long, ChunkData> chunks;
 
 	@Inject
 	public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSystemStats stats) {
@@ -31,15 +33,16 @@ public ChunkCache(ChunkLoader chunkLoader, ChunkSaver chunkSaver, CryptoFileSyst
 		this.chunks = CacheBuilder.newBuilder() //
 				.maximumSize(MAX_CACHED_CLEARTEXT_CHUNKS) //
 				.removalListener(this::removeChunk) //
-				.build(CacheLoader.from(this::loadChunk));
+				.build();
 	}
 
-	private ChunkData loadChunk(Long chunkIndex) {
+	private ChunkData loadChunk(long chunkIndex) throws IOException {
+		stats.addChunkCacheMiss();
 		try {
-			stats.addChunkCacheMiss();
 			return chunkLoader.load(chunkIndex);
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
+		} catch (AuthenticationFailedException e) {
+			// TODO provide means to pass an AuthenticationFailedException handler using an OpenOption
+			throw new IOException("Unauthentic ciphertext in chunk " + chunkIndex, e);
 		}
 	}
 
@@ -54,20 +57,10 @@ private void removeChunk(RemovalNotification<Long, ChunkData> removal) {
 	public ChunkData get(long chunkIndex) throws IOException {
 		try {
 			stats.addChunkCacheAccess();
-			return chunks.get(chunkIndex);
+			return chunks.get(chunkIndex, () -> loadChunk(chunkIndex));
 		} catch (ExecutionException e) {
-			assert e.getCause() != null; // no exception in ChunkLoader -> no executionException during chunk loading ;-)
+			assert e.getCause() instanceof IOException; // the only checked exception thrown by #loadChunk(long)
 			throw (IOException) e.getCause();
-		} catch (UncheckedExecutionException e) {
-			if (e.getCause() instanceof UncheckedIOException) {
-				UncheckedIOException uioe = (UncheckedIOException) e.getCause();
-				throw uioe.getCause();
-			} else if (e.getCause() instanceof AuthenticationFailedException) {
-				// TODO provide means to pass an AuthenticationFailedException handler using an OpenOption
-				throw new IOException(e.getCause());
-			} else {
-				throw e;
-			}
 		}
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java b/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java
index fb7e9157..278fa68b 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/ChunkLoader.java
@@ -1,6 +1,7 @@
 package org.cryptomator.cryptofs.fh;
 
 import org.cryptomator.cryptofs.CryptoFileSystemStats;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
 import org.cryptomator.cryptolib.api.Cryptor;
 
 import javax.inject.Inject;
@@ -23,7 +24,7 @@ public ChunkLoader(Cryptor cryptor, ChunkIO ciphertext, FileHeaderHolder headerH
 		this.stats = stats;
 	}
 
-	public ChunkData load(Long chunkIndex) throws IOException {
+	public ChunkData load(Long chunkIndex) throws IOException, AuthenticationFailedException {
 		stats.addChunkCacheMiss();
 		int payloadSize = cryptor.fileContentCryptor().cleartextChunkSize();
 		int chunkSize = cryptor.fileContentCryptor().ciphertextChunkSize();
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java
index c214ca2d..3180e37e 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/Migration.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migration.java
@@ -17,9 +17,14 @@ enum Migration {
 	FIVE_TO_SIX(5),
 
 	/**
-	 * Migrates vault format 5 to 6.
+	 * Migrates vault format 6 to 7.
+	 */
+	SIX_TO_SEVEN(6),
+
+	/**
+	 * Migrates vault format 7 to 8
 	 */
-	SIX_TO_SEVEN(6);
+	SEVEN_TO_EIGHT(7);
 
 	private final int applicableVersion;
 
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java
index 35165dde..ce4126fb 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationComponent.java
@@ -5,11 +5,23 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.migration;
 
+import dagger.BindsInstance;
 import dagger.Component;
 
+import java.security.SecureRandom;
+
 @Component(modules = {MigrationModule.class})
 interface MigrationComponent {
 
 	Migrators migrators();
 
+	@Component.Builder
+	interface Builder {
+
+		@BindsInstance
+		Builder csprng(SecureRandom csprng);
+
+		MigrationComponent build();
+	}
+
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
index 0d700add..8b777654 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
@@ -5,10 +5,6 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.migration;
 
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
 import dagger.MapKey;
 import dagger.Module;
 import dagger.Provides;
@@ -17,25 +13,26 @@
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.migration.v6.Version6Migrator;
 import org.cryptomator.cryptofs.migration.v7.Version7Migrator;
+import org.cryptomator.cryptofs.migration.v8.Version8Migrator;
+import org.cryptomator.cryptolib.Cryptors;
 import org.cryptomator.cryptolib.api.CryptorProvider;
 
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.security.SecureRandom;
+
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 @Module
 class MigrationModule {
 
-	private final CryptorProvider version1Cryptor;
-
-	MigrationModule(CryptorProvider version1Cryptor) {
-		this.version1Cryptor = version1Cryptor;
-	}
-
 	@Provides
-	CryptorProvider provideVersion1CryptorProvider() {
-		return version1Cryptor;
+	CryptorProvider provideVersion1CryptorProvider(SecureRandom csprng) {
+		return Cryptors.version1(csprng);
 	}
-	
+
 	@Provides
 	FileSystemCapabilityChecker provideFileSystemCapabilityChecker() {
 		return new FileSystemCapabilityChecker();
@@ -55,6 +52,13 @@ Migrator provideVersion7Migrator(Version7Migrator migrator) {
 		return migrator;
 	}
 
+	@Provides
+	@IntoMap
+	@MigratorKey(Migration.SEVEN_TO_EIGHT)
+	Migrator provideVersion8Migrator(Version8Migrator migrator) {
+		return migrator;
+	}
+
 	@Documented
 	@Target(METHOD)
 	@Retention(RUNTIME)
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
index 899da75d..02704807 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
@@ -5,16 +5,7 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.migration;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
-import java.util.Map;
-import java.util.Optional;
-
-import javax.inject.Inject;
-
+import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
@@ -22,30 +13,38 @@
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
 import org.cryptomator.cryptolib.Cryptors;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.KeyFile;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+import java.util.Optional;
 
 /**
  * Used to perform migration from an older vault format to a newer one.
  * <p>
  * Example Usage:
- * 
+ *
  * <pre>
  * <code>
- * if (Migrators.get().{@link #needsMigration(Path, String) needsMigration(pathToVault, masterkeyFileName)}) {
+ * if (Migrators.get().{@link #needsMigration(Path, String, String)} needsMigration(pathToVault, vaultConfigFilename, masterkeyFileName)}) {
  * 	Migrators.get().{@link #migrate(Path, String, String, CharSequence, MigrationProgressListener, MigrationContinuationListener) migrate(pathToVault, masterkeyFileName, passphrase, progressListener, continuationListener)};
  * }
  * </code>
  * </pre>
- * 
+ *
  * @since 1.4.0
  */
 public class Migrators {
 
-	private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder() //
-			.migrationModule(new MigrationModule(Cryptors.version1(strongSecureRandom()))) //
-			.build();
+	private static final MigrationComponent COMPONENT = DaggerMigrationComponent.builder().csprng(strongSecureRandom()).build();
 
 	private final Map<Migration, Migrator> migrators;
 	private final FileSystemCapabilityChecker fsCapabilityChecker;
@@ -70,46 +69,37 @@ public static Migrators get() {
 
 	/**
 	 * Inspects the vault and checks if it is supported by this library.
-	 * 
-	 * @param pathToVault Path to the vault's root
-	 * @param masterkeyFilename Name of the masterkey file located in the vault
+	 *
+	 * @param pathToVault         Path to the vault's root
+	 * @param vaultConfigFilename Name of the vault config file located in the vault
+	 * @param masterkeyFilename   Name of the masterkey file optionally located in the vault
 	 * @return <code>true</code> if the vault at the given path is of an older format than supported by this library
 	 * @throws IOException if an I/O error occurs parsing the masterkey file
 	 */
-	public boolean needsMigration(Path pathToVault, String masterkeyFilename) throws IOException {
-		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
-		byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
-		try {
-			KeyFile keyFile = KeyFile.parse(keyFileContents);
-			return keyFile.getVersion() < Constants.VAULT_VERSION;
-		} catch (IllegalArgumentException e) {
-			throw new IOException("Malformed masterkey file " + masterKeyPath, e);
-		}
+	public boolean needsMigration(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
+		int vaultVersion = determineVaultVersion(pathToVault, vaultConfigFilename, masterkeyFilename);
+		return vaultVersion < Constants.VAULT_VERSION;
 	}
 
 	/**
 	 * Performs the actual migration. This task may take a while and this method will block.
-	 * 
-	 * @param pathToVault Path to the vault's root
-	 * @param vaultConfigFilename Name of the vault config file located inside <code>pathToVault</code>
-	 * @param masterkeyFilename Name of the masterkey file located inside <code>pathToVault</code>
-	 * @param passphrase The passphrase needed to unlock the vault
-	 * @param progressListener Listener that will get notified of progress updates
+	 *
+	 * @param pathToVault          Path to the vault's root
+	 * @param vaultConfigFilename  Name of the vault config file located inside <code>pathToVault</code>
+	 * @param masterkeyFilename    Name of the masterkey file located inside <code>pathToVault</code>
+	 * @param passphrase           The passphrase needed to unlock the vault
+	 * @param progressListener     Listener that will get notified of progress updates
 	 * @param continuationListener Listener that will get asked if there are events that require feedback
-	 * @throws NoApplicableMigratorException If the vault can not be migrated, because no migrator could be found
-	 * @throws InvalidPassphraseException If the passphrase could not be used to unlock the vault
+	 * @throws NoApplicableMigratorException                          If the vault can not be migrated, because no migrator could be found
+	 * @throws InvalidPassphraseException                             If the passphrase could not be used to unlock the vault
 	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
-	 * @throws IOException if an I/O error occurs migrating the vault
+	 * @throws IOException                                            if an I/O error occurs migrating the vault
 	 */
-	public void migrate(Path pathToVault, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
+	public void migrate(Path pathToVault, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws NoApplicableMigratorException, CryptoException, IOException {
 		fsCapabilityChecker.assertAllCapabilities(pathToVault);
-		
-		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
-		byte[] keyFileContents = Files.readAllBytes(masterKeyPath);
-		KeyFile keyFile = KeyFile.parse(keyFileContents);
-
+		int vaultVersion = determineVaultVersion(pathToVault, vaultConfigFilename, masterkeyFilename);
 		try {
-			Migrator migrator = findApplicableMigrator(keyFile.getVersion()).orElseThrow(NoApplicableMigratorException::new);
+			Migrator migrator = findApplicableMigrator(vaultVersion).orElseThrow(NoApplicableMigratorException::new);
 			migrator.migrate(pathToVault, vaultConfigFilename, masterkeyFilename, passphrase, progressListener, continuationListener);
 		} catch (UnsupportedVaultFormatException e) {
 			// might be a tampered masterkey file, as this exception is also thrown if the vault version MAC is not authentic.
@@ -117,6 +107,19 @@ public void migrate(Path pathToVault, String vaultConfigFilename, String masterk
 		}
 	}
 
+	private int determineVaultVersion(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
+		Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
+		Path masterKeyPath = pathToVault.resolve(masterkeyFilename);
+		if (Files.exists(vaultConfigPath)) {
+			var jwt = Files.readString(vaultConfigPath);
+			return VaultConfig.decode(jwt).allegedVaultVersion();
+		} else if (Files.exists(masterKeyPath)) {
+			return MasterkeyFile.withContentFromFile(masterKeyPath).allegedVaultVersion();
+		} else {
+			throw new IOException("Did not find " + vaultConfigFilename + " nor " + masterkeyFilename);
+		}
+	}
+
 	private Optional<Migrator> findApplicableMigrator(int version) {
 		return migrators.entrySet().stream().filter(entry -> entry.getKey().isApplicable(version)).map(Map.Entry::getValue).findAny();
 	}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java
index a531565f..4f08321e 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationContinuationListener.java
@@ -3,6 +3,8 @@
 @FunctionalInterface
 public interface MigrationContinuationListener {
 
+	MigrationContinuationListener CANCEL_ALWAYS = event -> ContinuationResult.CANCEL;
+
 	/**
 	 * Invoked when the migration requires action.
 	 * <p>
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java
index 9fe9ef68..c8cc82e9 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/api/MigrationProgressListener.java
@@ -3,6 +3,8 @@
 @FunctionalInterface
 public interface MigrationProgressListener {
 
+	MigrationProgressListener IGNORE = (state, progress) -> {};
+
 	/**
 	 * Called on every step during migration that might change the progress.
 	 *
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
index d1bc9ba8..e933234c 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/api/Migrator.java
@@ -8,6 +8,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
 
@@ -25,9 +26,10 @@ public interface Migrator {
 	 * @param passphrase
 	 * @throws InvalidPassphraseException
 	 * @throws UnsupportedVaultFormatException
+	 * @throws CryptoException
 	 * @throws IOException
 	 */
-	default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase) throws InvalidPassphraseException, UnsupportedVaultFormatException, CryptoException, IOException {
 		migrate(vaultRoot, vaultConfigFilename, masterkeyFilename, passphrase, (state, progress) -> {});
 	}
 
@@ -41,9 +43,10 @@ default void migrate(Path vaultRoot, String vaultConfigFilename, String masterke
 	 * @param progressListener 
 	 * @throws InvalidPassphraseException
 	 * @throws UnsupportedVaultFormatException
+	 * @throws CryptoException
 	 * @throws IOException
 	 */
-	default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	default void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, CryptoException, IOException {
 		migrate(vaultRoot, vaultConfigFilename, masterkeyFilename, passphrase, progressListener, (event) -> MigrationContinuationListener.ContinuationResult.CANCEL);
 	}
 
@@ -58,8 +61,9 @@ default void migrate(Path vaultRoot, String vaultConfigFilename, String masterke
 	 * @param continuationListener
 	 * @throws InvalidPassphraseException
 	 * @throws UnsupportedVaultFormatException
+	 * @throws CryptoException
 	 * @throws IOException
 	 */
-	void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException;
+	void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, CryptoException, IOException;
 
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
index 9d12fb4b..99527464 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
@@ -5,27 +5,26 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.migration.v6;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
-import java.text.Normalizer;
-import java.text.Normalizer.Form;
-
-import javax.inject.Inject;
-
 import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
 import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
 import org.cryptomator.cryptofs.migration.api.Migrator;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.KeyFile;
-import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.security.SecureRandom;
+import java.text.Normalizer;
+import java.text.Normalizer.Form;
+import java.util.Optional;
+
 /**
  * Updates masterkey.cryptomator:
  *
@@ -35,21 +34,21 @@ public class Version6Migrator implements Migrator {
 
 	private static final Logger LOG = LoggerFactory.getLogger(Version6Migrator.class);
 
-	private final CryptorProvider cryptorProvider;
+	private final SecureRandom csprng;
 
 	@Inject
-	public Version6Migrator(CryptorProvider cryptorProvider) {
-		this.cryptorProvider = cryptorProvider;
+	public Version6Migrator(SecureRandom csprng) {
+		this.csprng = csprng;
 	}
 
 	@Override
-	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException {
 		LOG.info("Upgrading {} from version 5 to version 6.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
 		byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
-		KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
-		try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 5)) {
+		MasterkeyFile keyFile = MasterkeyFile.withContentFromFile(masterkeyFile);
+		try (Masterkey masterkey = keyFile.unlock(passphrase, new byte[0], Optional.of(5)).loadKeyAndClose()) {
 			// create backup, as soon as we know the password was correct:
 			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
@@ -57,7 +56,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
 			
 			// rewrite masterkey file with normalized passphrase:
-			byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(Normalizer.normalize(passphrase, Form.NFC), 6).serialize();
+			byte[] fileContentsAfterUpgrade = MasterkeyFile.lock(masterkey, Normalizer.normalize(passphrase, Form.NFC), new byte[0], 6, csprng);
 			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
 			LOG.info("Updated masterkey.");
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index 61b6f0b9..e041282d 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -14,11 +14,9 @@
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
 import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
 import org.cryptomator.cryptofs.migration.api.Migrator;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.KeyFile;
-import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -28,7 +26,9 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
+import java.security.SecureRandom;
 import java.util.EnumSet;
+import java.util.Optional;
 
 /**
  * Renames ciphertext names:
@@ -38,7 +38,7 @@
  *     <li>Dirs: 0BASE32== -> base64==.c9r/dir.c9r</li>
  *     <li>Symlinks: 1SBASE32== -> base64.c9r/symlink.c9r</li>
  * </ul>
- *
+ * <p>
  * Shortened names:
  * <ul>
  *     <li>shortened.lng -> shortened.c9s</li>
@@ -49,25 +49,24 @@ public class Version7Migrator implements Migrator {
 
 	private static final Logger LOG = LoggerFactory.getLogger(Version7Migrator.class);
 
-	private final CryptorProvider cryptorProvider;
+	private final SecureRandom csprng;
 
 	@Inject
-	public Version7Migrator(CryptorProvider cryptorProvider) {
-		this.cryptorProvider = cryptorProvider;
+	public Version7Migrator(SecureRandom csprng) {
+		this.csprng = csprng;
 	}
 
 	@Override
-	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException {
 		LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
-		byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
-		KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
-		try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 6)) {
+		MasterkeyFile keyFile = MasterkeyFile.withContentFromFile(masterkeyFile);
+		try (Masterkey masterkey = keyFile.unlock(passphrase, new byte[0], Optional.of(6)).loadKeyAndClose()) {
 			// create backup, as soon as we know the password was correct:
 			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
-			
+
 			// check file system capabilities:
 			int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(vaultRoot.resolve("c"), 46, 28, 220);
 			int pathLengthLimit = filenameLengthLimit + 48; // TODO
@@ -118,7 +117,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			Files.walkFileTree(vaultRoot.resolve("m"), DeletingFileVisitor.INSTANCE);
 
 			// rewrite masterkey file with normalized passphrase:
-			byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(passphrase, 7).serialize();
+			byte[] fileContentsAfterUpgrade = MasterkeyFile.lock(masterkey, passphrase, new byte[0], 7, csprng);
 			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
 			LOG.info("Updated masterkey.");
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 4f6980d7..455622f6 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -11,11 +11,9 @@
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
 import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
 import org.cryptomator.cryptofs.migration.api.Migrator;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
-import org.cryptomator.cryptolib.api.KeyFile;
-import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -25,7 +23,9 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
+import java.security.SecureRandom;
 import java.util.Arrays;
+import java.util.Optional;
 import java.util.UUID;
 
 /**
@@ -40,29 +40,28 @@ public class Version8Migrator implements Migrator {
 
 	private static final Logger LOG = LoggerFactory.getLogger(Version8Migrator.class);
 
-	private final CryptorProvider cryptorProvider;
+	private final SecureRandom csprng;
 
 	@Inject
-	public Version8Migrator(CryptorProvider cryptorProvider) {
-		this.cryptorProvider = cryptorProvider;
+	public Version8Migrator(SecureRandom csprng) {
+		this.csprng = csprng;
 	}
 
 	@Override
-	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws InvalidPassphraseException, UnsupportedVaultFormatException, IOException {
+	public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkeyFilename, CharSequence passphrase, MigrationProgressListener progressListener, MigrationContinuationListener continuationListener) throws CryptoException, IOException {
 		LOG.info("Upgrading {} from version 7 to version 8.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
 		Path vaultConfigFile = vaultRoot.resolve(vaultConfigFilename);
-		byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
 		byte[] rawKey = new byte[0];
-		KeyFile keyFile = KeyFile.parse(fileContentsBeforeUpgrade);
-		try (Cryptor cryptor = cryptorProvider.createFromKeyFile(keyFile, passphrase, 7)) {
+		MasterkeyFile keyFile = MasterkeyFile.withContentFromFile(masterkeyFile);
+		try (Masterkey masterkey = keyFile.unlock(passphrase, new byte[0], Optional.of(7)).loadKeyAndClose()) {
 			// create backup, as soon as we know the password was correct:
 			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
 
 			// create vaultconfig.cryptomator
-			rawKey = cryptor.getRawKey();
+			rawKey = masterkey.getEncoded();
 			Algorithm algorithm = Algorithm.HMAC256(rawKey);
 			var config = JWT.create() //
 					.withJWTId(UUID.randomUUID().toString()) //
@@ -77,7 +76,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
 
 			// rewrite masterkey file with normalized passphrase:
-			byte[] fileContentsAfterUpgrade = cryptor.writeKeysToMasterkeyFile(passphrase, 999).serialize();
+			byte[] fileContentsAfterUpgrade = MasterkeyFile.lock(masterkey, passphrase, new byte[0], 999, csprng);
 			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
 			LOG.info("Updated masterkey.");
 		} finally {
diff --git a/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java b/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java
index 283149e4..bd0c71aa 100644
--- a/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CopyOperationTest.java
@@ -25,6 +25,7 @@
 import static org.cryptomator.cryptofs.util.ByteBuffers.repeat;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
@@ -54,7 +55,7 @@ public void setup() {
 	public void testCopyWithEqualPathDoesNothing() throws IOException {
 		inTest.copy(aPathFromFsA, aPathFromFsA);
 
-		verifyZeroInteractions(aPathFromFsA);
+		verifyNoInteractions(aPathFromFsA);
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 627e2b54..46f4a0a1 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -9,6 +9,9 @@
 package org.cryptomator.cryptofs;
 
 import com.google.common.jimfs.Jimfs;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
@@ -60,7 +63,8 @@ public class Windows {
 
 		@BeforeAll
 		public void setupClass(@TempDir Path tmpDir) throws IOException {
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 		}
 
 		// tests https://github.com/cryptomator/cryptofs/issues/69
@@ -127,12 +131,13 @@ public class PlatformIndependent {
 		private Path file;
 
 		@BeforeAll
-		public void beforeAll() throws IOException {
+		public void beforeAll() throws IOException, MasterkeyLoadingFailedException {
 			inMemoryFs = Jimfs.newFileSystem();
 			Path vaultPath = inMemoryFs.getPath("vault");
 			Files.createDirectories(vaultPath);
-			CryptoFileSystemProvider.initialize(vaultPath, "vault.cryptomator", "MASTERKEY_FILE", ignored -> new byte[64]);
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).withFlags().build());
+			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+			CryptoFileSystemProvider.initialize(vaultPath, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
+			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, cryptoFileSystemProperties().withKeyLoader(keyLoader).withFlags().build());
 			file = fileSystem.getPath("/test.txt");
 		}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 6cc3a56e..f0140b37 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -72,6 +72,7 @@
 import static org.mockito.Mockito.doThrow;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
@@ -548,7 +549,7 @@ public void moveFileToItselfDoesNothing() throws IOException {
 				inTest.move(cleartextSource, cleartextSource);
 
 				verify(readonlyFlag).assertWritable();
-				verifyZeroInteractions(cleartextSource);
+				verifyNoInteractions(cleartextSource);
 			}
 
 			@Test
@@ -704,7 +705,7 @@ public void copyFileToItselfDoesNothing() throws IOException {
 				inTest.copy(cleartextSource, cleartextSource);
 
 				verify(readonlyFlag).assertWritable();
-				verifyZeroInteractions(cleartextSource);
+				verifyNoInteractions(cleartextSource);
 			}
 
 			@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 6c273b56..3bacaac5 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -1,6 +1,7 @@
 package org.cryptomator.cryptofs;
 
 import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.MatcherAssert;
@@ -22,7 +23,7 @@
 
 public class CryptoFileSystemPropertiesTest {
 
-	private final KeyLoader keyLoader = Mockito.mock(KeyLoader.class);
+	private final MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 
 	@Test
 	public void testSetNoPassphrase() {
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index c64d062d..ea73af4f 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -12,7 +12,9 @@
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
 import org.cryptomator.cryptofs.ch.CleartextFileChannel;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.hamcrest.MatcherAssert;
 import org.hamcrest.Matchers;
 import org.junit.jupiter.api.AfterAll;
@@ -73,14 +75,15 @@ public class CryptoFileSystemProviderIntegrationTest {
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	class WithLimitedPaths {
 
-		private KeyLoader keyLoader = ignored -> new byte[64];
+		private byte[] rawKey = new byte[64];
+		private MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(rawKey);
 		private CryptoFileSystem fs;
 		private Path shortFilePath;
 		private Path shortSymlinkPath;
 		private Path shortDirPath;
 
 		@BeforeAll
-		public void setup(@TempDir Path tmpDir) throws IOException {
+		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			CryptoFileSystemProvider.initialize(tmpDir, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 					.withFlags() //
@@ -90,7 +93,7 @@ public void setup(@TempDir Path tmpDir) throws IOException {
 					.build();
 			fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
 		}
-		
+
 		@BeforeEach
 		public void setupEach() throws IOException {
 			shortFilePath = fs.getPath("/short-enough.txt");
@@ -100,7 +103,7 @@ public void setupEach() throws IOException {
 			Files.createDirectory(shortDirPath);
 			Files.createSymbolicLink(shortSymlinkPath, shortFilePath);
 		}
-		
+
 		@AfterEach
 		public void tearDownEach() throws IOException {
 			Files.deleteIfExists(shortFilePath);
@@ -160,7 +163,7 @@ public void testCopyExceedingPathLengthLimit(String path) {
 			Assertions.assertTrue(Files.exists(src));
 			Assertions.assertTrue(Files.notExists(dst));
 		}
-		
+
 	}
 
 	@Nested
@@ -169,8 +172,8 @@ public void testCopyExceedingPathLengthLimit(String path) {
 	class InMemory {
 
 		private FileSystem tmpFs;
-		private KeyLoader keyLoader1;
-		private KeyLoader keyLoader2;
+		private MasterkeyLoader keyLoader1;
+		private MasterkeyLoader keyLoader2;
 		private Path pathToVault1;
 		private Path pathToVault2;
 		private Path vaultConfigFile1;
@@ -185,8 +188,8 @@ public void setup() throws IOException {
 			byte[] key2 = new byte[64];
 			Arrays.fill(key1, (byte) 0x55);
 			Arrays.fill(key2, (byte) 0x77);
-			keyLoader1 = ignored -> Arrays.copyOf(key1, 64);
-			keyLoader2 = ignored -> Arrays.copyOf(key2, 64);
+			keyLoader1 = ignored -> Masterkey.createFromRaw(key1);
+			keyLoader2 = ignored -> Masterkey.createFromRaw(key2);
 			pathToVault1 = tmpFs.getPath("/vaultDir1");
 			pathToVault2 = tmpFs.getPath("/vaultDir2");
 			Files.createDirectory(pathToVault1);
@@ -520,10 +523,10 @@ class PosixTests {
 		private FileSystem fs;
 
 		@BeforeAll
-		public void setup(@TempDir Path tmpDir) throws IOException {
+		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
-			KeyLoader keyLoader = ignored -> new byte[64];
+			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
 			CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 		}
@@ -610,10 +613,10 @@ class WindowsTests {
 		private FileSystem fs;
 
 		@BeforeAll
-		public void setup(@TempDir Path tmpDir) throws IOException {
+		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
-			KeyLoader keyLoader = ignored -> new byte[64];
+			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
 			CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 		}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index fd088071..489bb05b 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -2,9 +2,10 @@
 
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
-import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
 import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
@@ -26,7 +27,6 @@
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
-import java.nio.file.NoSuchFileException;
 import java.nio.file.NotDirectoryException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
@@ -46,17 +46,15 @@
 import static java.util.Arrays.asList;
 import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemProvider.containsVault;
-import static org.cryptomator.cryptofs.CryptoFileSystemProvider.newFileSystem;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.instanceOf;
-import static org.hamcrest.Matchers.is;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 public class CryptoFileSystemProviderTest {
 
+	private final MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
 	private final CryptoFileSystems fileSystems = mock(CryptoFileSystems.class);
 
 	private final CryptoPath cryptoPath = mock(CryptoPath.class);
@@ -168,31 +166,31 @@ public void testInitializeFailWithNotDirectoryException() {
 		Path pathToVault = fs.getPath("/vaultDir");
 
 		Assertions.assertThrows(NotDirectoryException.class, () -> {
-			CryptoFileSystemProvider.initialize(pathToVault, "irrelevant.txt", "irrelevant", ignored -> new byte[0]);
+			CryptoFileSystemProvider.initialize(pathToVault, "irrelevant.txt", "irrelevant", keyLoader);
 		});
 	}
 
 	@Test
-	public void testInitialize() throws IOException {
+	public void testInitialize() throws IOException, MasterkeyLoadingFailedException {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 		Path pathToVault = fs.getPath("/vaultDir");
 		Path vaultConfigFile = pathToVault.resolve("vault.cryptomator");
 		Path dataDir = pathToVault.resolve("d");
 
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", ignored -> new byte[64]);
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
 
 		Assertions.assertTrue(Files.isDirectory(dataDir));
 		Assertions.assertTrue(Files.isRegularFile(vaultConfigFile));
 	}
 
 	@Test
-	public void testNewFileSystem() throws IOException {
+	public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedException {
 		Path pathToVault = Path.of("/vaultDir");
 		URI uri = CryptoFileSystemUri.create(pathToVault);
 		CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 				.withFlags() //
-				.withKeyLoader(ignored -> new byte[64]) //
+				.withKeyLoader(keyLoader) //
 				.build();
 
 		inTest.newFileSystem(uri, properties);
@@ -284,7 +282,7 @@ public void testNewAsyncFileChannelReturnsAsyncDelegatingFileChannel() throws IO
 		when(cryptoFileSystem.newFileChannel(cryptoPath, options)).thenReturn(channel);
 
 		AsynchronousFileChannel result = inTest.newAsynchronousFileChannel(cryptoPath, options, executor);
-		
+
 		MatcherAssert.assertThat(result, instanceOf(AsyncDelegatingFileChannel.class));
 	}
 
@@ -405,7 +403,7 @@ public void testCheckAccessDelegatesToFileSystem() throws IOException {
 	}
 
 	@Test
-	public void testGetFileStoreDelegatesToFileSystem() throws IOException {
+	public void testGetFileStoreDelegatesToFileSystem() {
 		CryptoFileStore fileStore = mock(CryptoFileStore.class);
 		when(cryptoFileSystem.getFileStore()).thenReturn(fileStore);
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
index 5452bf4c..5a694017 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
@@ -1,6 +1,9 @@
 package org.cryptomator.cryptofs;
 
 import org.cryptomator.cryptofs.common.DeletingFileVisitor;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -68,11 +71,12 @@ public void testCreateWithPathComponents() throws URISyntaxException {
 	}
 
 	@Test
-	public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException {
+	public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException, MasterkeyLoadingFailedException {
 		Path tempDir = createTempDirectory("CryptoFileSystemUrisTest").toAbsolutePath();
 		try {
-			CryptoFileSystemProvider.initialize(tempDir, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+			CryptoFileSystemProvider.initialize(tempDir, "vault.cryptomator", "irrelevant", keyLoader);
+			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 			Path absolutePathToVault = fileSystem.getPath("a").toAbsolutePath();
 
 			URI uri = CryptoFileSystemUri.create(absolutePathToVault, "a", "b");
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 9542102c..c1a2536c 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -4,6 +4,9 @@
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
@@ -36,9 +39,10 @@ public class CryptoFileSystemsTest {
 	private final CryptoFileSystemProperties properties = mock(CryptoFileSystemProperties.class);
 	private final CryptoFileSystemComponent cryptoFileSystemComponent = mock(CryptoFileSystemComponent.class);
 	private final CryptoFileSystemImpl cryptoFileSystem = mock(CryptoFileSystemImpl.class);
-	private final VaultConfig.VaultConfigLoader configLoader = mock(VaultConfig.VaultConfigLoader.class);
+	private final VaultConfig.UnverifiedVaultConfig configLoader = mock(VaultConfig.UnverifiedVaultConfig.class);
+	private final MasterkeyLoader keyLoader = mock(MasterkeyLoader.class);
+	private final Masterkey masterkey = mock(Masterkey.class);
 	private final byte[] rawKey = new byte[64];
-	private final KeyLoader keyLoader = mock(KeyLoader.class);
 	private final VaultConfig vaultConfig = mock(VaultConfig.class);
 	private final VaultCipherMode cipherMode = mock(VaultCipherMode.class);
 	private final SecureRandom csprng = Mockito.mock(SecureRandom.class);
@@ -53,7 +57,7 @@ public class CryptoFileSystemsTest {
 	private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker, csprng);
 
 	@BeforeEach
-	public void setup() throws IOException {
+	public void setup() throws IOException, MasterkeyLoadingFailedException {
 		filesClass = Mockito.mockStatic(Files.class);
 		vaultConficClass = Mockito.mockStatic(VaultConfig.class);
 
@@ -65,11 +69,12 @@ public void setup() throws IOException {
 		vaultConficClass.when(() -> VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
 		when(VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
 		when(configLoader.getKeyId()).thenReturn("key-id");
-		when(keyLoader.loadKey("key-id")).thenReturn(rawKey);
-		when(configLoader.load(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
+		when(keyLoader.loadKey("key-id")).thenReturn(masterkey);
+		when(masterkey.getEncoded()).thenReturn(rawKey);
+		when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
 		when(vaultConfig.getCiphermode()).thenReturn(cipherMode);
 		when(cipherMode.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
-		when(cryptorProvider.createFromRawKey(rawKey)).thenReturn(cryptor);
+		when(cryptorProvider.withKey(masterkey)).thenReturn(cryptor);
 		when(cryptoFileSystemComponentBuilder.cryptor(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.vaultConfig(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.pathToVault(any())).thenReturn(cryptoFileSystemComponentBuilder);
@@ -91,7 +96,7 @@ public void testContainsReturnsFalseForNonContainedFileSystem() {
 	}
 
 	@Test
-	public void testContainsReturnsTrueForContainedFileSystem() throws IOException {
+	public void testContainsReturnsTrueForContainedFileSystem() throws IOException, MasterkeyLoadingFailedException {
 		CryptoFileSystemImpl impl = inTest.create(provider, pathToVault, properties);
 
 		Assertions.assertSame(cryptoFileSystem, impl);
@@ -105,7 +110,7 @@ public void testContainsReturnsTrueForContainedFileSystem() throws IOException {
 	}
 
 	@Test
-	public void testCreateThrowsFileSystemAlreadyExistsExceptionIfInvokedWithSamePathTwice() throws IOException {
+	public void testCreateThrowsFileSystemAlreadyExistsExceptionIfInvokedWithSamePathTwice() throws IOException, MasterkeyLoadingFailedException {
 		inTest.create(provider, pathToVault, properties);
 
 		Assertions.assertThrows(FileSystemAlreadyExistsException.class, () -> {
@@ -114,7 +119,7 @@ public void testCreateThrowsFileSystemAlreadyExistsExceptionIfInvokedWithSamePat
 	}
 
 	@Test
-	public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIsRemovedBefore() throws IOException {
+	public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIsRemovedBefore() throws IOException, MasterkeyLoadingFailedException {
 		CryptoFileSystemImpl fileSystem1 = inTest.create(provider, pathToVault, properties);
 		Assertions.assertTrue(inTest.contains(fileSystem1));
 		inTest.remove(fileSystem1);
@@ -125,7 +130,7 @@ public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIs
 	}
 
 	@Test
-	public void testGetReturnsFileSystemForPathIfItExists() throws IOException {
+	public void testGetReturnsFileSystemForPathIfItExists() throws IOException, MasterkeyLoadingFailedException {
 		CryptoFileSystemImpl fileSystem = inTest.create(provider, pathToVault, properties);
 
 		Assertions.assertTrue(inTest.contains(fileSystem));
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java
index 8340d524..6475fd38 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathTest.java
@@ -35,6 +35,7 @@
 import static org.hamcrest.Matchers.lessThan;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
@@ -268,7 +269,7 @@ public void testToRealPathDoesNotResolveSymlinksWhenNotFollowingLinks() throws I
 		Path normalizedAndAbsolute = new CryptoPath(fileSystem, symlinks, asList("a", "b"), true);
 
 		Assertions.assertEquals(normalizedAndAbsolute, inTest.toRealPath(LinkOption.NOFOLLOW_LINKS));
-		verifyZeroInteractions(symlinks);
+		verifyNoInteractions(symlinks);
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index 94f299dd..b79c4d1b 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -10,6 +10,9 @@
 
 import com.google.common.base.Strings;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Disabled;
@@ -41,11 +44,12 @@ public class DeleteNonEmptyCiphertextDirectoryIntegrationTest {
 	private static FileSystem fileSystem;
 
 	@BeforeAll
-	public static void setupClass(@TempDir Path tmpDir) throws IOException {
+	public static void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java b/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java
index 1ec8cb53..4744bb43 100644
--- a/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/MoveOperationTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
@@ -54,7 +55,7 @@ public void setup() {
 	public void testMoveWithEqualPathDoesNothing() throws IOException {
 		inTest.move(aPathFromFsA, aPathFromFsA);
 
-		verifyZeroInteractions(aPathFromFsA);
+		verifyNoInteractions(aPathFromFsA);
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
index 6e634c73..1cf8fe53 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
@@ -8,6 +8,9 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs;
 
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
@@ -25,18 +28,20 @@
 public class ReadmeCodeSamplesTest {
 
 	@Test
-	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException {
-		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
+		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", keyLoader);
+		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 
 		runCodeSample(fileSystem);
 	}
 
 	@Test
-	public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException {
+	public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		URI uri = CryptoFileSystemUri.create(storageLocation);
-		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-		FileSystem fileSystem = FileSystems.newFileSystem(uri, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", keyLoader);
+		FileSystem fileSystem = FileSystems.newFileSystem(uri, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 
 		runCodeSample(fileSystem);
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
index 742c07a0..433927a3 100644
--- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
@@ -8,6 +8,9 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs;
 
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -29,11 +32,12 @@ public class RealFileSystemIntegrationTest {
 	private static FileSystem fileSystem;
 
 	@BeforeAll
-	public static void setupClass(@TempDir Path tmpDir) throws IOException {
+	public static void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java b/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java
index 02fd7364..c1f0491b 100644
--- a/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java
@@ -9,6 +9,7 @@
 
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
@@ -43,7 +44,7 @@ public void testInitializeDoesNotCreateRootDirectoryIfReadonlyFlagIsSet() throws
 
 		inTest.initialize(cleartextRoot);
 
-		verifyZeroInteractions(filesWrapper);
+		verifyNoInteractions(filesWrapper);
 	}
 
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
index 3eec8355..9d13385d 100644
--- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
@@ -6,6 +6,9 @@
 import com.auth0.jwt.interfaces.DecodedJWT;
 import com.auth0.jwt.interfaces.Verification;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
@@ -21,27 +24,29 @@ public class VaultConfigTest {
 	@Test
 	public void testLoadMalformedToken() {
 		Assertions.assertThrows(VaultConfigLoadException.class, () -> {
-			VaultConfig.load("hello world", ignored -> new byte[64], 42);
+			VaultConfig.load("hello world", ignored -> null, 42);
 		});
 	}
 
 	@Nested
 	public class WithValidToken {
 
-		private byte[] key = new byte[64];
+		private byte[] rawKey = new byte[64];
+		private Masterkey key = Mockito.mock(Masterkey.class);
 		private VaultConfig originalConfig;
 		private String token;
 
 
 		@BeforeEach
 		public void setup() {
-			Arrays.fill(key, (byte) 0x55);
+			Arrays.fill(rawKey, (byte) 0x55);
+			Mockito.when(key.getEncoded()).thenReturn(rawKey);
 			originalConfig = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(220).build();
-			token = originalConfig.toToken("TEST_KEY", key);
+			token = originalConfig.toToken("TEST_KEY", rawKey);
 		}
 
 		@Test
-		public void testSuccessfulLoad() throws VaultConfigLoadException, KeyLoadingFailedException {
+		public void testSuccessfulLoad() throws VaultConfigLoadException, MasterkeyLoadingFailedException {
 			var loaded = VaultConfig.load(token, ignored -> key, originalConfig.getVaultVersion());
 
 			Assertions.assertEquals(originalConfig.getId(), loaded.getId());
@@ -53,7 +58,8 @@ public void testSuccessfulLoad() throws VaultConfigLoadException, KeyLoadingFail
 		@ParameterizedTest
 		@ValueSource(ints = {0, 1, 2, 3, 10, 20, 30, 63})
 		public void testLoadWithInvalidKey(int pos) {
-			key[pos] = (byte) 0x77;
+			rawKey[pos] = (byte) 0x77;
+			Mockito.when(key.getEncoded()).thenReturn(rawKey);
 
 			Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 				VaultConfig.load(token, ignored -> key, originalConfig.getVaultVersion());
@@ -73,19 +79,21 @@ public void testCreateNew() {
 	}
 
 	@Test
-	public void testLoadExisting() throws KeyLoadingFailedException, VaultConfigLoadException {
+	public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoadingFailedException {
 		var decodedJwt = Mockito.mock(DecodedJWT.class);
 		var formatClaim = Mockito.mock(Claim.class);
 		var ciphermodeClaim = Mockito.mock(Claim.class);
 		var maxFilenameLenClaim = Mockito.mock(Claim.class);
-		var keyLoader = Mockito.mock(KeyLoader.class);
+		var keyLoader = Mockito.mock(MasterkeyLoader.class);
+		var key = Mockito.mock(Masterkey.class);
 		var verification = Mockito.mock(Verification.class);
 		var verifier = Mockito.mock(JWTVerifier.class);
 		Mockito.when(decodedJwt.getKeyId()).thenReturn("key-id");
 		Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim);
 		Mockito.when(decodedJwt.getClaim("ciphermode")).thenReturn(ciphermodeClaim);
 		Mockito.when(decodedJwt.getClaim("maxFilenameLen")).thenReturn(maxFilenameLenClaim);
-		Mockito.when(keyLoader.loadKey("key-id")).thenReturn(new byte[64]);
+		Mockito.when(keyLoader.loadKey("key-id")).thenReturn(key);
+		Mockito.when(key.getEncoded()).thenReturn(new byte[64]);
 		Mockito.when(verification.withClaim("format", 42)).thenReturn(verification);
 		Mockito.when(verification.build()).thenReturn(verifier);
 		Mockito.when(verifier.verify(decodedJwt)).thenReturn(decodedJwt);
diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
index 3a46c54f..2b7778dd 100644
--- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
@@ -1,6 +1,9 @@
 package org.cryptomator.cryptofs;
 
 import com.google.common.jimfs.Jimfs;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -27,12 +30,13 @@ public class WriteFileWhileReadonlyChannelIsOpenTest {
 	private Path root;
 
 	@BeforeEach
-	public void setup() throws IOException {
+	public void setup() throws IOException, MasterkeyLoadingFailedException {
 		inMemoryFs = Jimfs.newFileSystem();
 		Path pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 		root = fileSystem.getPath("/");
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index bd51fa80..35dd526c 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -10,6 +10,9 @@
 
 import com.google.common.jimfs.Jimfs;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.hamcrest.BaseMatcher;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -52,12 +55,13 @@ public class FileAttributeIntegrationTest {
 	private static FileSystem fileSystem;
 
 	@BeforeAll
-	public static void setupClass() throws IOException {
+	public static void setupClass() throws IOException, MasterkeyLoadingFailedException {
 		inMemoryFs = Jimfs.newFileSystem();
 		pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", ignored -> new byte[64]);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(ignored -> new byte[64]).build());
+		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
 	}
 
 	@AfterAll
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java
index 1c771b0c..9bdb7f8b 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java
@@ -30,7 +30,7 @@ public void setup() {
 	}
 	
 	@Test
-	public void inflateDeflated() throws IOException {
+	public void inflateDeflated() throws IOException, AuthenticationFailedException {
 		Node deflated = new Node(Paths.get("foo.c9s"));
 		Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenReturn("foo.c9r");
 		Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenReturn("hello world.txt");
@@ -52,7 +52,7 @@ public void inflateUninflatableDueToIOException() throws IOException {
 	}
 
 	@Test
-	public void inflateUninflatableDueToInvalidCiphertext() throws IOException {
+	public void inflateUninflatableDueToInvalidCiphertext() throws IOException, AuthenticationFailedException {
 		Node deflated = new Node(Paths.get("foo.c9s"));
 		Mockito.when(longFileNameProvider.inflate(deflated.ciphertextPath)).thenReturn("foo.c9r");
 		Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.eq("foo"), Mockito.any())).thenThrow(new AuthenticationFailedException("peng!"));
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
index 3c1e0ba6..6cf26197 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
@@ -58,7 +58,7 @@ public void testInvalidBase64Pattern(String input) {
 	
 	@Test
 	@DisplayName("process canonical filename")
-	public void testProcessFullMatch() {
+	public void testProcessFullMatch() throws AuthenticationFailedException {
 		Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("helloWorld.txt");
 		Node input = new Node(Paths.get("aaaaBBBBccccDDDDeeeeFFFF.c9r"));
 
@@ -81,7 +81,7 @@ public void testProcessFullMatch() {
 			"foo_aaaaBBBBcccc_--_11112222_foo.c9r",
 			"aaaaBBBBccccDDDDeeeeFFFF___aaaaBBBBcccc_--_11112222----aaaaBBBBccccDDDDeeeeFFFF.c9r",
 	})
-	public void testProcessPartialMatch(String filename) {
+	public void testProcessPartialMatch(String filename) throws AuthenticationFailedException {
 		Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).then(invocation -> {
 			String ciphertext = invocation.getArgument(1);
 			if (ciphertext.equals("aaaaBBBBcccc_--_11112222")) {
@@ -107,7 +107,7 @@ public void testProcessPartialMatch(String filename) {
 			"aaaaBBBB$$$$DDDDeeeeFFFF.c9r",
 			"aaaaBBBBxxxxDDDDeeeeFFFF.c9r",
 	})
-	public void testProcessNoMatch(String filename) {
+	public void testProcessNoMatch(String filename) throws AuthenticationFailedException {
 		Mockito.when(fileNameCryptor.decryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenThrow(new AuthenticationFailedException("Invalid ciphertext."));
 		Node input = new Node(Paths.get(filename));
 
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java
index a15bf342..2e5bc541 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkCacheTest.java
@@ -2,16 +2,12 @@
 
 import org.cryptomator.cryptofs.CryptoFileSystemStats;
 import org.cryptomator.cryptolib.api.AuthenticationFailedException;
-import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
 import java.io.IOException;
-import java.util.List;
 
-import static java.util.Arrays.asList;
-import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -26,7 +22,7 @@ public class ChunkCacheTest {
 	private final ChunkCache inTest = new ChunkCache(chunkLoader, chunkSaver, stats);
 
 	@Test
-	public void testGetInvokesLoaderIfEntryNotInCache() throws IOException {
+	public void testGetInvokesLoaderIfEntryNotInCache() throws IOException, AuthenticationFailedException {
 		long index = 42L;
 		ChunkData data = mock(ChunkData.class);
 		when(chunkLoader.load(index)).thenReturn(data);
@@ -36,7 +32,7 @@ public void testGetInvokesLoaderIfEntryNotInCache() throws IOException {
 	}
 
 	@Test
-	public void testGetDoesNotInvokeLoaderIfEntryInCacheFromPreviousGet() throws IOException {
+	public void testGetDoesNotInvokeLoaderIfEntryInCacheFromPreviousGet() throws IOException, AuthenticationFailedException {
 		long index = 42L;
 		ChunkData data = mock(ChunkData.class);
 		when(chunkLoader.load(index)).thenReturn(data);
@@ -58,7 +54,7 @@ public void testGetDoesNotInvokeLoaderIfEntryInCacheFromPreviousSet() throws IOE
 	}
 
 	@Test
-	public void testGetInvokesSaverIfMaxEntriesInCacheAreReachedAndAnEntryNotInCacheIsRequested() throws IOException {
+	public void testGetInvokesSaverIfMaxEntriesInCacheAreReachedAndAnEntryNotInCacheIsRequested() throws IOException, AuthenticationFailedException {
 		long firstIndex = 42L;
 		long indexNotInCache = 40L;
 		ChunkData firstData = mock(ChunkData.class);
@@ -108,7 +104,7 @@ public void testGetInvokesSaverIfMaxEntriesInCacheAreReachedAndAnEntryInCacheIsS
 	}
 
 	@Test
-	public void testGetRethrowsAuthenticationFailedExceptionFromLoader() throws IOException {
+	public void testGetRethrowsAuthenticationFailedExceptionFromLoader() throws IOException, AuthenticationFailedException {
 		long index = 42L;
 		AuthenticationFailedException authenticationFailedException = new AuthenticationFailedException("Foo");
 		when(chunkLoader.load(index)).thenThrow(authenticationFailedException);
@@ -120,7 +116,7 @@ public void testGetRethrowsAuthenticationFailedExceptionFromLoader() throws IOEx
 	}
 
 	@Test
-	public void testGetThrowsUncheckedExceptionFromLoader() throws IOException {
+	public void testGetThrowsUncheckedExceptionFromLoader() throws IOException, AuthenticationFailedException {
 		long index = 42L;
 		RuntimeException uncheckedException = new RuntimeException();
 		when(chunkLoader.load(index)).thenThrow(uncheckedException);
@@ -132,7 +128,7 @@ public void testGetThrowsUncheckedExceptionFromLoader() throws IOException {
 	}
 
 	@Test
-	public void testInvalidateAllInvokesSaverForAllEntriesInCache() throws IOException {
+	public void testInvalidateAllInvokesSaverForAllEntriesInCache() throws IOException, AuthenticationFailedException {
 		long index = 42L;
 		long index2 = 43L;
 		ChunkData data = mock(ChunkData.class);
@@ -148,16 +144,7 @@ public void testInvalidateAllInvokesSaverForAllEntriesInCache() throws IOExcepti
 	}
 
 	@Test
-	@SuppressWarnings("unchecked")
-	public void testLoaderThrowsOnlyIOException() throws NoSuchMethodException {
-		List<Class<?>> exceptionsThrownByLoader = asList(ChunkLoader.class.getMethod("load", Long.class).getExceptionTypes());
-
-		// INFO: when adding exception types here add a corresponding test like testGetRethrowsIOExceptionFromLoader
-		MatcherAssert.assertThat(exceptionsThrownByLoader, containsInAnyOrder(IOException.class));
-	}
-
-	@Test
-	public void testGetRethrowsIOExceptionFromLoader() throws IOException {
+	public void testGetRethrowsIOExceptionFromLoader() throws IOException, AuthenticationFailedException {
 		long index = 42L;
 		IOException ioException = new IOException();
 		when(chunkLoader.load(index)).thenThrow(ioException);
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java
index 498e1b24..0705da4c 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkLoaderTest.java
@@ -1,6 +1,7 @@
 package org.cryptomator.cryptofs.fh;
 
 import org.cryptomator.cryptofs.CryptoFileSystemStats;
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.FileContentCryptor;
 import org.cryptomator.cryptolib.api.FileHeader;
@@ -49,7 +50,7 @@ public void setup() throws IOException {
 	}
 
 	@Test
-	public void testChunkLoaderReturnsEmptyDataOfChunkAfterEndOfFile() throws IOException {
+	public void testChunkLoaderReturnsEmptyDataOfChunkAfterEndOfFile() throws IOException, AuthenticationFailedException {
 		long chunkIndex = 482L;
 		long chunkOffset = chunkIndex * CIPHERTEXT_CHUNK_SIZE + HEADER_SIZE;
 		when(chunkIO.read(argThat(hasAtLeastRemaining(CIPHERTEXT_CHUNK_SIZE)), eq(chunkOffset))).thenReturn(-1);
@@ -63,7 +64,7 @@ public void testChunkLoaderReturnsEmptyDataOfChunkAfterEndOfFile() throws IOExce
 	}
 
 	@Test
-	public void testChunkLoaderReturnsDecryptedDataOfChunkInsideFile() throws IOException {
+	public void testChunkLoaderReturnsDecryptedDataOfChunkInsideFile() throws IOException, AuthenticationFailedException {
 		long chunkIndex = 482L;
 		long chunkOffset = chunkIndex * CIPHERTEXT_CHUNK_SIZE + HEADER_SIZE;
 		Supplier<ByteBuffer> decryptedData = () -> repeat(9).times(CLEARTEXT_CHUNK_SIZE).asByteBuffer();
@@ -81,7 +82,7 @@ public void testChunkLoaderReturnsDecryptedDataOfChunkInsideFile() throws IOExce
 	}
 
 	@Test
-	public void testChunkLoaderReturnsDecrytedDataOfChunkContainingEndOfFile() throws IOException {
+	public void testChunkLoaderReturnsDecrytedDataOfChunkContainingEndOfFile() throws IOException, AuthenticationFailedException {
 		long chunkIndex = 482L;
 		long chunkOffset = chunkIndex * CIPHERTEXT_CHUNK_SIZE + HEADER_SIZE;
 		Supplier<ByteBuffer> decryptedData = () -> repeat(9).times(CLEARTEXT_CHUNK_SIZE - 3).asByteBuffer();
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java b/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java
index 3b8b2830..c2a669ae 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/ChunkSaverTest.java
@@ -18,6 +18,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.hamcrest.MockitoHamcrest.argThat;
@@ -87,8 +88,8 @@ public void testChunkThatWasNotWrittenIsNotWritten() throws IOException {
 
 		inTest.save(chunkIndex, chunkData);
 
-		verifyZeroInteractions(chunkIO);
-		verifyZeroInteractions(stats);
+		verifyNoInteractions(chunkIO);
+		verifyNoInteractions(stats);
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
index 1f22043f..c8e92b69 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
@@ -1,5 +1,6 @@
 package org.cryptomator.cryptofs.fh;
 
+import org.cryptomator.cryptolib.api.AuthenticationFailedException;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.FileHeader;
 import org.cryptomator.cryptolib.api.FileHeaderCryptor;
@@ -51,7 +52,7 @@ class ExistingHeader {
 		private FileChannel channel = Mockito.mock(FileChannel.class);
 
 		@BeforeEach
-		public void setup() throws IOException {
+		public void setup() throws IOException, AuthenticationFailedException {
 			byte[] headerBytes = "leHeader".getBytes(StandardCharsets.US_ASCII);
 			when(fileHeaderCryptor.headerSize()).thenReturn(headerBytes.length);
 			when(channel.read(Mockito.any(ByteBuffer.class), Mockito.eq(0l))).thenAnswer(invocation -> {
@@ -65,7 +66,7 @@ public void setup() throws IOException {
 
 		@Test
 		@DisplayName("load")
-		public void testLoadExisting() throws IOException {
+		public void testLoadExisting() throws IOException, AuthenticationFailedException {
 			FileHeader loadedHeader1 = inTest.loadExisting(channel);
 			FileHeader loadedHeader2 = inTest.get();
 			FileHeader loadedHeader3 = inTest.get();
@@ -90,7 +91,7 @@ public void setup() throws IOException {
 		}
 
 		@AfterEach
-		public void tearDown() {
+		public void tearDown() throws AuthenticationFailedException {
 			verify(fileHeaderCryptor, Mockito.never()).decryptHeader(Mockito.any());
 		}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java
deleted file mode 100644
index fb43df89..00000000
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigrationComponentTest.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package org.cryptomator.cryptofs.migration;
-
-import org.cryptomator.cryptofs.mocks.NullSecureRandom;
-import org.cryptomator.cryptolib.Cryptors;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import java.security.NoSuchAlgorithmException;
-
-public class MigrationComponentTest {
-
-	@Test
-	public void testAvailableMigrators() throws NoSuchAlgorithmException {
-		MigrationModule migrationModule = new MigrationModule(Cryptors.version1(NullSecureRandom.INSTANCE));
-		TestMigrationComponent comp = DaggerTestMigrationComponent.builder().migrationModule(migrationModule).build();
-		Assertions.assertFalse(comp.availableMigrators().isEmpty());
-	}
-
-}
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
index 32130047..0f6b9e4f 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -5,108 +5,245 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.migration;
 
+import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
 import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
-import org.cryptomator.cryptolib.api.InvalidPassphraseException;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.channels.SeekableByteChannel;
 import java.nio.charset.StandardCharsets;
-import java.nio.file.FileSystem;
+import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.spi.FileSystemProvider;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.Map;
 
 public class MigratorsTest {
 
-	private ByteBuffer keyFile;
+	private MockedStatic<Files> filesClass;
 	private Path pathToVault;
 	private FileSystemCapabilityChecker fsCapabilityChecker;
+	private Path vaultConfigPath;
+	private Path masterkeyPath;
 
 	@BeforeEach
-	public void setup() throws IOException {
-		keyFile = StandardCharsets.UTF_8.encode("{\"version\": 0000}");
-		pathToVault = Mockito.mock(Path.class);
+	public void setup() {
+		filesClass = Mockito.mockStatic(Files.class);
+		pathToVault = Mockito.mock(Path.class, "path/to/vault");
 		fsCapabilityChecker = Mockito.mock(FileSystemCapabilityChecker.class);
+		vaultConfigPath = Mockito.mock(Path.class, "path/to/vault/vault.cryptomator");
+		masterkeyPath = Mockito.mock(Path.class, "path/to/vault/masterkey.cryptomator");
 
-		Path pathToMasterkey = Mockito.mock(Path.class);
-		FileSystem fs = Mockito.mock(FileSystem.class);
-		FileSystemProvider provider = Mockito.mock(FileSystemProvider.class);
-		SeekableByteChannel sbc = Mockito.mock(SeekableByteChannel.class);
-
-		Mockito.when(pathToVault.resolve("masterkey.cryptomator")).thenReturn(pathToMasterkey);
-		Mockito.when(pathToMasterkey.getFileSystem()).thenReturn(fs);
-		Mockito.when(fs.provider()).thenReturn(provider);
-		Mockito.when(provider.newByteChannel(Mockito.eq(pathToMasterkey), Mockito.any(), Mockito.any())).thenReturn(sbc);
-		Mockito.when(sbc.size()).thenReturn((long) keyFile.remaining());
-		Mockito.when(sbc.read(Mockito.any())).then(invocation -> {
-			ByteBuffer dst = invocation.getArgument(0);
-			int n = Math.min(keyFile.remaining(), dst.remaining());
-			byte[] tmp = new byte[n];
-			keyFile.get(tmp);
-			dst.put(tmp);
-			return n;
-		});
+		Mockito.when(pathToVault.resolve("masterkey.cryptomator")).thenReturn(masterkeyPath);
+		Mockito.when(pathToVault.resolve("vault.cryptomator")).thenReturn(vaultConfigPath);
 	}
 
-	@Test
-	public void testNeedsMigration() throws IOException {
-		Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
-		boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator");
-
-		Assertions.assertTrue(result);
+	@AfterEach
+	public void tearDown() {
+		filesClass.close();
 	}
 
 	@Test
-	public void testNeedsNoMigration() throws IOException {
-		keyFile = StandardCharsets.UTF_8.encode("{\"version\": 9999}");
+	@DisplayName("can't determine vault version without masterkey.cryptomator or vault.cryptomator")
+	public void throwsExceptionIfNeitherMasterkeyNorVaultConfigExists() {
+		filesClass.when(() -> Files.exists(vaultConfigPath)).thenReturn(false);
+		filesClass.when(() -> Files.exists(masterkeyPath)).thenReturn(false);
 
 		Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
-		boolean result = migrators.needsMigration(pathToVault, "masterkey.cryptomator");
-
-		Assertions.assertFalse(result);
-	}
 
-	@Test
-	public void testMigrateWithoutMigrators() {
-		Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
-		Assertions.assertThrows(NoApplicableMigratorException.class, () -> {
-			migrators.migrate(pathToVault, "vault.cryptomator","masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
+		IOException thrown = Assertions.assertThrows(IOException.class, () -> {
+			migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
 		});
+		MatcherAssert.assertThat(thrown.getMessage(), CoreMatchers.containsString("Did not find vault.cryptomator nor masterkey.cryptomator"));
 	}
-	
-	@Test
-	@SuppressWarnings("deprecation")
-	public void testMigrate() throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
-		MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class);
-		MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class);
-		Migrator migrator = Mockito.mock(Migrator.class);
-		Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
-		migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
-		Mockito.verify(migrator).migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
+
+	@Nested
+	public class WithExistingVaultConfig {
+
+		private MockedStatic<VaultConfig> vaultConfigClass;
+		private VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig;
+
+		@BeforeEach
+		public void setup() {
+			Assumptions.assumeFalse(Files.exists(masterkeyPath));
+			vaultConfigClass = Mockito.mockStatic(VaultConfig.class);
+			unverifiedVaultConfig = Mockito.mock(VaultConfig.UnverifiedVaultConfig.class);
+
+			filesClass.when(() -> Files.exists(vaultConfigPath)).thenReturn(true);
+			filesClass.when(() -> Files.readString(vaultConfigPath)).thenReturn("vault-config");
+			vaultConfigClass.when(() -> VaultConfig.decode("vault-config")).thenReturn(unverifiedVaultConfig);
+		}
+
+		@AfterEach
+		public void tearDown() {
+			vaultConfigClass.close();
+		}
+
+		@Test
+		@DisplayName("needs migration if vault version < Constants.VAULT_VERSION")
+		public void testNeedsMigration() throws IOException {
+			Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION - 1);
+			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+
+			boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
+
+			Assertions.assertTrue(result);
+		}
+
+		@Test
+		@DisplayName("needs no migration if vault version >= Constants.VAULT_VERSION")
+		public void testNeedsNoMigration() throws IOException {
+			Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION);
+			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+
+			boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
+
+			Assertions.assertFalse(result);
+		}
+
+		@Test
+		@DisplayName("throws NoApplicableMigratorException if no migrator was found")
+		public void testMigrateWithoutMigrators() {
+			Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(42);
+
+			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+			Assertions.assertThrows(NoApplicableMigratorException.class, () -> {
+				migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS);
+			});
+		}
+
+		@Test
+		@DisplayName("migrate successfully")
+		@SuppressWarnings("deprecation")
+		public void testMigrate() throws NoApplicableMigratorException, CryptoException, IOException {
+			MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class);
+			MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class);
+			Migrator migrator = Mockito.mock(Migrator.class);
+			Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(0);
+			Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
+
+			migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
+
+			Mockito.verify(migrator).migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
+		}
+
+		@Test
+		@DisplayName("migrate throws UnsupportedVaultFormatException")
+		@SuppressWarnings("deprecation")
+		public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, CryptoException, IOException {
+			Migrator migrator = Mockito.mock(Migrator.class);
+			Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
+			Mockito.when(unverifiedVaultConfig.allegedVaultVersion()).thenReturn(0);
+			Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any());
+
+			Assertions.assertThrows(IllegalStateException.class, () -> {
+				migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS);
+			});
+		}
+
 	}
 
-	@Test
-	@SuppressWarnings("deprecation")
-	public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, InvalidPassphraseException, IOException {
-		Migrator migrator = Mockito.mock(Migrator.class);
-		Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
-		Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any());
-		Assertions.assertThrows(IllegalStateException.class, () -> {
-			migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", (state, progress) -> {}, (event) -> ContinuationResult.CANCEL);
-		});
+	@Nested
+	public class WithExistingMasterkeyFile {
+
+		private MockedStatic<MasterkeyFile> masterkeyFileClass;
+		private MasterkeyFile masterkeyFile;
+
+		@BeforeEach
+		public void setup() {
+			Assumptions.assumeFalse(Files.exists(vaultConfigPath));
+			masterkeyFileClass = Mockito.mockStatic(MasterkeyFile.class);
+			masterkeyFile = Mockito.mock(MasterkeyFile.class);
+
+			filesClass.when(() -> Files.exists(masterkeyPath)).thenReturn(true);
+			masterkeyFileClass.when(() -> MasterkeyFile.withContentFromFile(masterkeyPath)).thenReturn(masterkeyFile);
+		}
+
+		@AfterEach
+		public void tearDown() {
+			masterkeyFileClass.close();
+		}
+
+		@Test
+		@DisplayName("needs migration if vault version < Constants.VAULT_VERSION")
+		public void testNeedsMigration() throws IOException {
+			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION - 1);
+			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+
+			boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
+
+			Assertions.assertTrue(result);
+		}
+
+		@Test
+		@DisplayName("needs no migration if vault version >= Constants.VAULT_VERSION")
+		public void testNeedsNoMigration() throws IOException {
+			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION);
+			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+
+			boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
+
+			Assertions.assertFalse(result);
+		}
+
+		@Test
+		@DisplayName("throws NoApplicableMigratorException if no migrator was found")
+		public void testMigrateWithoutMigrators() {
+			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(42);
+
+			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
+			Assertions.assertThrows(NoApplicableMigratorException.class, () -> {
+				migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS);
+			});
+		}
+
+		@Test
+		@DisplayName("migrate successfully")
+		@SuppressWarnings("deprecation")
+		public void testMigrate() throws NoApplicableMigratorException, CryptoException, IOException {
+			MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class);
+			MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class);
+			Migrator migrator = Mockito.mock(Migrator.class);
+			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(0);
+			Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
+
+			migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
+
+			Mockito.verify(migrator).migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
+		}
+
+		@Test
+		@DisplayName("migrate throws UnsupportedVaultFormatException")
+		@SuppressWarnings("deprecation")
+		public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, CryptoException, IOException {
+			Migrator migrator = Mockito.mock(Migrator.class);
+			Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
+			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(0);
+			Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any());
+
+			Assertions.assertThrows(IllegalStateException.class, () -> {
+				migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", MigrationProgressListener.IGNORE, MigrationContinuationListener.CANCEL_ALWAYS);
+			});
+		}
+
 	}
 
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java b/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java
deleted file mode 100644
index 40089d2f..00000000
--- a/src/test/java/org/cryptomator/cryptofs/migration/TestMigrationComponent.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package org.cryptomator.cryptofs.migration;
-
-import java.util.Map;
-
-import org.cryptomator.cryptofs.migration.api.Migrator;
-
-import dagger.Component;
-
-@Component(modules = {MigrationModule.class})
-interface TestMigrationComponent extends MigrationComponent {
-
-	Map<Migration, Migrator> availableMigrators();
-
-}
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
index 5bc3105e..effecd6c 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
@@ -2,36 +2,40 @@
 
 import com.google.common.jimfs.Configuration;
 import com.google.common.jimfs.Jimfs;
-import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptofs.common.MasterkeyBackupHelper;
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.mocks.NullSecureRandom;
-import org.cryptomator.cryptolib.Cryptors;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.KeyFile;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
+import java.util.Optional;
 
 public class Version6MigratorTest {
 
 	private FileSystem fs;
 	private Path pathToVault;
 	private Path masterkeyFile;
-	private CryptorProvider cryptorProvider;
+	private SecureRandom csprng = NullSecureRandom.INSTANCE;
 
 	@BeforeEach
 	public void setup() throws IOException {
-		cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE);
 		fs = Jimfs.newFileSystem(Configuration.unix());
 		pathToVault = fs.getPath("/vaultDir");
 		masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
@@ -45,28 +49,31 @@ public void teardown() throws IOException {
 	}
 
 	@Test
-	public void testMigrate() throws IOException {
+	public void testMigrate() throws IOException, CryptoException {
 		String oldPassword = Normalizer.normalize("ä", Form.NFD);
 		String newPassword = Normalizer.normalize("ä", Form.NFC);
 		Assertions.assertNotEquals(oldPassword, newPassword);
 
-		KeyFile beforeMigration = cryptorProvider.createNew().writeKeysToMasterkeyFile(oldPassword, 5);
-		Assertions.assertEquals(5, beforeMigration.getVersion());
-		Files.write(masterkeyFile, beforeMigration.serialize());
-		Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupHelper.generateFileIdSuffix(beforeMigration.serialize()) + Constants.MASTERKEY_BACKUP_SUFFIX);
+		Masterkey masterkey = Masterkey.createNew(csprng);
+		byte[] beforeMigration = MasterkeyFile.lock(masterkey, oldPassword, new byte[0], 5, csprng);
 
-		Migrator migrator = new Version6Migrator(cryptorProvider);
+		Files.write(masterkeyFile, beforeMigration);
+		Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupHelper.generateFileIdSuffix(beforeMigration) + Constants.MASTERKEY_BACKUP_SUFFIX);
+
+		Migrator migrator = new Version6Migrator(csprng);
 		migrator.migrate(pathToVault, null, "masterkey.cryptomator", oldPassword);
 
-		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
-		Assertions.assertEquals(6, afterMigration.getVersion());
-		try (Cryptor cryptor = cryptorProvider.createFromKeyFile(afterMigration, newPassword, 6)) {
-			Assertions.assertNotNull(cryptor);
+		byte[] afterMigration = Files.readAllBytes(masterkeyFile);
+		String afterMigrationJson = new String(afterMigration, StandardCharsets.UTF_8);
+		MatcherAssert.assertThat(afterMigrationJson, CoreMatchers.containsString("\"version\": 6"));
+
+		try (var keyLoader = MasterkeyFile.withContent(new ByteArrayInputStream(afterMigration)).unlock(newPassword, new byte[0], Optional.of(6))) {
+			Assertions.assertNotNull(keyLoader);
 		}
 
 		Assertions.assertTrue(Files.exists(masterkeyBackupFile));
-		KeyFile backupKey = KeyFile.parse(Files.readAllBytes(masterkeyBackupFile));
-		Assertions.assertEquals(5, backupKey.getVersion());
+		String backedUpJson = Files.readString(masterkeyBackupFile, StandardCharsets.UTF_8);
+		MatcherAssert.assertThat(backedUpJson, CoreMatchers.containsString("\"version\": 5"));
 	}
 
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
index 2ebcfa25..eb841c7c 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
@@ -4,19 +4,22 @@
 import com.google.common.jimfs.Jimfs;
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.mocks.NullSecureRandom;
-import org.cryptomator.cryptolib.Cryptors;
-import org.cryptomator.cryptolib.api.Cryptor;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.KeyFile;
+import org.cryptomator.cryptolib.api.CryptoException;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 
 public class Version7MigratorTest {
 
@@ -25,11 +28,10 @@ public class Version7MigratorTest {
 	private Path dataDir;
 	private Path metaDir;
 	private Path masterkeyFile;
-	private CryptorProvider cryptorProvider;
+	private SecureRandom csprng = NullSecureRandom.INSTANCE;
 
 	@BeforeEach
 	public void setup() throws IOException {
-		cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE);
 		fs = Jimfs.newFileSystem(Configuration.unix());
 		vaultRoot = fs.getPath("/vaultDir");
 		dataDir = vaultRoot.resolve("d");
@@ -38,10 +40,10 @@ public void setup() throws IOException {
 		Files.createDirectory(vaultRoot);
 		Files.createDirectory(dataDir);
 		Files.createDirectory(metaDir);
-		try (Cryptor cryptor = cryptorProvider.createNew()) {
-			KeyFile keyFile = cryptor.writeKeysToMasterkeyFile("test", 6);
-			Files.write(masterkeyFile, keyFile.serialize());
-		}
+
+		Masterkey masterkey = Masterkey.createNew(csprng);
+		byte[] unmigrated = MasterkeyFile.lock(masterkey, "test", new byte[0], 6, csprng);
+		Files.write(masterkeyFile, unmigrated);
 	}
 
 	@AfterEach
@@ -50,22 +52,19 @@ public void teardown() throws IOException {
 	}
 
 	@Test
-	public void testKeyfileGetsUpdates() throws IOException {
-		KeyFile beforeMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
-		Assertions.assertEquals(6, beforeMigration.getVersion());
-
-		Migrator migrator = new Version7Migrator(cryptorProvider);
+	public void testKeyfileGetsUpdates() throws CryptoException, IOException {
+		Migrator migrator = new Version7Migrator(csprng);
 		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
-		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
-		Assertions.assertEquals(7, afterMigration.getVersion());
+		String migrated = Files.readString(masterkeyFile, StandardCharsets.UTF_8);
+		MatcherAssert.assertThat(migrated, CoreMatchers.containsString("\"version\": 7"));
 	}
 
 	@Test
-	public void testMDirectoryGetsDeleted() throws IOException {
-		Migrator migrator = new Version7Migrator(cryptorProvider);
+	public void testMDirectoryGetsDeleted() throws CryptoException, IOException {
+		Migrator migrator = new Version7Migrator(csprng);
 		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
-		
+
 		Assertions.assertFalse(Files.exists(metaDir));
 	}
 
@@ -76,8 +75,8 @@ public void testMigrationFailsIfEncounteringUnsyncediCloudContent() throws IOExc
 		Path fileBeforeMigration = dir.resolve("MZUWYZLOMFWWK===.icloud");
 		Files.createFile(fileBeforeMigration);
 
-		Migrator migrator = new Version7Migrator(cryptorProvider);
-		
+		Migrator migrator = new Version7Migrator(csprng);
+
 		IOException e = Assertions.assertThrows(PreMigrationVisitor.PreMigrationChecksFailedException.class, () -> {
 			migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 		});
@@ -85,14 +84,14 @@ public void testMigrationFailsIfEncounteringUnsyncediCloudContent() throws IOExc
 	}
 
 	@Test
-	public void testMigrationOfNormalFile() throws IOException {
+	public void testMigrationOfNormalFile() throws CryptoException, IOException {
 		Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG");
 		Files.createDirectories(dir);
 		Path fileBeforeMigration = dir.resolve("MZUWYZLOMFWWK===");
 		Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r");
 		Files.createFile(fileBeforeMigration);
 
-		Migrator migrator = new Version7Migrator(cryptorProvider);
+		Migrator migrator = new Version7Migrator(csprng);
 		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		Assertions.assertFalse(Files.exists(fileBeforeMigration));
@@ -100,14 +99,14 @@ public void testMigrationOfNormalFile() throws IOException {
 	}
 
 	@Test
-	public void testMigrationOfNormalDirectory() throws IOException {
+	public void testMigrationOfNormalDirectory() throws CryptoException, IOException {
 		Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG");
 		Files.createDirectories(dir);
 		Path fileBeforeMigration = dir.resolve("0MZUWYZLOMFWWK===");
 		Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r/dir.c9r");
 		Files.createFile(fileBeforeMigration);
 
-		Migrator migrator = new Version7Migrator(cryptorProvider);
+		Migrator migrator = new Version7Migrator(csprng);
 		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		Assertions.assertFalse(Files.exists(fileBeforeMigration));
@@ -115,14 +114,14 @@ public void testMigrationOfNormalDirectory() throws IOException {
 	}
 
 	@Test
-	public void testMigrationOfNormalSymlink() throws IOException {
+	public void testMigrationOfNormalSymlink() throws CryptoException, IOException {
 		Path dir = dataDir.resolve("AA/BBBBBCCCCCDDDDDEEEEEFFFFFGGGGG");
 		Files.createDirectories(dir);
 		Path fileBeforeMigration = dir.resolve("1SMZUWYZLOMFWWK===");
 		Path fileAfterMigration = dir.resolve("ZmlsZW5hbWU=.c9r/symlink.c9r");
 		Files.createFile(fileBeforeMigration);
 
-		Migrator migrator = new Version7Migrator(cryptorProvider);
+		Migrator migrator = new Version7Migrator(csprng);
 		migrator.migrate(vaultRoot, null, "masterkey.cryptomator", "test");
 
 		Assertions.assertFalse(Files.exists(fileBeforeMigration));
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index dedd2af7..4bfb21c5 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -7,8 +7,12 @@
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.mocks.NullSecureRandom;
 import org.cryptomator.cryptolib.Cryptors;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.CryptorProvider;
-import org.cryptomator.cryptolib.api.KeyFile;
+import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Assumptions;
@@ -16,9 +20,11 @@
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.security.SecureRandom;
 
 public class Version8MigratorTest {
 
@@ -26,11 +32,10 @@ public class Version8MigratorTest {
 	private Path pathToVault;
 	private Path masterkeyFile;
 	private Path vaultConfigFile;
-	private CryptorProvider cryptorProvider;
+	private SecureRandom csprng = NullSecureRandom.INSTANCE;
 
 	@BeforeEach
 	public void setup() throws IOException {
-		cryptorProvider = Cryptors.version1(NullSecureRandom.INSTANCE);
 		fs = Jimfs.newFileSystem(Configuration.unix());
 		pathToVault = fs.getPath("/vaultDir");
 		masterkeyFile = pathToVault.resolve("masterkey.cryptomator");
@@ -44,17 +49,17 @@ public void teardown() throws IOException {
 	}
 
 	@Test
-	public void testMigrate() throws IOException {
-		KeyFile beforeMigration = cryptorProvider.createNew().writeKeysToMasterkeyFile("topsecret", 7);
-		Assumptions.assumeTrue(beforeMigration.getVersion() == 7);
+	public void testMigrate() throws CryptoException, IOException {
+		Masterkey masterkey = Masterkey.createNew(csprng);
+		byte[] unmigrated = MasterkeyFile.lock(masterkey, "topsecret", new byte[0], 7, csprng);
 		Assumptions.assumeFalse(Files.exists(vaultConfigFile));
-		Files.write(masterkeyFile, beforeMigration.serialize());
+		Files.write(masterkeyFile, unmigrated);
 
-		Migrator migrator = new Version8Migrator(cryptorProvider);
+		Migrator migrator = new Version8Migrator(csprng);
 		migrator.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "topsecret");
 
-		KeyFile afterMigration = KeyFile.parse(Files.readAllBytes(masterkeyFile));
-		Assertions.assertEquals(999, afterMigration.getVersion());
+		String migrated = Files.readString(masterkeyFile, StandardCharsets.UTF_8);
+		MatcherAssert.assertThat(migrated, CoreMatchers.containsString("\"version\": 999"));
 		Assertions.assertTrue(Files.exists(vaultConfigFile));
 		DecodedJWT token = JWT.decode(Files.readString(vaultConfigFile));
 		Assertions.assertNotNull(token.getId());

From 08c3871aa0ee92afa43d9274c13cfb40bbd0bfc8 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 7 Dec 2020 12:55:27 +0100
Subject: [PATCH 07/70] Use CryptoFileSystemProperties during vault
 initialization

---
 .../cryptofs/CryptoFileSystemProperties.java  |  9 -------
 .../cryptofs/CryptoFileSystemProvider.java    | 24 +++++++++----------
 ...toFileChannelWriteReadIntegrationTest.java |  5 ++--
 ...yptoFileSystemProviderIntegrationTest.java | 18 ++++++++------
 .../CryptoFileSystemProviderTest.java         |  6 +++--
 .../cryptofs/CryptoFileSystemUriTest.java     |  6 ++---
 ...ptyCiphertextDirectoryIntegrationTest.java | 24 +++++++++----------
 .../cryptofs/ReadmeCodeSamplesTest.java       | 10 ++++----
 .../RealFileSystemIntegrationTest.java        |  6 ++---
 ...iteFileWhileReadonlyChannelIsOpenTest.java |  6 ++---
 .../attr/FileAttributeIntegrationTest.java    |  7 +++---
 11 files changed, 60 insertions(+), 61 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 05259575..50134717 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -95,15 +95,6 @@ public enum FileSystemFlags {
 		 * If present, the vault is opened in read-only mode.
 		 */
 		READONLY,
-
-		/**
-		 * If present, the maximum ciphertext path length (beginning from the root of the vault directory).
-		 * <p>
-		 * If exceeding the limit during a file operation, an exception is thrown.
-		 *
-		 * @since 1.9.8
-		 */
-		MAX_PATH_LENGTH,
 	}
 
 	private final Set<Entry<String, Object>> entries;
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 18056194..f9b83495 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -118,25 +118,25 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP
 	/**
 	 * Creates a new vault at the given directory path.
 	 *
-	 * @param pathToVault         Path to an existing directory
-	 * @param vaultConfigFilename Name of the vault config file
-	 * @param keyId               The ID of the key to associate with this vault
-	 * @param keyLoader           A key loader providing the masterkey for this new vault
-	 * @throws NotDirectoryException                                  If the given path is not an existing directory.
-	 * @throws IOException                                            If the vault structure could not be initialized due to I/O errors
-	 * @throws MasterkeyLoadingFailedException
+	 * @param pathToVault Path to an existing directory
+	 * @param properties  Parameters to use when writing the vault configuration
+	 * @param keyId       ID of the master key to use for this vault
+	 * @throws NotDirectoryException           If the given path is not an existing directory.
+	 * @throws IOException                     If the vault structure could not be initialized due to I/O errors
+	 * @throws MasterkeyLoadingFailedException If thrown by the supplied keyLoader
 	 * @since 2.0.0
 	 */
-	public static void initialize(Path pathToVault, String vaultConfigFilename, String keyId, MasterkeyLoader keyLoader) throws NotDirectoryException, IOException, MasterkeyLoadingFailedException {
+	public static void initialize(Path pathToVault, CryptoFileSystemProperties properties, String keyId) throws NotDirectoryException, IOException, MasterkeyLoadingFailedException {
 		if (!Files.isDirectory(pathToVault)) {
 			throw new NotDirectoryException(pathToVault.toString());
 		}
 		byte[] rawKey = new byte[0];
-		try (Masterkey key = keyLoader.loadKey(keyId)) {
+		try (Masterkey key = properties.keyLoader().loadKey(keyId)) {
 			rawKey = key.getEncoded();
 			// save vault config:
-			Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
-			var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(Constants.MAX_CIPHERTEXT_NAME_LENGTH).build();
+			Path vaultConfigPath = pathToVault.resolve(properties.vaultConfigFilename());
+			// TODO move ciphermode to properties
+			var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(properties.maxNameLength()).build();
 			var token = config.toToken(keyId, rawKey);
 			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
 			// create "d" dir:
@@ -145,7 +145,7 @@ public static void initialize(Path pathToVault, String vaultConfigFilename, Stri
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}
-		assert containsVault(pathToVault, vaultConfigFilename);
+		assert containsVault(pathToVault, properties.vaultConfigFilename());
 	}
 
 	/**
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 46f4a0a1..42c6a83a 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -136,8 +136,9 @@ public void beforeAll() throws IOException, MasterkeyLoadingFailedException {
 			Path vaultPath = inMemoryFs.getPath("vault");
 			Files.createDirectories(vaultPath);
 			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			CryptoFileSystemProvider.initialize(vaultPath, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, cryptoFileSystemProperties().withKeyLoader(keyLoader).withFlags().build());
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+			CryptoFileSystemProvider.initialize(vaultPath, properties, "MASTERKEY_FILE");
+			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, properties);
 			file = fileSystem.getPath("/test.txt");
 		}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index ea73af4f..2d7076d6 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -84,13 +84,13 @@ class WithLimitedPaths {
 
 		@BeforeAll
 		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
-			CryptoFileSystemProvider.initialize(tmpDir, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 					.withFlags() //
 					.withMasterkeyFilename("masterkey.cryptomator") //
 					.withKeyLoader(keyLoader) //
 					.withMaxPathLength(100)
 					.build();
+			CryptoFileSystemProvider.initialize(tmpDir, properties, "MASTERKEY_FILE");
 			fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
 		}
 
@@ -209,11 +209,13 @@ public void teardown() throws IOException {
 		public void initializeVaults() {
 			Assertions.assertAll(
 					() -> {
-						CryptoFileSystemProvider.initialize(pathToVault1, "vault.cryptomator", "MASTERKEY_FILE", keyLoader1);
+						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader1).build();
+						CryptoFileSystemProvider.initialize(pathToVault1, properties, "MASTERKEY_FILE");
 						Assertions.assertTrue(Files.isDirectory(pathToVault1.resolve("d")));
 						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile1));
 					}, () -> {
-						CryptoFileSystemProvider.initialize(pathToVault2, "vault.cryptomator", "MASTERKEY_FILE", keyLoader2);
+						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader2).build();
+						CryptoFileSystemProvider.initialize(pathToVault2, properties, "MASTERKEY_FILE");
 						Assertions.assertTrue(Files.isDirectory(pathToVault2.resolve("d")));
 						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile2));
 					});
@@ -527,8 +529,9 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
-			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+			CryptoFileSystemProvider.initialize(pathToVault, properties, "MASTERKEY_FILE");
+			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
 		}
 
 		@Nested
@@ -617,8 +620,9 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
-			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+			CryptoFileSystemProvider.initialize(pathToVault, properties, "MASTERKEY_FILE");
+			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
 		}
 
 		@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 489bb05b..b65d91df 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -164,9 +164,10 @@ public void testGetSchemeReturnsCryptomatorScheme() {
 	public void testInitializeFailWithNotDirectoryException() {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 		Path pathToVault = fs.getPath("/vaultDir");
+		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 
 		Assertions.assertThrows(NotDirectoryException.class, () -> {
-			CryptoFileSystemProvider.initialize(pathToVault, "irrelevant.txt", "irrelevant", keyLoader);
+			CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
 		});
 	}
 
@@ -176,9 +177,10 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException
 		Path pathToVault = fs.getPath("/vaultDir");
 		Path vaultConfigFile = pathToVault.resolve("vault.cryptomator");
 		Path dataDir = pathToVault.resolve("d");
+		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "MASTERKEY_FILE", keyLoader);
+		CryptoFileSystemProvider.initialize(pathToVault, properties, "MASTERKEY_FILE");
 
 		Assertions.assertTrue(Files.isDirectory(dataDir));
 		Assertions.assertTrue(Files.isRegularFile(vaultConfigFile));
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
index 5a694017..16605b5d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
@@ -16,7 +16,6 @@
 import java.nio.file.Paths;
 
 import static java.nio.file.Files.createTempDirectory;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 
 public class CryptoFileSystemUriTest {
 
@@ -75,8 +74,9 @@ public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException
 		Path tempDir = createTempDirectory("CryptoFileSystemUrisTest").toAbsolutePath();
 		try {
 			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			CryptoFileSystemProvider.initialize(tempDir, "vault.cryptomator", "irrelevant", keyLoader);
-			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+			CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+			CryptoFileSystemProvider.initialize(tempDir, properties, "irrelevant");
+			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, properties);
 			Path absolutePathToVault = fileSystem.getPath("a").toAbsolutePath();
 
 			URI uri = CryptoFileSystemUri.create(absolutePathToVault, "a", "b");
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index b79c4d1b..9b4417aa 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -26,14 +26,11 @@
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
 import static java.nio.file.StandardOpenOption.CREATE_NEW;
-import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
+import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH;
 
 /**
  * Regression tests https://github.com/cryptomator/cryptofs/issues/17.
@@ -48,8 +45,9 @@ public static void setupClass(@TempDir Path tmpDir) throws IOException, Masterke
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
 
 	@Test
@@ -58,7 +56,7 @@ public void testDeleteCiphertextDirectoryContainingNonCryptoFile() throws IOExce
 		Files.createDirectory(cleartextDirectory);
 
 		Path ciphertextDirectory = firstEmptyCiphertextDirectory();
-		createFile(ciphertextDirectory, "foo01234.txt", new byte[] {65});
+		createFile(ciphertextDirectory, "foo01234.txt", new byte[]{65});
 
 		Files.delete(cleartextDirectory);
 	}
@@ -77,9 +75,9 @@ public void testDeleteCiphertextDirectoryContainingDirectories() throws IOExcept
 		// .... text.data
 		Path foo0123 = createFolder(ciphertextDirectory, "foo0123");
 		Path foobar = createFolder(foo0123, "foobar");
-		createFile(foo0123, "test.txt", new byte[] {65});
-		createFile(foo0123, "text.data", new byte[] {65});
-		createFile(foobar, "test.baz", new byte[] {65});
+		createFile(foo0123, "test.txt", new byte[]{65});
+		createFile(foo0123, "text.data", new byte[]{65});
+		createFile(foobar, "test.baz", new byte[]{65});
 
 		Files.delete(cleartextDirectory);
 	}
@@ -92,7 +90,7 @@ public void testDeleteDirectoryContainingLongNameFileWithoutMetadata() throws IO
 
 		Path ciphertextDirectory = firstEmptyCiphertextDirectory();
 		Path longNameDir = createFolder(ciphertextDirectory, "HHEZJURE.c9s");
-		createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[] {65});
+		createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[]{65});
 
 		Files.delete(cleartextDirectory);
 	}
@@ -106,7 +104,7 @@ public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() thro
 		Path ciphertextDirectory = firstEmptyCiphertextDirectory();
 		Path longNameDir = createFolder(ciphertextDirectory, "HHEZJURE.c9s");
 		createFile(longNameDir, Constants.INFLATED_FILE_NAME, "HHEZJUREHHEZJUREHHEZJURE".getBytes());
-		createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[] {65});
+		createFile(longNameDir, Constants.CONTENTS_FILE_NAME, new byte[]{65});
 
 		Files.delete(cleartextDirectory);
 	}
@@ -115,7 +113,7 @@ public void testDeleteDirectoryContainingUnauthenticLongNameDirectoryFile() thro
 	public void testDeleteNonEmptyDir() throws IOException {
 		Path cleartextDirectory = fileSystem.getPath("/d");
 		Files.createDirectory(cleartextDirectory);
-		createFile(cleartextDirectory, "test", new byte[] {65});
+		createFile(cleartextDirectory, "test", new byte[]{65});
 
 		Assertions.assertThrows(DirectoryNotEmptyException.class, () -> {
 			Files.delete(cleartextDirectory);
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
index 1cf8fe53..8bbdca1c 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
@@ -30,8 +30,9 @@ public class ReadmeCodeSamplesTest {
 	@Test
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", keyLoader);
-		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProvider.initialize(storageLocation, properties, "irrelevant");
+		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, properties);
 
 		runCodeSample(fileSystem);
 	}
@@ -40,8 +41,9 @@ public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		URI uri = CryptoFileSystemUri.create(storageLocation);
 		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProvider.initialize(storageLocation, "vault.cryptomator", "irrelevant", keyLoader);
-		FileSystem fileSystem = FileSystems.newFileSystem(uri, CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProvider.initialize(storageLocation, properties, "irrelevant");
+		FileSystem fileSystem = FileSystems.newFileSystem(uri, properties);
 
 		runCodeSample(fileSystem);
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
index 433927a3..7589fbcc 100644
--- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
@@ -23,7 +23,6 @@
 import java.nio.file.Path;
 import java.nio.file.attribute.UserPrincipal;
 
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
 
 public class RealFileSystemIntegrationTest {
@@ -36,8 +35,9 @@ public static void setupClass(@TempDir Path tmpDir) throws IOException, Masterke
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
index 2b7778dd..c3c0ce81 100644
--- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
@@ -17,7 +17,6 @@
 import java.nio.file.StandardOpenOption;
 
 import static java.nio.file.StandardOpenOption.READ;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
 
 /**
@@ -35,8 +34,9 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		Path pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 		root = fileSystem.getPath("/");
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index 35dd526c..6fe7f503 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -9,6 +9,7 @@
 package org.cryptomator.cryptofs.attr;
 
 import com.google.common.jimfs.Jimfs;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
@@ -42,7 +43,6 @@
 import static java.lang.Boolean.TRUE;
 import static java.lang.System.currentTimeMillis;
 import static java.nio.file.Files.readAttributes;
-import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.Matchers.greaterThan;
@@ -60,8 +60,9 @@ public static void setupClass() throws IOException, MasterkeyLoadingFailedExcept
 		pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProvider.initialize(pathToVault, "vault.cryptomator", "irrelevant", keyLoader);
-		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
 
 	@AfterAll

From 1ae75cf57e047da6cb74f2c4143509a5a09422ac Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 7 Dec 2020 13:32:18 +0100
Subject: [PATCH 08/70] Renamed cipher mode to cipher combo (as ciphermode is
 misleading)

---
 .../cryptofs/CryptoFileSystemProperties.java  | 32 +++++++++++++++++--
 .../cryptofs/CryptoFileSystemProvider.java    |  4 +--
 .../cryptofs/CryptoFileSystems.java           |  2 +-
 ...tCipherMode.java => VaultCipherCombo.java} |  8 +++--
 .../org/cryptomator/cryptofs/VaultConfig.java | 20 ++++++------
 .../migration/v8/Version8Migrator.java        |  2 +-
 .../CryptoFileSystemPropertiesTest.java       |  6 ++++
 .../cryptofs/CryptoFileSystemsTest.java       |  6 ++--
 .../cryptomator/cryptofs/VaultConfigTest.java | 14 ++++----
 .../migration/v8/Version8MigratorTest.java    |  2 +-
 10 files changed, 65 insertions(+), 31 deletions(-)
 rename src/main/java/org/cryptomator/cryptofs/{VaultCipherMode.java => VaultCipherCombo.java} (78%)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 50134717..7499509a 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -62,7 +62,6 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 
 	static final MasterkeyLoader DEFAULT_KEYLOADER = null;
 
-
 	/**
 	 * Key identifying the name of the vault config file located inside the vault directory.
 	 *
@@ -96,6 +95,15 @@ public enum FileSystemFlags {
 		 */
 		READONLY,
 	}
+	/**
+	 * Key identifying the combination of ciphers to use in a vault. Only meaningful during vault initialization.
+	 *
+	 * @since 2.0.0
+	 */
+	public static final String PROPERTY_CIPHER_COMBO = "cipherCombo";
+
+	// TODO: change to SIV_GCM with issue 94
+	static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC;
 
 	private final Set<Entry<String, Object>> entries;
 
@@ -106,7 +114,8 @@ private CryptoFileSystemProperties(Builder builder) {
 				Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
 				Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
 				Map.entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), //
-				Map.entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength) //
+				Map.entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength), //
+				Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo) //
 		);
 	}
 
@@ -114,6 +123,10 @@ MasterkeyLoader keyLoader() {
 		return (MasterkeyLoader) get(PROPERTY_KEYLOADER);
 	}
 
+	public VaultCipherCombo cipherCombo() {
+		return (VaultCipherCombo) get(PROPERTY_CIPHER_COMBO);
+	}
+
 	@SuppressWarnings("unchecked")
 	public Set<FileSystemFlags> flags() {
 		return (Set<FileSystemFlags>) get(PROPERTY_FILESYSTEM_FLAGS);
@@ -187,6 +200,7 @@ public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
 	 */
 	public static class Builder {
 
+		public VaultCipherCombo cipherCombo = DEFAULT_CIPHER_COMBO;
 		private MasterkeyLoader keyLoader = DEFAULT_KEYLOADER;
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
@@ -204,6 +218,7 @@ private Builder(Map<String, ?> properties) {
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
 			checkedSet(Integer.class, PROPERTY_MAX_PATH_LENGTH, properties, this::withMaxPathLength);
 			checkedSet(Integer.class, PROPERTY_MAX_NAME_LENGTH, properties, this::withMaxNameLength);
+			checkedSet(VaultCipherCombo.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo);
 		}
 
 		private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties, Consumer<T> setter) {
@@ -242,6 +257,19 @@ public Builder withMaxNameLength(int maxNameLength) {
 			return this;
 		}
 
+
+		/**
+		 * Sets the cipher combo used during vault initialization.
+		 *
+		 * @param cipherCombo The cipher combo
+		 * @return this
+		 * @since 2.0.0
+		 */
+		public Builder withCipherCombo(VaultCipherCombo cipherCombo) {
+			this.cipherCombo = cipherCombo;
+			return this;
+		}
+
 		/**
 		 * Sets the keyLoader for a CryptoFileSystem.
 		 *
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index f9b83495..0d27e4e3 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -12,7 +12,6 @@
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
 import java.io.IOException;
@@ -135,8 +134,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 			rawKey = key.getEncoded();
 			// save vault config:
 			Path vaultConfigPath = pathToVault.resolve(properties.vaultConfigFilename());
-			// TODO move ciphermode to properties
-			var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(properties.maxNameLength()).build();
+			var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).maxFilenameLength(properties.maxNameLength()).build();
 			var token = config.toToken(keyId, rawKey);
 			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
 			// create "d" dir:
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 12e90ecc..7bb928d4 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -67,7 +67,7 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 
 	// synchronized access to non-threadsafe cryptoFileSystemComponentBuilder required
 	private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, Masterkey masterkey, VaultConfig config) {
-		Cryptor cryptor = config.getCiphermode().getCryptorProvider(csprng).withKey(masterkey);
+		Cryptor cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(masterkey);
 		return cryptoFileSystemComponentBuilder //
 				.cryptor(cryptor) //
 				.vaultConfig(config) //
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java b/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
similarity index 78%
rename from src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java
rename to src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
index c752a00b..fa2ec1c3 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultCipherMode.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
@@ -1,13 +1,15 @@
 package org.cryptomator.cryptofs;
 
 import org.cryptomator.cryptolib.Cryptors;
-import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.CryptorProvider;
 
 import java.security.SecureRandom;
 import java.util.function.Function;
 
-public enum VaultCipherMode {
+/**
+ * A combination of different ciphers and/or cipher modes in a Cryptomator vault.
+ */
+public enum VaultCipherCombo {
 	/**
 	 * AES-SIV for file name encryption
 	 * AES-CTR + HMAC for content encryption
@@ -23,7 +25,7 @@ public enum VaultCipherMode {
 
 	private final Function<SecureRandom, CryptorProvider> cryptorProvider;
 
-	VaultCipherMode(Function<SecureRandom, CryptorProvider> cryptorProvider) {
+	VaultCipherCombo(Function<SecureRandom, CryptorProvider> cryptorProvider) {
 		this.cryptorProvider = cryptorProvider;
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
index be4c342d..8389f324 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -30,25 +30,25 @@
 public class VaultConfig {
 
 	private static final String JSON_KEY_VAULTVERSION = "format";
-	private static final String JSON_KEY_CIPHERCONFIG = "ciphermode";
+	private static final String JSON_KEY_CIPHERCONFIG = "cipherCombo";
 	private static final String JSON_KEY_MAXFILENAMELEN = "maxFilenameLen";
 
 	private final String id;
 	private final int vaultVersion;
-	private final VaultCipherMode ciphermode;
+	private final VaultCipherCombo cipherCombo;
 	private final int maxFilenameLength;
 
 	private VaultConfig(DecodedJWT verifiedConfig) {
 		this.id = verifiedConfig.getId();
 		this.vaultVersion = verifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt();
-		this.ciphermode = VaultCipherMode.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString());
+		this.cipherCombo = VaultCipherCombo.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString());
 		this.maxFilenameLength = verifiedConfig.getClaim(JSON_KEY_MAXFILENAMELEN).asInt();
 	}
 
 	private VaultConfig(VaultConfigBuilder builder) {
 		this.id = builder.id;
 		this.vaultVersion = builder.vaultVersion;
-		this.ciphermode = builder.ciphermode;
+		this.cipherCombo = builder.cipherCombo;
 		this.maxFilenameLength = builder.maxFilenameLength;
 	}
 
@@ -60,8 +60,8 @@ public int getVaultVersion() {
 		return vaultVersion;
 	}
 
-	public VaultCipherMode getCiphermode() {
-		return ciphermode;
+	public VaultCipherCombo getCipherCombo() {
+		return cipherCombo;
 	}
 
 	public int getMaxFilenameLength() {
@@ -73,7 +73,7 @@ public String toToken(String keyId, byte[] rawKey) {
 				.withKeyId(keyId) //
 				.withJWTId(id) //
 				.withClaim(JSON_KEY_VAULTVERSION, vaultVersion) //
-				.withClaim(JSON_KEY_CIPHERCONFIG, ciphermode.name()) //
+				.withClaim(JSON_KEY_CIPHERCONFIG, cipherCombo.name()) //
 				.withClaim(JSON_KEY_MAXFILENAMELEN, maxFilenameLength) //
 				.sign(Algorithm.HMAC256(rawKey));
 	}
@@ -176,11 +176,11 @@ public static class VaultConfigBuilder {
 
 		private final String id = UUID.randomUUID().toString();
 		private final int vaultVersion = Constants.VAULT_VERSION;
-		private VaultCipherMode ciphermode;
+		private VaultCipherCombo cipherCombo;
 		private int maxFilenameLength;
 
-		public VaultConfigBuilder cipherMode(VaultCipherMode ciphermode) {
-			this.ciphermode = ciphermode;
+		public VaultConfigBuilder cipherCombo(VaultCipherCombo cipherCombo) {
+			this.cipherCombo = cipherCombo;
 			return this;
 		}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 455622f6..2410f321 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -67,7 +67,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 					.withJWTId(UUID.randomUUID().toString()) //
 					.withKeyId("MASTERKEY_FILE") //
 					.withClaim("format", 8) //
-					.withClaim("ciphermode", "SIV_CTRMAC") //
+					.withClaim("cipherCombo", "SIV_CTRMAC") //
 					.withClaim("maxFilenameLen", 220) //
 					.sign(algorithm);
 			Files.writeString(vaultConfigFile, config, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 3bacaac5..2f30b9a7 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -49,6 +49,7 @@ public void testSetReadonlyFlag() {
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
 
@@ -71,6 +72,7 @@ public void testSetMasterkeyFilenameAndReadonlyFlag() {
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
 
@@ -97,6 +99,7 @@ public void testFromMap() {
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, 255), //
+						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
 
@@ -119,6 +122,7 @@ public void testWrapMapWithTrueReadonly() {
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
 
@@ -141,6 +145,7 @@ public void testWrapMapWithFalseReadonly() {
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class))));
 	}
 
@@ -193,6 +198,7 @@ public void testWrapMapWithoutReadonly() {
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
 						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class))
 				)
 		);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index c1a2536c..bdb2fee2 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -44,7 +44,7 @@ public class CryptoFileSystemsTest {
 	private final Masterkey masterkey = mock(Masterkey.class);
 	private final byte[] rawKey = new byte[64];
 	private final VaultConfig vaultConfig = mock(VaultConfig.class);
-	private final VaultCipherMode cipherMode = mock(VaultCipherMode.class);
+	private final VaultCipherCombo cipherCombo = mock(VaultCipherCombo.class);
 	private final SecureRandom csprng = Mockito.mock(SecureRandom.class);
 	private final CryptorProvider cryptorProvider = mock(CryptorProvider.class);
 	private final Cryptor cryptor = mock(Cryptor.class);
@@ -72,8 +72,8 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(keyLoader.loadKey("key-id")).thenReturn(masterkey);
 		when(masterkey.getEncoded()).thenReturn(rawKey);
 		when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
-		when(vaultConfig.getCiphermode()).thenReturn(cipherMode);
-		when(cipherMode.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
+		when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo);
+		when(cipherCombo.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
 		when(cryptorProvider.withKey(masterkey)).thenReturn(cryptor);
 		when(cryptoFileSystemComponentBuilder.cryptor(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.vaultConfig(any())).thenReturn(cryptoFileSystemComponentBuilder);
diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
index 9d13385d..4e347b95 100644
--- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
@@ -41,7 +41,7 @@ public class WithValidToken {
 		public void setup() {
 			Arrays.fill(rawKey, (byte) 0x55);
 			Mockito.when(key.getEncoded()).thenReturn(rawKey);
-			originalConfig = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(220).build();
+			originalConfig = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).maxFilenameLength(220).build();
 			token = originalConfig.toToken("TEST_KEY", rawKey);
 		}
 
@@ -51,7 +51,7 @@ public void testSuccessfulLoad() throws VaultConfigLoadException, MasterkeyLoadi
 
 			Assertions.assertEquals(originalConfig.getId(), loaded.getId());
 			Assertions.assertEquals(originalConfig.getVaultVersion(), loaded.getVaultVersion());
-			Assertions.assertEquals(originalConfig.getCiphermode(), loaded.getCiphermode());
+			Assertions.assertEquals(originalConfig.getCipherCombo(), loaded.getCipherCombo());
 			Assertions.assertEquals(originalConfig.getMaxFilenameLength(), loaded.getMaxFilenameLength());
 		}
 
@@ -70,11 +70,11 @@ public void testLoadWithInvalidKey(int pos) {
 
 	@Test
 	public void testCreateNew() {
-		var config = VaultConfig.createNew().cipherMode(VaultCipherMode.SIV_CTRMAC).maxFilenameLength(220).build();
+		var config = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).maxFilenameLength(220).build();
 
 		Assertions.assertNotNull(config.getId());
 		Assertions.assertEquals(Constants.VAULT_VERSION, config.getVaultVersion());
-		Assertions.assertEquals(VaultCipherMode.SIV_CTRMAC, config.getCiphermode());
+		Assertions.assertEquals(VaultCipherCombo.SIV_CTRMAC, config.getCipherCombo());
 		Assertions.assertEquals(220, config.getMaxFilenameLength());
 	}
 
@@ -82,7 +82,7 @@ public void testCreateNew() {
 	public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoadingFailedException {
 		var decodedJwt = Mockito.mock(DecodedJWT.class);
 		var formatClaim = Mockito.mock(Claim.class);
-		var ciphermodeClaim = Mockito.mock(Claim.class);
+		var cipherComboClaim = Mockito.mock(Claim.class);
 		var maxFilenameLenClaim = Mockito.mock(Claim.class);
 		var keyLoader = Mockito.mock(MasterkeyLoader.class);
 		var key = Mockito.mock(Masterkey.class);
@@ -90,7 +90,7 @@ public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoading
 		var verifier = Mockito.mock(JWTVerifier.class);
 		Mockito.when(decodedJwt.getKeyId()).thenReturn("key-id");
 		Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim);
-		Mockito.when(decodedJwt.getClaim("ciphermode")).thenReturn(ciphermodeClaim);
+		Mockito.when(decodedJwt.getClaim("cipherCombo")).thenReturn(cipherComboClaim);
 		Mockito.when(decodedJwt.getClaim("maxFilenameLen")).thenReturn(maxFilenameLenClaim);
 		Mockito.when(keyLoader.loadKey("key-id")).thenReturn(key);
 		Mockito.when(key.getEncoded()).thenReturn(new byte[64]);
@@ -98,7 +98,7 @@ public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoading
 		Mockito.when(verification.build()).thenReturn(verifier);
 		Mockito.when(verifier.verify(decodedJwt)).thenReturn(decodedJwt);
 		Mockito.when(formatClaim.asInt()).thenReturn(42);
-		Mockito.when(ciphermodeClaim.asString()).thenReturn("SIV_CTRMAC");
+		Mockito.when(cipherComboClaim.asString()).thenReturn("SIV_CTRMAC");
 		Mockito.when(maxFilenameLenClaim.asInt()).thenReturn(220);
 		try (var jwtMock = Mockito.mockStatic(JWT.class)) {
 			jwtMock.when(() -> JWT.decode("jwt-vault-config")).thenReturn(decodedJwt);
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 4bfb21c5..34ff3700 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -65,7 +65,7 @@ public void testMigrate() throws CryptoException, IOException {
 		Assertions.assertNotNull(token.getId());
 		Assertions.assertEquals("MASTERKEY_FILE", token.getKeyId());
 		Assertions.assertEquals(8, token.getClaim("format").asInt());
-		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("ciphermode").asString());
+		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("cipherCombo").asString());
 		Assertions.assertEquals(220, token.getClaim("maxFilenameLen").asInt());
 	}
 

From a1b12f3e3755163c62dca60e532216e1c528950b Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 7 Dec 2020 17:25:24 +0100
Subject: [PATCH 09/70] fix for jackson vulnerability

---
 pom.xml | 11 +++++++++++
 1 file changed, 11 insertions(+)

diff --git a/pom.xml b/pom.xml
index 956bd1ed..ddb22255 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,17 @@
 		</repository>
 	</repositories>
 
+	<dependencyManagement>
+		<dependencies>
+			<!-- temporary fix for XXE attack, can be removed once java-jwt is updated -->
+			<dependency>
+				<groupId>com.fasterxml.jackson.core</groupId>
+				<artifactId>jackson-databind</artifactId>
+				<version>2.10.5.1</version>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+
 	<dependencies>
 		<dependency>
 			<groupId>org.cryptomator</groupId>

From d2a184b8b67898c8f102a19419e9262ef1265f57 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 8 Dec 2020 13:26:31 +0100
Subject: [PATCH 10/70] changed `containsVault` check to respect old vault
 format

---
 .../cryptofs/CryptoFileSystemProvider.java    |  8 +++--
 ...yptoFileSystemProviderIntegrationTest.java |  4 +--
 .../CryptoFileSystemProviderTest.java         | 31 ++++++++++++++++---
 3 files changed, 33 insertions(+), 10 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 0d27e4e3..63ffec92 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -143,7 +143,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}
-		assert containsVault(pathToVault, properties.vaultConfigFilename());
+		assert containsVault(pathToVault, properties.vaultConfigFilename(), properties.masterkeyFilename());
 	}
 
 	/**
@@ -151,13 +151,15 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 	 *
 	 * @param pathToVault         A directory path
 	 * @param vaultConfigFilename Name of the vault config file
+	 * @param masterkeyFilename   Name of the masterkey file
 	 * @return <code>true</code> if the directory seems to contain a vault.
 	 * @since 2.0.0
 	 */
-	public static boolean containsVault(Path pathToVault, String vaultConfigFilename) {
+	public static boolean containsVault(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) {
 		Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
+		Path masterkeyPath = pathToVault.resolve(masterkeyFilename);
 		Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
-		return Files.isReadable(vaultConfigPath) && Files.isDirectory(dataDirPath);
+		return (Files.isReadable(vaultConfigPath) || Files.isReadable(masterkeyPath)) && Files.isDirectory(dataDirPath);
 	}
 
 	/**
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 2d7076d6..2d5a4aa7 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -225,8 +225,8 @@ public void initializeVaults() {
 		@Order(2)
 		@DisplayName("get filesystem with incorrect credentials")
 		public void testGetFsWithWrongCredentials() {
-			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault1, "vault.cryptomator"));
-			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault2, "vault.cryptomator"));
+			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault1, "vault.cryptomator", "masterkey.cryptomator"));
+			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault2, "vault.cryptomator", "masterkey.cryptomator"));
 			Assertions.assertAll(
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault1);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index b65d91df..48bed8a8 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -200,10 +200,27 @@ public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedExcept
 		Mockito.verify(fileSystems).create(Mockito.same(inTest), Mockito.eq(pathToVault), Mockito.eq(properties));
 	}
 
+	@Test
+	public void testContainsVaultReturnsTrueIfDirectoryContainsVaultConfigFileAndDataDir() throws IOException {
+		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
+
+		String vaultConfigFilename = "vaultconfig.foo.baz";
+		String masterkeyFilename = "masterkey.foo.baz";
+		Path pathToVault = fs.getPath("/vaultDir");
+
+		Path vaultConfigFile = pathToVault.resolve(vaultConfigFilename);
+		Path dataDir = pathToVault.resolve("d");
+		Files.createDirectories(dataDir);
+		Files.write(vaultConfigFile, new byte[0]);
+
+		Assertions.assertTrue(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
+	}
+
 	@Test
 	public void testContainsVaultReturnsTrueIfDirectoryContainsMasterkeyFileAndDataDir() throws IOException {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 
+		String vaultConfigFilename = "vaultconfig.foo.baz";
 		String masterkeyFilename = "masterkey.foo.baz";
 		Path pathToVault = fs.getPath("/vaultDir");
 
@@ -212,34 +229,38 @@ public void testContainsVaultReturnsTrueIfDirectoryContainsMasterkeyFileAndDataD
 		Files.createDirectories(dataDir);
 		Files.write(masterkeyFile, new byte[0]);
 
-		Assertions.assertTrue(containsVault(pathToVault, masterkeyFilename));
+		Assertions.assertTrue(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
 	}
 
 	@Test
-	public void testContainsVaultReturnsFalseIfDirectoryContainsNoMasterkeyFileButDataDir() throws IOException {
+	public void testContainsVaultReturnsFalseIfDirectoryContainsOnlyDataDir() throws IOException {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 
+		String vaultConfigFilename = "vaultconfig.foo.baz";
 		String masterkeyFilename = "masterkey.foo.baz";
 		Path pathToVault = fs.getPath("/vaultDir");
 
 		Path dataDir = pathToVault.resolve("d");
 		Files.createDirectories(dataDir);
 
-		Assertions.assertFalse(containsVault(pathToVault, masterkeyFilename));
+		Assertions.assertFalse(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
 	}
 
 	@Test
-	public void testContainsVaultReturnsFalseIfDirectoryContainsMasterkeyFileButNoDataDir() throws IOException {
+	public void testContainsVaultReturnsFalseIfDirectoryContainsNoDataDir() throws IOException {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 
+		String vaultConfigFilename = "vaultconfig.foo.baz";
 		String masterkeyFilename = "masterkey.foo.baz";
 		Path pathToVault = fs.getPath("/vaultDir");
 
+		Path vaultConfigFile = pathToVault.resolve(vaultConfigFilename);
 		Path masterkeyFile = pathToVault.resolve(masterkeyFilename);
 		Files.createDirectories(pathToVault);
+		Files.write(vaultConfigFile, new byte[0]);
 		Files.write(masterkeyFile, new byte[0]);
 
-		Assertions.assertFalse(containsVault(pathToVault, masterkeyFilename));
+		Assertions.assertFalse(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
 	}
 
 	@Test

From 22843d8e80d7c39b80bef09f0f538a05a1e83344 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 8 Dec 2020 13:34:18 +0100
Subject: [PATCH 11/70] fixes #94

---
 .../cryptofs/CryptoFileSystemProperties.java        |  3 +--
 .../org/cryptomator/cryptofs/VaultCipherCombo.java  | 13 ++++++-------
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 7499509a..f942c22d 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -102,8 +102,7 @@ public enum FileSystemFlags {
 	 */
 	public static final String PROPERTY_CIPHER_COMBO = "cipherCombo";
 
-	// TODO: change to SIV_GCM with issue 94
-	static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_CTRMAC;
+	static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_GCM;
 
 	private final Set<Entry<String, Object>> entries;
 
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java b/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
index fa2ec1c3..101cdd0c 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
@@ -14,14 +14,13 @@ public enum VaultCipherCombo {
 	 * AES-SIV for file name encryption
 	 * AES-CTR + HMAC for content encryption
 	 */
-	SIV_CTRMAC(Cryptors::version1);
+	SIV_CTRMAC(Cryptors::version1),
 
-// TODO enable eventually (issue 94):
-//	/**
-//	 * AES-SIV for file name encryption
-//	 * AES-GCM for content encryption
-//	 */
-//	SIV_GCM(Cryptors::version2);
+	/**
+	 * AES-SIV for file name encryption
+	 * AES-GCM for content encryption
+	 */
+	SIV_GCM(Cryptors::version2);
 
 	private final Function<SecureRandom, CryptorProvider> cryptorProvider;
 

From 790c20cabda06921fb1443d61b295825625733fa Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 8 Dec 2020 13:41:16 +0100
Subject: [PATCH 12/70] bump version and suppress false positive in owasp
 dependency check

---
 pom.xml         | 2 +-
 suppression.xml | 5 +++++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index ddb22255..4a3bda84 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptofs</artifactId>
-	<version>1.10.0-SNAPSHOT</version>
+	<version>2.0.0-SNAPSHOT</version>
 	<name>Cryptomator Crypto Filesystem</name>
 	<description>This library provides the Java filesystem provider used by Cryptomator.</description>
 	<url>https://github.com/cryptomator/cryptofs</url>
diff --git a/suppression.xml b/suppression.xml
index 9edca825..23090b9b 100644
--- a/suppression.xml
+++ b/suppression.xml
@@ -6,4 +6,9 @@
 		<gav>org.slf4j:slf4j-api:1.7.25</gav>
 		<cve>CVE-2018-8088</cve>
 	</suppress>
+	<suppress>
+		<notes><![CDATA[ Upstream fix backported from 2.11.0 to 2.10.5.1, see https://github.com/FasterXML/jackson-databind/issues/2589#issuecomment-714833837. ]]></notes>
+		<gav>com.fasterxml.jackson.core:jackson-databind:2.10.5.1</gav>
+		<cve>CVE-2020-25649</cve>
+	</suppress>
 </suppressions>
\ No newline at end of file

From 883cd55422aacfd8d39090e1abc951d893b98349 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 8 Dec 2020 13:46:27 +0100
Subject: [PATCH 13/70] preparing 2.0.0

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 4a3bda84..8fc4ddeb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptofs</artifactId>
-	<version>2.0.0-SNAPSHOT</version>
+	<version>2.0.0</version>
 	<name>Cryptomator Crypto Filesystem</name>
 	<description>This library provides the Java filesystem provider used by Cryptomator.</description>
 	<url>https://github.com/cryptomator/cryptofs</url>

From 96a4465368457c2bc68a9f7e7ab4fe5e4168dc46 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 8 Dec 2020 13:55:19 +0100
Subject: [PATCH 14/70] use release profile for release builds

---
 .github/workflows/build.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index f6a99184..849d810f 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -50,7 +50,7 @@ jobs:
           path: target/cryptofs-${{ env.BUILD_VERSION }}.jar # what about target/dependency-list.txt?
       - name: Build and deploy to jcenter
         if: startsWith(github.ref, 'refs/tags/')
-        run: mvn -B deploy -DskipTests
+        run: mvn -B deploy -DskipTests -Prelease
         env:
           BINTRAY_USERNAME: cryptobot
           BINTRAY_API_KEY: ${{ secrets.BINTRAY_API_KEY }}

From 33d7ef102f9f6838ca1944815e01e762b6ca9201 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 8 Dec 2020 14:05:55 +0100
Subject: [PATCH 15/70] javadoc fixes

---
 .../cryptomator/cryptofs/CryptoFileSystemProvider.java |  1 -
 .../org/cryptomator/cryptofs/CryptoFileSystemUri.java  |  1 +
 .../cryptofs/migration/v7/Version7Migrator.java        | 10 +++++-----
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 63ffec92..2256a727 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -72,7 +72,6 @@
  * <p>
  * Afterwards you can use the created {@code FileSystem} to create paths, do directory listings, create files and so on.
  * <p>
- * <p>
  * To create a new FileSystem from a URI using {@link FileSystems#newFileSystem(URI, Map)} you may have a look at {@link CryptoFileSystemUri}.
  *
  * @see CryptoFileSystemUri
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java
index 15f4299f..fbf866bc 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemUri.java
@@ -69,6 +69,7 @@ static Path uncCompatibleUriToPath(URI uri) {
 	 * 
 	 * @param pathToVault path to the vault
 	 * @param pathComponentsInsideVault path components to node inside the vault
+	 * @return An URI pointing to the root of the CryptoFileSystem
 	 */
 	public static URI create(Path pathToVault, String... pathComponentsInsideVault) {
 		try {
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index e041282d..0503d72b 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -34,15 +34,15 @@
  * Renames ciphertext names:
  *
  * <ul>
- *     <li>Files: BASE32== -> base64==.c9r</li>
- *     <li>Dirs: 0BASE32== -> base64==.c9r/dir.c9r</li>
- *     <li>Symlinks: 1SBASE32== -> base64.c9r/symlink.c9r</li>
+ *     <li>Files: BASE32== → base64==.c9r</li>
+ *     <li>Dirs: 0BASE32== → base64==.c9r/dir.c9r</li>
+ *     <li>Symlinks: 1SBASE32== → base64.c9r/symlink.c9r</li>
  * </ul>
  * <p>
  * Shortened names:
  * <ul>
- *     <li>shortened.lng -> shortened.c9s</li>
- *     <li>m/shortened.lng -> shortened.c9s/contents.c9r</li>
+ *     <li>shortened.lng → shortened.c9s</li>
+ *     <li>m/shortened.lng → shortened.c9s/contents.c9r</li>
  * </ul>
  */
 public class Version7Migrator implements Migrator {

From de4d0faee36f42acdfc2b956be34cfbba87f7bb1 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 18 Jan 2021 15:29:49 +0100
Subject: [PATCH 16/70] dependency updates

---
 pom.xml | 21 +++++----------------
 1 file changed, 5 insertions(+), 16 deletions(-)

diff --git a/pom.xml b/pom.xml
index 4a3bda84..b935f7e5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,14 +18,14 @@
 
 		<!-- dependencies -->
 		<cryptolib.version>2.0.0-beta2</cryptolib.version>
-		<jwt.version>3.11.0</jwt.version>
-		<dagger.version>2.29.1</dagger.version>
-		<guava.version>30.0-jre</guava.version>
+		<jwt.version>3.12.0</jwt.version>
+		<dagger.version>2.31</dagger.version>
+		<guava.version>30.1-jre</guava.version>
 		<slf4j.version>1.7.30</slf4j.version>
 
 		<!-- test dependencies -->
 		<junit.jupiter.version>5.7.0</junit.jupiter.version>
-		<mockito.version>3.6.0</mockito.version>
+		<mockito.version>3.7.7</mockito.version>
 		<hamcrest.version>2.2</hamcrest.version>
 	</properties>
 
@@ -62,17 +62,6 @@
 		</repository>
 	</repositories>
 
-	<dependencyManagement>
-		<dependencies>
-			<!-- temporary fix for XXE attack, can be removed once java-jwt is updated -->
-			<dependency>
-				<groupId>com.fasterxml.jackson.core</groupId>
-				<artifactId>jackson-databind</artifactId>
-				<version>2.10.5.1</version>
-			</dependency>
-		</dependencies>
-	</dependencyManagement>
-
 	<dependencies>
 		<dependency>
 			<groupId>org.cryptomator</groupId>
@@ -167,7 +156,7 @@
 					<plugin>
 						<groupId>org.owasp</groupId>
 						<artifactId>dependency-check-maven</artifactId>
-						<version>6.0.3</version>
+						<version>6.0.5</version>
 						<configuration>
 							<cveValidForHours>24</cveValidForHours>
 							<failBuildOnCVSS>0</failBuildOnCVSS>

From 0e5cd91f905745d31e3318a771217cdceb27c0cb Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 25 Jan 2021 19:12:08 +0100
Subject: [PATCH 17/70] adjustments to new cryptolib API

---
 pom.xml                                       |  2 +-
 .../cryptofs/CryptoFileSystemComponent.java   |  1 -
 .../cryptofs/CryptoFileSystemProperties.java  | 70 ++++++++++-------
 .../cryptofs/CryptoFileSystemProvider.java    | 29 ++++---
 .../CryptoFileSystemProviderComponent.java    | 10 ++-
 .../CryptoFileSystemProviderModule.java       | 10 ---
 .../cryptofs/CryptoFileSystems.java           |  3 +-
 .../org/cryptomator/cryptofs/VaultConfig.java |  5 +-
 .../cryptofs/common/Constants.java            |  3 +
 .../cryptofs/migration/Migrators.java         |  5 +-
 .../migration/v6/Version6Migrator.java        | 11 +--
 .../migration/v7/Version7Migrator.java        | 11 +--
 .../migration/v8/Version8Migrator.java        | 10 +--
 ...toFileChannelWriteReadIntegrationTest.java | 17 ++--
 .../CryptoFileSystemPropertiesTest.java       | 78 ++++++++-----------
 ...yptoFileSystemProviderIntegrationTest.java | 50 +++++++-----
 .../CryptoFileSystemProviderTest.java         | 17 ++--
 .../cryptofs/CryptoFileSystemUriTest.java     |  9 ++-
 .../cryptofs/CryptoFileSystemsTest.java       |  7 +-
 ...ptyCiphertextDirectoryIntegrationTest.java | 10 ++-
 .../cryptofs/ReadmeCodeSamplesTest.java       | 17 ++--
 .../RealFileSystemIntegrationTest.java        | 10 ++-
 .../cryptomator/cryptofs/VaultConfigTest.java | 30 +++----
 ...iteFileWhileReadonlyChannelIsOpenTest.java | 10 ++-
 .../attr/FileAttributeIntegrationTest.java    | 10 ++-
 .../cryptofs/migration/MigratorsTest.java     | 25 +++---
 .../migration/v6/Version6MigratorTest.java    | 12 +--
 .../migration/v7/Version7MigratorTest.java    |  6 +-
 .../migration/v8/Version8MigratorTest.java    |  8 +-
 29 files changed, 264 insertions(+), 222 deletions(-)

diff --git a/pom.xml b/pom.xml
index b935f7e5..2d156f01 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-beta2</cryptolib.version>
+		<cryptolib.version>2.0.0-beta3</cryptolib.version>
 		<jwt.version>3.12.0</jwt.version>
 		<dagger.version>2.31</dagger.version>
 		<guava.version>30.1-jre</guava.version>
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
index da268113..d82b2675 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemComponent.java
@@ -1,6 +1,5 @@
 package org.cryptomator.cryptofs;
 
-import com.auth0.jwt.interfaces.DecodedJWT;
 import dagger.BindsInstance;
 import dagger.Subcomponent;
 import org.cryptomator.cryptolib.api.Cryptor;
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index f942c22d..a8571317 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -11,6 +11,7 @@
 import com.google.common.base.Strings;
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
+import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
 import java.net.URI;
 import java.nio.file.FileSystems;
@@ -18,6 +19,7 @@
 import java.util.AbstractMap;
 import java.util.Collection;
 import java.util.EnumSet;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -58,9 +60,9 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 	 *
 	 * @since 2.0.0
 	 */
-	public static final String PROPERTY_KEYLOADER = "keyLoader";
+	public static final String PROPERTY_KEYLOADERS = "keyLoaders";
 
-	static final MasterkeyLoader DEFAULT_KEYLOADER = null;
+	static final Collection<MasterkeyLoader> DEFAULT_KEYLOADERS = Set.of();
 
 	/**
 	 * Key identifying the name of the vault config file located inside the vault directory.
@@ -95,6 +97,7 @@ public enum FileSystemFlags {
 		 */
 		READONLY,
 	}
+
 	/**
 	 * Key identifying the combination of ciphers to use in a vault. Only meaningful during vault initialization.
 	 *
@@ -108,7 +111,7 @@ public enum FileSystemFlags {
 
 	private CryptoFileSystemProperties(Builder builder) {
 		this.entries = Set.of( //
-				Map.entry(PROPERTY_KEYLOADER, builder.keyLoader), //
+				Map.entry(PROPERTY_KEYLOADERS, builder.keyLoaders), //
 				Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
 				Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
 				Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
@@ -118,8 +121,24 @@ private CryptoFileSystemProperties(Builder builder) {
 		);
 	}
 
-	MasterkeyLoader keyLoader() {
-		return (MasterkeyLoader) get(PROPERTY_KEYLOADER);
+	Collection<MasterkeyLoader> keyLoaders() {
+		return (Collection<MasterkeyLoader>) get(PROPERTY_KEYLOADERS);
+	}
+
+	/**
+	 * Selects the first applicable MasterkeyLoader that supports the given scheme.
+	 *
+	 * @param scheme An URI scheme used in key IDs
+	 * @return A key loader
+	 * @throws MasterkeyLoadingFailedException If the scheme is not supported by any key loader
+	 */
+	MasterkeyLoader keyLoader(String scheme) throws MasterkeyLoadingFailedException {
+		for (MasterkeyLoader loader : keyLoaders()) {
+			if (loader.supportsScheme(scheme)) {
+				return loader;
+			}
+		}
+		throw new MasterkeyLoadingFailedException("No key loader for key type: " + scheme);
 	}
 
 	public VaultCipherCombo cipherCombo() {
@@ -200,7 +219,7 @@ public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
 	public static class Builder {
 
 		public VaultCipherCombo cipherCombo = DEFAULT_CIPHER_COMBO;
-		private MasterkeyLoader keyLoader = DEFAULT_KEYLOADER;
+		private Collection<MasterkeyLoader> keyLoaders = new HashSet<>(DEFAULT_KEYLOADERS);
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
 		private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
@@ -211,7 +230,7 @@ private Builder() {
 		}
 
 		private Builder(Map<String, ?> properties) {
-			checkedSet(MasterkeyLoader.class, PROPERTY_KEYLOADER, properties, this::withKeyLoader);
+			checkedSet(Collection.class, PROPERTY_KEYLOADERS, properties, this::withKeyLoaders);
 			checkedSet(String.class, PROPERTY_VAULTCONFIG_FILENAME, properties, this::withVaultConfigFilename);
 			checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
@@ -270,14 +289,26 @@ public Builder withCipherCombo(VaultCipherCombo cipherCombo) {
 		}
 
 		/**
-		 * Sets the keyLoader for a CryptoFileSystem.
+		 * Sets the keyLoaders for a CryptoFileSystem.
 		 *
-		 * @param keyLoader A keyLoader used during initialization
+		 * @param keyLoaders A set of keyLoaders to load the key configured in the vault configuration
 		 * @return this
 		 * @since 2.0.0
 		 */
-		public Builder withKeyLoader(MasterkeyLoader keyLoader) {
-			this.keyLoader = keyLoader;
+		public Builder withKeyLoaders(MasterkeyLoader... keyLoaders) {
+			return withKeyLoaders(asList(keyLoaders));
+		}
+
+		/**
+		 * Sets the keyLoaders for a CryptoFileSystem.
+		 *
+		 * @param keyLoaders A set of keyLoaders to load the key configured in the vault configuration
+		 * @return this
+		 * @since 2.0.0
+		 */
+		public Builder withKeyLoaders(Collection<MasterkeyLoader> keyLoaders) {
+			this.keyLoaders.clear();
+			this.keyLoaders.addAll(keyLoaders);
 			return this;
 		}
 
@@ -305,19 +336,6 @@ public Builder withFlags(Collection<FileSystemFlags> flags) {
 			return this;
 		}
 
-		/**
-		 * Sets the readonly flag for a CryptoFileSystem.
-		 *
-		 * @return this
-		 * @deprecated Will be removed in 2.0.0. Use {@link #withFlags(FileSystemFlags...) withFlags(FileSystemFlags.READONLY)}
-		 */
-		@Deprecated
-		public Builder withReadonlyFlag() {
-			flags.add(FileSystemFlags.READONLY);
-			return this;
-		}
-
-
 		/**
 		 * Sets the name of the vault config file located inside the vault directory.
 		 *
@@ -354,8 +372,8 @@ public CryptoFileSystemProperties build() {
 		}
 
 		private void validate() {
-			if (keyLoader == null) {
-				throw new IllegalStateException("keyloader is required");
+			if (keyLoaders.isEmpty()) {
+				throw new IllegalStateException("at least one keyloader is required");
 			}
 			if (Strings.nullToEmpty(masterkeyFilename).trim().isEmpty()) {
 				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 2256a727..21183c3e 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -38,6 +38,8 @@
 import java.nio.file.attribute.FileAttribute;
 import java.nio.file.attribute.FileAttributeView;
 import java.nio.file.spi.FileSystemProvider;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Set;
@@ -86,7 +88,15 @@ public class CryptoFileSystemProvider extends FileSystemProvider {
 	private final CopyOperation copyOperation;
 
 	public CryptoFileSystemProvider() {
-		this(DaggerCryptoFileSystemProviderComponent.create());
+		this(DaggerCryptoFileSystemProviderComponent.builder().csprng(strongSecureRandom()).build());
+	}
+
+	private static SecureRandom strongSecureRandom() {
+		try {
+			return SecureRandom.getInstanceStrong();
+		} catch (NoSuchAlgorithmException e) {
+			throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
+		}
 	}
 
 	/**
@@ -107,8 +117,9 @@ public CryptoFileSystemProvider() {
 	 * @throws FileSystemNeedsMigrationException                      if the vault format needs to get updated and <code>properties</code> did not contain a flag for implicit migration.
 	 * @throws FileSystemCapabilityChecker.MissingCapabilityException If the underlying filesystem lacks features required to store a vault
 	 * @throws IOException                                            if an I/O error occurs creating the file system
+	 * @throws MasterkeyLoadingFailedException                        if the masterkey for this vault could not be loaded
 	 */
-	public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException {
+	public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemProperties properties) throws FileSystemNeedsMigrationException, IOException, MasterkeyLoadingFailedException {
 		URI uri = CryptoFileSystemUri.create(pathToVault.toAbsolutePath());
 		return (CryptoFileSystem) FileSystems.newFileSystem(uri, properties);
 	}
@@ -124,17 +135,17 @@ public static CryptoFileSystem newFileSystem(Path pathToVault, CryptoFileSystemP
 	 * @throws MasterkeyLoadingFailedException If thrown by the supplied keyLoader
 	 * @since 2.0.0
 	 */
-	public static void initialize(Path pathToVault, CryptoFileSystemProperties properties, String keyId) throws NotDirectoryException, IOException, MasterkeyLoadingFailedException {
+	public static void initialize(Path pathToVault, CryptoFileSystemProperties properties, URI keyId) throws NotDirectoryException, IOException, MasterkeyLoadingFailedException {
 		if (!Files.isDirectory(pathToVault)) {
 			throw new NotDirectoryException(pathToVault.toString());
 		}
 		byte[] rawKey = new byte[0];
-		try (Masterkey key = properties.keyLoader().loadKey(keyId)) {
+		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId)) {
 			rawKey = key.getEncoded();
 			// save vault config:
 			Path vaultConfigPath = pathToVault.resolve(properties.vaultConfigFilename());
 			var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).maxFilenameLength(properties.maxNameLength()).build();
-			var token = config.toToken(keyId, rawKey);
+			var token = config.toToken(keyId.toString(), rawKey);
 			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
 			// create "d" dir:
 			Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
@@ -175,14 +186,10 @@ public String getScheme() {
 	}
 
 	@Override
-	public CryptoFileSystem newFileSystem(URI uri, Map<String, ?> rawProperties) throws IOException {
+	public CryptoFileSystem newFileSystem(URI uri, Map<String, ?> rawProperties) throws IOException, MasterkeyLoadingFailedException {
 		CryptoFileSystemUri parsedUri = CryptoFileSystemUri.parse(uri);
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.wrap(rawProperties);
-		try {
-			return fileSystems.create(this, parsedUri.pathToVault(), properties);
-		} catch (MasterkeyLoadingFailedException e) {
-			throw new IOException("Used invalid key to init filesystem.", e);
-		}
+		return fileSystems.create(this, parsedUri.pathToVault(), properties);
 	}
 
 	@Override
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
index dfd2e4bc..78d47e44 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderComponent.java
@@ -2,9 +2,9 @@
 
 import dagger.BindsInstance;
 import dagger.Component;
-import org.cryptomator.cryptolib.api.CryptorProvider;
 
 import javax.inject.Singleton;
+import java.security.SecureRandom;
 
 @Singleton
 @Component(modules = {CryptoFileSystemProviderModule.class})
@@ -16,4 +16,12 @@ interface CryptoFileSystemProviderComponent {
 
 	CopyOperation copyOperation();
 
+	@Component.Builder
+	interface Builder {
+		@BindsInstance
+		Builder csprng(SecureRandom csprng);
+
+		CryptoFileSystemProviderComponent build();
+	}
+
 }
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
index 17c13a17..782137f4 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProviderModule.java
@@ -10,14 +10,4 @@
 @Module(subcomponents = {CryptoFileSystemComponent.class})
 public class CryptoFileSystemProviderModule {
 
-	@Provides
-	@Singleton
-	public SecureRandom provideCSPRNG() {
-		try {
-			return SecureRandom.getInstanceStrong();
-		} catch (NoSuchAlgorithmException e) {
-			throw new IllegalStateException("A strong algorithm must exist in every Java platform.", e);
-		}
-	}
-
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 7bb928d4..d30b69cc 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -48,8 +48,9 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 		var token = readVaultConfigFile(normalizedPathToVault, properties);
 
 		var configLoader = VaultConfig.decode(token);
+		var keyId = configLoader.getKeyId();
 		byte[] rawKey = new byte[0];
-		try (Masterkey key = properties.keyLoader().loadKey(configLoader.getKeyId())) {
+		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId)) {
 			rawKey = key.getEncoded();
 			var config = configLoader.verify(rawKey, Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
index 8389f324..f6aa87e8 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -12,6 +12,7 @@
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
+import java.net.URI;
 import java.util.Arrays;
 import java.util.UUID;
 
@@ -134,8 +135,8 @@ private UnverifiedVaultConfig(DecodedJWT unverifiedConfig) {
 		/**
 		 * @return The ID of the key required to {@link #verify(byte[], int) load} this config
 		 */
-		public String getKeyId() {
-			return unverifiedConfig.getKeyId();
+		public URI getKeyId() {
+			return URI.create(unverifiedConfig.getKeyId());
 		}
 
 		/**
diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
index 84a7bff4..3d68600f 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
@@ -10,6 +10,9 @@
 
 public final class Constants {
 
+	private Constants() {
+	}
+
 	public static final int VAULT_VERSION = 8;
 	public static final String MASTERKEY_BACKUP_SUFFIX = ".bkup";
 	public static final String DATA_DIR_NAME = "d";
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
index 02704807..09a43b95 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/Migrators.java
@@ -12,11 +12,10 @@
 import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
-import org.cryptomator.cryptolib.Cryptors;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 
 import javax.inject.Inject;
 import java.io.IOException;
@@ -114,7 +113,7 @@ private int determineVaultVersion(Path pathToVault, String vaultConfigFilename,
 			var jwt = Files.readString(vaultConfigPath);
 			return VaultConfig.decode(jwt).allegedVaultVersion();
 		} else if (Files.exists(masterKeyPath)) {
-			return MasterkeyFile.withContentFromFile(masterKeyPath).allegedVaultVersion();
+			return MasterkeyFileAccess.readAllegedVaultVersion(Files.readAllBytes(masterKeyPath));
 		} else {
 			throw new IOException("Did not find " + vaultConfigFilename + " nor " + masterkeyFilename);
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
index 99527464..e07e6bc8 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v6/Version6Migrator.java
@@ -11,7 +11,7 @@
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -19,11 +19,9 @@
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
 import java.security.SecureRandom;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
-import java.util.Optional;
 
 /**
  * Updates masterkey.cryptomator:
@@ -47,8 +45,8 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
 		byte[] fileContentsBeforeUpgrade = Files.readAllBytes(masterkeyFile);
-		MasterkeyFile keyFile = MasterkeyFile.withContentFromFile(masterkeyFile);
-		try (Masterkey masterkey = keyFile.unlock(passphrase, new byte[0], Optional.of(5)).loadKeyAndClose()) {
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
+		try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) {
 			// create backup, as soon as we know the password was correct:
 			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
@@ -56,8 +54,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
 			
 			// rewrite masterkey file with normalized passphrase:
-			byte[] fileContentsAfterUpgrade = MasterkeyFile.lock(masterkey, Normalizer.normalize(passphrase, Form.NFC), new byte[0], 6, csprng);
-			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
+			masterkeyFileAccess.persist(masterkey, masterkeyFile, Normalizer.normalize(passphrase, Form.NFC), 6);
 			LOG.info("Updated masterkey.");
 		}
 		LOG.info("Upgraded {} from version 5 to version 6.", vaultRoot);
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index 0503d72b..b52efb5e 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -16,7 +16,7 @@
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -25,10 +25,8 @@
 import java.nio.file.FileVisitOption;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardOpenOption;
 import java.security.SecureRandom;
 import java.util.EnumSet;
-import java.util.Optional;
 
 /**
  * Renames ciphertext names:
@@ -61,8 +59,8 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 		LOG.info("Upgrading {} from version 6 to version 7.", vaultRoot);
 		progressListener.update(MigrationProgressListener.ProgressState.INITIALIZING, 0.0);
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
-		MasterkeyFile keyFile = MasterkeyFile.withContentFromFile(masterkeyFile);
-		try (Masterkey masterkey = keyFile.unlock(passphrase, new byte[0], Optional.of(6)).loadKeyAndClose()) {
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
+		try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) {
 			// create backup, as soon as we know the password was correct:
 			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
@@ -117,8 +115,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			Files.walkFileTree(vaultRoot.resolve("m"), DeletingFileVisitor.INSTANCE);
 
 			// rewrite masterkey file with normalized passphrase:
-			byte[] fileContentsAfterUpgrade = MasterkeyFile.lock(masterkey, passphrase, new byte[0], 7, csprng);
-			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
+			masterkeyFileAccess.persist(masterkey, masterkeyFile, passphrase, 7);
 			LOG.info("Updated masterkey.");
 		}
 		LOG.info("Upgraded {} from version 6 to version 7.", vaultRoot);
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 2410f321..7481b73b 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -13,7 +13,7 @@
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -25,7 +25,6 @@
 import java.nio.file.StandardOpenOption;
 import java.security.SecureRandom;
 import java.util.Arrays;
-import java.util.Optional;
 import java.util.UUID;
 
 /**
@@ -54,8 +53,8 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 		Path masterkeyFile = vaultRoot.resolve(masterkeyFilename);
 		Path vaultConfigFile = vaultRoot.resolve(vaultConfigFilename);
 		byte[] rawKey = new byte[0];
-		MasterkeyFile keyFile = MasterkeyFile.withContentFromFile(masterkeyFile);
-		try (Masterkey masterkey = keyFile.unlock(passphrase, new byte[0], Optional.of(7)).loadKeyAndClose()) {
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
+		try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFile, passphrase)) {
 			// create backup, as soon as we know the password was correct:
 			Path masterkeyBackupFile = MasterkeyBackupHelper.attemptMasterKeyBackup(masterkeyFile);
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
@@ -76,8 +75,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			progressListener.update(MigrationProgressListener.ProgressState.FINALIZING, 0.0);
 
 			// rewrite masterkey file with normalized passphrase:
-			byte[] fileContentsAfterUpgrade = MasterkeyFile.lock(masterkey, passphrase, new byte[0], 999, csprng);
-			Files.write(masterkeyFile, fileContentsAfterUpgrade, StandardOpenOption.TRUNCATE_EXISTING);
+			masterkeyFileAccess.persist(masterkey, masterkeyFile, passphrase, 999);
 			LOG.info("Updated masterkey.");
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 42c6a83a..aae4e59a 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -25,8 +25,10 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.channels.FileLock;
@@ -62,9 +64,10 @@ public class Windows {
 		private FileSystem fileSystem;
 
 		@BeforeAll
-		public void setupClass(@TempDir Path tmpDir) throws IOException {
-			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withKeyLoader(keyLoader).build());
+		public void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
+			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withKeyLoaders(keyLoader).build());
 		}
 
 		// tests https://github.com/cryptomator/cryptofs/issues/69
@@ -135,9 +138,11 @@ public void beforeAll() throws IOException, MasterkeyLoadingFailedException {
 			inMemoryFs = Jimfs.newFileSystem();
 			Path vaultPath = inMemoryFs.getPath("vault");
 			Files.createDirectories(vaultPath);
-			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-			CryptoFileSystemProvider.initialize(vaultPath, properties, "MASTERKEY_FILE");
+			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProvider.initialize(vaultPath, properties, URI.create("test:key"));
 			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, properties);
 			file = fileSystem.getPath("/test.txt");
 		}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 2f30b9a7..41f5414c 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -1,6 +1,6 @@
 package org.cryptomator.cryptofs;
 
-import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties.*;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -10,11 +10,13 @@
 import org.junit.jupiter.api.Test;
 import org.mockito.Mockito;
 
-import java.nio.charset.StandardCharsets;
+import java.util.Collection;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Objects;
+import java.util.Set;
 
 import static org.cryptomator.cryptofs.CryptoFileSystemProperties.*;
 import static org.hamcrest.CoreMatchers.sameInstance;
@@ -33,41 +35,19 @@ public void testSetNoPassphrase() {
 	}
 
 	@Test
-	@SuppressWarnings({"unchecked", "deprecation"})
-	public void testSetReadonlyFlag() {
-		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withKeyLoader(keyLoader) //
-				.withReadonlyFlag() //
-				.build();
-
-		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
-		MatcherAssert.assertThat(inTest.readonly(), is(true));
-		MatcherAssert.assertThat(inTest.entrySet(),
-				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADER, keyLoader), //
-						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
-						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
-						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
-						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
-	}
-
-	@Test
-	@SuppressWarnings({"unchecked", "deprecation"})
 	public void testSetMasterkeyFilenameAndReadonlyFlag() {
 		String masterkeyFilename = "aMasterkeyFilename";
 		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withKeyLoader(keyLoader) //
+				.withKeyLoaders(keyLoader) //
 				.withMasterkeyFilename(masterkeyFilename) //
-				.withReadonlyFlag() //
+				.withFlags(FileSystemFlags.READONLY)
 				.build();
 
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADER, keyLoader), //
+						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -77,11 +57,10 @@ public void testSetMasterkeyFilenameAndReadonlyFlag() {
 	}
 
 	@Test
-	@SuppressWarnings({"unchecked"})
 	public void testFromMap() {
 		Map<String, Object> map = new HashMap<>();
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_KEYLOADER, keyLoader);
+		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_MAX_PATH_LENGTH, 1000);
 		map.put(PROPERTY_MAX_NAME_LENGTH, 255);
@@ -94,7 +73,7 @@ public void testFromMap() {
 		MatcherAssert.assertThat(inTest.maxNameLength(), is(255));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADER, keyLoader), //
+						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), //
@@ -104,11 +83,10 @@ public void testFromMap() {
 	}
 
 	@Test
-	@SuppressWarnings("unchecked")
 	public void testWrapMapWithTrueReadonly() {
 		Map<String, Object> map = new HashMap<>();
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_KEYLOADER, keyLoader);
+		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
@@ -117,7 +95,7 @@ public void testWrapMapWithTrueReadonly() {
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADER, keyLoader), //
+						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -127,11 +105,10 @@ public void testWrapMapWithTrueReadonly() {
 	}
 
 	@Test
-	@SuppressWarnings("unchecked")
 	public void testWrapMapWithFalseReadonly() {
 		Map<String, Object> map = new HashMap<>();
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_KEYLOADER, keyLoader);
+		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
@@ -140,7 +117,7 @@ public void testWrapMapWithFalseReadonly() {
 		MatcherAssert.assertThat(inTest.readonly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADER, keyLoader), //
+						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -183,17 +160,16 @@ public void testWrapMapWithInvalidPassphrase() {
 	}
 
 	@Test
-	@SuppressWarnings({"unchecked", "deprecation"})
 	public void testWrapMapWithoutReadonly() {
 		Map<String, Object> map = new HashMap<>();
-		map.put(PROPERTY_KEYLOADER, keyLoader);
+		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
 
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
 		MatcherAssert.assertThat(inTest.readonly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADER, keyLoader), //
+						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
@@ -213,7 +189,7 @@ public void testWrapMapWithoutPassphrase() {
 
 	@Test
 	public void testWrapCryptoFileSystemProperties() {
-		CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 
 		MatcherAssert.assertThat(CryptoFileSystemProperties.wrap(inTest), is(sameInstance(inTest)));
 	}
@@ -222,7 +198,7 @@ public void testWrapCryptoFileSystemProperties() {
 	public void testMapIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withKeyLoader(keyLoader) //
+					.withKeyLoaders(keyLoader) //
 					.build() //
 					.put("test", "test");
 		});
@@ -232,7 +208,7 @@ public void testMapIsImmutable() {
 	public void testEntrySetIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withKeyLoader(keyLoader) //
+					.withKeyLoaders(keyLoader) //
 					.build() //
 					.entrySet() //
 					.add(null);
@@ -243,7 +219,7 @@ public void testEntrySetIsImmutable() {
 	public void testEntryIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withKeyLoader(keyLoader) //
+					.withKeyLoaders(keyLoader) //
 					.build() //
 					.entrySet() //
 					.iterator().next() //
@@ -252,7 +228,7 @@ public void testEntryIsImmutable() {
 	}
 
 	private <K, V> Matcher<Map.Entry<K, V>> anEntry(K key, V value) {
-		return new TypeSafeDiagnosingMatcher<Map.Entry<K, V>>(Map.Entry.class) {
+		return new TypeSafeDiagnosingMatcher<>(Map.Entry.class) {
 			@Override
 			public void describeTo(Description description) {
 				description.appendText("an entry ").appendValue(key).appendText(" = ").appendValue(value);
@@ -268,11 +244,21 @@ protected boolean matchesSafely(Entry<K, V> item, Description mismatchDescriptio
 			}
 
 			private boolean keyMatches(K itemKey) {
-				return key == null ? itemKey == null : key.equals(itemKey);
+				return Objects.equals(key, itemKey);
 			}
 
+			@SuppressWarnings("rawtypes")
 			private boolean valueMatches(V itemValue) {
-				return value == null ? itemValue == null : value.equals(itemValue);
+				if (value instanceof Collection && itemValue instanceof Collection) {
+					return valuesMatch((Collection) value, (Collection) itemValue);
+				} else {
+					return Objects.equals(value, itemValue);
+				}
+			}
+
+			@SuppressWarnings("rawtypes")
+			private boolean valuesMatch(Collection<?> value, Collection<?> itemValue) {
+				return value.containsAll(itemValue) && itemValue.containsAll(value);
 			}
 		};
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 2d5a4aa7..dab62050 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -36,6 +36,7 @@
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.net.URI;
@@ -76,7 +77,7 @@ public class CryptoFileSystemProviderIntegrationTest {
 	class WithLimitedPaths {
 
 		private byte[] rawKey = new byte[64];
-		private MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(rawKey);
+		private MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		private CryptoFileSystem fs;
 		private Path shortFilePath;
 		private Path shortSymlinkPath;
@@ -84,13 +85,15 @@ class WithLimitedPaths {
 
 		@BeforeAll
 		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
+			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(rawKey));
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 					.withFlags() //
 					.withMasterkeyFilename("masterkey.cryptomator") //
-					.withKeyLoader(keyLoader) //
+					.withKeyLoaders(keyLoader) //
 					.withMaxPathLength(100)
 					.build();
-			CryptoFileSystemProvider.initialize(tmpDir, properties, "MASTERKEY_FILE");
+			CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
 		}
 
@@ -182,14 +185,18 @@ class InMemory {
 		private FileSystem fs2;
 
 		@BeforeAll
-		public void setup() throws IOException {
+		public void setup() throws IOException, MasterkeyLoadingFailedException {
 			tmpFs = Jimfs.newFileSystem(Configuration.unix());
 			byte[] key1 = new byte[64];
 			byte[] key2 = new byte[64];
 			Arrays.fill(key1, (byte) 0x55);
 			Arrays.fill(key2, (byte) 0x77);
-			keyLoader1 = ignored -> Masterkey.createFromRaw(key1);
-			keyLoader2 = ignored -> Masterkey.createFromRaw(key2);
+			keyLoader1 = Mockito.mock(MasterkeyLoader.class);
+			keyLoader2 = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader1.supportsScheme("test")).thenReturn(true);
+			Mockito.when(keyLoader2.supportsScheme("test")).thenReturn(true);
+			Mockito.when(keyLoader1.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(key1));
+			Mockito.when(keyLoader2.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(key2));
 			pathToVault1 = tmpFs.getPath("/vaultDir1");
 			pathToVault2 = tmpFs.getPath("/vaultDir2");
 			Files.createDirectory(pathToVault1);
@@ -209,13 +216,13 @@ public void teardown() throws IOException {
 		public void initializeVaults() {
 			Assertions.assertAll(
 					() -> {
-						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader1).build();
-						CryptoFileSystemProvider.initialize(pathToVault1, properties, "MASTERKEY_FILE");
+						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader1).build();
+						CryptoFileSystemProvider.initialize(pathToVault1, properties, URI.create("test:key"));
 						Assertions.assertTrue(Files.isDirectory(pathToVault1.resolve("d")));
 						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile1));
 					}, () -> {
-						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader2).build();
-						CryptoFileSystemProvider.initialize(pathToVault2, properties, "MASTERKEY_FILE");
+						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader2).build();
+						CryptoFileSystemProvider.initialize(pathToVault2, properties, URI.create("test:key"));
 						Assertions.assertTrue(Files.isDirectory(pathToVault2.resolve("d")));
 						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile2));
 					});
@@ -233,7 +240,7 @@ public void testGetFsWithWrongCredentials() {
 						CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 								.withFlags() //
 								.withMasterkeyFilename("masterkey.cryptomator") //
-								.withKeyLoader(keyLoader2) //
+								.withKeyLoaders(keyLoader2) //
 								.build();
 						Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 							FileSystems.newFileSystem(fsUri, properties);
@@ -244,7 +251,7 @@ public void testGetFsWithWrongCredentials() {
 						CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 								.withFlags() //
 								.withMasterkeyFilename("masterkey.cryptomator") //
-								.withKeyLoader(keyLoader1) //
+								.withKeyLoaders(keyLoader1) //
 								.build();
 						Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 							FileSystems.newFileSystem(fsUri, properties);
@@ -261,7 +268,7 @@ public void testGetFsViaNioApi() {
 			Assertions.assertAll(
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault1);
-						fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader1).build());
+						fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoaders(keyLoader1).build());
 						Assertions.assertTrue(fs1 instanceof CryptoFileSystemImpl);
 
 						FileSystem sameFs = FileSystems.getFileSystem(fsUri);
@@ -269,7 +276,7 @@ public void testGetFsViaNioApi() {
 					},
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault2);
-						fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader2).build());
+						fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoaders(keyLoader2).build());
 						Assertions.assertTrue(fs2 instanceof CryptoFileSystemImpl);
 
 						FileSystem sameFs = FileSystems.getFileSystem(fsUri);
@@ -528,9 +535,11 @@ class PosixTests {
 		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
-			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-			CryptoFileSystemProvider.initialize(pathToVault, properties, "MASTERKEY_FILE");
+			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
 		}
 
@@ -619,9 +628,10 @@ class WindowsTests {
 		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
-			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-			CryptoFileSystemProvider.initialize(pathToVault, properties, "MASTERKEY_FILE");
+			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
 		}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 48bed8a8..0b0ce156 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -54,7 +54,7 @@
 
 public class CryptoFileSystemProviderTest {
 
-	private final MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
+	private final MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 	private final CryptoFileSystems fileSystems = mock(CryptoFileSystems.class);
 
 	private final CryptoPath cryptoPath = mock(CryptoPath.class);
@@ -112,7 +112,10 @@ private static final Stream<InvocationWhichShouldFail> shouldFailWithRelativePat
 
 	@BeforeEach
 	@SuppressWarnings("deprecation")
-	public void setup() {
+	public void setup() throws MasterkeyLoadingFailedException {
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+
 		CryptoFileSystemProviderComponent component = mock(CryptoFileSystemProviderComponent.class);
 		when(component.fileSystems()).thenReturn(fileSystems);
 		when(component.copyOperation()).thenReturn(copyOperation);
@@ -164,10 +167,10 @@ public void testGetSchemeReturnsCryptomatorScheme() {
 	public void testInitializeFailWithNotDirectoryException() {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 		Path pathToVault = fs.getPath("/vaultDir");
-		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 
 		Assertions.assertThrows(NotDirectoryException.class, () -> {
-			CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		});
 	}
 
@@ -177,10 +180,10 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException
 		Path pathToVault = fs.getPath("/vaultDir");
 		Path vaultConfigFile = pathToVault.resolve("vault.cryptomator");
 		Path dataDir = pathToVault.resolve("d");
-		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
+		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 
 		Files.createDirectory(pathToVault);
-		CryptoFileSystemProvider.initialize(pathToVault, properties, "MASTERKEY_FILE");
+		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 
 		Assertions.assertTrue(Files.isDirectory(dataDir));
 		Assertions.assertTrue(Files.isRegularFile(vaultConfigFile));
@@ -192,7 +195,7 @@ public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedExcept
 		URI uri = CryptoFileSystemUri.create(pathToVault);
 		CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 				.withFlags() //
-				.withKeyLoader(keyLoader) //
+				.withKeyLoaders(keyLoader) //
 				.build();
 
 		inTest.newFileSystem(uri, properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
index 16605b5d..d1dfc2a8 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
@@ -6,6 +6,7 @@
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.net.URI;
@@ -73,9 +74,11 @@ public void testCreateWithPathComponents() throws URISyntaxException {
 	public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException, MasterkeyLoadingFailedException {
 		Path tempDir = createTempDirectory("CryptoFileSystemUrisTest").toAbsolutePath();
 		try {
-			MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-			CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-			CryptoFileSystemProvider.initialize(tempDir, properties, "irrelevant");
+			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProvider.initialize(tempDir, properties, URI.create("test:key"));
 			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, properties);
 			Path absolutePathToVault = fileSystem.getPath("a").toAbsolutePath();
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index bdb2fee2..66db8b19 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -16,6 +16,7 @@
 import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.net.URI;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystemAlreadyExistsException;
 import java.nio.file.FileSystemNotFoundException;
@@ -64,12 +65,12 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(pathToVault.normalize()).thenReturn(normalizedPathToVault);
 		when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath);
 		when(properties.vaultConfigFilename()).thenReturn("vault.cryptomator");
-		when(properties.keyLoader()).thenReturn(keyLoader);
+		when(properties.keyLoader(Mockito.any())).thenReturn(keyLoader);
 		filesClass.when(() -> Files.readString(configFilePath, StandardCharsets.US_ASCII)).thenReturn("jwt-vault-config");
 		vaultConficClass.when(() -> VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
 		when(VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
-		when(configLoader.getKeyId()).thenReturn("key-id");
-		when(keyLoader.loadKey("key-id")).thenReturn(masterkey);
+		when(configLoader.getKeyId()).thenReturn(URI.create("test:key"));
+		when(keyLoader.loadKey(Mockito.any())).thenReturn(masterkey);
 		when(masterkey.getEncoded()).thenReturn(rawKey);
 		when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
 		when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo);
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index 9b4417aa..b32e7260 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -18,10 +18,12 @@
 import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
 
 import java.io.File;
 import java.io.IOException;
 import java.io.UncheckedIOException;
+import java.net.URI;
 import java.nio.file.DirectoryNotEmptyException;
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
@@ -44,9 +46,11 @@ public class DeleteNonEmptyCiphertextDirectoryIntegrationTest {
 	public static void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
-		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
index 8bbdca1c..ebfc1d9f 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
@@ -14,6 +14,7 @@
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
 
 import java.io.IOException;
 import java.net.URI;
@@ -29,9 +30,11 @@ public class ReadmeCodeSamplesTest {
 
 	@Test
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
-		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-		CryptoFileSystemProvider.initialize(storageLocation, properties, "irrelevant");
+		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key"));
 		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, properties);
 
 		runCodeSample(fileSystem);
@@ -40,9 +43,11 @@ public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path
 	@Test
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		URI uri = CryptoFileSystemUri.create(storageLocation);
-		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-		CryptoFileSystemProvider.initialize(storageLocation, properties, "irrelevant");
+		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key"));
 		FileSystem fileSystem = FileSystems.newFileSystem(uri, properties);
 
 		runCodeSample(fileSystem);
diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
index 7589fbcc..2431642f 100644
--- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
@@ -15,8 +15,10 @@
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
+import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.net.URI;
 import java.nio.file.FileSystem;
 import java.nio.file.FileSystems;
 import java.nio.file.Files;
@@ -34,9 +36,11 @@ public class RealFileSystemIntegrationTest {
 	public static void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
-		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
index 4e347b95..8fec1870 100644
--- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
@@ -21,33 +21,39 @@
 
 public class VaultConfigTest {
 
+	private MasterkeyLoader masterkeyLoader = Mockito.mock(MasterkeyLoader.class);
+	private byte[] rawKey = new byte[64];
+	private Masterkey key = Mockito.mock(Masterkey.class);
+
+	@BeforeEach
+	public void setup() throws MasterkeyLoadingFailedException {
+		Arrays.fill(rawKey, (byte) 0x55);
+		Mockito.when(masterkeyLoader.loadKey(Mockito.any())).thenReturn(key);
+		Mockito.when(key.getEncoded()).thenReturn(rawKey);
+	}
+
 	@Test
 	public void testLoadMalformedToken() {
 		Assertions.assertThrows(VaultConfigLoadException.class, () -> {
-			VaultConfig.load("hello world", ignored -> null, 42);
+			VaultConfig.load("hello world", masterkeyLoader, 42);
 		});
 	}
 
 	@Nested
 	public class WithValidToken {
 
-		private byte[] rawKey = new byte[64];
-		private Masterkey key = Mockito.mock(Masterkey.class);
 		private VaultConfig originalConfig;
 		private String token;
 
-
 		@BeforeEach
-		public void setup() {
-			Arrays.fill(rawKey, (byte) 0x55);
-			Mockito.when(key.getEncoded()).thenReturn(rawKey);
+		public void setup() throws MasterkeyLoadingFailedException {
 			originalConfig = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).maxFilenameLength(220).build();
 			token = originalConfig.toToken("TEST_KEY", rawKey);
 		}
 
 		@Test
 		public void testSuccessfulLoad() throws VaultConfigLoadException, MasterkeyLoadingFailedException {
-			var loaded = VaultConfig.load(token, ignored -> key, originalConfig.getVaultVersion());
+			var loaded = VaultConfig.load(token, masterkeyLoader, originalConfig.getVaultVersion());
 
 			Assertions.assertEquals(originalConfig.getId(), loaded.getId());
 			Assertions.assertEquals(originalConfig.getVaultVersion(), loaded.getVaultVersion());
@@ -62,7 +68,7 @@ public void testLoadWithInvalidKey(int pos) {
 			Mockito.when(key.getEncoded()).thenReturn(rawKey);
 
 			Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
-				VaultConfig.load(token, ignored -> key, originalConfig.getVaultVersion());
+				VaultConfig.load(token, masterkeyLoader, originalConfig.getVaultVersion());
 			});
 		}
 
@@ -84,15 +90,13 @@ public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoading
 		var formatClaim = Mockito.mock(Claim.class);
 		var cipherComboClaim = Mockito.mock(Claim.class);
 		var maxFilenameLenClaim = Mockito.mock(Claim.class);
-		var keyLoader = Mockito.mock(MasterkeyLoader.class);
 		var key = Mockito.mock(Masterkey.class);
 		var verification = Mockito.mock(Verification.class);
 		var verifier = Mockito.mock(JWTVerifier.class);
-		Mockito.when(decodedJwt.getKeyId()).thenReturn("key-id");
+		Mockito.when(decodedJwt.getKeyId()).thenReturn("test:key");
 		Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim);
 		Mockito.when(decodedJwt.getClaim("cipherCombo")).thenReturn(cipherComboClaim);
 		Mockito.when(decodedJwt.getClaim("maxFilenameLen")).thenReturn(maxFilenameLenClaim);
-		Mockito.when(keyLoader.loadKey("key-id")).thenReturn(key);
 		Mockito.when(key.getEncoded()).thenReturn(new byte[64]);
 		Mockito.when(verification.withClaim("format", 42)).thenReturn(verification);
 		Mockito.when(verification.build()).thenReturn(verifier);
@@ -104,7 +108,7 @@ public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoading
 			jwtMock.when(() -> JWT.decode("jwt-vault-config")).thenReturn(decodedJwt);
 			jwtMock.when(() -> JWT.require(Mockito.any())).thenReturn(verification);
 
-			var config = VaultConfig.load("jwt-vault-config", keyLoader, 42);
+			var config = VaultConfig.load("jwt-vault-config", masterkeyLoader, 42);
 			Assertions.assertNotNull(config);
 			Assertions.assertEquals(42, config.getVaultVersion());
 		}
diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
index c3c0ce81..dee2dddc 100644
--- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
@@ -7,8 +7,10 @@
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.FileSystem;
@@ -33,9 +35,11 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		inMemoryFs = Jimfs.newFileSystem();
 		Path pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
-		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 		root = fileSystem.getPath("/");
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index 6fe7f503..de6ddb9a 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -23,8 +23,10 @@
 import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.net.URI;
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.FileSystem;
@@ -59,9 +61,11 @@ public static void setupClass() throws IOException, MasterkeyLoadingFailedExcept
 		inMemoryFs = Jimfs.newFileSystem();
 		pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
-		MasterkeyLoader keyLoader = ignored -> Masterkey.createFromRaw(new byte[64]);
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
-		CryptoFileSystemProvider.initialize(pathToVault, properties, "irrelevant");
+		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
index 0f6b9e4f..f17acd8b 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -9,13 +9,12 @@
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener;
-import org.cryptomator.cryptofs.migration.api.MigrationContinuationListener.ContinuationResult;
 import org.cryptomator.cryptofs.migration.api.MigrationProgressListener;
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
@@ -29,8 +28,6 @@
 import org.mockito.Mockito;
 
 import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collections;
@@ -164,28 +161,24 @@ public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorExcep
 	@Nested
 	public class WithExistingMasterkeyFile {
 
-		private MockedStatic<MasterkeyFile> masterkeyFileClass;
-		private MasterkeyFile masterkeyFile;
+		private MockedStatic<MasterkeyFileAccess> masterkeyFileAccessClass;
 
 		@BeforeEach
 		public void setup() {
 			Assumptions.assumeFalse(Files.exists(vaultConfigPath));
-			masterkeyFileClass = Mockito.mockStatic(MasterkeyFile.class);
-			masterkeyFile = Mockito.mock(MasterkeyFile.class);
-
+			masterkeyFileAccessClass = Mockito.mockStatic(MasterkeyFileAccess.class);
 			filesClass.when(() -> Files.exists(masterkeyPath)).thenReturn(true);
-			masterkeyFileClass.when(() -> MasterkeyFile.withContentFromFile(masterkeyPath)).thenReturn(masterkeyFile);
 		}
 
 		@AfterEach
 		public void tearDown() {
-			masterkeyFileClass.close();
+			masterkeyFileAccessClass.close();
 		}
 
 		@Test
 		@DisplayName("needs migration if vault version < Constants.VAULT_VERSION")
 		public void testNeedsMigration() throws IOException {
-			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION - 1);
+			masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(Constants.VAULT_VERSION - 1);
 			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
 
 			boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
@@ -196,7 +189,7 @@ public void testNeedsMigration() throws IOException {
 		@Test
 		@DisplayName("needs no migration if vault version >= Constants.VAULT_VERSION")
 		public void testNeedsNoMigration() throws IOException {
-			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(Constants.VAULT_VERSION);
+			masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(Constants.VAULT_VERSION);
 			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
 
 			boolean result = migrators.needsMigration(pathToVault, "vault.cryptomator", "masterkey.cryptomator");
@@ -207,7 +200,7 @@ public void testNeedsNoMigration() throws IOException {
 		@Test
 		@DisplayName("throws NoApplicableMigratorException if no migrator was found")
 		public void testMigrateWithoutMigrators() {
-			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(42);
+			masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(1337);
 
 			Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
 			Assertions.assertThrows(NoApplicableMigratorException.class, () -> {
@@ -222,7 +215,7 @@ public void testMigrate() throws NoApplicableMigratorException, CryptoException,
 			MigrationProgressListener progressListener = Mockito.mock(MigrationProgressListener.class);
 			MigrationContinuationListener continuationListener = Mockito.mock(MigrationContinuationListener.class);
 			Migrator migrator = Mockito.mock(Migrator.class);
-			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(0);
+			masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(0);
 			Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
 
 			migrators.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "secret", progressListener, continuationListener);
@@ -236,7 +229,7 @@ public void testMigrate() throws NoApplicableMigratorException, CryptoException,
 		public void testMigrateUnsupportedVaultFormat() throws NoApplicableMigratorException, CryptoException, IOException {
 			Migrator migrator = Mockito.mock(Migrator.class);
 			Migrators migrators = new Migrators(Map.of(Migration.ZERO_TO_ONE, migrator), fsCapabilityChecker);
-			Mockito.when(masterkeyFile.allegedVaultVersion()).thenReturn(0);
+			masterkeyFileAccessClass.when(() -> MasterkeyFileAccess.readAllegedVaultVersion(Mockito.any())).thenReturn(0);
 			Mockito.doThrow(new UnsupportedVaultFormatException(Integer.MAX_VALUE, 1)).when(migrator).migrate(Mockito.any(), Mockito.anyString(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any());
 
 			Assertions.assertThrows(IllegalStateException.class, () -> {
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
index effecd6c..00df36ef 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
@@ -8,7 +8,7 @@
 import org.cryptomator.cryptofs.mocks.NullSecureRandom;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
@@ -16,7 +16,6 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.FileSystem;
@@ -25,7 +24,6 @@
 import java.security.SecureRandom;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
-import java.util.Optional;
 
 public class Version6MigratorTest {
 
@@ -55,7 +53,9 @@ public void testMigrate() throws IOException, CryptoException {
 		Assertions.assertNotEquals(oldPassword, newPassword);
 
 		Masterkey masterkey = Masterkey.createNew(csprng);
-		byte[] beforeMigration = MasterkeyFile.lock(masterkey, oldPassword, new byte[0], 5, csprng);
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
+		masterkeyFileAccess.persist(masterkey, masterkeyFile, oldPassword, 5);
+		byte[] beforeMigration = Files.readAllBytes(masterkeyFile);
 
 		Files.write(masterkeyFile, beforeMigration);
 		Path masterkeyBackupFile = pathToVault.resolve("masterkey.cryptomator" + MasterkeyBackupHelper.generateFileIdSuffix(beforeMigration) + Constants.MASTERKEY_BACKUP_SUFFIX);
@@ -67,8 +67,8 @@ public void testMigrate() throws IOException, CryptoException {
 		String afterMigrationJson = new String(afterMigration, StandardCharsets.UTF_8);
 		MatcherAssert.assertThat(afterMigrationJson, CoreMatchers.containsString("\"version\": 6"));
 
-		try (var keyLoader = MasterkeyFile.withContent(new ByteArrayInputStream(afterMigration)).unlock(newPassword, new byte[0], Optional.of(6))) {
-			Assertions.assertNotNull(keyLoader);
+		try (var key = masterkeyFileAccess.load(masterkeyFile, newPassword)) {
+			Assertions.assertNotNull(key);
 		}
 
 		Assertions.assertTrue(Files.exists(masterkeyBackupFile));
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
index eb841c7c..ec776e0d 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
@@ -6,7 +6,7 @@
 import org.cryptomator.cryptofs.mocks.NullSecureRandom;
 import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
@@ -42,8 +42,8 @@ public void setup() throws IOException {
 		Files.createDirectory(metaDir);
 
 		Masterkey masterkey = Masterkey.createNew(csprng);
-		byte[] unmigrated = MasterkeyFile.lock(masterkey, "test", new byte[0], 6, csprng);
-		Files.write(masterkeyFile, unmigrated);
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
+		masterkeyFileAccess.persist(masterkey, masterkeyFile, "test", 6);
 	}
 
 	@AfterEach
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 34ff3700..809482ef 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -6,11 +6,9 @@
 import com.google.common.jimfs.Jimfs;
 import org.cryptomator.cryptofs.migration.api.Migrator;
 import org.cryptomator.cryptofs.mocks.NullSecureRandom;
-import org.cryptomator.cryptolib.Cryptors;
 import org.cryptomator.cryptolib.api.CryptoException;
-import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.common.MasterkeyFile;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.AfterEach;
@@ -51,9 +49,9 @@ public void teardown() throws IOException {
 	@Test
 	public void testMigrate() throws CryptoException, IOException {
 		Masterkey masterkey = Masterkey.createNew(csprng);
-		byte[] unmigrated = MasterkeyFile.lock(masterkey, "topsecret", new byte[0], 7, csprng);
+		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
+		masterkeyFileAccess.persist(masterkey, masterkeyFile, "topsecret", 7);
 		Assumptions.assumeFalse(Files.exists(vaultConfigFile));
-		Files.write(masterkeyFile, unmigrated);
 
 		Migrator migrator = new Version8Migrator(csprng);
 		migrator.migrate(pathToVault, "vault.cryptomator", "masterkey.cryptomator", "topsecret");

From 593fe234e74869b31420ba51d7cca05d849863c4 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Wed, 27 Jan 2021 11:27:48 +0100
Subject: [PATCH 18/70] updated dependencies

---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 2d156f01..abcd124c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>org.cryptomator</groupId>
 	<artifactId>cryptofs</artifactId>
-	<version>2.0.0-SNAPSHOT</version>
+	<version>2.0.0</version>
 	<name>Cryptomator Crypto Filesystem</name>
 	<description>This library provides the Java filesystem provider used by Cryptomator.</description>
 	<url>https://github.com/cryptomator/cryptofs</url>
@@ -17,7 +17,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-beta3</cryptolib.version>
+		<cryptolib.version>2.0.0-beta4</cryptolib.version>
 		<jwt.version>3.12.0</jwt.version>
 		<dagger.version>2.31</dagger.version>
 		<guava.version>30.1-jre</guava.version>

From fd532f9897e1a7c118432fb77954b2da1a5ee5a2 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 12 Feb 2021 16:30:16 +0100
Subject: [PATCH 19/70] java-jwt 3.12.0 no longer depends on affected
 jackson-databind version

---
 suppression.xml | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/suppression.xml b/suppression.xml
index 23090b9b..9edca825 100644
--- a/suppression.xml
+++ b/suppression.xml
@@ -6,9 +6,4 @@
 		<gav>org.slf4j:slf4j-api:1.7.25</gav>
 		<cve>CVE-2018-8088</cve>
 	</suppress>
-	<suppress>
-		<notes><![CDATA[ Upstream fix backported from 2.11.0 to 2.10.5.1, see https://github.com/FasterXML/jackson-databind/issues/2589#issuecomment-714833837. ]]></notes>
-		<gav>com.fasterxml.jackson.core:jackson-databind:2.10.5.1</gav>
-		<cve>CVE-2020-25649</cve>
-	</suppress>
 </suppressions>
\ No newline at end of file

From 754a419b481226e7ff40fe12d420f52aaf7d6374 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 12 Feb 2021 16:30:45 +0100
Subject: [PATCH 20/70] slf4j 1.7.30 no longer affected

---
 suppression.xml | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/suppression.xml b/suppression.xml
index 9edca825..cc17d63b 100644
--- a/suppression.xml
+++ b/suppression.xml
@@ -1,9 +1,4 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- This file lists false positives found by org.owasp:dependency-check-maven build plugin -->
 <suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.2.xsd">
-	<suppress>
-		<notes><![CDATA[ Vulnerability affects org.slf4j.ext.EventData, which is part of slf4j-ext and therefore falsly reported for slf4j-api. ]]></notes>
-		<gav>org.slf4j:slf4j-api:1.7.25</gav>
-		<cve>CVE-2018-8088</cve>
-	</suppress>
 </suppressions>
\ No newline at end of file

From 6f22a58ae81a4eba8b1f190714ef8ad16c9d1a2c Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 5 Mar 2021 14:58:20 +0100
Subject: [PATCH 21/70] bumped cryptolib version (moved to maven central)

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 150315f9..a5aeafc5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-beta4</cryptolib.version>
+		<cryptolib.version>2.0.0-beta5</cryptolib.version>
 		<jwt.version>3.12.0</jwt.version>
 		<dagger.version>2.31</dagger.version>
 		<guava.version>30.1-jre</guava.version>

From 86930f37d4429fdaaf7ae640fa8f3318d5db707f Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 10 Mar 2021 11:31:09 +0100
Subject: [PATCH 22/70] fix vaultformat 8 migrator (see #97)

* change entry of kid field to correct scheme and scheme-specific part
---
 .../org/cryptomator/cryptofs/migration/v8/Version8Migrator.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 7481b73b..87f0032f 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -64,7 +64,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			Algorithm algorithm = Algorithm.HMAC256(rawKey);
 			var config = JWT.create() //
 					.withJWTId(UUID.randomUUID().toString()) //
-					.withKeyId("MASTERKEY_FILE") //
+					.withKeyId("masterkeyfile:masterkey.cryptomator") //
 					.withClaim("format", 8) //
 					.withClaim("cipherCombo", "SIV_CTRMAC") //
 					.withClaim("maxFilenameLen", 220) //

From 48015a7d293c55efaee16fef64d07659f1702ff6 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 10 Mar 2021 12:06:12 +0100
Subject: [PATCH 23/70] fix unit test for v8 migration

---
 .../cryptomator/cryptofs/migration/v8/Version8MigratorTest.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 809482ef..f754e672 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -61,7 +61,7 @@ public void testMigrate() throws CryptoException, IOException {
 		Assertions.assertTrue(Files.exists(vaultConfigFile));
 		DecodedJWT token = JWT.decode(Files.readString(vaultConfigFile));
 		Assertions.assertNotNull(token.getId());
-		Assertions.assertEquals("MASTERKEY_FILE", token.getKeyId());
+		Assertions.assertEquals("masterkeyfile:masterkey.cryptomator", token.getKeyId());
 		Assertions.assertEquals(8, token.getClaim("format").asInt());
 		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("cipherCombo").asString());
 		Assertions.assertEquals(220, token.getClaim("maxFilenameLen").asInt());

From c6c0b73dc83ba56d34c1595b3a5ec0aa77c47342 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Thu, 11 Mar 2021 12:03:53 +0100
Subject: [PATCH 24/70] fix unit tests: * use absoute path to vault in
 cryptoFileSystemProviderTest * add method catch for
 keyloader.supportsScheme(String s) in keyloader mock * initialize filesystem
 properly by using static facotry method in cryptoFileChannelRW integration
 test

---
 .../cryptofs/CryptoFileChannelWriteReadIntegrationTest.java  | 5 ++++-
 .../cryptofs/CryptoFileSystemProviderIntegrationTest.java    | 1 +
 .../cryptomator/cryptofs/CryptoFileSystemProviderTest.java   | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index aae4e59a..d5d8ea51 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -66,8 +66,11 @@ public class Windows {
 		@BeforeAll
 		public void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.supportsScheme(Mockito.any())).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
-			fileSystem = new CryptoFileSystemProvider().newFileSystem(create(tmpDir), cryptoFileSystemProperties().withKeyLoaders(keyLoader).build());
+			CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
+			fileSystem = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
 		}
 
 		// tests https://github.com/cryptomator/cryptofs/issues/69
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index dab62050..8677bb00 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -629,6 +629,7 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
+			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
 			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 0b0ce156..ffb96148 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -200,7 +200,7 @@ public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedExcept
 
 		inTest.newFileSystem(uri, properties);
 
-		Mockito.verify(fileSystems).create(Mockito.same(inTest), Mockito.eq(pathToVault), Mockito.eq(properties));
+		Mockito.verify(fileSystems).create(Mockito.same(inTest), Mockito.eq(pathToVault.toAbsolutePath()), Mockito.eq(properties));
 	}
 
 	@Test

From 1535197adf73f027022a9b691c6829f2577bce00 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Fri, 12 Mar 2021 11:46:19 +0100
Subject: [PATCH 25/70] Closes #98

---
 .../cryptofs/CryptoFileSystemImpl.java        |  4 +-
 .../cryptofs/CryptoFileSystemProvider.java    |  9 ++--
 .../cryptomator/cryptofs/FilesWrapper.java    | 26 ----------
 .../cryptofs/RootDirectoryInitializer.java    | 34 -------------
 .../cryptofs/CryptoFileSystemImplTest.java    |  3 +-
 .../RootDirectoryInitializerTest.java         | 50 -------------------
 6 files changed, 8 insertions(+), 118 deletions(-)
 delete mode 100644 src/main/java/org/cryptomator/cryptofs/FilesWrapper.java
 delete mode 100644 src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java
 delete mode 100644 src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index fb10f3ef..df40ab7c 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -105,7 +105,7 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
 								PathMatcherFactory pathMatcherFactory, DirectoryStreamFactory directoryStreamFactory, DirectoryIdProvider dirIdProvider,
 								AttributeProvider fileAttributeProvider, AttributeByNameProvider fileAttributeByNameProvider, AttributeViewProvider fileAttributeViewProvider,
 								OpenCryptoFiles openCryptoFiles, Symlinks symlinks, FinallyUtil finallyUtil, CiphertextDirectoryDeleter ciphertextDirDeleter, ReadonlyFlag readonlyFlag,
-								CryptoFileSystemProperties fileSystemProperties, RootDirectoryInitializer rootDirectoryInitializer) {
+								CryptoFileSystemProperties fileSystemProperties) {
 		this.provider = provider;
 		this.cryptoFileSystems = cryptoFileSystems;
 		this.pathToVault = pathToVault;
@@ -129,8 +129,6 @@ public CryptoFileSystemImpl(CryptoFileSystemProvider provider, CryptoFileSystems
 
 		this.rootPath = cryptoPathFactory.rootFor(this);
 		this.emptyPath = cryptoPathFactory.emptyFor(this);
-
-		rootDirectoryInitializer.initialize(rootPath);
 	}
 
 	@Override
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 21183c3e..05750378 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -11,6 +11,7 @@
 import org.cryptomator.cryptofs.ch.AsyncDelegatingFileChannel;
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
+import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
@@ -147,9 +148,11 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 			var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).maxFilenameLength(properties.maxNameLength()).build();
 			var token = config.toToken(keyId.toString(), rawKey);
 			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
-			// create "d" dir:
-			Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
-			Files.createDirectories(dataDirPath);
+			// create "d" dir and root:
+			Cryptor cryptor = config.getCipherCombo().getCryptorProvider(new SecureRandom()).withKey(key); //TODO: show we create a secure random instance like in CryptoFileSystemProviderComponent?
+			String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
+			Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
+			Files.createDirectories(vaultCipherRootPath);
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/FilesWrapper.java b/src/main/java/org/cryptomator/cryptofs/FilesWrapper.java
deleted file mode 100644
index 05e8fe90..00000000
--- a/src/main/java/org/cryptomator/cryptofs/FilesWrapper.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.cryptomator.cryptofs;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.attribute.FileAttribute;
-
-/**
- * Mockable wrapper around {@link Files} operations.
- * 
- * @author Markus Kreusch
- */
-@Singleton
-class FilesWrapper {
-
-	@Inject
-	public FilesWrapper() {
-	}
-
-	public Path createDirectories(Path dir, FileAttribute<?>... attrs) throws IOException {
-		return Files.createDirectories(dir, attrs);
-	}
-
-}
diff --git a/src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java b/src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java
deleted file mode 100644
index 1b3a69cb..00000000
--- a/src/main/java/org/cryptomator/cryptofs/RootDirectoryInitializer.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package org.cryptomator.cryptofs;
-
-import javax.inject.Inject;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.nio.file.Path;
-
-@CryptoFileSystemScoped
-class RootDirectoryInitializer {
-
-	private final CryptoPathMapper cryptoPathMapper;
-	private final ReadonlyFlag readonlyFlag;
-	private final FilesWrapper files;
-
-	@Inject
-	public RootDirectoryInitializer(CryptoPathMapper cryptoPathMapper, ReadonlyFlag readonlyFlag, FilesWrapper files) {
-		this.cryptoPathMapper = cryptoPathMapper;
-		this.readonlyFlag = readonlyFlag;
-		this.files = files;
-	}
-
-	public void initialize(CryptoPath cleartextRoot) {
-		if (readonlyFlag.isSet()) {
-			return;
-		}
-		try {
-			Path ciphertextRoot = cryptoPathMapper.getCiphertextDir(cleartextRoot).path;
-			files.createDirectories(ciphertextRoot);
-		} catch (IOException e) {
-			throw new UncheckedIOException(e);
-		}
-	}
-
-}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index f0140b37..1d878ebb 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -95,7 +95,6 @@ public class CryptoFileSystemImplTest {
 	private final PathMatcherFactory pathMatcherFactory = mock(PathMatcherFactory.class);
 	private final CryptoPathFactory cryptoPathFactory = mock(CryptoPathFactory.class);
 	private final CryptoFileSystemStats stats = mock(CryptoFileSystemStats.class);
-	private final RootDirectoryInitializer rootDirectoryInitializer = mock(RootDirectoryInitializer.class);
 	private final DirectoryStreamFactory directoryStreamFactory = mock(DirectoryStreamFactory.class);
 	private final FinallyUtil finallyUtil = mock(FinallyUtil.class);
 	private final CiphertextDirectoryDeleter ciphertextDirDeleter = mock(CiphertextDirectoryDeleter.class);
@@ -124,7 +123,7 @@ public void setup() {
 				pathMatcherFactory, directoryStreamFactory, dirIdProvider,
 				fileAttributeProvider, fileAttributeByNameProvider, fileAttributeViewProvider,
 				openCryptoFiles, symlinks, finallyUtil, ciphertextDirDeleter, readonlyFlag,
-				fileSystemProperties, rootDirectoryInitializer);
+				fileSystemProperties);
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java b/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java
deleted file mode 100644
index c1f0491b..00000000
--- a/src/test/java/org/cryptomator/cryptofs/RootDirectoryInitializerTest.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package org.cryptomator.cryptofs;
-
-import org.cryptomator.cryptofs.CryptoPathMapper.CiphertextDirectory;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.nio.file.Path;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class RootDirectoryInitializerTest {
-
-	private final CryptoPathMapper cryptoPathMapper = mock(CryptoPathMapper.class);
-	private final ReadonlyFlag readonlyFlag = mock(ReadonlyFlag.class);
-	private final FilesWrapper filesWrapper = mock(FilesWrapper.class);
-
-	private final CryptoPath cleartextRoot = mock(CryptoPath.class);
-	private final Path ciphertextRoot = mock(Path.class);
-
-	private RootDirectoryInitializer inTest = new RootDirectoryInitializer(cryptoPathMapper, readonlyFlag, filesWrapper);
-
-	@BeforeEach
-	public void setup() throws IOException {
-		when(cryptoPathMapper.getCiphertextDir(cleartextRoot)).thenReturn(new CiphertextDirectory("", ciphertextRoot));
-	}
-
-	@Test
-	public void testInitializeCreatesRootDirectoryIfReadonlyFlagIsNotSet() throws IOException {
-		when(readonlyFlag.isSet()).thenReturn(false);
-
-		inTest.initialize(cleartextRoot);
-
-		verify(filesWrapper).createDirectories(ciphertextRoot);
-	}
-
-	@Test
-	public void testInitializeDoesNotCreateRootDirectoryIfReadonlyFlagIsSet() throws IOException {
-		when(readonlyFlag.isSet()).thenReturn(true);
-
-		inTest.initialize(cleartextRoot);
-
-		verifyNoInteractions(filesWrapper);
-	}
-
-}

From 0785942930f0d2f44d8e8e4f6ac649edf255b8c1 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Fri, 12 Mar 2021 12:48:47 +0100
Subject: [PATCH 26/70] adapt unit test of vault initialization

---
 .../cryptofs/CryptoFileSystemProviderTest.java           | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index ffb96148..655395e8 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -37,6 +37,7 @@
 import java.nio.file.spi.FileSystemProvider;
 import java.util.HashSet;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.stream.Stream;
@@ -187,6 +188,14 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException
 
 		Assertions.assertTrue(Files.isDirectory(dataDir));
 		Assertions.assertTrue(Files.isRegularFile(vaultConfigFile));
+
+		Optional<Path> preRootDir = Files.list(dataDir).findFirst();
+		Assertions.assertTrue(preRootDir.isPresent());
+		Assertions.assertTrue(Files.isDirectory(preRootDir.get()));
+
+		Optional<Path> rootDir = Files.list(preRootDir.get()).findFirst();
+		Assertions.assertTrue(rootDir.isPresent());
+		Assertions.assertTrue(Files.isDirectory(rootDir.get()));
 	}
 
 	@Test

From ba887995fc5f532d2c53bca2bc33fc9d0b3deaac Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Fri, 12 Mar 2021 15:16:56 +0100
Subject: [PATCH 27/70] add check for content root existence in
 CryptoFIleSystems and add/adapt unit tests

---
 .../cryptofs/ContentRootMissingException.java | 10 ++++++++
 .../cryptofs/CryptoFileSystems.java           | 25 +++++++++++++++----
 .../cryptofs/CryptoFileSystemsTest.java       | 17 +++++++++++++
 3 files changed, 47 insertions(+), 5 deletions(-)
 create mode 100644 src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java

diff --git a/src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java b/src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java
new file mode 100644
index 00000000..d74a2f99
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/ContentRootMissingException.java
@@ -0,0 +1,10 @@
+package org.cryptomator.cryptofs;
+
+import java.nio.file.NoSuchFileException;
+
+public class ContentRootMissingException extends NoSuchFileException {
+
+	public ContentRootMissingException(String msg) {
+		super(msg);
+	}
+}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index d30b69cc..06e169bb 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -54,9 +54,11 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 			rawKey = key.getEncoded();
 			var config = configLoader.verify(rawKey, Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
+			Cryptor cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key);
+			checkVaultRootExistence(pathToVault, cryptor);
 			return fileSystems.compute(normalizedPathToVault, (path, fs) -> {
 				if (fs == null) {
-					return create(provider, normalizedPathToVault, adjustedProperties, key, config);
+					return create(provider, normalizedPathToVault, adjustedProperties, cryptor, config);
 				} else {
 					throw new FileSystemAlreadyExistsException();
 				}
@@ -66,9 +68,22 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 		}
 	}
 
+	/**
+	 * Checks if the vault has a content root folder. If not, an exception is raised.
+	 * @param pathToVault Path to the vault root
+	 * @param cryptor Cryptor object initialized with the correct masterkey
+	 * @throws ContentRootMissingException If the existence of encrypted vault content root cannot be ensured
+	 */
+	private void checkVaultRootExistence(Path pathToVault, Cryptor cryptor) throws ContentRootMissingException {
+		String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
+		Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
+		if(! Files.exists(vaultCipherRootPath)){
+			throw new ContentRootMissingException("The encrypted root directory of the vault "+pathToVault+" is missing.");
+		}
+	}
+
 	// synchronized access to non-threadsafe cryptoFileSystemComponentBuilder required
-	private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, Masterkey masterkey, VaultConfig config) {
-		Cryptor cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(masterkey);
+	private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathToVault, CryptoFileSystemProperties properties, Cryptor cryptor, VaultConfig config) {
 		return cryptoFileSystemComponentBuilder //
 				.cryptor(cryptor) //
 				.vaultConfig(config) //
@@ -83,9 +98,9 @@ private synchronized CryptoFileSystemImpl create(CryptoFileSystemProvider provid
 	 * Attempts to read a vault config file
 	 *
 	 * @param pathToVault path to the vault's root
-	 * @param properties  properties used when attempting to construct a fs for this vault
+	 * @param properties properties used when attempting to construct a fs for this vault
 	 * @return The contents of the file decoded in ASCII
-	 * @throws IOException                       If the file could not be read
+	 * @throws IOException If the file could not be read
 	 * @throws FileSystemNeedsMigrationException If the file doesn't exists, but a legacy masterkey file was found instead
 	 */
 	private String readVaultConfigFile(Path pathToVault, CryptoFileSystemProperties properties) throws IOException, FileSystemNeedsMigrationException {
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 66db8b19..f7138e2d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -4,6 +4,7 @@
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.CryptorProvider;
+import org.cryptomator.cryptolib.api.FileNameCryptor;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
@@ -35,6 +36,9 @@ public class CryptoFileSystemsTest {
 	private final Path pathToVault = mock(Path.class, "vaultPath");
 	private final Path normalizedPathToVault = mock(Path.class, "normalizedVaultPath");
 	private final Path configFilePath = mock(Path.class, "normalizedVaultPath/vault.cryptomator");
+	private final Path dataDirPath = mock(Path.class, "normalizedVaultPath/d");
+	private final Path preContenRootPath = mock(Path.class, "normalizedVaultPath/d/AB");
+	private final Path contenRootPath = mock(Path.class, "normalizedVaultPath/d/AB/CDEFGHIJKLMNOP");
 	private final FileSystemCapabilityChecker capabilityChecker = mock(FileSystemCapabilityChecker.class);
 	private final CryptoFileSystemProvider provider = mock(CryptoFileSystemProvider.class);
 	private final CryptoFileSystemProperties properties = mock(CryptoFileSystemProperties.class);
@@ -49,6 +53,7 @@ public class CryptoFileSystemsTest {
 	private final SecureRandom csprng = Mockito.mock(SecureRandom.class);
 	private final CryptorProvider cryptorProvider = mock(CryptorProvider.class);
 	private final Cryptor cryptor = mock(Cryptor.class);
+	private final FileNameCryptor fileNameCryptor = mock(FileNameCryptor.class);
 	private final CryptoFileSystemComponent.Builder cryptoFileSystemComponentBuilder = mock(CryptoFileSystemComponent.Builder.class);
 
 
@@ -76,6 +81,12 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo);
 		when(cipherCombo.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
 		when(cryptorProvider.withKey(masterkey)).thenReturn(cryptor);
+		when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
+		when(fileNameCryptor.hashDirectoryId("")).thenReturn("ABCDEFGHIJKLMNOP");
+		when(pathToVault.resolve(Constants.DATA_DIR_NAME)).thenReturn(dataDirPath);
+		when(dataDirPath.resolve("AB")).thenReturn(preContenRootPath);
+		when(preContenRootPath.resolve("CDEFGHIJKLMNOP")).thenReturn(contenRootPath);
+		filesClass.when(() -> Files.exists(contenRootPath)).thenReturn(true);
 		when(cryptoFileSystemComponentBuilder.cryptor(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.vaultConfig(any())).thenReturn(cryptoFileSystemComponentBuilder);
 		when(cryptoFileSystemComponentBuilder.pathToVault(any())).thenReturn(cryptoFileSystemComponentBuilder);
@@ -130,6 +141,12 @@ public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIs
 		Assertions.assertTrue(inTest.contains(fileSystem2));
 	}
 
+	@Test
+	public void testCreateThrowsIOExceptionIfContentRootExistenceCheckFails() {
+		filesClass.when(() -> Files.exists(contenRootPath)).thenReturn(false);
+		Assertions.assertThrows(IOException.class, () -> inTest.create(provider, pathToVault, properties));
+	}
+
 	@Test
 	public void testGetReturnsFileSystemForPathIfItExists() throws IOException, MasterkeyLoadingFailedException {
 		CryptoFileSystemImpl fileSystem = inTest.create(provider, pathToVault, properties);

From a03974cfa5e29a26c128b1144a58e8f104c92f16 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Fri, 12 Mar 2021 15:32:32 +0100
Subject: [PATCH 28/70] use strong secure random instance [ci skip]

---
 .../java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 05750378..70e3eeba 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -149,7 +149,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 			var token = config.toToken(keyId.toString(), rawKey);
 			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
 			// create "d" dir and root:
-			Cryptor cryptor = config.getCipherCombo().getCryptorProvider(new SecureRandom()).withKey(key); //TODO: show we create a secure random instance like in CryptoFileSystemProviderComponent?
+			Cryptor cryptor = config.getCipherCombo().getCryptorProvider(strongSecureRandom()).withKey(key);
 			String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
 			Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
 			Files.createDirectories(vaultCipherRootPath);

From caf1f2f7e20a357eafe22040d8bc8a57baab2114 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Mon, 15 Mar 2021 10:50:14 +0100
Subject: [PATCH 29/70] autoclose & destroy cryptor after usage in cryptofs
 initialize method

---
 .../org/cryptomator/cryptofs/CryptoFileSystemProvider.java  | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 70e3eeba..151981c2 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -141,15 +141,15 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 			throw new NotDirectoryException(pathToVault.toString());
 		}
 		byte[] rawKey = new byte[0];
-		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId)) {
+		var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).maxFilenameLength(properties.maxNameLength()).build();
+		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId);
+			 Cryptor cryptor = config.getCipherCombo().getCryptorProvider(strongSecureRandom()).withKey(key)) {
 			rawKey = key.getEncoded();
 			// save vault config:
 			Path vaultConfigPath = pathToVault.resolve(properties.vaultConfigFilename());
-			var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).maxFilenameLength(properties.maxNameLength()).build();
 			var token = config.toToken(keyId.toString(), rawKey);
 			Files.writeString(vaultConfigPath, token, StandardCharsets.US_ASCII, WRITE, CREATE_NEW);
 			// create "d" dir and root:
-			Cryptor cryptor = config.getCipherCombo().getCryptorProvider(strongSecureRandom()).withKey(key);
 			String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
 			Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
 			Files.createDirectories(vaultCipherRootPath);

From 7b4213d87578f8b20320eb4c177e340149e30cda Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Mon, 15 Mar 2021 10:51:27 +0100
Subject: [PATCH 30/70] reformatting

---
 src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 06e169bb..c324c46f 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -77,8 +77,8 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 	private void checkVaultRootExistence(Path pathToVault, Cryptor cryptor) throws ContentRootMissingException {
 		String dirHash = cryptor.fileNameCryptor().hashDirectoryId(Constants.ROOT_DIR_ID);
 		Path vaultCipherRootPath = pathToVault.resolve(Constants.DATA_DIR_NAME).resolve(dirHash.substring(0, 2)).resolve(dirHash.substring(2));
-		if(! Files.exists(vaultCipherRootPath)){
-			throw new ContentRootMissingException("The encrypted root directory of the vault "+pathToVault+" is missing.");
+		if (!Files.exists(vaultCipherRootPath)) {
+			throw new ContentRootMissingException("The encrypted root directory of the vault " + pathToVault + " is missing.");
 		}
 	}
 

From 450bdc83c0011508ed3ba6c7762896360b248877 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Mon, 15 Mar 2021 11:14:47 +0100
Subject: [PATCH 31/70] close cryptor on any error during filesystem creation

---
 .../cryptofs/CryptoFileSystems.java           | 23 +++++++++++--------
 1 file changed, 14 insertions(+), 9 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index c324c46f..5f6dc9a4 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -54,15 +54,20 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 			rawKey = key.getEncoded();
 			var config = configLoader.verify(rawKey, Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
-			Cryptor cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key);
-			checkVaultRootExistence(pathToVault, cryptor);
-			return fileSystems.compute(normalizedPathToVault, (path, fs) -> {
-				if (fs == null) {
-					return create(provider, normalizedPathToVault, adjustedProperties, cryptor, config);
-				} else {
-					throw new FileSystemAlreadyExistsException();
-				}
-			});
+			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key);
+			try {
+				checkVaultRootExistence(pathToVault, cryptor);
+				return fileSystems.compute(normalizedPathToVault, (path, fs) -> {
+					if (fs == null) {
+						return create(provider, normalizedPathToVault, adjustedProperties, cryptor, config);
+					} else {
+						throw new FileSystemAlreadyExistsException();
+					}
+				});
+			} catch (Exception e){ //on any exception, close the cryptor
+				cryptor.destroy();
+				throw e;
+			}
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}

From f137db677154741464900556d4ab9936040ac0a2 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Mon, 15 Mar 2021 11:17:15 +0100
Subject: [PATCH 32/70] [ci skip] apply format

---
 src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 5f6dc9a4..c6207567 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -64,7 +64,7 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 						throw new FileSystemAlreadyExistsException();
 					}
 				});
-			} catch (Exception e){ //on any exception, close the cryptor
+			} catch (Exception e) { //on any exception, destroy the cryptor
 				cryptor.destroy();
 				throw e;
 			}

From 197b5243bd53829bd2d8f4582032cc51d40c1942 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 16 Mar 2021 06:52:52 +0100
Subject: [PATCH 33/70] make sure, "key" used in cryptor survives the
 try-with-resource block

---
 src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index c6207567..ba0c4423 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -54,7 +54,8 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 			rawKey = key.getEncoded();
 			var config = configLoader.verify(rawKey, Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
-			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key);
+			var keyCopy = Masterkey.createFromRaw(key.getEncoded()); // TODO replace with key.clone() eventually
+			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(keyCopy);
 			try {
 				checkVaultRootExistence(pathToVault, cryptor);
 				return fileSystems.compute(normalizedPathToVault, (path, fs) -> {

From 85e7fc2f60c5fe931cb5bdf5eb37b2a527b62f42 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 16 Mar 2021 07:20:32 +0100
Subject: [PATCH 34/70] fixed unit test

---
 .../org/cryptomator/cryptofs/CryptoFileSystemsTest.java  | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index f7138e2d..5cdfccb1 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -59,13 +59,15 @@ public class CryptoFileSystemsTest {
 
 	private MockedStatic<VaultConfig> vaultConficClass;
 	private MockedStatic<Files> filesClass;
+	private MockedStatic<Masterkey> masterkeyClass;
 
 	private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker, csprng);
 
 	@BeforeEach
 	public void setup() throws IOException, MasterkeyLoadingFailedException {
-		filesClass = Mockito.mockStatic(Files.class);
 		vaultConficClass = Mockito.mockStatic(VaultConfig.class);
+		filesClass = Mockito.mockStatic(Files.class);
+		masterkeyClass = Mockito.mockStatic(Masterkey.class);
 
 		when(pathToVault.normalize()).thenReturn(normalizedPathToVault);
 		when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath);
@@ -100,6 +102,7 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 	public void tearDown() {
 		vaultConficClass.close();
 		filesClass.close();
+		masterkeyClass.close();
 	}
 
 	@Test
@@ -143,7 +146,11 @@ public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIs
 
 	@Test
 	public void testCreateThrowsIOExceptionIfContentRootExistenceCheckFails() {
+		Masterkey clonedMasterkey = Mockito.mock(Masterkey.class, "clonedMasterkey");
 		filesClass.when(() -> Files.exists(contenRootPath)).thenReturn(false);
+		masterkeyClass.when(() -> Masterkey.createFromRaw(Mockito.any())).thenReturn(clonedMasterkey);
+		when(cryptorProvider.withKey(clonedMasterkey)).thenReturn(cryptor);
+
 		Assertions.assertThrows(IOException.class, () -> inTest.create(provider, pathToVault, properties));
 	}
 

From 5237e83d1d83b0fea26cb940e8bbb4ad3cb70767 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 16 Mar 2021 07:24:58 +0100
Subject: [PATCH 35/70] fixed more tests

---
 .../org/cryptomator/cryptofs/CryptoFileSystemsTest.java     | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 5cdfccb1..0f8d5669 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -47,6 +47,7 @@ public class CryptoFileSystemsTest {
 	private final VaultConfig.UnverifiedVaultConfig configLoader = mock(VaultConfig.UnverifiedVaultConfig.class);
 	private final MasterkeyLoader keyLoader = mock(MasterkeyLoader.class);
 	private final Masterkey masterkey = mock(Masterkey.class);
+	private final Masterkey clonedMasterkey = Mockito.mock(Masterkey.class);
 	private final byte[] rawKey = new byte[64];
 	private final VaultConfig vaultConfig = mock(VaultConfig.class);
 	private final VaultCipherCombo cipherCombo = mock(VaultCipherCombo.class);
@@ -80,6 +81,8 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(keyLoader.loadKey(Mockito.any())).thenReturn(masterkey);
 		when(masterkey.getEncoded()).thenReturn(rawKey);
 		when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
+		masterkeyClass.when(() -> Masterkey.createFromRaw(rawKey)).thenReturn(clonedMasterkey);
+		when(cryptorProvider.withKey(clonedMasterkey)).thenReturn(cryptor);
 		when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo);
 		when(cipherCombo.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
 		when(cryptorProvider.withKey(masterkey)).thenReturn(cryptor);
@@ -146,10 +149,7 @@ public void testCreateDoesNotThrowFileSystemAlreadyExistsExceptionIfFileSystemIs
 
 	@Test
 	public void testCreateThrowsIOExceptionIfContentRootExistenceCheckFails() {
-		Masterkey clonedMasterkey = Mockito.mock(Masterkey.class, "clonedMasterkey");
 		filesClass.when(() -> Files.exists(contenRootPath)).thenReturn(false);
-		masterkeyClass.when(() -> Masterkey.createFromRaw(Mockito.any())).thenReturn(clonedMasterkey);
-		when(cryptorProvider.withKey(clonedMasterkey)).thenReturn(cryptor);
 
 		Assertions.assertThrows(IOException.class, () -> inTest.create(provider, pathToVault, properties));
 	}

From e9c02b86db60f9385178db5b9e88ffc8237b8a54 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 16 Mar 2021 14:01:47 +0100
Subject: [PATCH 36/70] Adjusted to new masterkey API

---
 pom.xml                                               |  2 +-
 .../org/cryptomator/cryptofs/CryptoFileSystems.java   |  9 ++-------
 .../CryptoFileChannelWriteReadIntegrationTest.java    |  4 ++--
 .../CryptoFileSystemProviderIntegrationTest.java      | 11 +++++------
 .../cryptofs/CryptoFileSystemProviderTest.java        |  2 +-
 .../cryptomator/cryptofs/CryptoFileSystemUriTest.java |  2 +-
 .../cryptomator/cryptofs/CryptoFileSystemsTest.java   |  5 +----
 ...eteNonEmptyCiphertextDirectoryIntegrationTest.java |  2 +-
 .../cryptomator/cryptofs/ReadmeCodeSamplesTest.java   |  4 ++--
 .../cryptofs/RealFileSystemIntegrationTest.java       |  2 +-
 .../WriteFileWhileReadonlyChannelIsOpenTest.java      |  2 +-
 .../cryptofs/attr/FileAttributeIntegrationTest.java   |  2 +-
 .../cryptofs/migration/v6/Version6MigratorTest.java   |  2 +-
 .../cryptofs/migration/v7/Version7MigratorTest.java   |  2 +-
 .../cryptofs/migration/v8/Version8MigratorTest.java   |  2 +-
 15 files changed, 22 insertions(+), 31 deletions(-)

diff --git a/pom.xml b/pom.xml
index a5aeafc5..471ca4eb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-beta5</cryptolib.version>
+		<cryptolib.version>2.0.0-beta6</cryptolib.version>
 		<jwt.version>3.12.0</jwt.version>
 		<dagger.version>2.31</dagger.version>
 		<guava.version>30.1-jre</guava.version>
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index ba0c4423..438b9577 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -49,13 +49,10 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 
 		var configLoader = VaultConfig.decode(token);
 		var keyId = configLoader.getKeyId();
-		byte[] rawKey = new byte[0];
 		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId)) {
-			rawKey = key.getEncoded();
-			var config = configLoader.verify(rawKey, Constants.VAULT_VERSION);
+			var config = configLoader.verify(key.getEncoded(), Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
-			var keyCopy = Masterkey.createFromRaw(key.getEncoded()); // TODO replace with key.clone() eventually
-			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(keyCopy);
+			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key.clone());
 			try {
 				checkVaultRootExistence(pathToVault, cryptor);
 				return fileSystems.compute(normalizedPathToVault, (path, fs) -> {
@@ -69,8 +66,6 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 				cryptor.destroy();
 				throw e;
 			}
-		} finally {
-			Arrays.fill(rawKey, (byte) 0x00);
 		}
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index d5d8ea51..7bca22ee 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -67,7 +67,7 @@ public class Windows {
 		public void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 			Mockito.when(keyLoader.supportsScheme(Mockito.any())).thenReturn(true);
-			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 			CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
 			fileSystem = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
@@ -143,7 +143,7 @@ public void beforeAll() throws IOException, MasterkeyLoadingFailedException {
 			Files.createDirectories(vaultPath);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 			CryptoFileSystemProvider.initialize(vaultPath, properties, URI.create("test:key"));
 			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 8677bb00..97ef9c39 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -76,7 +76,6 @@ public class CryptoFileSystemProviderIntegrationTest {
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	class WithLimitedPaths {
 
-		private byte[] rawKey = new byte[64];
 		private MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		private CryptoFileSystem fs;
 		private Path shortFilePath;
@@ -86,7 +85,7 @@ class WithLimitedPaths {
 		@BeforeAll
 		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(rawKey));
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 					.withFlags() //
 					.withMasterkeyFilename("masterkey.cryptomator") //
@@ -195,8 +194,8 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 			keyLoader2 = Mockito.mock(MasterkeyLoader.class);
 			Mockito.when(keyLoader1.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader2.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader1.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(key1));
-			Mockito.when(keyLoader2.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(key2));
+			Mockito.when(keyLoader1.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key1));
+			Mockito.when(keyLoader2.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key2));
 			pathToVault1 = tmpFs.getPath("/vaultDir1");
 			pathToVault2 = tmpFs.getPath("/vaultDir2");
 			Files.createDirectory(pathToVault1);
@@ -537,7 +536,7 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
@@ -630,7 +629,7 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 655395e8..ed488d7b 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -115,7 +115,7 @@ private static final Stream<InvocationWhichShouldFail> shouldFailWithRelativePat
 	@SuppressWarnings("deprecation")
 	public void setup() throws MasterkeyLoadingFailedException {
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		when(keyLoader.loadKey(Mockito.any())).thenReturn(new Masterkey(new byte[64]));
 
 		CryptoFileSystemProviderComponent component = mock(CryptoFileSystemProviderComponent.class);
 		when(component.fileSystems()).thenReturn(fileSystems);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
index d1dfc2a8..fe60e12d 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
@@ -76,7 +76,7 @@ public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException
 		try {
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 			CryptoFileSystemProvider.initialize(tempDir, properties, URI.create("test:key"));
 			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 0f8d5669..43456ad2 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -60,7 +60,6 @@ public class CryptoFileSystemsTest {
 
 	private MockedStatic<VaultConfig> vaultConficClass;
 	private MockedStatic<Files> filesClass;
-	private MockedStatic<Masterkey> masterkeyClass;
 
 	private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker, csprng);
 
@@ -68,7 +67,6 @@ public class CryptoFileSystemsTest {
 	public void setup() throws IOException, MasterkeyLoadingFailedException {
 		vaultConficClass = Mockito.mockStatic(VaultConfig.class);
 		filesClass = Mockito.mockStatic(Files.class);
-		masterkeyClass = Mockito.mockStatic(Masterkey.class);
 
 		when(pathToVault.normalize()).thenReturn(normalizedPathToVault);
 		when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath);
@@ -80,8 +78,8 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(configLoader.getKeyId()).thenReturn(URI.create("test:key"));
 		when(keyLoader.loadKey(Mockito.any())).thenReturn(masterkey);
 		when(masterkey.getEncoded()).thenReturn(rawKey);
+		when(masterkey.clone()).thenReturn(clonedMasterkey);
 		when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
-		masterkeyClass.when(() -> Masterkey.createFromRaw(rawKey)).thenReturn(clonedMasterkey);
 		when(cryptorProvider.withKey(clonedMasterkey)).thenReturn(cryptor);
 		when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo);
 		when(cipherCombo.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
@@ -105,7 +103,6 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 	public void tearDown() {
 		vaultConficClass.close();
 		filesClass.close();
-		masterkeyClass.close();
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index b32e7260..021d3d86 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -48,7 +48,7 @@ public static void setupClass(@TempDir Path tmpDir) throws IOException, Masterke
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
index ebfc1d9f..30c1a857 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
@@ -32,7 +32,7 @@ public class ReadmeCodeSamplesTest {
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 		CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key"));
 		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, properties);
@@ -45,7 +45,7 @@ public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path
 		URI uri = CryptoFileSystemUri.create(storageLocation);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 		CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key"));
 		FileSystem fileSystem = FileSystems.newFileSystem(uri, properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
index 2431642f..d0c65013 100644
--- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
@@ -38,7 +38,7 @@ public static void setupClass(@TempDir Path tmpDir) throws IOException, Masterke
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
index dee2dddc..203f600f 100644
--- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
@@ -37,7 +37,7 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index de6ddb9a..9b5e59c6 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -63,7 +63,7 @@ public static void setupClass() throws IOException, MasterkeyLoadingFailedExcept
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
-		Mockito.when(keyLoader.loadKey(Mockito.any())).thenReturn(Masterkey.createFromRaw(new byte[64]));
+		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
index 00df36ef..b099554e 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v6/Version6MigratorTest.java
@@ -52,7 +52,7 @@ public void testMigrate() throws IOException, CryptoException {
 		String newPassword = Normalizer.normalize("ä", Form.NFC);
 		Assertions.assertNotEquals(oldPassword, newPassword);
 
-		Masterkey masterkey = Masterkey.createNew(csprng);
+		Masterkey masterkey = Masterkey.generate(csprng);
 		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
 		masterkeyFileAccess.persist(masterkey, masterkeyFile, oldPassword, 5);
 		byte[] beforeMigration = Files.readAllBytes(masterkeyFile);
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
index ec776e0d..63b8ce50 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/Version7MigratorTest.java
@@ -41,7 +41,7 @@ public void setup() throws IOException {
 		Files.createDirectory(dataDir);
 		Files.createDirectory(metaDir);
 
-		Masterkey masterkey = Masterkey.createNew(csprng);
+		Masterkey masterkey = Masterkey.generate(csprng);
 		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
 		masterkeyFileAccess.persist(masterkey, masterkeyFile, "test", 6);
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index f754e672..716e5bfa 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -48,7 +48,7 @@ public void teardown() throws IOException {
 
 	@Test
 	public void testMigrate() throws CryptoException, IOException {
-		Masterkey masterkey = Masterkey.createNew(csprng);
+		Masterkey masterkey = Masterkey.generate(csprng);
 		MasterkeyFileAccess masterkeyFileAccess = new MasterkeyFileAccess(new byte[0], csprng);
 		masterkeyFileAccess.persist(masterkey, masterkeyFile, "topsecret", 7);
 		Assumptions.assumeFalse(Files.exists(vaultConfigFile));

From bab52ca50293363493ba9a1222e78e9d805e9ed6 Mon Sep 17 00:00:00 2001
From: Tobias Hagemann <tobias.hagemann@skymatic.de>
Date: Mon, 22 Mar 2021 12:26:35 +0100
Subject: [PATCH 37/70] Update README.md

[ci skip]
---
 README.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/README.md b/README.md
index cf1af941..353aae74 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
 ![cryptomator](cryptomator.png)
 
 [![Build](https://github.com/cryptomator/cryptofs/workflows/Build/badge.svg)](https://github.com/cryptomator/cryptofs/actions?query=workflow%3ABuild)
-[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs)
-[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/app/cryptomator/cryptofs?utm_source=github.com&utm_medium=referral&utm_content=cryptomator/cryptofs&utm_campaign=Badge_Coverage)
+[![Codacy Badge](https://api.codacy.com/project/badge/Grade/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/gh/cryptomator/cryptofs/dashboard)
+[![Codacy Badge](https://api.codacy.com/project/badge/Coverage/7248ca7d466843f785f79f33374302c2)](https://www.codacy.com/gh/cryptomator/cryptofs/dashboard)
 [![Known Vulnerabilities](https://snyk.io/test/github/cryptomator/cryptofs/badge.svg)](https://snyk.io/test/github/cryptomator/cryptofs)
 
 **CryptoFS:** Implementation of the [Cryptomator](https://github.com/cryptomator/cryptomator) encryption scheme.

From 19fd7fe5b15d3125ce324dc64739c581079b1f7a Mon Sep 17 00:00:00 2001
From: Tobias Hagemann <tobias.hagemann@skymatic.de>
Date: Mon, 22 Mar 2021 12:28:06 +0100
Subject: [PATCH 38/70] updated slack notification

[ci skip]
---
 .github/workflows/publish-github.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml
index 32a3041b..8be609b4 100644
--- a/.github/workflows/publish-github.yml
+++ b/.github/workflows/publish-github.yml
@@ -35,6 +35,6 @@ jobs:
           SLACK_ICON_EMOJI: ':bot:'
           SLACK_CHANNEL: 'cryptomator-desktop'
           SLACK_TITLE: "Published ${{ github.event.repository.name }} ${{ github.event.release.tag_name }}"
-          SLACK_MESSAGE: "Ready to <https://github.com/${{ github.repository }}/actions?query=workflow%3A%22Publish+to+Maven+Central%22|deploy to Maven Central>."
+          SLACK_MESSAGE: "Ready to <https://github.com/${{ github.repository }}/actions/workflows/publish-central.yml|deploy to Maven Central>."
           SLACK_FOOTER:
-          MSG_MINIMAL: true
\ No newline at end of file
+          MSG_MINIMAL: true

From 0d17a7a3515063203f924f59fc4d92051527d558 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 22 Mar 2021 14:32:06 +0100
Subject: [PATCH 39/70] update to JDK 16

---
 .github/workflows/build.yml           |  2 +-
 .github/workflows/codeql-analysis.yml |  2 +-
 .github/workflows/publish-central.yml |  2 +-
 .github/workflows/publish-github.yml  |  2 +-
 README.md                             |  2 +-
 pom.xml                               | 23 +----------------------
 6 files changed, 6 insertions(+), 27 deletions(-)

diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 36fde6cc..37afd896 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,7 +10,7 @@ jobs:
       - uses: actions/checkout@v2
       - uses: actions/setup-java@v1
         with:
-          java-version: 11
+          java-version: 16
       - uses: actions/cache@v2
         with:
           path: ~/.m2/repository
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 51683060..d100a440 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,7 +20,7 @@ jobs:
         fetch-depth: 2
     - uses: actions/setup-java@v1
       with:
-        java-version: 11
+        java-version: 16
     - uses: actions/cache@v2
       with:
         path: ~/.m2/repository
diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml
index f22b7a96..d9be480e 100644
--- a/.github/workflows/publish-central.yml
+++ b/.github/workflows/publish-central.yml
@@ -15,7 +15,7 @@ jobs:
           ref: "refs/tags/${{ github.event.inputs.tag }}"
       - uses: actions/setup-java@v1
         with:
-          java-version: 11
+          java-version: 16
           server-id: ossrh # Value of the distributionManagement/repository/id field of the pom.xml
           server-username: MAVEN_USERNAME # env variable for username in deploy
           server-password: MAVEN_PASSWORD # env variable for token in deploy
diff --git a/.github/workflows/publish-github.yml b/.github/workflows/publish-github.yml
index 8be609b4..3923b47f 100644
--- a/.github/workflows/publish-github.yml
+++ b/.github/workflows/publish-github.yml
@@ -10,7 +10,7 @@ jobs:
       - uses: actions/checkout@v2
       - uses: actions/setup-java@v1
         with:
-          java-version: 11
+          java-version: 16
           gpg-private-key: ${{ secrets.RELEASES_GPG_PRIVATE_KEY }} # Value of the GPG private key to import
           gpg-passphrase: MAVEN_GPG_PASSPHRASE # env variable for GPG private key passphrase
       - uses: actions/cache@v2
diff --git a/README.md b/README.md
index 353aae74..ccd045d0 100644
--- a/README.md
+++ b/README.md
@@ -93,7 +93,7 @@ For more details on how to use the constructed `FileSystem`, you may consult the
 
 ### Dependencies
 
-* Java 11
+* Java 16 (will be updated to 17 in late 2021)
 * Maven 3
 
 ### Run Maven
diff --git a/pom.xml b/pom.xml
index 471ca4eb..6f9b3e62 100644
--- a/pom.xml
+++ b/pom.xml
@@ -116,33 +116,12 @@
 
 	<build>
 		<plugins>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-enforcer-plugin</artifactId>
-				<version>3.0.0-M3</version>
-				<executions>
-					<execution>
-						<id>enforce-java</id>
-						<goals>
-							<goal>enforce</goal>
-						</goals>
-						<configuration>
-							<rules>
-								<requireJavaVersion>
-									<message>You need at least JDK 11.0.3 to build this project.</message>
-									<version>[11.0.3,)</version>
-								</requireJavaVersion>
-							</rules>
-						</configuration>
-					</execution>
-				</executions>
-			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-compiler-plugin</artifactId>
 				<version>3.8.1</version>
 				<configuration>
-					<release>11</release>
+					<release>16</release>
 					<showWarnings>true</showWarnings>
 					<annotationProcessorPaths>
 						<path>

From 01b1680d0930a7cff96d92d9ad1c477ff8f05e8c Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 22 Mar 2021 14:33:42 +0100
Subject: [PATCH 40/70] update IDE config

[ci skip]
---
 .idea/misc.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.idea/misc.xml b/.idea/misc.xml
index 2fc03d02..d7e555a5 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -7,5 +7,5 @@
       </list>
     </option>
   </component>
-  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="14" project-jdk-type="JavaSDK" />
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_16" project-jdk-name="16" project-jdk-type="JavaSDK" />
 </project>
\ No newline at end of file

From 124aac654c9791ba0325207b369c268a9230de05 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 22 Mar 2021 14:38:41 +0100
Subject: [PATCH 41/70] apply pattern-matching-instanceof where applicable

---
 .../org/cryptomator/cryptofs/CiphertextFilePath.java   |  3 +--
 .../cryptofs/CryptoFileSystemProperties.java           |  4 ++--
 src/main/java/org/cryptomator/cryptofs/CryptoPath.java | 10 ++++------
 .../org/cryptomator/cryptofs/CryptoPathMapper.java     |  6 ++----
 .../org/cryptomator/cryptofs/attr/AttributeModule.java |  8 ++++----
 .../cryptofs/CryptoFileSystemPropertiesTest.java       |  5 ++---
 .../cryptofs/attr/FileAttributeIntegrationTest.java    |  7 +++----
 7 files changed, 18 insertions(+), 25 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java
index fdd2c2ed..e655861a 100644
--- a/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java
+++ b/src/main/java/org/cryptomator/cryptofs/CiphertextFilePath.java
@@ -47,8 +47,7 @@ public int hashCode() {
 
 	@Override
 	public boolean equals(Object obj) {
-		if (obj instanceof CiphertextFilePath) {
-			CiphertextFilePath other = (CiphertextFilePath) obj;
+		if (obj instanceof CiphertextFilePath other) {
 			return this.path.equals(other.path) && this.deflatedFileName.equals(other.deflatedFileName);
 		} else {
 			return false;
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index a8571317..a0543a65 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -202,8 +202,8 @@ public static Builder cryptoFileSystemPropertiesFrom(Map<String, ?> properties)
 	 * @throws IllegalArgumentException if a value in the {@code Map} does not have the expected type or if a required value is missing
 	 */
 	public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
-		if (properties instanceof CryptoFileSystemProperties) {
-			return (CryptoFileSystemProperties) properties;
+		if (properties instanceof CryptoFileSystemProperties p) {
+			return p;
 		} else {
 			try {
 				return cryptoFileSystemPropertiesFrom(properties).build();
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java
index 044a0cdc..e6165aa8 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoPath.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoPath.java
@@ -55,10 +55,9 @@ static CryptoPath castAndAssertAbsolute(Path path) {
 	}
 
 	static CryptoPath cast(Path path) {
-		if (path instanceof CryptoPath) {
-			CryptoPath cryptoPath = (CryptoPath) path;
-			cryptoPath.getFileSystem().assertOpen();
-			return cryptoPath;
+		if (path instanceof CryptoPath p) {
+			p.getFileSystem().assertOpen();
+			return p;
 		} else {
 			throw new ProviderMismatchException("Used a path from different provider: " + path);
 		}
@@ -351,8 +350,7 @@ public int hashCode() {
 
 	@Override
 	public boolean equals(Object obj) {
-		if (obj instanceof CryptoPath) {
-			CryptoPath other = (CryptoPath) obj;
+		if (obj instanceof CryptoPath other) {
 			return this.fileSystem.equals(other.fileSystem) //
 					&& this.compareTo(other) == 0;
 		} else {
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
index fbb8688e..3bab4afc 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
@@ -198,8 +198,7 @@ public int hashCode() {
 		public boolean equals(Object obj) {
 			if (obj == this) {
 				return true;
-			} else if (obj instanceof CiphertextDirectory) {
-				CiphertextDirectory other = (CiphertextDirectory) obj;
+			} else if (obj instanceof CiphertextDirectory other) {
 				return this.dirId.equals(other.dirId) && this.path.equals(other.path);
 			} else {
 				return false;
@@ -225,8 +224,7 @@ public int hashCode() {
 		public boolean equals(Object obj) {
 			if (obj == this) {
 				return true;
-			} else if (obj instanceof DirIdAndName) {
-				DirIdAndName other = (DirIdAndName) obj;
+			} else if (obj instanceof DirIdAndName other) {
 				return this.dirId.equals(other.dirId) && this.name.equals(other.name);
 			} else {
 				return false;
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java
index 52fc9ca3..46b70c0d 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeModule.java
@@ -28,8 +28,8 @@ public static Optional<OpenCryptoFile> provideOpenCryptoFile(OpenCryptoFiles ope
 	@Provides
 	@AttributeScoped
 	public static PosixFileAttributes providePosixFileAttributes(BasicFileAttributes ciphertextAttributes) {
-		if (ciphertextAttributes instanceof PosixFileAttributes) {
-			return (PosixFileAttributes) ciphertextAttributes;
+		if (ciphertextAttributes instanceof PosixFileAttributes attr) {
+			return attr;
 		} else {
 			throw new IllegalStateException("Attempted to inject instance of type " + ciphertextAttributes.getClass() + " but expected PosixFileAttributes.");
 		}
@@ -38,8 +38,8 @@ public static PosixFileAttributes providePosixFileAttributes(BasicFileAttributes
 	@Provides
 	@AttributeScoped
 	public static DosFileAttributes provideDosFileAttributes(BasicFileAttributes ciphertextAttributes) {
-		if (ciphertextAttributes instanceof DosFileAttributes) {
-			return (DosFileAttributes) ciphertextAttributes;
+		if (ciphertextAttributes instanceof DosFileAttributes attr) {
+			return attr;
 		} else {
 			throw new IllegalStateException("Attempted to inject instance of type " + ciphertextAttributes.getClass() + " but expected DosFileAttributes.");
 		}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 41f5414c..676174e8 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -247,10 +247,9 @@ private boolean keyMatches(K itemKey) {
 				return Objects.equals(key, itemKey);
 			}
 
-			@SuppressWarnings("rawtypes")
 			private boolean valueMatches(V itemValue) {
-				if (value instanceof Collection && itemValue instanceof Collection) {
-					return valuesMatch((Collection) value, (Collection) itemValue);
+				if (value instanceof Collection v && itemValue instanceof Collection c) {
+					return valuesMatch(v, c);
 				} else {
 					return Objects.equals(value, itemValue);
 				}
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index 9b5e59c6..11a6ee09 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -197,12 +197,11 @@ public void testFileAttributeViewUpdatesAfterMove() throws IOException {
 	}
 
 	private static Matcher<FileTime> isAfter(FileTime previousFileTime) {
-		return new BaseMatcher<FileTime>() {
+		return new BaseMatcher<>() {
 			@Override
 			public boolean matches(Object item) {
-				if (item instanceof FileTime) {
-					FileTime subject = (FileTime) item;
-					return subject.compareTo(previousFileTime) > 0;
+				if (item instanceof FileTime ft) {
+					return ft.compareTo(previousFileTime) > 0;
 				} else {
 					return false;
 				}

From 250b37199ea3dda761fa1e95f3c550f185b2274f Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 22 Mar 2021 15:02:42 +0100
Subject: [PATCH 42/70] use switch expressions where applicable

---
 .../cryptofs/CryptoFileSystemImpl.java        | 82 ++++++-------------
 .../attr/AbstractCryptoFileAttributeView.java | 20 ++---
 .../cryptofs/attr/AttributeProvider.java      | 15 ++--
 .../attr/CryptoBasicFileAttributes.java       | 15 +---
 .../dir/CiphertextDirectoryDeleter.java       |  9 +-
 5 files changed, 48 insertions(+), 93 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index df40ab7c..c941c747 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -21,12 +21,9 @@
 import org.cryptomator.cryptofs.dir.DirectoryStreamFactory;
 import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
 import org.cryptomator.cryptolib.api.Cryptor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import java.io.IOException;
-import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.AccessDeniedException;
 import java.nio.file.AccessMode;
@@ -38,7 +35,6 @@
 import java.nio.file.DirectoryStream.Filter;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.FileStore;
-import java.nio.file.FileSystemException;
 import java.nio.file.Files;
 import java.nio.file.LinkOption;
 import java.nio.file.NoSuchFileException;
@@ -267,16 +263,11 @@ void checkAccess(CryptoPath cleartextPath, AccessMode... modes) throws IOExcepti
 	}
 
 	private boolean hasAccess(Set<PosixFilePermission> permissions, AccessMode accessMode) {
-		switch (accessMode) {
-			case READ:
-				return permissions.contains(PosixFilePermission.OWNER_READ);
-			case WRITE:
-				return permissions.contains(PosixFilePermission.OWNER_WRITE);
-			case EXECUTE:
-				return permissions.contains(PosixFilePermission.OWNER_EXECUTE);
-			default:
-				throw new UnsupportedOperationException("AccessMode " + accessMode + " not supported.");
-		}
+		return switch (accessMode) {
+			case READ -> permissions.contains(PosixFilePermission.OWNER_READ);
+			case WRITE -> permissions.contains(PosixFilePermission.OWNER_WRITE);
+			case EXECUTE -> permissions.contains(PosixFilePermission.OWNER_EXECUTE);
+		};
 	}
 
 	boolean isHidden(CryptoPath cleartextPath) throws IOException {
@@ -340,22 +331,23 @@ FileChannel newFileChannel(CryptoPath cleartextPath, Set<? extends OpenOption> o
 				throw e;
 			}
 		}
-		switch (ciphertextFileType) {
-			case SYMLINK:
-				if (options.noFollowLinks()) {
-					throw new UnsupportedOperationException("Unsupported OpenOption LinkOption.NOFOLLOW_LINKS. Can not create file channel for symbolic link.");
-				} else {
-					CryptoPath resolvedPath = symlinks.resolveRecursively(cleartextPath);
-					return newFileChannel(resolvedPath, options, attrs);
-				}
-			case FILE:
-				return newFileChannel(cleartextPath, options, attrs);
-			default:
-				throw new UnsupportedOperationException("Can not create file channel for " + ciphertextFileType.name());
+		return switch (ciphertextFileType) {
+			case SYMLINK -> newFileChannelFromSymlink(cleartextPath, options, attrs);
+			case FILE -> newFileChannelFromFile(cleartextPath, options, attrs);
+			case DIRECTORY -> throw new UnsupportedOperationException("Can not create file channel for " + ciphertextFileType.name());
+		};
+	}
+
+	private FileChannel newFileChannelFromSymlink(CryptoPath cleartextPath, EffectiveOpenOptions options, FileAttribute<?>... attrs) throws IOException {
+		if (options.noFollowLinks()) {
+			throw new UnsupportedOperationException("Unsupported OpenOption LinkOption.NOFOLLOW_LINKS. Can not create file channel for symbolic link.");
+		} else {
+			CryptoPath resolvedPath = symlinks.resolveRecursively(cleartextPath);
+			return newFileChannelFromFile(resolvedPath, options, attrs);
 		}
 	}
 
-	private FileChannel newFileChannel(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute<?>... attrs) throws IOException {
+	private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute<?>... attrs) throws IOException {
 		CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath);
 		Path ciphertextFilePath = ciphertextPath.getFilePath();
 		assertCiphertextPathLengthMeetsLimitations(ciphertextFilePath);
@@ -384,12 +376,8 @@ void delete(CryptoPath cleartextPath) throws IOException {
 		CiphertextFileType ciphertextFileType = cryptoPathMapper.getCiphertextFileType(cleartextPath);
 		CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextPath);
 		switch (ciphertextFileType) {
-			case DIRECTORY:
-				deleteDirectory(cleartextPath, ciphertextPath);
-				return;
-			default:
-				Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE);
-				return;
+			case DIRECTORY -> deleteDirectory(cleartextPath, ciphertextPath);
+			case FILE, SYMLINK -> Files.walkFileTree(ciphertextPath.getRawPath(), DeletingFileVisitor.INSTANCE);
 		}
 	}
 
@@ -420,17 +408,9 @@ void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption...
 			cryptoPathMapper.assertNonExisting(cleartextTarget);
 		}
 		switch (ciphertextFileType) {
-			case SYMLINK:
-				copySymlink(cleartextSource, cleartextTarget, options);
-				return;
-			case FILE:
-				copyFile(cleartextSource, cleartextTarget, options);
-				return;
-			case DIRECTORY:
-				copyDirectory(cleartextSource, cleartextTarget, options);
-				return;
-			default:
-				throw new UnsupportedOperationException("Unhandled node type " + ciphertextFileType);
+			case SYMLINK -> copySymlink(cleartextSource, cleartextTarget, options);
+			case FILE -> copyFile(cleartextSource, cleartextTarget, options);
+			case DIRECTORY -> copyDirectory(cleartextSource, cleartextTarget, options);
 		}
 	}
 
@@ -526,17 +506,9 @@ void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption...
 			cryptoPathMapper.assertNonExisting(cleartextTarget);
 		}
 		switch (ciphertextFileType) {
-			case SYMLINK:
-				moveSymlink(cleartextSource, cleartextTarget, options);
-				return;
-			case FILE:
-				moveFile(cleartextSource, cleartextTarget, options);
-				return;
-			case DIRECTORY:
-				moveDirectory(cleartextSource, cleartextTarget, options);
-				return;
-			default:
-				throw new UnsupportedOperationException("Unhandled node type " + ciphertextFileType);
+			case SYMLINK -> moveSymlink(cleartextSource, cleartextTarget, options);
+			case FILE -> moveFile(cleartextSource, cleartextTarget, options);
+			case DIRECTORY -> moveDirectory(cleartextSource, cleartextTarget, options);
 		}
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java
index 6e45aeaf..abd91859 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/AbstractCryptoFileAttributeView.java
@@ -8,13 +8,13 @@
  *******************************************************************************/
 package org.cryptomator.cryptofs.attr;
 
-import org.cryptomator.cryptofs.common.ArrayUtils;
-import org.cryptomator.cryptofs.common.CiphertextFileType;
 import org.cryptomator.cryptofs.CryptoPath;
 import org.cryptomator.cryptofs.CryptoPathMapper;
-import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
 import org.cryptomator.cryptofs.Symlinks;
+import org.cryptomator.cryptofs.common.ArrayUtils;
+import org.cryptomator.cryptofs.common.CiphertextFileType;
 import org.cryptomator.cryptofs.fh.OpenCryptoFile;
+import org.cryptomator.cryptofs.fh.OpenCryptoFiles;
 
 import java.io.IOException;
 import java.nio.file.LinkOption;
@@ -50,19 +50,19 @@ protected Optional<OpenCryptoFile> getOpenCryptoFile() throws IOException {
 
 	private Path getCiphertextPath(CryptoPath path) throws IOException {
 		CiphertextFileType type = pathMapper.getCiphertextFileType(path);
-		switch (type) {
+		return switch (type) {
 			case SYMLINK:
 				if (ArrayUtils.contains(linkOptions, LinkOption.NOFOLLOW_LINKS)) {
-					return pathMapper.getCiphertextFilePath(path).getSymlinkFilePath();
+					yield pathMapper.getCiphertextFilePath(path).getSymlinkFilePath();
 				} else {
 					CryptoPath resolved = symlinks.resolveRecursively(path);
-					return getCiphertextPath(resolved);
+					yield getCiphertextPath(resolved);
 				}
 			case DIRECTORY:
-				return pathMapper.getCiphertextDir(path).path;
-			default:
-				return pathMapper.getCiphertextFilePath(path).getFilePath();
-		}
+				yield pathMapper.getCiphertextDir(path).path;
+			case FILE:
+				yield pathMapper.getCiphertextFilePath(path).getFilePath();
+		};
 	}
 
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java
index 6a7c0db6..d9c445bb 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/AttributeProvider.java
@@ -62,16 +62,11 @@ public <A extends BasicFileAttributes> A readAttributes(CryptoPath cleartextPath
 	}
 
 	private Path getCiphertextPath(CryptoPath path, CiphertextFileType type) throws IOException {
-		switch (type) {
-			case SYMLINK:
-				return pathMapper.getCiphertextFilePath(path).getSymlinkFilePath();
-			case DIRECTORY:
-				return pathMapper.getCiphertextDir(path).path;
-			case FILE:
-				return pathMapper.getCiphertextFilePath(path).getFilePath();
-			default:
-				throw new UnsupportedOperationException("Unhandled node type " + type);
-		}
+		return switch (type) {
+			case SYMLINK -> pathMapper.getCiphertextFilePath(path).getSymlinkFilePath();
+			case DIRECTORY -> pathMapper.getCiphertextDir(path).path;
+			case FILE -> pathMapper.getCiphertextFilePath(path).getFilePath();
+		};
 	}
 
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java
index 3f84ec1c..92a8d320 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java
@@ -37,17 +37,10 @@ class CryptoBasicFileAttributes implements BasicFileAttributes {
 	@Inject
 	public CryptoBasicFileAttributes(BasicFileAttributes delegate, CiphertextFileType ciphertextFileType, Path ciphertextPath, Cryptor cryptor, Optional<OpenCryptoFile> openCryptoFile) {
 		this.ciphertextFileType = ciphertextFileType;
-		switch (ciphertextFileType) {
-			case SYMLINK:
-			case DIRECTORY:
-				this.size = delegate.size();
-				break;
-			case FILE:
-				this.size = getPlaintextFileSize(ciphertextPath, delegate.size(), openCryptoFile, cryptor);
-				break;
-			default:
-				throw new IllegalArgumentException("Unsupported ciphertext file type: " + ciphertextFileType);
-		}
+		this.size = switch (ciphertextFileType) {
+			case SYMLINK, DIRECTORY -> delegate.size();
+			case FILE -> getPlaintextFileSize(ciphertextPath, delegate.size(), openCryptoFile, cryptor);
+		};
 		this.lastModifiedTime =  openCryptoFile.map(OpenCryptoFile::getLastModifiedTime).orElseGet(delegate::lastModifiedTime);
 		this.lastAccessTime = openCryptoFile.map(openFile -> FileTime.from(Instant.now())).orElseGet(delegate::lastAccessTime);
 		this.creationTime = delegate.creationTime();
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java b/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java
index 1efdcc4f..acb1adeb 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/CiphertextDirectoryDeleter.java
@@ -45,13 +45,8 @@ public void deleteCiphertextDirIncludingNonCiphertextFiles(Path ciphertextDir, C
 			 * case 2 is true.
 			 */
 			switch (deleteNonCiphertextFiles(ciphertextDir, cleartextDir)) {
-			case NO_FILES_DELETED:
-				throw e;
-			case SOME_FILES_DELETED:
-				Files.delete(ciphertextDir);
-				break;
-			default:
-				throw new IllegalStateException("Unexpected enum constant");
+				case NO_FILES_DELETED -> throw e;
+				case SOME_FILES_DELETED-> Files.delete(ciphertextDir);
 			}
 		}
 	}

From 0d179298026e5369c94a32a834e7a2e93392da4d Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 22 Mar 2021 15:47:28 +0100
Subject: [PATCH 43/70] replaced mock

---
 .../cryptofs/ch/CleartextFileLockTest.java    |  7 +-
 .../cryptofs/ch/DummyFileChannel.java         | 97 -------------------
 2 files changed, 4 insertions(+), 100 deletions(-)
 delete mode 100644 src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java

diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java
index 64ba9ed4..9171bead 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java
@@ -21,7 +21,8 @@ public class CleartextFileLockTest {
 
 	@BeforeEach
 	public void setup() {
-		channel = Mockito.spy(new DummyFileChannel());
+		channel = Mockito.mock(FileChannel.class);
+		Mockito.when(channel.isOpen()).thenReturn(true);
 	}
 
 	@Nested
@@ -99,7 +100,7 @@ class ClosedChannel {
 
 			@BeforeEach
 			public void setup() throws IOException {
-				channel.close();
+				Mockito.when(channel.isOpen()).thenReturn(false);
 			}
 
 			@Test
@@ -187,7 +188,7 @@ class ClosedChannel {
 
 			@BeforeEach
 			public void setup() throws IOException {
-				channel.close();
+				Mockito.when(channel.isOpen()).thenReturn(false);
 			}
 
 			@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java b/src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java
deleted file mode 100644
index ab9c5abc..00000000
--- a/src/test/java/org/cryptomator/cryptofs/ch/DummyFileChannel.java
+++ /dev/null
@@ -1,97 +0,0 @@
-package org.cryptomator.cryptofs.ch;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.nio.MappedByteBuffer;
-import java.nio.channels.FileChannel;
-import java.nio.channels.FileLock;
-import java.nio.channels.ReadableByteChannel;
-import java.nio.channels.WritableByteChannel;
-
-class DummyFileChannel extends FileChannel {
-
-	@Override
-	public int read(ByteBuffer dst) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public int write(ByteBuffer src) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public long position() throws IOException {
-		return 0;
-	}
-
-	@Override
-	public FileChannel position(long newPosition) throws IOException {
-		return null;
-	}
-
-	@Override
-	public long size() throws IOException {
-		return 0;
-	}
-
-	@Override
-	public FileChannel truncate(long size) throws IOException {
-		return null;
-	}
-
-	@Override
-	public void force(boolean metaData) throws IOException {
-	}
-
-	@Override
-	public long transferTo(long position, long count, WritableByteChannel target) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public int read(ByteBuffer dst, long position) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public int write(ByteBuffer src, long position) throws IOException {
-		return 0;
-	}
-
-	@Override
-	public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
-		return null;
-	}
-
-	@Override
-	public FileLock lock(long position, long size, boolean shared) throws IOException {
-		return null;
-	}
-
-	@Override
-	public FileLock tryLock(long position, long size, boolean shared) throws IOException {
-		return null;
-	}
-
-	@Override
-	protected void implCloseChannel() throws IOException {
-	}
-
-}
-

From fb9fb78ae23c97ea69929ed76d59ee0b2eed7cd5 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 16 Apr 2021 11:44:35 +0200
Subject: [PATCH 44/70] shorten long names when reaching configured limit
 instead of hardcoded 220

---
 .../cryptofs/CryptoFileSystemProperties.java  |  4 ++--
 .../cryptofs/CryptoPathMapper.java            |  6 +++--
 .../cryptofs/common/Constants.java            |  1 -
 .../cryptofs/dir/C9rConflictResolver.java     | 20 ++++++++++++-----
 .../cryptofs/CryptoPathMapperTest.java        | 22 ++++++++++---------
 ...ptyCiphertextDirectoryIntegrationTest.java |  2 --
 .../cryptofs/dir/C9rConflictResolverTest.java | 14 +++++++++++-
 7 files changed, 46 insertions(+), 23 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 7329773d..4c218d9b 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -167,11 +167,11 @@ String masterkeyFilename() {
 		return (String) get(PROPERTY_MASTERKEY_FILENAME);
 	}
 
-	int maxPathLength() {
+	public int maxPathLength() {
 		return (int) get(PROPERTY_MAX_PATH_LENGTH);
 	}
 	
-	int maxNameLength() {
+	public int maxNameLength() {
 		return (int) get(PROPERTY_MAX_NAME_LENGTH);
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
index fbb8688e..8cc1783e 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
@@ -48,17 +48,19 @@ public class CryptoPathMapper {
 	private final Path dataRoot;
 	private final DirectoryIdProvider dirIdProvider;
 	private final LongFileNameProvider longFileNameProvider;
+	private final CryptoFileSystemProperties fileSystemProperties;
 	private final LoadingCache<DirIdAndName, String> ciphertextNames;
 	private final Cache<CryptoPath, CiphertextDirectory> ciphertextDirectories;
 
 	private final CiphertextDirectory rootDirectory;
 
 	@Inject
-	CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider) {
+	CryptoPathMapper(@PathToVault Path pathToVault, Cryptor cryptor, DirectoryIdProvider dirIdProvider, LongFileNameProvider longFileNameProvider, CryptoFileSystemProperties fileSystemProperties) {
 		this.dataRoot = pathToVault.resolve(DATA_DIR_NAME);
 		this.cryptor = cryptor;
 		this.dirIdProvider = dirIdProvider;
 		this.longFileNameProvider = longFileNameProvider;
+		this.fileSystemProperties = fileSystemProperties;
 		this.ciphertextNames = CacheBuilder.newBuilder().maximumSize(MAX_CACHED_CIPHERTEXT_NAMES).build(CacheLoader.from(this::getCiphertextFileName));
 		this.ciphertextDirectories = CacheBuilder.newBuilder().maximumSize(MAX_CACHED_DIR_PATHS).expireAfterWrite(MAX_CACHE_AGE).build();
 		this.rootDirectory = resolveDirectory(Constants.ROOT_DIR_ID);
@@ -127,7 +129,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws
 	public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) {
 		String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parentDirId, cleartextName));
 		Path c9rPath = parentCiphertextDir.resolve(ciphertextName);
-		if (ciphertextName.length() > Constants.MAX_CIPHERTEXT_NAME_LENGTH) {
+		if (ciphertextName.length() > fileSystemProperties.maxNameLength()) {
 			LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath);
 			return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName));
 		} else {
diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
index 0ad683f8..5bbfa7e5 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
@@ -23,7 +23,6 @@ public final class Constants {
 
 	public static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303
 	public static final int MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r
-	public static final int MAX_CLEARTEXT_NAME_LENGTH = 146; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303
 	public static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77
 	public static final int MAX_CIPHERTEXT_PATH_LENGTH = MAX_CIPHERTEXT_NAME_LENGTH + MAX_ADDITIONAL_PATH_LENGTH;
 	public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
index dc34068d..f404ee5e 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
@@ -4,6 +4,7 @@
 import com.google.common.io.BaseEncoding;
 import com.google.common.io.MoreFiles;
 import com.google.common.io.RecursiveDeleteOption;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties;
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.slf4j.Logger;
@@ -23,8 +24,6 @@
 import java.util.stream.Stream;
 
 import static org.cryptomator.cryptofs.common.Constants.DIR_FILE_NAME;
-import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH;
-import static org.cryptomator.cryptofs.common.Constants.MAX_CLEARTEXT_NAME_LENGTH;
 import static org.cryptomator.cryptofs.common.Constants.MAX_DIR_FILE_LENGTH;
 import static org.cryptomator.cryptofs.common.Constants.MAX_SYMLINK_LENGTH;
 import static org.cryptomator.cryptofs.common.Constants.SYMLINK_FILE_NAME;
@@ -36,11 +35,13 @@ class C9rConflictResolver {
 
 	private final Cryptor cryptor;
 	private final byte[] dirId;
+	private final CryptoFileSystemProperties fileSystemProperties;
 
 	@Inject
-	public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId) {
+	public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId, CryptoFileSystemProperties fileSystemProperties) {
 		this.cryptor = cryptor;
 		this.dirId = dirId.getBytes(StandardCharsets.US_ASCII);
+		this.fileSystemProperties = fileSystemProperties;
 	}
 
 	public Stream<Node> process(Node node) {
@@ -79,6 +80,13 @@ private Stream<Node> resolveConflict(Node conflicting, Path canonicalPath) throw
 		}
 	}
 
+	// visible for testing
+	int calcMaxCleartextNameLength(int maxCiphertextNameLength) {
+		// math explained in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303;
+		// subtract 4 for file extension, base64-decode, subtract 16 for IV
+		return (maxCiphertextNameLength - 4) / 4 * 3 - 16;
+	}
+
 	/**
 	 * Resolves a conflict by renaming the conflicting file.
 	 *
@@ -91,9 +99,11 @@ private Stream<Node> resolveConflict(Node conflicting, Path canonicalPath) throw
 	private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException {
 		assert Files.exists(canonicalPath);
 		final int beginOfFileExtension = cleartext.lastIndexOf('.');
+		final int maxCiphertextNameLength = fileSystemProperties.maxNameLength();
+		final int maxCleartextNameLength = calcMaxCleartextNameLength(maxCiphertextNameLength);
 		final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : "";
 		final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext;
-		final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), MAX_CLEARTEXT_NAME_LENGTH - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)"
+		final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), maxCleartextNameLength - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)"
 		String alternativeCleartext;
 		String alternativeCiphertext;
 		String alternativeCiphertextName;
@@ -105,7 +115,7 @@ private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, Str
 			alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX;
 			alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName);
 		} while (Files.exists(alternativePath));
-		assert alternativeCiphertextName.length() <= MAX_CIPHERTEXT_NAME_LENGTH;
+		assert alternativeCiphertextName.length() <= maxCiphertextNameLength;
 		LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
 		Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
 		Node node = new Node(alternativePath);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
index 9384fce0..2efe7eda 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
@@ -35,6 +35,7 @@ public class CryptoPathMapperTest {
 	private final FileNameCryptor fileNameCryptor = Mockito.mock(FileNameCryptor.class);
 	private final DirectoryIdProvider dirIdProvider = Mockito.mock(DirectoryIdProvider.class);
 	private final LongFileNameProvider longFileNameProvider = Mockito.mock(LongFileNameProvider.class);
+	private final CryptoFileSystemProperties fileSystemProperties = Mockito.mock(CryptoFileSystemProperties.class);
 	private final Symlinks symlinks = Mockito.mock(Symlinks.class);
 	private final CryptoFileSystemImpl fileSystem = Mockito.mock(CryptoFileSystemImpl.class);
 
@@ -45,6 +46,7 @@ public void setup() {
 		CryptoPath empty = cryptoPathFactory.emptyFor(fileSystem);
 		Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
 		Mockito.when(pathToVault.resolve("d")).thenReturn(dataRoot);
+		Mockito.when(fileSystemProperties.maxNameLength()).thenReturn(220);
 		Mockito.when(fileSystem.getPath(ArgumentMatchers.anyString(), ArgumentMatchers.any())).thenAnswer(invocation -> {
 			String first = invocation.getArgument(0);
 			if (invocation.getArguments().length == 1) {
@@ -68,7 +70,7 @@ public void testPathEncryptionForRoot() throws IOException {
 		Path d0000 = Mockito.mock(Path.class);
 		Mockito.when(d00.resolve("00")).thenReturn(d0000);
 
-		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 		Path path = mapper.getCiphertextDir(fileSystem.getRootPath()).path;
 		Assertions.assertEquals(d0000, path);
 	}
@@ -92,7 +94,7 @@ public void testPathEncryptionForFoo() throws IOException {
 		Path d0001 = Mockito.mock(Path.class);
 		Mockito.when(d00.resolve("01")).thenReturn(d0001);
 
-		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 		Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo")).path;
 		Assertions.assertEquals(d0001, path);
 	}
@@ -126,7 +128,7 @@ public void testPathEncryptionForFooBar() throws IOException {
 		Path d0002 = Mockito.mock(Path.class);
 		Mockito.when(d00.resolve("02")).thenReturn(d0002);
 
-		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 		Path path = mapper.getCiphertextDir(fileSystem.getPath("/foo/bar")).path;
 		Assertions.assertEquals(d0002, path);
 	}
@@ -163,7 +165,7 @@ public void testPathEncryptionForFooBarBaz() throws IOException {
 		Mockito.when(d0002.resolve("zab.c9r")).thenReturn(d0002zab);
 		Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("baz"), Mockito.any())).thenReturn("zab");
 
-		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+		CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 		Path path = mapper.getCiphertextFilePath(fileSystem.getPath("/foo/bar/baz")).getRawPath();
 		Assertions.assertEquals(d0002zab, path);
 	}
@@ -211,7 +213,7 @@ public void setup() throws IOException {
 
 		@Test
 		public void testGetCiphertextFileTypeOfRootPath() throws IOException {
-			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 			CiphertextFileType type = mapper.getCiphertextFileType(fileSystem.getRootPath());
 			Assertions.assertEquals(CiphertextFileType.DIRECTORY, type);
 		}
@@ -220,7 +222,7 @@ public void testGetCiphertextFileTypeOfRootPath() throws IOException {
 		public void testGetCiphertextFileTypeForNonexistingFile() throws IOException {
 			Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class);
 
-			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 
 			CryptoPath path = fileSystem.getPath("/CLEAR");
 			Assertions.assertThrows(NoSuchFileException.class, () -> {
@@ -233,7 +235,7 @@ public void testGetCiphertextFileTypeForFile() throws IOException {
 			Mockito.when(underlyingFileSystemProvider.readAttributes(c9rPath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(c9rAttrs);
 			Mockito.when(c9rAttrs.isDirectory()).thenReturn(false);
 
-			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 
 			CryptoPath path = fileSystem.getPath("/CLEAR");
 			CiphertextFileType type = mapper.getCiphertextFileType(path);
@@ -248,7 +250,7 @@ public void testGetCiphertextFileTypeForDirectory() throws IOException {
 			Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class);
 			Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class);
 
-			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 
 			CryptoPath path = fileSystem.getPath("/CLEAR");
 			CiphertextFileType type = mapper.getCiphertextFileType(path);
@@ -263,7 +265,7 @@ public void testGetCiphertextFileTypeForSymlink() throws IOException {
 			Mockito.when(underlyingFileSystemProvider.readAttributes(symlinkFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenReturn(Mockito.mock(BasicFileAttributes.class));
 			Mockito.when(underlyingFileSystemProvider.readAttributes(contentsFilePath, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS)).thenThrow(NoSuchFileException.class);
 
-			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 
 			CryptoPath path = fileSystem.getPath("/CLEAR");
 			CiphertextFileType type = mapper.getCiphertextFileType(path);
@@ -280,7 +282,7 @@ public void testGetCiphertextFileTypeForShortenedFile() throws IOException {
 			Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.eq("LONGCLEAR"), Mockito.any())).thenReturn(Strings.repeat("A", 1000));
 			Mockito.when(longFileNameProvider.deflate(Mockito.any())).thenReturn(new LongFileNameProvider.DeflatedFileName(c9rPath, null, null));
 
-			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider);
+			CryptoPathMapper mapper = new CryptoPathMapper(pathToVault, cryptor, dirIdProvider, longFileNameProvider, fileSystemProperties);
 
 			CryptoPath path = fileSystem.getPath("/LONGCLEAR");
 			CiphertextFileType type = mapper.getCiphertextFileType(path);
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index d8d47c54..1cc8a3cf 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -23,8 +23,6 @@
 import java.nio.file.FileSystem;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
 import java.util.stream.Stream;
 
 import static java.nio.file.StandardOpenOption.CREATE_NEW;
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
index 2e3944ea..753f3a3d 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
@@ -1,5 +1,6 @@
 package org.cryptomator.cryptofs.dir;
 
+import org.cryptomator.cryptofs.CryptoFileSystemProperties;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.FileNameCryptor;
 import org.junit.jupiter.api.Assertions;
@@ -7,6 +8,7 @@
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.io.TempDir;
 import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.Mockito;
 
@@ -20,14 +22,17 @@ class C9rConflictResolverTest {
 
 	private Cryptor cryptor;
 	private FileNameCryptor fileNameCryptor;
+	private CryptoFileSystemProperties fileSystemProperties;
 	private C9rConflictResolver conflictResolver;
 
 	@BeforeEach
 	public void setup() {
 		cryptor = Mockito.mock(Cryptor.class);
 		fileNameCryptor = Mockito.mock(FileNameCryptor.class);
+		fileSystemProperties = Mockito.mock(CryptoFileSystemProperties.class);
 		Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
-		conflictResolver = new C9rConflictResolver(cryptor, "foo");
+		Mockito.when(fileSystemProperties.maxNameLength()).thenReturn(220);
+		conflictResolver = new C9rConflictResolver(cryptor, "foo", fileSystemProperties);
 	}
 	
 	@Test
@@ -126,4 +131,11 @@ public void testResolveConflictingSymlinkTrivially(@TempDir Path dir) throws IOE
 		Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
 	}
 
+	@ParameterizedTest
+	@CsvSource({"220, 146", "219, 143", "218, 143", "217, 143", "216, 143", "215, 140"})
+	public void testCalcMaxCleartextNameLength(int input, int expectedResult) {
+		int result = conflictResolver.calcMaxCleartextNameLength(input);
+		Assertions.assertEquals(expectedResult, result);
+	}
+
 }
\ No newline at end of file

From 484f4169244f0217befeb79b909135d78e7753c3 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 16 Apr 2021 15:38:25 +0200
Subject: [PATCH 45/70] renamed constant to shorteningThreshold (related to
 #95, #102)

---
 .../cryptofs/CryptoFileSystemProvider.java    |  2 +-
 .../cryptofs/CryptoPathMapper.java            |  2 +-
 .../org/cryptomator/cryptofs/VaultConfig.java | 20 +++++++++----------
 .../cryptofs/common/Constants.java            |  1 +
 .../migration/v8/Version8Migrator.java        |  2 +-
 .../cryptomator/cryptofs/VaultConfigTest.java | 10 +++++-----
 .../migration/v8/Version8MigratorTest.java    |  2 +-
 7 files changed, 20 insertions(+), 19 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 151981c2..0a1330f5 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -141,7 +141,7 @@ 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()).maxFilenameLength(properties.maxNameLength()).build();
+		var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(Constants.DEFAULT_SHORTENING_THRESHOLD).build();
 		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId);
 			 Cryptor cryptor = config.getCipherCombo().getCryptorProvider(strongSecureRandom()).withKey(key)) {
 			rawKey = key.getEncoded();
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
index c941b13e..c32cc6cd 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoPathMapper.java
@@ -129,7 +129,7 @@ public CiphertextFilePath getCiphertextFilePath(CryptoPath cleartextPath) throws
 	public CiphertextFilePath getCiphertextFilePath(Path parentCiphertextDir, String parentDirId, String cleartextName) {
 		String ciphertextName = ciphertextNames.getUnchecked(new DirIdAndName(parentDirId, cleartextName));
 		Path c9rPath = parentCiphertextDir.resolve(ciphertextName);
-		if (ciphertextName.length() > vaultConfig.getMaxFilenameLength()) {
+		if (ciphertextName.length() > vaultConfig.getShorteningThreshold()) {
 			LongFileNameProvider.DeflatedFileName deflatedFileName = longFileNameProvider.deflate(c9rPath);
 			return new CiphertextFilePath(deflatedFileName.c9sPath, Optional.of(deflatedFileName));
 		} else {
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
index f6aa87e8..9574f3b3 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -32,25 +32,25 @@ public class VaultConfig {
 
 	private static final String JSON_KEY_VAULTVERSION = "format";
 	private static final String JSON_KEY_CIPHERCONFIG = "cipherCombo";
-	private static final String JSON_KEY_MAXFILENAMELEN = "maxFilenameLen";
+	private static final String JSON_KEY_SHORTENING_THRESHOLD = "shorteningThreshold";
 
 	private final String id;
 	private final int vaultVersion;
 	private final VaultCipherCombo cipherCombo;
-	private final int maxFilenameLength;
+	private final int shorteningThreshold;
 
 	private VaultConfig(DecodedJWT verifiedConfig) {
 		this.id = verifiedConfig.getId();
 		this.vaultVersion = verifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt();
 		this.cipherCombo = VaultCipherCombo.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString());
-		this.maxFilenameLength = verifiedConfig.getClaim(JSON_KEY_MAXFILENAMELEN).asInt();
+		this.shorteningThreshold = verifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt();
 	}
 
 	private VaultConfig(VaultConfigBuilder builder) {
 		this.id = builder.id;
 		this.vaultVersion = builder.vaultVersion;
 		this.cipherCombo = builder.cipherCombo;
-		this.maxFilenameLength = builder.maxFilenameLength;
+		this.shorteningThreshold = builder.shorteningThreshold;
 	}
 
 	public String getId() {
@@ -65,8 +65,8 @@ public VaultCipherCombo getCipherCombo() {
 		return cipherCombo;
 	}
 
-	public int getMaxFilenameLength() {
-		return maxFilenameLength;
+	public int getShorteningThreshold() {
+		return shorteningThreshold;
 	}
 
 	public String toToken(String keyId, byte[] rawKey) {
@@ -75,7 +75,7 @@ public String toToken(String keyId, byte[] rawKey) {
 				.withJWTId(id) //
 				.withClaim(JSON_KEY_VAULTVERSION, vaultVersion) //
 				.withClaim(JSON_KEY_CIPHERCONFIG, cipherCombo.name()) //
-				.withClaim(JSON_KEY_MAXFILENAMELEN, maxFilenameLength) //
+				.withClaim(JSON_KEY_SHORTENING_THRESHOLD, shorteningThreshold) //
 				.sign(Algorithm.HMAC256(rawKey));
 	}
 
@@ -178,15 +178,15 @@ public static class VaultConfigBuilder {
 		private final String id = UUID.randomUUID().toString();
 		private final int vaultVersion = Constants.VAULT_VERSION;
 		private VaultCipherCombo cipherCombo;
-		private int maxFilenameLength;
+		private int shorteningThreshold;
 
 		public VaultConfigBuilder cipherCombo(VaultCipherCombo cipherCombo) {
 			this.cipherCombo = cipherCombo;
 			return this;
 		}
 
-		public VaultConfigBuilder maxFilenameLength(int maxFilenameLength) {
-			this.maxFilenameLength = maxFilenameLength;
+		public VaultConfigBuilder shorteningThreshold(int shorteningThreshold) {
+			this.shorteningThreshold = shorteningThreshold;
 			return this;
 		}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
index eca0fce5..1ad24fe8 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
@@ -24,6 +24,7 @@ private Constants() {
 	public static final String CONTENTS_FILE_NAME = "contents.c9r";
 	public static final String INFLATED_FILE_NAME = "name.c9s";
 
+	public static final int DEFAULT_SHORTENING_THRESHOLD = 220;
 	public static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303
 	public static final int MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r
 	public static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
index 87f0032f..afa56b10 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v8/Version8Migrator.java
@@ -67,7 +67,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 					.withKeyId("masterkeyfile:masterkey.cryptomator") //
 					.withClaim("format", 8) //
 					.withClaim("cipherCombo", "SIV_CTRMAC") //
-					.withClaim("maxFilenameLen", 220) //
+					.withClaim("shorteningThreshold", 220) //
 					.sign(algorithm);
 			Files.writeString(vaultConfigFile, config, StandardCharsets.US_ASCII, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW);
 			LOG.info("Wrote vault config to {}.", vaultConfigFile);
diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
index 8fec1870..546c0aba 100644
--- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
@@ -47,7 +47,7 @@ public class WithValidToken {
 
 		@BeforeEach
 		public void setup() throws MasterkeyLoadingFailedException {
-			originalConfig = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).maxFilenameLength(220).build();
+			originalConfig = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).shorteningThreshold(220).build();
 			token = originalConfig.toToken("TEST_KEY", rawKey);
 		}
 
@@ -58,7 +58,7 @@ public void testSuccessfulLoad() throws VaultConfigLoadException, MasterkeyLoadi
 			Assertions.assertEquals(originalConfig.getId(), loaded.getId());
 			Assertions.assertEquals(originalConfig.getVaultVersion(), loaded.getVaultVersion());
 			Assertions.assertEquals(originalConfig.getCipherCombo(), loaded.getCipherCombo());
-			Assertions.assertEquals(originalConfig.getMaxFilenameLength(), loaded.getMaxFilenameLength());
+			Assertions.assertEquals(originalConfig.getShorteningThreshold(), loaded.getShorteningThreshold());
 		}
 
 		@ParameterizedTest
@@ -76,12 +76,12 @@ public void testLoadWithInvalidKey(int pos) {
 
 	@Test
 	public void testCreateNew() {
-		var config = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).maxFilenameLength(220).build();
+		var config = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).shorteningThreshold(220).build();
 
 		Assertions.assertNotNull(config.getId());
 		Assertions.assertEquals(Constants.VAULT_VERSION, config.getVaultVersion());
 		Assertions.assertEquals(VaultCipherCombo.SIV_CTRMAC, config.getCipherCombo());
-		Assertions.assertEquals(220, config.getMaxFilenameLength());
+		Assertions.assertEquals(220, config.getShorteningThreshold());
 	}
 
 	@Test
@@ -96,7 +96,7 @@ public void testLoadExisting() throws VaultConfigLoadException, MasterkeyLoading
 		Mockito.when(decodedJwt.getKeyId()).thenReturn("test:key");
 		Mockito.when(decodedJwt.getClaim("format")).thenReturn(formatClaim);
 		Mockito.when(decodedJwt.getClaim("cipherCombo")).thenReturn(cipherComboClaim);
-		Mockito.when(decodedJwt.getClaim("maxFilenameLen")).thenReturn(maxFilenameLenClaim);
+		Mockito.when(decodedJwt.getClaim("shorteningThreshold")).thenReturn(maxFilenameLenClaim);
 		Mockito.when(key.getEncoded()).thenReturn(new byte[64]);
 		Mockito.when(verification.withClaim("format", 42)).thenReturn(verification);
 		Mockito.when(verification.build()).thenReturn(verifier);
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
index 716e5bfa..cd15fe73 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v8/Version8MigratorTest.java
@@ -64,7 +64,7 @@ public void testMigrate() throws CryptoException, IOException {
 		Assertions.assertEquals("masterkeyfile:masterkey.cryptomator", token.getKeyId());
 		Assertions.assertEquals(8, token.getClaim("format").asInt());
 		Assertions.assertEquals("SIV_CTRMAC", token.getClaim("cipherCombo").asString());
-		Assertions.assertEquals(220, token.getClaim("maxFilenameLen").asInt());
+		Assertions.assertEquals(220, token.getClaim("shorteningThreshold").asInt());
 	}
 
 }
\ No newline at end of file

From 16695e97abdddd0be42f6b0ece59e984b076b0a3 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 16 Apr 2021 15:55:57 +0200
Subject: [PATCH 46/70] fixes #102

---
 .../cryptofs/CryptoFileSystemImpl.java        | 27 ++++-----
 .../cryptofs/CryptoFileSystemProperties.java  | 55 +++++--------------
 .../cryptofs/FileNameTooLongException.java    |  6 +-
 .../cryptofs/LongFileNameProvider.java        |  3 +-
 .../cryptofs/common/Constants.java            |  4 --
 .../common/FileSystemCapabilityChecker.java   |  7 ++-
 .../cryptofs/dir/C9rConflictResolver.java     |  2 +-
 .../migration/v7/Version7Migrator.java        |  4 +-
 .../cryptofs/CryptoFileSystemImplTest.java    | 36 ++++++------
 .../CryptoFileSystemPropertiesTest.java       | 22 +++-----
 ...yptoFileSystemProviderIntegrationTest.java | 16 +++---
 .../cryptofs/CryptoPathMapperTest.java        |  2 +-
 ...ptyCiphertextDirectoryIntegrationTest.java |  4 +-
 .../cryptofs/dir/C9rConflictResolverTest.java |  3 +-
 14 files changed, 71 insertions(+), 120 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index c941c747..46e1ad86 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -281,6 +281,7 @@ boolean isHidden(CryptoPath cleartextPath) throws IOException {
 
 	void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws IOException {
 		readonlyFlag.assertWritable();
+		assertCleartextNameLengthAllowed(cleartextDir);
 		CryptoPath cleartextParentDir = cleartextDir.getParent();
 		if (cleartextParentDir == null) {
 			return;
@@ -292,7 +293,6 @@ void createDirectory(CryptoPath cleartextDir, FileAttribute<?>... attrs) throws
 		cryptoPathMapper.assertNonExisting(cleartextDir);
 		CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextDir);
 		Path ciphertextDirFile = ciphertextPath.getDirFilePath();
-		assertCiphertextPathLengthMeetsLimitations(ciphertextDirFile);
 		CiphertextDirectory ciphertextDir = cryptoPathMapper.getCiphertextDir(cleartextDir);
 		// atomically check for FileAlreadyExists and create otherwise:
 		Files.createDirectory(ciphertextPath.getRawPath());
@@ -348,9 +348,9 @@ private FileChannel newFileChannelFromSymlink(CryptoPath cleartextPath, Effectiv
 	}
 
 	private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute<?>... attrs) throws IOException {
+		assertCleartextNameLengthAllowed(cleartextFilePath);
 		CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath);
 		Path ciphertextFilePath = ciphertextPath.getFilePath();
-		assertCiphertextPathLengthMeetsLimitations(ciphertextFilePath);
 		if (options.createNew() && openCryptoFiles.get(ciphertextFilePath).isPresent()) {
 			throw new FileAlreadyExistsException(cleartextFilePath.toString());
 		} else {
@@ -400,6 +400,7 @@ private void deleteDirectory(CryptoPath cleartextPath, CiphertextFilePath cipher
 
 	void copy(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... options) throws IOException {
 		readonlyFlag.assertWritable();
+		assertCleartextNameLengthAllowed(cleartextTarget);
 		if (cleartextSource.equals(cleartextTarget)) {
 			return;
 		}
@@ -418,7 +419,6 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
 		if (ArrayUtils.contains(options, LinkOption.NOFOLLOW_LINKS)) {
 			CiphertextFilePath ciphertextSourceFile = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
 			CiphertextFilePath ciphertextTargetFile = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
-			assertCiphertextPathLengthMeetsLimitations(ciphertextTargetFile.getSymlinkFilePath());
 			CopyOption[] resolvedOptions = ArrayUtils.without(options, LinkOption.NOFOLLOW_LINKS).toArray(CopyOption[]::new);
 			Files.createDirectories(ciphertextTargetFile.getRawPath());
 			Files.copy(ciphertextSourceFile.getSymlinkFilePath(), ciphertextTargetFile.getSymlinkFilePath(), resolvedOptions);
@@ -434,7 +434,6 @@ private void copySymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
 	private void copyFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption[] options) throws IOException {
 		CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
 		CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
-		assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getFilePath());
 		if (ciphertextTarget.isShortened()) {
 			Files.createDirectories(ciphertextTarget.getRawPath());
 		}
@@ -498,6 +497,7 @@ private void copyAttributes(Path src, Path dst) throws IOException {
 
 	void move(CryptoPath cleartextSource, CryptoPath cleartextTarget, CopyOption... options) throws IOException {
 		readonlyFlag.assertWritable();
+		assertCleartextNameLengthAllowed(cleartextTarget);
 		if (cleartextSource.equals(cleartextTarget)) {
 			return;
 		}
@@ -517,7 +517,6 @@ private void moveSymlink(CryptoPath cleartextSource, CryptoPath cleartextTarget,
 		// "the symbolic link itself, not the target of the link, is moved"
 		CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
 		CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
-		assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getSymlinkFilePath());
 		try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
 			Files.move(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath(), options);
 			if (ciphertextTarget.isShortened()) {
@@ -534,7 +533,6 @@ private void moveFile(CryptoPath cleartextSource, CryptoPath cleartextTarget, Co
 		// we need to re-map the OpenCryptoFile entry.
 		CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
 		CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
-		assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getFilePath());
 		try (OpenCryptoFiles.TwoPhaseMove twoPhaseMove = openCryptoFiles.prepareMove(ciphertextSource.getRawPath(), ciphertextTarget.getRawPath())) {
 			if (ciphertextTarget.isShortened()) {
 				Files.createDirectory(ciphertextTarget.getRawPath());
@@ -553,7 +551,6 @@ private void moveDirectory(CryptoPath cleartextSource, CryptoPath cleartextTarge
 		// Hence there is no need to re-map OpenCryptoFile entries.
 		CiphertextFilePath ciphertextSource = cryptoPathMapper.getCiphertextFilePath(cleartextSource);
 		CiphertextFilePath ciphertextTarget = cryptoPathMapper.getCiphertextFilePath(cleartextTarget);
-		assertCiphertextPathLengthMeetsLimitations(ciphertextTarget.getDirFilePath());
 		if (ArrayUtils.contains(options, StandardCopyOption.REPLACE_EXISTING)) {
 			// check if not attempting to move atomically:
 			if (ArrayUtils.contains(options, StandardCopyOption.ATOMIC_MOVE)) {
@@ -593,8 +590,8 @@ CryptoFileStore getFileStore() {
 
 	void createSymbolicLink(CryptoPath cleartextPath, Path target, FileAttribute<?>... attrs) throws IOException {
 		assertOpen();
-		CiphertextFilePath ciphertextFilePath = cryptoPathMapper.getCiphertextFilePath(cleartextPath);
-		assertCiphertextPathLengthMeetsLimitations(ciphertextFilePath.getSymlinkFilePath());
+		readonlyFlag.assertWritable();
+		assertCleartextNameLengthAllowed(cleartextPath);
 		symlinks.createSymbolicLink(cleartextPath, target, attrs);
 	}
 
@@ -612,13 +609,11 @@ CryptoPath getRootPath() {
 	CryptoPath getEmptyPath() {
 		return emptyPath;
 	}
-	
-	void assertCiphertextPathLengthMeetsLimitations(Path cdrFilePath) throws FileNameTooLongException {
-		Path vaultRelativePath = pathToVault.relativize(cdrFilePath);
-		String fileName = vaultRelativePath.getName(3).toString(); // fourth path element (d/xx/yyyyy/file.c9r/symlink.c9r)
-		String path = vaultRelativePath.toString();
-		if (fileName.length() > fileSystemProperties.maxNameLength() || path.length() > fileSystemProperties.maxPathLength()) {
-			throw new FileNameTooLongException(path, fileSystemProperties.maxPathLength(), fileSystemProperties.maxNameLength());
+
+	void assertCleartextNameLengthAllowed(CryptoPath cleartextPath) throws FileNameTooLongException {
+		String filename = cleartextPath.getFileName().toString();
+		if (filename.length() > fileSystemProperties.maxCleartextNameLength()) {
+			throw new FileNameTooLongException(cleartextPath.toString(), fileSystemProperties.maxCleartextNameLength());
 		}
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index fe8d1836..1f64b181 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -38,22 +38,13 @@
 public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 
 	/**
-	 * Maximum ciphertext path length.
+	 * Maximum cleartext filename length.
 	 *
-	 * @since 1.9.8
-	 */
-	public static final String PROPERTY_MAX_PATH_LENGTH = "maxPathLength";
-
-	static final int DEFAULT_MAX_PATH_LENGTH = Constants.MAX_CIPHERTEXT_PATH_LENGTH;
-
-	/**
-	 * Maximum filename length of .c9r files.
-	 *
-	 * @since 1.9.9
+	 * @since 2.0.0
 	 */
-	public static final String PROPERTY_MAX_NAME_LENGTH = "maxNameLength";
+	public static final String PROPERTY_MAX_CLEARTEXT_NAME_LENGTH = "maxCleartextNameLength";
 
-	static final int DEFAULT_MAX_NAME_LENGTH = Constants.MAX_CIPHERTEXT_NAME_LENGTH;
+	static final int DEFAULT_MAX_CLEARTEXT_NAME_LENGTH = LongFileNameProvider.MAX_FILENAME_BUFFER_SIZE;
 
 	/**
 	 * Key identifying the key loader used during initialization.
@@ -115,8 +106,7 @@ private CryptoFileSystemProperties(Builder builder) {
 				Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
 				Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
 				Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
-				Map.entry(PROPERTY_MAX_PATH_LENGTH, builder.maxPathLength), //
-				Map.entry(PROPERTY_MAX_NAME_LENGTH, builder.maxNameLength), //
+				Map.entry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, builder.maxCleartextNameLength), //
 				Map.entry(PROPERTY_CIPHER_COMBO, builder.cipherCombo) //
 		);
 	}
@@ -162,12 +152,8 @@ String masterkeyFilename() {
 		return (String) get(PROPERTY_MASTERKEY_FILENAME);
 	}
 
-	public int maxPathLength() {
-		return (int) get(PROPERTY_MAX_PATH_LENGTH);
-	}
-	
-	public int maxNameLength() {
-		return (int) get(PROPERTY_MAX_NAME_LENGTH);
+	public int maxCleartextNameLength() {
+		return (int) get(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH);
 	}
 
 	@Override
@@ -223,8 +209,7 @@ public static class Builder {
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
 		private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
-		private int maxPathLength = DEFAULT_MAX_PATH_LENGTH;
-		private int maxNameLength = DEFAULT_MAX_NAME_LENGTH;
+		private int maxCleartextNameLength = DEFAULT_MAX_CLEARTEXT_NAME_LENGTH;
 
 		private Builder() {
 		}
@@ -234,8 +219,7 @@ private Builder(Map<String, ?> properties) {
 			checkedSet(String.class, PROPERTY_VAULTCONFIG_FILENAME, properties, this::withVaultConfigFilename);
 			checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
-			checkedSet(Integer.class, PROPERTY_MAX_PATH_LENGTH, properties, this::withMaxPathLength);
-			checkedSet(Integer.class, PROPERTY_MAX_NAME_LENGTH, properties, this::withMaxNameLength);
+			checkedSet(Integer.class, PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, properties, this::withMaxCleartextNameLength);
 			checkedSet(VaultCipherCombo.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo);
 		}
 
@@ -250,28 +234,15 @@ private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties
 			}
 		}
 
-
-		/**
-		 * Sets the maximum ciphertext path length for a CryptoFileSystem.
-		 *
-		 * @param maxPathLength The maximum ciphertext path length allowed
-		 * @return this
-		 * @since 1.9.8
-		 */
-		public Builder withMaxPathLength(int maxPathLength) {
-			this.maxPathLength = maxPathLength;
-			return this;
-		}
-
 		/**
 		 * Sets the maximum ciphertext filename length for a CryptoFileSystem.
 		 *
-		 * @param maxNameLength The maximum ciphertext filename length allowed
+		 * @param maxCleartextNameLength The maximum ciphertext filename length allowed
 		 * @return this
-		 * @since 1.9.9
+		 * @since 2.0.0
 		 */
-		public Builder withMaxNameLength(int maxNameLength) {
-			this.maxNameLength = maxNameLength;
+		public Builder withMaxCleartextNameLength(int maxCleartextNameLength) {
+			this.maxCleartextNameLength = maxCleartextNameLength;
 			return this;
 		}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
index 127798db..fcd582b1 100644
--- a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
+++ b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
@@ -7,12 +7,12 @@
  * Indicates that an operation failed, as it would result in a ciphertext path that is too long for the underlying file system.
  *
  * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedFileNameLength(Path) 
- * @since 1.9.8
+ * @since 2.0.0
  */
 public class FileNameTooLongException extends FileSystemException {
 	
-	public FileNameTooLongException(String c9rPathRelativeToVaultRoot, int maxPathLength, int maxNameLength) {
-		super(c9rPathRelativeToVaultRoot, null, "File name or path too long. Max ciphertext path name length is " + maxPathLength + ". Max ciphertext name is " + maxNameLength);
+	public FileNameTooLongException(String path, int maxNameLength) {
+		super(path, null, "File name or path too long. Max cleartext filename name length is " + maxNameLength);
 	}
 	
 }
diff --git a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java
index 0ebab05e..e427a2b0 100644
--- a/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/LongFileNameProvider.java
@@ -36,7 +36,8 @@
 @CryptoFileSystemScoped
 public class LongFileNameProvider {
 
-	private static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name.
+	public static final int MAX_FILENAME_BUFFER_SIZE = 10 * 1024; // no sane person gives a file a 10kb long name.
+
 	private static final BaseEncoding BASE64 = BaseEncoding.base64Url();
 	private static final Duration MAX_CACHE_AGE = Duration.ofMinutes(1);
 
diff --git a/src/main/java/org/cryptomator/cryptofs/common/Constants.java b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
index 1ad24fe8..c9085c44 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/Constants.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/Constants.java
@@ -25,10 +25,6 @@ private Constants() {
 	public static final String INFLATED_FILE_NAME = "name.c9s";
 
 	public static final int DEFAULT_SHORTENING_THRESHOLD = 220;
-	public static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303
-	public static final int MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r
-	public static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77
-	public static final int MAX_CIPHERTEXT_PATH_LENGTH = MAX_CIPHERTEXT_NAME_LENGTH + MAX_ADDITIONAL_PATH_LENGTH;
 	public static final int MAX_SYMLINK_LENGTH = 32767; // max path length on NTFS and FAT32: 32k-1
 	public static final int MAX_DIR_FILE_LENGTH = 36; // UUIDv4: hex-encoded 16 byte int + 4 hyphens = 36 ASCII chars
 
diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
index b122e4bc..f3ea0f78 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
@@ -21,6 +21,9 @@
 public class FileSystemCapabilityChecker {
 
 	private static final Logger LOG = LoggerFactory.getLogger(FileSystemCapabilityChecker.class);
+	private static final int MAX_CIPHERTEXT_NAME_LENGTH = 220; // inclusive. calculations done in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303
+	private static final int MIN_CIPHERTEXT_NAME_LENGTH = 28; // base64(iv).c9r
+	private static final int MAX_ADDITIONAL_PATH_LENGTH = 48; // beginning at d/... see https://github.com/cryptomator/cryptofs/issues/77
 
 	public enum Capability {
 		/**
@@ -98,8 +101,8 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio
 	 * @throws IOException If unable to perform this check
 	 */
 	public int determineSupportedFileNameLength(Path pathToVault) throws IOException {
-		int subPathLength = Constants.MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/"
-		return determineSupportedFileNameLength(pathToVault.resolve("c"), subPathLength, Constants.MIN_CIPHERTEXT_NAME_LENGTH, Constants.MAX_CIPHERTEXT_NAME_LENGTH);
+		int subPathLength = MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/"
+		return determineSupportedFileNameLength(pathToVault.resolve("c"), subPathLength, MIN_CIPHERTEXT_NAME_LENGTH, MAX_CIPHERTEXT_NAME_LENGTH);
 	}
 
 	/**
diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
index 7a5b1a1b..0dd1a748 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
@@ -99,7 +99,7 @@ int calcMaxCleartextNameLength(int maxCiphertextNameLength) {
 	private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException {
 		assert Files.exists(canonicalPath);
 		final int beginOfFileExtension = cleartext.lastIndexOf('.');
-		final int maxCiphertextNameLength = vaultConfig.getMaxFilenameLength();
+		final int maxCiphertextNameLength = vaultConfig.getShorteningThreshold();
 		final int maxCleartextNameLength = calcMaxCleartextNameLength(maxCiphertextNameLength);
 		final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : "";
 		final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext;
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index b52efb5e..c883f909 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -94,13 +94,13 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			// fail if ciphertext paths are too long:
 			if (preMigrationVisitor.getMaxCiphertextPathLength() > pathLengthLimit) {
 				LOG.error("Migration aborted due to unsupported path length (required {}) of underlying file system (supports {}). Vault is unchanged.", preMigrationVisitor.getMaxCiphertextPathLength(), pathLengthLimit);
-				throw new FileNameTooLongException(preMigrationVisitor.getLongestPath().toString(), pathLengthLimit, filenameLengthLimit);
+				throw new FileNameTooLongException(preMigrationVisitor.getLongestPath().toString(), filenameLengthLimit);
 			}
 
 			// fail if ciphertext names are too long:
 			if (preMigrationVisitor.getMaxCiphertextNameLength() > filenameLengthLimit) {
 				LOG.error("Migration aborted due to unsupported filename length (required {}) of underlying file system (supports {}). Vault is unchanged.", preMigrationVisitor.getMaxCiphertextNameLength(), filenameLengthLimit);
-				throw new FileNameTooLongException(preMigrationVisitor.getPathWithLongestName().toString(), pathLengthLimit, filenameLengthLimit);
+				throw new FileNameTooLongException(preMigrationVisitor.getPathWithLongestName().toString(), filenameLengthLimit);
 			}
 
 			// start migration:
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index 1d878ebb..e5740192 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -74,7 +74,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 import static org.mockito.internal.verification.VerificationModeFactory.atLeast;
 
@@ -115,8 +114,7 @@ public void setup() {
 			return other;
 		});
 		
-		when(fileSystemProperties.maxPathLength()).thenReturn(Constants.MAX_CIPHERTEXT_PATH_LENGTH);
-		when(fileSystemProperties.maxNameLength()).thenReturn(Constants.MAX_CIPHERTEXT_NAME_LENGTH);
+		when(fileSystemProperties.maxCleartextNameLength()).thenReturn(32768);
 
 		inTest = new CryptoFileSystemImpl(provider, cryptoFileSystems, pathToVault, cryptor,
 				fileStore, stats, cryptoPathMapper, cryptoPathFactory,
@@ -360,6 +358,7 @@ public class NewFileChannel {
 
 		@BeforeEach
 		public void setup() throws IOException {
+			when(cleartextPath.getFileName()).thenReturn(cleartextPath);
 			when(cryptoPathMapper.getCiphertextFileType(cleartextPath)).thenReturn(CiphertextFileType.FILE);
 			when(cryptoPathMapper.getCiphertextFilePath(cleartextPath)).thenReturn(ciphertextPath);
 			when(ciphertextPath.getFilePath()).thenReturn(ciphertextFilePath);
@@ -509,6 +508,7 @@ public class CopyAndMove {
 
 		@BeforeEach
 		public void setup() throws IOException {
+			when(cleartextDestination.getFileName()).thenReturn(cleartextDestination);
 			when(ciphertextSource.getRawPath()).thenReturn(ciphertextSourceFile);
 			when(ciphertextSource.getFilePath()).thenReturn(ciphertextSourceFile);
 			when(ciphertextSource.getSymlinkFilePath()).thenReturn(ciphertextSourceFile);
@@ -545,10 +545,12 @@ public class Move {
 
 			@Test
 			public void moveFileToItselfDoesNothing() throws IOException {
+				when(cleartextSource.getFileName()).thenReturn(cleartextSource);
+
 				inTest.move(cleartextSource, cleartextSource);
 
 				verify(readonlyFlag).assertWritable();
-				verifyNoInteractions(cleartextSource);
+				verifyNoInteractions(cryptoPathMapper);
 			}
 
 			@Test
@@ -701,10 +703,12 @@ public void setup() throws IOException, ReflectiveOperationException {
 
 			@Test
 			public void copyFileToItselfDoesNothing() throws IOException {
+				when(cleartextSource.getFileName()).thenReturn(cleartextSource);
+
 				inTest.copy(cleartextSource, cleartextSource);
 
 				verify(readonlyFlag).assertWritable();
-				verifyNoInteractions(cleartextSource);
+				verifyNoInteractions(cryptoPathMapper);
 			}
 
 			@Test
@@ -744,6 +748,7 @@ public void copySymlink() throws IOException {
 			public void copySymlinkTarget() throws IOException {
 				when(cryptoPathMapper.getCiphertextFileType(cleartextSource)).thenReturn(CiphertextFileType.SYMLINK);
 				when(cryptoPathMapper.getCiphertextFileType(cleartextDestination)).thenThrow(NoSuchFileException.class);
+				when(destinationLinkTarget.getFileName()).thenReturn(destinationLinkTarget);
 
 				CopyOption option1 = mock(CopyOption.class);
 				CopyOption option2 = mock(CopyOption.class);
@@ -932,30 +937,30 @@ public class CreateDirectory {
 
 		private final CryptoFileSystemProvider provider = mock(CryptoFileSystemProvider.class);
 		private final CryptoFileSystemImpl fileSystem = mock(CryptoFileSystemImpl.class);
+		private final CryptoPath path = mock(CryptoPath.class, "path");
+		private final CryptoPath parent = mock(CryptoPath.class, "parent");
 
 		@BeforeEach
 		public void setup() {
 			when(fileSystem.provider()).thenReturn(provider);
+			when(path.getFileName()).thenReturn(path);
+			when(path.getParent()).thenReturn(parent);
 		}
 
 		@Test
 		public void createDirectoryIfPathHasNoParentDoesNothing() throws IOException {
-			CryptoPath path = mock(CryptoPath.class);
 			when(path.getParent()).thenReturn(null);
 
 			inTest.createDirectory(path);
 
 			verify(readonlyFlag).assertWritable();
 			verify(path).getParent();
-			verifyNoMoreInteractions(path);
+			verifyNoMoreInteractions(cryptoPathMapper);
 		}
 
 		@Test
 		public void createDirectoryIfPathsParentDoesNotExistsThrowsNoSuchFileException() throws IOException {
-			CryptoPath path = mock(CryptoPath.class);
-			CryptoPath parent = mock(CryptoPath.class);
 			Path ciphertextParent = mock(Path.class);
-			when(path.getParent()).thenReturn(parent);
 			when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("foo", ciphertextParent));
 			when(ciphertextParent.getFileSystem()).thenReturn(fileSystem);
 			doThrow(NoSuchFileException.class).when(provider).checkAccess(ciphertextParent);
@@ -967,11 +972,8 @@ public void createDirectoryIfPathsParentDoesNotExistsThrowsNoSuchFileException()
 		}
 
 		@Test
-		public void createDirectoryIfPathCyphertextFileDoesExistThrowsFileAlreadyException() throws IOException {
-			CryptoPath path = mock(CryptoPath.class);
-			CryptoPath parent = mock(CryptoPath.class);
+		public void createDirectoryIfPathCiphertextFileDoesExistThrowsFileAlreadyException() throws IOException {
 			Path ciphertextParent = mock(Path.class);
-			when(path.getParent()).thenReturn(parent);
 			when(cryptoPathMapper.getCiphertextDir(parent)).thenReturn(new CiphertextDirectory("foo", ciphertextParent));
 			when(ciphertextParent.getFileSystem()).thenReturn(fileSystem);
 			doThrow(new FileAlreadyExistsException(path.toString())).when(cryptoPathMapper).assertNonExisting(path);
@@ -984,8 +986,6 @@ public void createDirectoryIfPathCyphertextFileDoesExistThrowsFileAlreadyExcepti
 
 		@Test
 		public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOException {
-			CryptoPath path = mock(CryptoPath.class, "path");
-			CryptoPath parent = mock(CryptoPath.class, "parent");
 			Path ciphertextParent = mock(Path.class, "ciphertextParent");
 			Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
 			Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
@@ -993,7 +993,6 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio
 			CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext");
 			String dirId = "DirId1234ABC";
 			FileChannelMock channel = new FileChannelMock(100);
-			when(path.getParent()).thenReturn(parent);
 			when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
 			when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
 			when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath));
@@ -1016,8 +1015,6 @@ public void createDirectoryCreatesDirectoryIfConditonsAreMet() throws IOExceptio
 
 		@Test
 		public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() throws IOException {
-			CryptoPath path = mock(CryptoPath.class, "path");
-			CryptoPath parent = mock(CryptoPath.class, "parent");
 			Path ciphertextParent = mock(Path.class, "ciphertextParent");
 			Path ciphertextRawPath = mock(Path.class, "d/00/00/path.c9r");
 			Path ciphertextDirFile = mock(Path.class, "d/00/00/path.c9r/dir.c9r");
@@ -1025,7 +1022,6 @@ public void createDirectoryClearsDirIdAndDeletesDirFileIfCreatingDirFails() thro
 			CiphertextFilePath ciphertextPath = mock(CiphertextFilePath.class, "ciphertext");
 			String dirId = "DirId1234ABC";
 			FileChannelMock channel = new FileChannelMock(100);
-			when(path.getParent()).thenReturn(parent);
 			when(ciphertextRawPath.resolve("dir.c9r")).thenReturn(ciphertextDirFile);
 			when(cryptoPathMapper.getCiphertextFilePath(path)).thenReturn(ciphertextPath);
 			when(cryptoPathMapper.getCiphertextDir(path)).thenReturn(new CiphertextDirectory(dirId, ciphertextDirPath));
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 676174e8..78fc7cac 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -1,6 +1,5 @@
 package org.cryptomator.cryptofs;
 
-import org.cryptomator.cryptofs.CryptoFileSystemProperties.*;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -50,8 +49,7 @@ public void testSetMasterkeyFilenameAndReadonlyFlag() {
 						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
 						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
@@ -62,22 +60,19 @@ public void testFromMap() {
 		String masterkeyFilename = "aMasterkeyFilename";
 		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
-		map.put(PROPERTY_MAX_PATH_LENGTH, 1000);
-		map.put(PROPERTY_MAX_NAME_LENGTH, 255);
+		map.put(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
 		CryptoFileSystemProperties inTest = cryptoFileSystemPropertiesFrom(map).build();
 
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(masterkeyFilename));
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
-		MatcherAssert.assertThat(inTest.maxPathLength(), is(1000));
-		MatcherAssert.assertThat(inTest.maxNameLength(), is(255));
+		MatcherAssert.assertThat(inTest.maxCleartextNameLength(), is(255));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
 						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, 1000), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, 255), //
+						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255), //
 						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
@@ -98,8 +93,7 @@ public void testWrapMapWithTrueReadonly() {
 						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
 						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY))));
 	}
@@ -120,8 +114,7 @@ public void testWrapMapWithFalseReadonly() {
 						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
 						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class))));
 	}
@@ -172,8 +165,7 @@ public void testWrapMapWithoutReadonly() {
 						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
-						anEntry(PROPERTY_MAX_PATH_LENGTH, DEFAULT_MAX_PATH_LENGTH), //
-						anEntry(PROPERTY_MAX_NAME_LENGTH, DEFAULT_MAX_NAME_LENGTH), //
+						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
 						anEntry(PROPERTY_CIPHER_COMBO, DEFAULT_CIPHER_COMBO), //
 						anEntry(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class))
 				)
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 97ef9c39..65b1b845 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -90,7 +90,7 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 					.withFlags() //
 					.withMasterkeyFilename("masterkey.cryptomator") //
 					.withKeyLoaders(keyLoader) //
-					.withMaxPathLength(100)
+					.withMaxCleartextNameLength(50)
 					.build();
 			CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
@@ -116,7 +116,7 @@ public void tearDownEach() throws IOException {
 		@DisplayName("expect create file to fail with FileNameTooLongException")
 		@Test
 		public void testCreateFileExceedingPathLengthLimit() {
-			Path p = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100");
+			Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
 			Assertions.assertThrows(FileNameTooLongException.class, () -> {
 				Files.createFile(p);
 			});
@@ -125,7 +125,7 @@ public void testCreateFileExceedingPathLengthLimit() {
 		@DisplayName("expect create directory to fail with FileNameTooLongException")
 		@Test
 		public void testCreateDirExceedingPathLengthLimit() {
-			Path p = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100");
+			Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
 			Assertions.assertThrows(FileNameTooLongException.class, () -> {
 				Files.createDirectory(p);
 			});
@@ -134,18 +134,18 @@ public void testCreateDirExceedingPathLengthLimit() {
 		@DisplayName("expect create symlink to fail with FileNameTooLongException")
 		@Test
 		public void testCreateSymlinkExceedingPathLengthLimit() {
-			Path p = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100");
+			Path p = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
 			Assertions.assertThrows(FileNameTooLongException.class, () -> {
 				Files.createSymbolicLink(p, shortFilePath);
 			});
 		}
 
 		@DisplayName("expect move to fail with FileNameTooLongException")
-		@ParameterizedTest(name = "move {0} -> this-should-result-in-ciphertext-path-longer-than-100")
+		@ParameterizedTest(name = "move {0} -> this-cleartext-filename-is-longer-than-50-characters")
 		@ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"})
 		public void testMoveExceedingPathLengthLimit(String path) {
 			Path src = fs.getPath(path);
-			Path dst = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100");
+			Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
 			Assertions.assertThrows(FileNameTooLongException.class, () -> {
 				Files.move(src, dst);
 			});
@@ -154,11 +154,11 @@ public void testMoveExceedingPathLengthLimit(String path) {
 		}
 
 		@DisplayName("expect copy to fail with FileNameTooLongException")
-		@ParameterizedTest(name = "copy {0} -> this-should-result-in-ciphertext-path-longer-than-100")
+		@ParameterizedTest(name = "copy {0} -> this-cleartext-filename-is-longer-than-50-characters")
 		@ValueSource(strings = {"/short-enough.txt", "/short-enough-dir", "/symlink.txt"})
 		public void testCopyExceedingPathLengthLimit(String path) {
 			Path src = fs.getPath(path);
-			Path dst = fs.getPath("/this-should-result-in-ciphertext-path-longer-than-100");
+			Path dst = fs.getPath("/this-cleartext-filename-is-longer-than-50-characters");
 			Assertions.assertThrows(FileNameTooLongException.class, () -> {
 				Files.copy(src, dst, LinkOption.NOFOLLOW_LINKS);
 			});
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
index e0512869..76ac28f1 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoPathMapperTest.java
@@ -46,7 +46,7 @@ public void setup() {
 		CryptoPath empty = cryptoPathFactory.emptyFor(fileSystem);
 		Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
 		Mockito.when(pathToVault.resolve("d")).thenReturn(dataRoot);
-		Mockito.when(vaultConfig.getMaxFilenameLength()).thenReturn(220);
+		Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(220);
 		Mockito.when(fileSystem.getPath(ArgumentMatchers.anyString(), ArgumentMatchers.any())).thenAnswer(invocation -> {
 			String first = invocation.getArgument(0);
 			if (invocation.getArguments().length == 1) {
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index 021d3d86..fe566bac 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -32,7 +32,6 @@
 
 import static java.nio.file.StandardOpenOption.CREATE_NEW;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
-import static org.cryptomator.cryptofs.common.Constants.MAX_CIPHERTEXT_NAME_LENGTH;
 
 /**
  * Regression tests https://github.com/cryptomator/cryptofs/issues/17.
@@ -125,14 +124,13 @@ public void testDeleteNonEmptyDir() throws IOException {
 	}
 
 	@Test
-	@Disabled // c9s not yet implemented
 	public void testDeleteDirectoryContainingLongNamedDirectory() throws IOException {
 		Path cleartextDirectory = fileSystem.getPath("/e");
 		Files.createDirectory(cleartextDirectory);
 
 		// a
 		// .. LongNameaaa...
-		String name = "LongName" + Strings.repeat("a", MAX_CIPHERTEXT_NAME_LENGTH);
+		String name = "LongName" + Strings.repeat("a", Constants.DEFAULT_SHORTENING_THRESHOLD);
 		createFolder(cleartextDirectory, name);
 
 		Assertions.assertThrows(DirectoryNotEmptyException.class, () -> {
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
index 678e3f16..096bd538 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
@@ -1,6 +1,5 @@
 package org.cryptomator.cryptofs.dir;
 
-import org.cryptomator.cryptofs.CryptoFileSystemProperties;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.FileNameCryptor;
@@ -32,7 +31,7 @@ public void setup() {
 		fileNameCryptor = Mockito.mock(FileNameCryptor.class);
 		vaultConfig = Mockito.mock(VaultConfig.class);
 		Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
-		Mockito.when(vaultConfig.getMaxFilenameLength()).thenReturn(220);
+		Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(220);
 		conflictResolver = new C9rConflictResolver(cryptor, "foo", vaultConfig);
 	}
 	

From bbc3c86662fedfa347fa0ddcd2edf5445321d469 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 16 Apr 2021 16:30:13 +0200
Subject: [PATCH 47/70] Updated FileSystemCapabilityChecker API

(related to #102)
---
 .../cryptofs/FileNameTooLongException.java    |  2 +-
 .../common/FileSystemCapabilityChecker.java   | 22 +++++++++++++------
 .../migration/v7/Version7Migrator.java        |  2 +-
 .../FileSystemCapabilityCheckerTest.java      | 22 +++++++++++++++----
 4 files changed, 35 insertions(+), 13 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
index fcd582b1..cdcdc598 100644
--- a/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
+++ b/src/main/java/org/cryptomator/cryptofs/FileNameTooLongException.java
@@ -6,7 +6,7 @@
 /**
  * Indicates that an operation failed, as it would result in a ciphertext path that is too long for the underlying file system.
  *
- * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedFileNameLength(Path) 
+ * @see org.cryptomator.cryptofs.common.FileSystemCapabilityChecker#determineSupportedCleartextFileNameLength(Path)
  * @since 2.0.0
  */
 public class FileNameTooLongException extends FileSystemException {
diff --git a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
index f3ea0f78..f6162bfd 100644
--- a/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
+++ b/src/main/java/org/cryptomator/cryptofs/common/FileSystemCapabilityChecker.java
@@ -93,6 +93,14 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio
 		}
 	}
 
+	public int determineSupportedCleartextFileNameLength(Path pathToVault) throws IOException {
+		int maxCiphertextLen = determineSupportedCiphertextFileNameLength(pathToVault);
+		assert maxCiphertextLen >= MIN_CIPHERTEXT_NAME_LENGTH;
+		// math explained in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303;
+		// subtract 4 for file extension, base64-decode, subtract 16 for IV
+		return (maxCiphertextLen - 4) / 4 * 3 - 16;
+	}
+
 	/**
 	 * Determinse the number of chars a ciphertext filename (including its extension) is allowed to have inside a vault's <code>d/XX/YYYYYYYYYYYYYYYYYYYYYYYYYYYYYY/</code> directory.
 	 *
@@ -100,9 +108,9 @@ public void assertWriteAccess(Path pathToVault) throws MissingCapabilityExceptio
 	 * @return Number of chars a .c9r file is allowed to have
 	 * @throws IOException If unable to perform this check
 	 */
-	public int determineSupportedFileNameLength(Path pathToVault) throws IOException {
+	public int determineSupportedCiphertextFileNameLength(Path pathToVault) throws IOException {
 		int subPathLength = MAX_ADDITIONAL_PATH_LENGTH - 2; // subtract "c/"
-		return determineSupportedFileNameLength(pathToVault.resolve("c"), subPathLength, MIN_CIPHERTEXT_NAME_LENGTH, MAX_CIPHERTEXT_NAME_LENGTH);
+		return determineSupportedCiphertextFileNameLength(pathToVault.resolve("c"), subPathLength, MIN_CIPHERTEXT_NAME_LENGTH, MAX_CIPHERTEXT_NAME_LENGTH);
 	}
 
 	/**
@@ -115,7 +123,7 @@ public int determineSupportedFileNameLength(Path pathToVault) throws IOException
 	 * @return The supported filename length inside a subdirectory of <code>dir</code> with <code>subPathLength</code> chars
 	 * @throws IOException If unable to perform this check
 	 */
-	public int determineSupportedFileNameLength(Path dir, int subPathLength, int minFileNameLength, int maxFileNameLength) throws IOException {
+	public int determineSupportedCiphertextFileNameLength(Path dir, int subPathLength, int minFileNameLength, int maxFileNameLength) throws IOException {
 		Preconditions.checkArgument(subPathLength >= 6, "subPathLength must be larger than charcount(a/nnn/)");
 		Preconditions.checkArgument(minFileNameLength > 0);
 		Preconditions.checkArgument(maxFileNameLength <= 999);
@@ -130,13 +138,13 @@ public int determineSupportedFileNameLength(Path dir, int subPathLength, int min
 				throw new IOException("Unable to read dir");
 			}
 			// perform actual check:
-			return determineSupportedFileNameLength(fillerDir, minFileNameLength, maxFileNameLength + 1);
+			return determineSupportedCiphertextFileNameLength(fillerDir, minFileNameLength, maxFileNameLength + 1);
 		} finally {
 			deleteRecursivelySilently(fillerDir);
 		}
 	}
 
-	private int determineSupportedFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) {
+	private int determineSupportedCiphertextFileNameLength(Path p, int lowerBoundIncl, int upperBoundExcl) {
 		assert lowerBoundIncl < upperBoundExcl;
 		int mid = (lowerBoundIncl + upperBoundExcl) / 2;
 		assert mid < upperBoundExcl;
@@ -145,9 +153,9 @@ private int determineSupportedFileNameLength(Path p, int lowerBoundIncl, int upp
 		}
 		assert lowerBoundIncl < mid;
 		if (canHandleFileNameLength(p, mid)) {
-			return determineSupportedFileNameLength(p, mid, upperBoundExcl);
+			return determineSupportedCiphertextFileNameLength(p, mid, upperBoundExcl);
 		} else {
-			return determineSupportedFileNameLength(p, lowerBoundIncl, mid);
+			return determineSupportedCiphertextFileNameLength(p, lowerBoundIncl, mid);
 		}
 	}
 
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
index c883f909..e61c3cab 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/v7/Version7Migrator.java
@@ -66,7 +66,7 @@ public void migrate(Path vaultRoot, String vaultConfigFilename, String masterkey
 			LOG.info("Backed up masterkey from {} to {}.", masterkeyFile.getFileName(), masterkeyBackupFile.getFileName());
 
 			// check file system capabilities:
-			int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(vaultRoot.resolve("c"), 46, 28, 220);
+			int filenameLengthLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(vaultRoot.resolve("c"), 46, 28, 220);
 			int pathLengthLimit = filenameLengthLimit + 48; // TODO
 			PreMigrationVisitor preMigrationVisitor;
 			if (filenameLengthLimit >= 220) {
diff --git a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
index 3a58f8f0..4f7fefd1 100644
--- a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
@@ -3,12 +3,13 @@
 import org.cryptomator.cryptofs.mocks.DirectoryStreamMock;
 import org.cryptomator.cryptofs.mocks.SeekableByteChannelMock;
 import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.mockito.Mockito;
 
 import java.io.IOException;
@@ -62,7 +63,7 @@ public void testUnlimitedLength() throws IOException {
 				return checkDirMock;
 			});
 
-			int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault);
+			int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault);
 
 			Assertions.assertEquals(220, determinedLimit);
 		}
@@ -95,7 +96,7 @@ public void testLimitedLengthDuringDirListing() throws IOException {
 				return checkDirMock;
 			});
 
-			int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault);
+			int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault);
 			
 			Assertions.assertEquals(limit, determinedLimit);
 		}
@@ -125,10 +126,23 @@ public void testLimitedLengthDuringFileCreation() throws IOException {
 				return checkDirMock;
 			});
 
-			int determinedLimit = new FileSystemCapabilityChecker().determineSupportedFileNameLength(pathToVault);
+			int determinedLimit = new FileSystemCapabilityChecker().determineSupportedCiphertextFileNameLength(pathToVault);
 
 			Assertions.assertEquals(limit, determinedLimit);
 		}
+
+		@DisplayName("determineSupportedCleartextFileNameLength(...)")
+		@ParameterizedTest(name = "ciphertext length {0} -> cleartext length {1}")
+		@CsvSource({"220, 146", "219, 143", "218, 143", "217, 143", "216, 143", "215, 140"})
+		public void testDetermineSupportedCleartextFileNameLength(int ciphertextLimit, int expectedCleartextLimit) throws IOException {
+			Path path = Mockito.mock(Path.class);
+			FileSystemCapabilityChecker checker = Mockito.spy(new FileSystemCapabilityChecker());
+			Mockito.doReturn(ciphertextLimit).when(checker).determineSupportedCiphertextFileNameLength(path);
+
+			int result = checker.determineSupportedCleartextFileNameLength(path);
+
+			Assertions.assertEquals(expectedCleartextLimit, result);
+		}
 		
 	}
 

From 9eba1d9e0f1ab77e76a5fe2db9d8a662d58fc246 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 16 Apr 2021 16:48:45 +0200
Subject: [PATCH 48/70] added test case hitting shortening threshold

---
 .../cryptofs/dir/C9rConflictResolver.java     | 19 ++++---------
 .../cryptofs/dir/C9rConflictResolverTest.java | 28 +++++++++++++------
 2 files changed, 26 insertions(+), 21 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
index 0dd1a748..33570fd1 100644
--- a/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
+++ b/src/main/java/org/cryptomator/cryptofs/dir/C9rConflictResolver.java
@@ -35,13 +35,15 @@ class C9rConflictResolver {
 
 	private final Cryptor cryptor;
 	private final byte[] dirId;
-	private final VaultConfig vaultConfig;
+	private final int maxC9rFileNameLength;
+	private final int maxCleartextFileNameLength;
 
 	@Inject
 	public C9rConflictResolver(Cryptor cryptor, @Named("dirId") String dirId, VaultConfig vaultConfig) {
 		this.cryptor = cryptor;
 		this.dirId = dirId.getBytes(StandardCharsets.US_ASCII);
-		this.vaultConfig = vaultConfig;
+		this.maxC9rFileNameLength = vaultConfig.getShorteningThreshold();
+		this.maxCleartextFileNameLength = (maxC9rFileNameLength - 4) / 4 * 3 - 16; // math from FileSystemCapabilityChecker.determineSupportedCleartextFileNameLength()
 	}
 
 	public Stream<Node> process(Node node) {
@@ -80,13 +82,6 @@ private Stream<Node> resolveConflict(Node conflicting, Path canonicalPath) throw
 		}
 	}
 
-	// visible for testing
-	int calcMaxCleartextNameLength(int maxCiphertextNameLength) {
-		// math explained in https://github.com/cryptomator/cryptofs/issues/60#issuecomment-523238303;
-		// subtract 4 for file extension, base64-decode, subtract 16 for IV
-		return (maxCiphertextNameLength - 4) / 4 * 3 - 16;
-	}
-
 	/**
 	 * Resolves a conflict by renaming the conflicting file.
 	 *
@@ -99,11 +94,9 @@ int calcMaxCleartextNameLength(int maxCiphertextNameLength) {
 	private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, String cleartext) throws IOException {
 		assert Files.exists(canonicalPath);
 		final int beginOfFileExtension = cleartext.lastIndexOf('.');
-		final int maxCiphertextNameLength = vaultConfig.getShorteningThreshold();
-		final int maxCleartextNameLength = calcMaxCleartextNameLength(maxCiphertextNameLength);
 		final String fileExtension = (beginOfFileExtension > 0) ? cleartext.substring(beginOfFileExtension) : "";
 		final String basename = (beginOfFileExtension > 0) ? cleartext.substring(0, beginOfFileExtension) : cleartext;
-		final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), maxCleartextNameLength - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)"
+		final String lengthRestrictedBasename = basename.substring(0, Math.min(basename.length(), maxCleartextFileNameLength - fileExtension.length() - 5)); // 5 chars for conflict suffix " (42)"
 		String alternativeCleartext;
 		String alternativeCiphertext;
 		String alternativeCiphertextName;
@@ -115,7 +108,7 @@ private Node renameConflictingFile(Path canonicalPath, Path conflictingPath, Str
 			alternativeCiphertextName = alternativeCiphertext + Constants.CRYPTOMATOR_FILE_SUFFIX;
 			alternativePath = canonicalPath.resolveSibling(alternativeCiphertextName);
 		} while (Files.exists(alternativePath));
-		assert alternativeCiphertextName.length() <= maxCiphertextNameLength;
+		assert alternativeCiphertextName.length() <= maxC9rFileNameLength;
 		LOG.info("Moving conflicting file {} to {}", conflictingPath, alternativePath);
 		Files.move(conflictingPath, alternativePath, StandardCopyOption.ATOMIC_MOVE);
 		Node node = new Node(alternativePath);
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
index 096bd538..75113ae3 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
@@ -31,7 +31,7 @@ public void setup() {
 		fileNameCryptor = Mockito.mock(FileNameCryptor.class);
 		vaultConfig = Mockito.mock(VaultConfig.class);
 		Mockito.when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
-		Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(220);
+		Mockito.when(vaultConfig.getShorteningThreshold()).thenReturn(44); // results in max cleartext size = 14
 		conflictResolver = new C9rConflictResolver(cryptor, "foo", vaultConfig);
 	}
 	
@@ -77,6 +77,25 @@ public void testResolveConflictingFileByChoosingNewName(@TempDir Path dir) throw
 		Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
 	}
 
+	@Test
+	public void testResolveConflictingFileByChoosingNewLengthLimitedName(@TempDir Path dir) throws IOException {
+		Files.createFile(dir.resolve("foo (1).c9r"));
+		Files.createFile(dir.resolve("foo.c9r"));
+		Mockito.when(fileNameCryptor.encryptFilename(Mockito.any(), Mockito.any(), Mockito.any())).thenReturn("baz");
+		Node unresolved = new Node(dir.resolve("foo (1).c9r"));
+		unresolved.cleartextName = "hello world.txt";
+		unresolved.extractedCiphertext = "foo";
+
+		Stream<Node> result = conflictResolver.process(unresolved);
+		Node resolved = result.findAny().get();
+
+		Assertions.assertNotEquals(unresolved, resolved);
+		Assertions.assertEquals("baz.c9r", resolved.fullCiphertextFileName);
+		Assertions.assertEquals("hello (1).txt", resolved.cleartextName);
+		Assertions.assertTrue(Files.exists(resolved.ciphertextPath));
+		Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
+	}
+
 	@Test
 	public void testResolveConflictingFileTrivially(@TempDir Path dir) throws IOException {
 		Files.createFile(dir.resolve("foo (1).c9r"));
@@ -131,11 +150,4 @@ public void testResolveConflictingSymlinkTrivially(@TempDir Path dir) throws IOE
 		Assertions.assertFalse(Files.exists(unresolved.ciphertextPath));
 	}
 
-	@ParameterizedTest
-	@CsvSource({"220, 146", "219, 143", "218, 143", "217, 143", "216, 143", "215, 140"})
-	public void testCalcMaxCleartextNameLength(int input, int expectedResult) {
-		int result = conflictResolver.calcMaxCleartextNameLength(input);
-		Assertions.assertEquals(expectedResult, result);
-	}
-
 }
\ No newline at end of file

From aa51d2114d35132220c536fb6da35a4d47f7a537 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 19 Apr 2021 13:03:34 +0200
Subject: [PATCH 49/70] allow reading the shortening threshold from an
 unverified vault config

---
 src/main/java/org/cryptomator/cryptofs/VaultConfig.java | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
index 9574f3b3..18356d0a 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -140,12 +140,19 @@ public URI getKeyId() {
 		}
 
 		/**
-		 * @return The unverified vault version (JWT signature not verified)
+		 * @return The unverified vault version (signature not verified)
 		 */
 		public int allegedVaultVersion() {
 			return unverifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt();
 		}
 
+		/**
+		 * @return The unverified shortening threshold (signature not verified)
+		 */
+		public int allegedShorteningThreshold() {
+			return unverifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt();
+		}
+
 		/**
 		 * Decodes a vault configuration stored in JWT format.
 		 *

From f84d710dcf2c6a903a37c2088eac2245171fd12b Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Tue, 20 Apr 2021 13:17:00 +0200
Subject: [PATCH 50/70] allow readonly file channels even when exceeding
 maxCleartextNameLength

---
 .../cryptofs/CryptoFileSystemImpl.java        |  4 +-
 .../cryptofs/CryptoFileSystemProperties.java  |  8 ++--
 .../cryptofs/CryptoFileSystemImplTest.java    | 45 +++++++++++++++++++
 3 files changed, 53 insertions(+), 4 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
index 46e1ad86..5da6de99 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemImpl.java
@@ -348,7 +348,9 @@ private FileChannel newFileChannelFromSymlink(CryptoPath cleartextPath, Effectiv
 	}
 
 	private FileChannel newFileChannelFromFile(CryptoPath cleartextFilePath, EffectiveOpenOptions options, FileAttribute<?>... attrs) throws IOException {
-		assertCleartextNameLengthAllowed(cleartextFilePath);
+		if (options.create() || options.createNew()) {
+			assertCleartextNameLengthAllowed(cleartextFilePath);
+		}
 		CiphertextFilePath ciphertextPath = cryptoPathMapper.getCiphertextFilePath(cleartextFilePath);
 		Path ciphertextFilePath = ciphertextPath.getFilePath();
 		if (options.createNew() && openCryptoFiles.get(ciphertextFilePath).isPresent()) {
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 1f64b181..b917a60f 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -152,7 +152,7 @@ String masterkeyFilename() {
 		return (String) get(PROPERTY_MASTERKEY_FILENAME);
 	}
 
-	public int maxCleartextNameLength() {
+	int maxCleartextNameLength() {
 		return (int) get(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH);
 	}
 
@@ -235,9 +235,11 @@ private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties
 		}
 
 		/**
-		 * Sets the maximum ciphertext filename length for a CryptoFileSystem.
+		 * Sets the maximum cleartext filename length for a CryptoFileSystem. This value is checked during write
+		 * operations. Read access to nodes with longer names should be unaffected. Setting this value to {@code 0} or
+		 * a negative value effectively disables write access.
 		 *
-		 * @param maxCleartextNameLength The maximum ciphertext filename length allowed
+		 * @param maxCleartextNameLength The maximum cleartext filename length allowed
 		 * @return this
 		 * @since 2.0.0
 		 */
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
index e5740192..cc75fdea 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemImplTest.java
@@ -19,6 +19,7 @@
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.MatcherAssert;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
@@ -367,6 +368,50 @@ public void setup() throws IOException {
 			when(openCryptoFile.newFileChannel(any())).thenReturn(fileChannel);
 		}
 
+		@Nested
+		public class LimitedCleartextNameLength {
+
+			@BeforeEach
+			public void setup() throws IOException {
+				Assumptions.assumeTrue(cleartextPath.getFileName().toString().length() == 9);
+			}
+
+			@Test
+			@DisplayName("read-only always works")
+			public void testNewFileChannelReadOnlyDespiteMaxName() throws IOException {
+				Mockito.doReturn(0).when(fileSystemProperties).maxCleartextNameLength();
+
+				FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.READ));
+
+				Assertions.assertSame(fileChannel, ch);
+				verify(readonlyFlag, Mockito.never()).assertWritable();
+			}
+
+			@Test
+			@DisplayName("create new fails when exceeding limit")
+			public void testNewFileChannelCreate1() {
+				Mockito.doReturn(0).when(fileSystemProperties).maxCleartextNameLength();
+
+				Assertions.assertThrows(FileNameTooLongException.class, () -> {
+					inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.WRITE, StandardOpenOption.CREATE));
+				});
+
+				verifyNoInteractions(openCryptoFiles);
+			}
+
+			@Test
+			@DisplayName("create new succeeds when within limit")
+			public void testNewFileChannelCreate2() throws IOException {
+				Mockito.doReturn(10).when(fileSystemProperties).maxCleartextNameLength();
+
+				FileChannel ch = inTest.newFileChannel(cleartextPath, EnumSet.of(StandardOpenOption.READ));
+
+				Assertions.assertSame(fileChannel, ch);
+				verify(readonlyFlag, Mockito.never()).assertWritable();
+			}
+
+		}
+
 		@Test
 		@DisplayName("newFileChannel read-only")
 		public void testNewFileChannelReadOnly() throws IOException {

From f5e9748fd02e8b8e979bd5c12989a2638cb68571 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Thu, 22 Apr 2021 22:11:01 +0200
Subject: [PATCH 51/70] support only one single key loader that must be
 provided by the user

---
 pom.xml                                       |  2 +-
 .../cryptofs/CryptoFileSystemProperties.java  | 57 ++++---------------
 .../cryptofs/CryptoFileSystemProvider.java    |  2 +-
 .../cryptofs/CryptoFileSystems.java           |  2 +-
 ...toFileChannelWriteReadIntegrationTest.java |  6 +-
 .../CryptoFileSystemPropertiesTest.java       | 29 +++++-----
 ...yptoFileSystemProviderIntegrationTest.java | 23 +++-----
 .../CryptoFileSystemProviderTest.java         |  7 +--
 .../cryptofs/CryptoFileSystemUriTest.java     |  3 +-
 .../cryptofs/CryptoFileSystemsTest.java       |  2 +-
 ...ptyCiphertextDirectoryIntegrationTest.java |  3 +-
 .../cryptofs/ReadmeCodeSamplesTest.java       |  6 +-
 .../RealFileSystemIntegrationTest.java        |  4 +-
 ...iteFileWhileReadonlyChannelIsOpenTest.java |  4 +-
 .../attr/FileAttributeIntegrationTest.java    |  3 +-
 15 files changed, 53 insertions(+), 100 deletions(-)

diff --git a/pom.xml b/pom.xml
index 6f9b3e62..e72520e9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -17,7 +17,7 @@
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-beta6</cryptolib.version>
+		<cryptolib.version>2.0.0-beta7</cryptolib.version>
 		<jwt.version>3.12.0</jwt.version>
 		<dagger.version>2.31</dagger.version>
 		<guava.version>30.1-jre</guava.version>
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index b917a60f..39d3c04d 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -9,9 +9,7 @@
 package org.cryptomator.cryptofs;
 
 import com.google.common.base.Strings;
-import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
-import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
 import java.net.URI;
 import java.nio.file.FileSystems;
@@ -19,7 +17,6 @@
 import java.util.AbstractMap;
 import java.util.Collection;
 import java.util.EnumSet;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -51,9 +48,7 @@ public class CryptoFileSystemProperties extends AbstractMap<String, Object> {
 	 *
 	 * @since 2.0.0
 	 */
-	public static final String PROPERTY_KEYLOADERS = "keyLoaders";
-
-	static final Collection<MasterkeyLoader> DEFAULT_KEYLOADERS = Set.of();
+	public static final String PROPERTY_KEYLOADER = "keyLoader";
 
 	/**
 	 * Key identifying the name of the vault config file located inside the vault directory.
@@ -102,7 +97,7 @@ public enum FileSystemFlags {
 
 	private CryptoFileSystemProperties(Builder builder) {
 		this.entries = Set.of( //
-				Map.entry(PROPERTY_KEYLOADERS, builder.keyLoaders), //
+				Map.entry(PROPERTY_KEYLOADER, builder.keyLoader), //
 				Map.entry(PROPERTY_FILESYSTEM_FLAGS, builder.flags), //
 				Map.entry(PROPERTY_VAULTCONFIG_FILENAME, builder.vaultConfigFilename), //
 				Map.entry(PROPERTY_MASTERKEY_FILENAME, builder.masterkeyFilename), //
@@ -111,24 +106,8 @@ private CryptoFileSystemProperties(Builder builder) {
 		);
 	}
 
-	Collection<MasterkeyLoader> keyLoaders() {
-		return (Collection<MasterkeyLoader>) get(PROPERTY_KEYLOADERS);
-	}
-
-	/**
-	 * Selects the first applicable MasterkeyLoader that supports the given scheme.
-	 *
-	 * @param scheme An URI scheme used in key IDs
-	 * @return A key loader
-	 * @throws MasterkeyLoadingFailedException If the scheme is not supported by any key loader
-	 */
-	MasterkeyLoader keyLoader(String scheme) throws MasterkeyLoadingFailedException {
-		for (MasterkeyLoader loader : keyLoaders()) {
-			if (loader.supportsScheme(scheme)) {
-				return loader;
-			}
-		}
-		throw new MasterkeyLoadingFailedException("No key loader for key type: " + scheme);
+	MasterkeyLoader keyLoader() {
+		return (MasterkeyLoader) get(PROPERTY_KEYLOADER);
 	}
 
 	public VaultCipherCombo cipherCombo() {
@@ -205,7 +184,7 @@ public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
 	public static class Builder {
 
 		public VaultCipherCombo cipherCombo = DEFAULT_CIPHER_COMBO;
-		private Collection<MasterkeyLoader> keyLoaders = new HashSet<>(DEFAULT_KEYLOADERS);
+		private MasterkeyLoader keyLoader = null;
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
 		private String masterkeyFilename = DEFAULT_MASTERKEY_FILENAME;
@@ -215,7 +194,7 @@ private Builder() {
 		}
 
 		private Builder(Map<String, ?> properties) {
-			checkedSet(Collection.class, PROPERTY_KEYLOADERS, properties, this::withKeyLoaders);
+			checkedSet(MasterkeyLoader.class, PROPERTY_KEYLOADER, properties, this::withKeyLoader);
 			checkedSet(String.class, PROPERTY_VAULTCONFIG_FILENAME, properties, this::withVaultConfigFilename);
 			checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
@@ -262,26 +241,14 @@ public Builder withCipherCombo(VaultCipherCombo cipherCombo) {
 		}
 
 		/**
-		 * Sets the keyLoaders for a CryptoFileSystem.
-		 *
-		 * @param keyLoaders A set of keyLoaders to load the key configured in the vault configuration
-		 * @return this
-		 * @since 2.0.0
-		 */
-		public Builder withKeyLoaders(MasterkeyLoader... keyLoaders) {
-			return withKeyLoaders(asList(keyLoaders));
-		}
-
-		/**
-		 * Sets the keyLoaders for a CryptoFileSystem.
+		 * Sets the keyloader for a CryptoFileSystem.
 		 *
-		 * @param keyLoaders A set of keyLoaders to load the key configured in the vault configuration
+		 * @param keyLoader A factory creating a {@link MasterkeyLoader} capable of handling the given {@code scheme}.
 		 * @return this
 		 * @since 2.0.0
 		 */
-		public Builder withKeyLoaders(Collection<MasterkeyLoader> keyLoaders) {
-			this.keyLoaders.clear();
-			this.keyLoaders.addAll(keyLoaders);
+		public Builder withKeyLoader(MasterkeyLoader keyLoader) {
+			this.keyLoader = keyLoader;
 			return this;
 		}
 
@@ -345,8 +312,8 @@ public CryptoFileSystemProperties build() {
 		}
 
 		private void validate() {
-			if (keyLoaders.isEmpty()) {
-				throw new IllegalStateException("at least one keyloader is required");
+			if (keyLoader == null) {
+				throw new IllegalStateException("keyLoader is required");
 			}
 			if (Strings.nullToEmpty(masterkeyFilename).trim().isEmpty()) {
 				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 0a1330f5..4a401c84 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -142,7 +142,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 		}
 		byte[] rawKey = new byte[0];
 		var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(Constants.DEFAULT_SHORTENING_THRESHOLD).build();
-		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId);
+		try (Masterkey key = properties.keyLoader().loadKey(keyId);
 			 Cryptor cryptor = config.getCipherCombo().getCryptorProvider(strongSecureRandom()).withKey(key)) {
 			rawKey = key.getEncoded();
 			// save vault config:
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 438b9577..16c5e2ca 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -49,7 +49,7 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 
 		var configLoader = VaultConfig.decode(token);
 		var keyId = configLoader.getKeyId();
-		try (Masterkey key = properties.keyLoader(keyId.getScheme()).loadKey(keyId)) {
+		try (Masterkey key = properties.keyLoader().loadKey(keyId)) {
 			var config = configLoader.verify(key.getEncoded(), Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
 			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key.clone());
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
index 7bca22ee..3b12c4c0 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileChannelWriteReadIntegrationTest.java
@@ -66,9 +66,8 @@ public class Windows {
 		@BeforeAll
 		public void setupClass(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-			Mockito.when(keyLoader.supportsScheme(Mockito.any())).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-			CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 			CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
 			fileSystem = CryptoFileSystemProvider.newFileSystem(tmpDir, properties);
 		}
@@ -142,9 +141,8 @@ public void beforeAll() throws IOException, MasterkeyLoadingFailedException {
 			Path vaultPath = inMemoryFs.getPath("vault");
 			Files.createDirectories(vaultPath);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 			CryptoFileSystemProvider.initialize(vaultPath, properties, URI.create("test:key"));
 			fileSystem = new CryptoFileSystemProvider().newFileSystem(vaultPath, properties);
 			file = fileSystem.getPath("/test.txt");
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
index 78fc7cac..185dbc08 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemPropertiesTest.java
@@ -15,7 +15,6 @@
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Objects;
-import java.util.Set;
 
 import static org.cryptomator.cryptofs.CryptoFileSystemProperties.*;
 import static org.hamcrest.CoreMatchers.sameInstance;
@@ -37,7 +36,7 @@ public void testSetNoPassphrase() {
 	public void testSetMasterkeyFilenameAndReadonlyFlag() {
 		String masterkeyFilename = "aMasterkeyFilename";
 		CryptoFileSystemProperties inTest = cryptoFileSystemProperties() //
-				.withKeyLoaders(keyLoader) //
+				.withKeyLoader(keyLoader) //
 				.withMasterkeyFilename(masterkeyFilename) //
 				.withFlags(FileSystemFlags.READONLY)
 				.build();
@@ -46,7 +45,7 @@ public void testSetMasterkeyFilenameAndReadonlyFlag() {
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
@@ -58,7 +57,7 @@ public void testSetMasterkeyFilenameAndReadonlyFlag() {
 	public void testFromMap() {
 		Map<String, Object> map = new HashMap<>();
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
@@ -69,7 +68,7 @@ public void testFromMap() {
 		MatcherAssert.assertThat(inTest.maxCleartextNameLength(), is(255));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, 255), //
@@ -81,7 +80,7 @@ public void testFromMap() {
 	public void testWrapMapWithTrueReadonly() {
 		Map<String, Object> map = new HashMap<>();
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.of(FileSystemFlags.READONLY));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
@@ -90,7 +89,7 @@ public void testWrapMapWithTrueReadonly() {
 		MatcherAssert.assertThat(inTest.readonly(), is(true));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
@@ -102,7 +101,7 @@ public void testWrapMapWithTrueReadonly() {
 	public void testWrapMapWithFalseReadonly() {
 		Map<String, Object> map = new HashMap<>();
 		String masterkeyFilename = "aMasterkeyFilename";
-		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		map.put(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename);
 		map.put(PROPERTY_FILESYSTEM_FLAGS, EnumSet.noneOf(FileSystemFlags.class));
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
@@ -111,7 +110,7 @@ public void testWrapMapWithFalseReadonly() {
 		MatcherAssert.assertThat(inTest.readonly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, masterkeyFilename), //
 						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
@@ -155,14 +154,14 @@ public void testWrapMapWithInvalidPassphrase() {
 	@Test
 	public void testWrapMapWithoutReadonly() {
 		Map<String, Object> map = new HashMap<>();
-		map.put(PROPERTY_KEYLOADERS, Set.of(keyLoader));
+		map.put(PROPERTY_KEYLOADER, keyLoader);
 		CryptoFileSystemProperties inTest = CryptoFileSystemProperties.wrap(map);
 
 		MatcherAssert.assertThat(inTest.masterkeyFilename(), is(DEFAULT_MASTERKEY_FILENAME));
 		MatcherAssert.assertThat(inTest.readonly(), is(false));
 		MatcherAssert.assertThat(inTest.entrySet(),
 				containsInAnyOrder( //
-						anEntry(PROPERTY_KEYLOADERS, Set.of(keyLoader)), //
+						anEntry(PROPERTY_KEYLOADER, keyLoader), //
 						anEntry(PROPERTY_VAULTCONFIG_FILENAME, DEFAULT_VAULTCONFIG_FILENAME), //
 						anEntry(PROPERTY_MASTERKEY_FILENAME, DEFAULT_MASTERKEY_FILENAME), //
 						anEntry(PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, DEFAULT_MAX_CLEARTEXT_NAME_LENGTH), //
@@ -181,7 +180,7 @@ public void testWrapMapWithoutPassphrase() {
 
 	@Test
 	public void testWrapCryptoFileSystemProperties() {
-		CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties inTest = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 
 		MatcherAssert.assertThat(CryptoFileSystemProperties.wrap(inTest), is(sameInstance(inTest)));
 	}
@@ -190,7 +189,7 @@ public void testWrapCryptoFileSystemProperties() {
 	public void testMapIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withKeyLoaders(keyLoader) //
+					.withKeyLoader(keyLoader) //
 					.build() //
 					.put("test", "test");
 		});
@@ -200,7 +199,7 @@ public void testMapIsImmutable() {
 	public void testEntrySetIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withKeyLoaders(keyLoader) //
+					.withKeyLoader(keyLoader) //
 					.build() //
 					.entrySet() //
 					.add(null);
@@ -211,7 +210,7 @@ public void testEntrySetIsImmutable() {
 	public void testEntryIsImmutable() {
 		Assertions.assertThrows(UnsupportedOperationException.class, () -> {
 			cryptoFileSystemProperties() //
-					.withKeyLoaders(keyLoader) //
+					.withKeyLoader(keyLoader) //
 					.build() //
 					.entrySet() //
 					.iterator().next() //
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 65b1b845..5ae7a36a 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -84,12 +84,11 @@ class WithLimitedPaths {
 
 		@BeforeAll
 		public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFailedException {
-			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
 			CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 					.withFlags() //
 					.withMasterkeyFilename("masterkey.cryptomator") //
-					.withKeyLoaders(keyLoader) //
+					.withKeyLoader(keyLoader) //
 					.withMaxCleartextNameLength(50)
 					.build();
 			CryptoFileSystemProvider.initialize(tmpDir, properties, URI.create("test:key"));
@@ -192,8 +191,6 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 			Arrays.fill(key2, (byte) 0x77);
 			keyLoader1 = Mockito.mock(MasterkeyLoader.class);
 			keyLoader2 = Mockito.mock(MasterkeyLoader.class);
-			Mockito.when(keyLoader1.supportsScheme("test")).thenReturn(true);
-			Mockito.when(keyLoader2.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader1.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key1));
 			Mockito.when(keyLoader2.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(key2));
 			pathToVault1 = tmpFs.getPath("/vaultDir1");
@@ -215,12 +212,12 @@ public void teardown() throws IOException {
 		public void initializeVaults() {
 			Assertions.assertAll(
 					() -> {
-						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader1).build();
+						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader1).build();
 						CryptoFileSystemProvider.initialize(pathToVault1, properties, URI.create("test:key"));
 						Assertions.assertTrue(Files.isDirectory(pathToVault1.resolve("d")));
 						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile1));
 					}, () -> {
-						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader2).build();
+						var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader2).build();
 						CryptoFileSystemProvider.initialize(pathToVault2, properties, URI.create("test:key"));
 						Assertions.assertTrue(Files.isDirectory(pathToVault2.resolve("d")));
 						Assertions.assertTrue(Files.isRegularFile(vaultConfigFile2));
@@ -239,7 +236,7 @@ public void testGetFsWithWrongCredentials() {
 						CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 								.withFlags() //
 								.withMasterkeyFilename("masterkey.cryptomator") //
-								.withKeyLoaders(keyLoader2) //
+								.withKeyLoader(keyLoader2) //
 								.build();
 						Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 							FileSystems.newFileSystem(fsUri, properties);
@@ -250,7 +247,7 @@ public void testGetFsWithWrongCredentials() {
 						CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 								.withFlags() //
 								.withMasterkeyFilename("masterkey.cryptomator") //
-								.withKeyLoaders(keyLoader1) //
+								.withKeyLoader(keyLoader1) //
 								.build();
 						Assertions.assertThrows(VaultKeyInvalidException.class, () -> {
 							FileSystems.newFileSystem(fsUri, properties);
@@ -267,7 +264,7 @@ public void testGetFsViaNioApi() {
 			Assertions.assertAll(
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault1);
-						fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoaders(keyLoader1).build());
+						fs1 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader1).build());
 						Assertions.assertTrue(fs1 instanceof CryptoFileSystemImpl);
 
 						FileSystem sameFs = FileSystems.getFileSystem(fsUri);
@@ -275,7 +272,7 @@ public void testGetFsViaNioApi() {
 					},
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault2);
-						fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoaders(keyLoader2).build());
+						fs2 = FileSystems.newFileSystem(fsUri, cryptoFileSystemProperties().withKeyLoader(keyLoader2).build());
 						Assertions.assertTrue(fs2 instanceof CryptoFileSystemImpl);
 
 						FileSystem sameFs = FileSystems.getFileSystem(fsUri);
@@ -535,9 +532,8 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
 		}
@@ -628,9 +624,8 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 			Path pathToVault = tmpDir.resolve("vaultDir1");
 			Files.createDirectories(pathToVault);
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 			fs = CryptoFileSystemProvider.newFileSystem(pathToVault, properties);
 		}
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index ed488d7b..d786d9d0 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -114,7 +114,6 @@ private static final Stream<InvocationWhichShouldFail> shouldFailWithRelativePat
 	@BeforeEach
 	@SuppressWarnings("deprecation")
 	public void setup() throws MasterkeyLoadingFailedException {
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		when(keyLoader.loadKey(Mockito.any())).thenReturn(new Masterkey(new byte[64]));
 
 		CryptoFileSystemProviderComponent component = mock(CryptoFileSystemProviderComponent.class);
@@ -168,7 +167,7 @@ public void testGetSchemeReturnsCryptomatorScheme() {
 	public void testInitializeFailWithNotDirectoryException() {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 		Path pathToVault = fs.getPath("/vaultDir");
-		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 
 		Assertions.assertThrows(NotDirectoryException.class, () -> {
 			CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
@@ -181,7 +180,7 @@ public void testInitialize() throws IOException, MasterkeyLoadingFailedException
 		Path pathToVault = fs.getPath("/vaultDir");
 		Path vaultConfigFile = pathToVault.resolve("vault.cryptomator");
 		Path dataDir = pathToVault.resolve("d");
-		var properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		var properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 
 		Files.createDirectory(pathToVault);
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
@@ -204,7 +203,7 @@ public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedExcept
 		URI uri = CryptoFileSystemUri.create(pathToVault);
 		CryptoFileSystemProperties properties = cryptoFileSystemProperties() //
 				.withFlags() //
-				.withKeyLoaders(keyLoader) //
+				.withKeyLoader(keyLoader) //
 				.build();
 
 		inTest.newFileSystem(uri, properties);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
index fe60e12d..d6d186b5 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemUriTest.java
@@ -75,9 +75,8 @@ public void testCreateWithPathToVaultFromNonDefaultProvider() throws IOException
 		Path tempDir = createTempDirectory("CryptoFileSystemUrisTest").toAbsolutePath();
 		try {
 			MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-			Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 			Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-			CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+			CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 			CryptoFileSystemProvider.initialize(tempDir, properties, URI.create("test:key"));
 			FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(tempDir, properties);
 			Path absolutePathToVault = fileSystem.getPath("a").toAbsolutePath();
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index 43456ad2..fd2e6bca 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -71,7 +71,7 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(pathToVault.normalize()).thenReturn(normalizedPathToVault);
 		when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath);
 		when(properties.vaultConfigFilename()).thenReturn("vault.cryptomator");
-		when(properties.keyLoader(Mockito.any())).thenReturn(keyLoader);
+		when(properties.keyLoader()).thenReturn(keyLoader);
 		filesClass.when(() -> Files.readString(configFilePath, StandardCharsets.US_ASCII)).thenReturn("jwt-vault-config");
 		vaultConficClass.when(() -> VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
 		when(VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
diff --git a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
index fe566bac..21754ca2 100644
--- a/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DeleteNonEmptyCiphertextDirectoryIntegrationTest.java
@@ -46,9 +46,8 @@ public static void setupClass(@TempDir Path tmpDir) throws IOException, Masterke
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
index 30c1a857..fc0037e5 100644
--- a/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ReadmeCodeSamplesTest.java
@@ -31,9 +31,8 @@ public class ReadmeCodeSamplesTest {
 	@Test
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 		CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key"));
 		FileSystem fileSystem = CryptoFileSystemProvider.newFileSystem(storageLocation, properties);
 
@@ -44,9 +43,8 @@ public void testReadmeCodeSampleUsingFileSystemConstructionMethodA(@TempDir Path
 	public void testReadmeCodeSampleUsingFileSystemConstructionMethodB(@TempDir Path storageLocation) throws IOException, MasterkeyLoadingFailedException {
 		URI uri = CryptoFileSystemUri.create(storageLocation);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 		CryptoFileSystemProvider.initialize(storageLocation, properties, URI.create("test:key"));
 		FileSystem fileSystem = FileSystems.newFileSystem(uri, properties);
 
diff --git a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
index d0c65013..6706ddf3 100644
--- a/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/RealFileSystemIntegrationTest.java
@@ -25,6 +25,7 @@
 import java.nio.file.Path;
 import java.nio.file.attribute.UserPrincipal;
 
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
 
 public class RealFileSystemIntegrationTest {
@@ -37,9 +38,8 @@ public static void setupClass(@TempDir Path tmpDir) throws IOException, Masterke
 		pathToVault = tmpDir.resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}
diff --git a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
index 203f600f..2e39d116 100644
--- a/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/WriteFileWhileReadonlyChannelIsOpenTest.java
@@ -19,6 +19,7 @@
 import java.nio.file.StandardOpenOption;
 
 import static java.nio.file.StandardOpenOption.READ;
+import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
 import static org.cryptomator.cryptofs.CryptoFileSystemUri.create;
 
 /**
@@ -36,9 +37,8 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		Path pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties properties = cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 		root = fileSystem.getPath("/");
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
index 11a6ee09..ffb26afa 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/FileAttributeIntegrationTest.java
@@ -62,9 +62,8 @@ public static void setupClass() throws IOException, MasterkeyLoadingFailedExcept
 		pathToVault = inMemoryFs.getRootDirectories().iterator().next().resolve("vault");
 		Files.createDirectory(pathToVault);
 		MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
-		Mockito.when(keyLoader.supportsScheme("test")).thenReturn(true);
 		Mockito.when(keyLoader.loadKey(Mockito.any())).thenAnswer(ignored -> new Masterkey(new byte[64]));
-		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoaders(keyLoader).build();
+		CryptoFileSystemProperties properties = CryptoFileSystemProperties.cryptoFileSystemProperties().withKeyLoader(keyLoader).build();
 		CryptoFileSystemProvider.initialize(pathToVault, properties, URI.create("test:key"));
 		fileSystem = new CryptoFileSystemProvider().newFileSystem(create(pathToVault), properties);
 	}

From 4958fcd64425a1cdb0bce22964955a829ce66488 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Tue, 4 May 2021 11:53:24 +0200
Subject: [PATCH 52/70] move versions of non-default build plugins to
 properties

---
 pom.xml | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)

diff --git a/pom.xml b/pom.xml
index e72520e9..2fc86268 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,11 @@
 		<junit.jupiter.version>5.7.0</junit.jupiter.version>
 		<mockito.version>3.7.7</mockito.version>
 		<hamcrest.version>2.2</hamcrest.version>
+
+		<!-- build plugin dependencies -->
+		<dependency-check.version>6.1.6</dependency-check.version>
+		<jacoco.version>0.8.6</jacoco.version>
+		<nexus-staging.version>1.6.8</nexus-staging.version>
 	</properties>
 
 	<licenses>
@@ -219,7 +224,7 @@
 					<plugin>
 						<groupId>org.owasp</groupId>
 						<artifactId>dependency-check-maven</artifactId>
-						<version>6.1.0</version>
+						<version>${dependency-check.version}</version>
 						<configuration>
 							<cveValidForHours>24</cveValidForHours>
 							<failBuildOnCVSS>0</failBuildOnCVSS>
@@ -246,7 +251,7 @@
 					<plugin>
 						<groupId>org.jacoco</groupId>
 						<artifactId>jacoco-maven-plugin</artifactId>
-						<version>0.8.6</version>
+						<version>${jacoco.version}</version>
 						<executions>
 							<execution>
 								<id>prepare-agent</id>
@@ -307,7 +312,7 @@
 					<plugin>
 						<groupId>org.sonatype.plugins</groupId>
 						<artifactId>nexus-staging-maven-plugin</artifactId>
-						<version>1.6.8</version>
+						<version>${nexus-staging.version}</version>
 						<extensions>true</extensions>
 						<configuration>
 							<serverId>ossrh</serverId>

From 450624c371faf0b6583a19ef8b022796a4008627 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Tue, 4 May 2021 13:18:10 +0200
Subject: [PATCH 53/70] move used JDK to more prominent place

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 2fc86268..54bf57bd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -15,6 +15,7 @@
 
 	<properties>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
 		<cryptolib.version>2.0.0-beta7</cryptolib.version>
@@ -126,7 +127,6 @@
 				<artifactId>maven-compiler-plugin</artifactId>
 				<version>3.8.1</version>
 				<configuration>
-					<release>16</release>
 					<showWarnings>true</showWarnings>
 					<annotationProcessorPaths>
 						<path>

From fe410fdef30bf59bc007fb56828501a6439423fc Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Tue, 4 May 2021 13:18:23 +0200
Subject: [PATCH 54/70] bump dependencies

---
 pom.xml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/pom.xml b/pom.xml
index 54bf57bd..a3edce77 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,14 +19,14 @@
 
 		<!-- dependencies -->
 		<cryptolib.version>2.0.0-beta7</cryptolib.version>
-		<jwt.version>3.12.0</jwt.version>
-		<dagger.version>2.31</dagger.version>
-		<guava.version>30.1-jre</guava.version>
+		<jwt.version>3.15.0</jwt.version>
+		<dagger.version>2.35.1</dagger.version>
+		<guava.version>30.1.1-jre</guava.version>
 		<slf4j.version>1.7.30</slf4j.version>
 
 		<!-- test dependencies -->
-		<junit.jupiter.version>5.7.0</junit.jupiter.version>
-		<mockito.version>3.7.7</mockito.version>
+		<junit.jupiter.version>5.7.1</junit.jupiter.version>
+		<mockito.version>3.9.0</mockito.version>
 		<hamcrest.version>2.2</hamcrest.version>
 
 		<!-- build plugin dependencies -->

From a967c22809963e040ef217d9116b359e31d2c8d9 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 5 May 2021 12:10:38 +0200
Subject: [PATCH 55/70] closes #103

---
 .../cryptofs/CryptoFileSystemProvider.java    | 31 +++++++--
 .../cryptomator/cryptofs/DirStructure.java    | 64 +++++++++++++++++++
 2 files changed, 88 insertions(+), 7 deletions(-)
 create mode 100644 src/main/java/org/cryptomator/cryptofs/DirStructure.java

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 4a401c84..6e74293b 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -161,18 +161,35 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 
 	/**
 	 * Checks if the folder represented by the given path exists and contains a valid vault structure.
+	 * <p>
+	 * See {@link DirStructure#VAULT} for the criterias of being a valid vault.
 	 *
-	 * @param pathToVault         A directory path
+	 * @param pathToAssumedVault A directory path
 	 * @param vaultConfigFilename Name of the vault config file
-	 * @param masterkeyFilename   Name of the masterkey file
+	 * @param masterkeyFilename Name of the masterkey file
 	 * @return <code>true</code> if the directory seems to contain a vault.
 	 * @since 2.0.0
 	 */
-	public static boolean containsVault(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) {
-		Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
-		Path masterkeyPath = pathToVault.resolve(masterkeyFilename);
-		Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
-		return (Files.isReadable(vaultConfigPath) || Files.isReadable(masterkeyPath)) && Files.isDirectory(dataDirPath);
+	public static boolean containsVault(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) {
+		try {
+			return checkDirStructure(pathToAssumedVault, vaultConfigFilename, masterkeyFilename) == DirStructure.VAULT;
+		} catch (IOException e) {
+			return false;
+		}
+	}
+
+	/**
+	 * Convenience method for {@link DirStructure#checkDirStructure(Path, String, String)}.
+	 *
+	 * @param pathToAssumedVault
+	 * @param vaultConfigFilename
+	 * @param masterkeyFilename
+	 * @return a {@link DirStructure} object
+	 * @throws IOException
+	 * @since 2.0.0
+	 */
+	public static DirStructure checkDirStructure(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
+		return DirStructure.checkDirStructure(pathToAssumedVault, vaultConfigFilename, masterkeyFilename);
 	}
 
 	/**
diff --git a/src/main/java/org/cryptomator/cryptofs/DirStructure.java b/src/main/java/org/cryptomator/cryptofs/DirStructure.java
new file mode 100644
index 00000000..037d0733
--- /dev/null
+++ b/src/main/java/org/cryptomator/cryptofs/DirStructure.java
@@ -0,0 +1,64 @@
+package org.cryptomator.cryptofs;
+
+import org.cryptomator.cryptofs.common.Constants;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Enumeration of the vault directory structure resemblances.
+ * <p>
+ * A valid vault must contain a `d` directory.
+ * If the vault version is 8, it must also contains a vault config file.
+ * If the vault version is smaller than 8, it must also contain a masterkey file.
+ * <p>
+ * In the latter case, to distinct between a damaged vault 8 directory and a legacy vault the masterkey file must be read.
+ * For efficiency reasons, this class only checks for existence/readability of the above elements.
+ * Hence, if the result of {@link #checkDirStructure(Path, String, String)} is {@link #MAYBE_LEGACY}, one needs to parse
+ * the masterkey file and read out the vault version to determine this case.
+ *
+ * @since 2.0.0
+ */
+public enum DirStructure {
+
+	/**
+	 * Dir contains a <code>d</code> dir as well as a vault config file.
+	 */
+	VAULT,
+
+	/**
+	 * Dir contains a <code>d</code> dir and a masterkey file, but misses a vault config file.
+	 * Either needs migration to a newer format or damaged.
+	 */
+	MAYBE_LEGACY,
+
+	/**
+	 * Dir does not qualify as vault.
+	 */
+	UNRELATED;
+
+
+	/**
+	 * Analyzes the structure of the given directory under certain vault existence criteria.
+	 *
+	 * @param pathToVault A directory path
+	 * @param vaultConfigFilename Name of the vault config file
+	 * @param masterkeyFilename Name of the masterkey file
+	 * @return enum indicating what this directory might be
+	 * @throws IOException if the directory itself or certain components cannot be accessed/read.
+	 */
+	public static DirStructure checkDirStructure(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
+		Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
+		Path masterkeyPath = pathToVault.resolve(masterkeyFilename);
+		Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
+		if (Files.isDirectory(dataDirPath)) {
+			if (Files.isReadable(vaultConfigPath)) {
+				return VAULT;
+			} else if (Files.isReadable(masterkeyPath)) {
+				return MAYBE_LEGACY;
+			}
+		}
+		return UNRELATED;
+	}
+}

From 2cae7a59dc6131f0f063de1a6df43f2484a52524 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 5 May 2021 13:35:48 +0200
Subject: [PATCH 56/70] change behavior and add unit tests

---
 .../cryptofs/CryptoFileSystemProvider.java    |  4 +-
 .../cryptomator/cryptofs/DirStructure.java    |  7 +-
 .../cryptofs/DirStructureTest.java            | 75 +++++++++++++++++++
 3 files changed, 83 insertions(+), 3 deletions(-)
 create mode 100644 src/test/java/org/cryptomator/cryptofs/DirStructureTest.java

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 6e74293b..33e64236 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -162,7 +162,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 	/**
 	 * Checks if the folder represented by the given path exists and contains a valid vault structure.
 	 * <p>
-	 * See {@link DirStructure#VAULT} for the criterias of being a valid vault.
+	 * See {@link DirStructure#VAULT} for the criteria of being a valid vault.
 	 *
 	 * @param pathToAssumedVault A directory path
 	 * @param vaultConfigFilename Name of the vault config file
@@ -179,7 +179,7 @@ public static boolean containsVault(Path pathToAssumedVault, String vaultConfigF
 	}
 
 	/**
-	 * Convenience method for {@link DirStructure#checkDirStructure(Path, String, String)}.
+	 * Delegate to {@link DirStructure#checkDirStructure(Path, String, String)}.
 	 *
 	 * @param pathToAssumedVault
 	 * @param vaultConfigFilename
diff --git a/src/main/java/org/cryptomator/cryptofs/DirStructure.java b/src/main/java/org/cryptomator/cryptofs/DirStructure.java
index 037d0733..2e33d1c3 100644
--- a/src/main/java/org/cryptomator/cryptofs/DirStructure.java
+++ b/src/main/java/org/cryptomator/cryptofs/DirStructure.java
@@ -4,7 +4,9 @@
 
 import java.io.IOException;
 import java.nio.file.Files;
+import java.nio.file.NotDirectoryException;
 import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
 
 /**
  * Enumeration of the vault directory structure resemblances.
@@ -46,9 +48,12 @@ public enum DirStructure {
 	 * @param vaultConfigFilename Name of the vault config file
 	 * @param masterkeyFilename Name of the masterkey file
 	 * @return enum indicating what this directory might be
-	 * @throws IOException if the directory itself or certain components cannot be accessed/read.
+	 * @throws IOException if the provided path is not a directory, does not exist or cannot be read
 	 */
 	public static DirStructure checkDirStructure(Path pathToVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
+		if(! Files.readAttributes(pathToVault, BasicFileAttributes.class).isDirectory()) {
+			throw new NotDirectoryException(pathToVault.toString());
+		}
 		Path vaultConfigPath = pathToVault.resolve(vaultConfigFilename);
 		Path masterkeyPath = pathToVault.resolve(masterkeyFilename);
 		Path dataDirPath = pathToVault.resolve(Constants.DATA_DIR_NAME);
diff --git a/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java b/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java
new file mode 100644
index 00000000..f35818bc
--- /dev/null
+++ b/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java
@@ -0,0 +1,75 @@
+package org.cryptomator.cryptofs;
+
+import org.cryptomator.cryptofs.common.Constants;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.stream.Stream;
+
+public class DirStructureTest {
+
+	private static final String KEY = "key";
+	private static final String CONFIG = "config";
+
+	@TempDir
+	Path vaultPath;
+
+	@Test
+	public void testNonExistingVaultPathThrowsIOException() {
+		Path vaultPath = Path.of("this/certainly/does/not/exist");
+		Assumptions.assumeTrue(Files.notExists(vaultPath));
+
+		Assertions.assertThrows(IOException.class, () -> DirStructure.checkDirStructure(vaultPath, CONFIG, KEY));
+	}
+
+	@Test
+	public void testNonDirectoryVaultPathThrowsIOException() throws IOException {
+		Path tmp = vaultPath.resolve("this");
+		Files.createFile(tmp);
+		Assumptions.assumeTrue(Files.exists(tmp));
+
+		Assertions.assertThrows(IOException.class, () -> DirStructure.checkDirStructure(tmp, CONFIG, KEY));
+	}
+
+	@ParameterizedTest(name = "Testing all combinations of data dir, config and masterkey file existence.")
+	@MethodSource("provideAllCases")
+	public void testAllCombosOfDataAndConfigAndKey(boolean createDataDir, boolean createConfig, boolean createKey, DirStructure expectedResult) throws IOException {
+		Path keyPath = vaultPath.resolve(KEY);
+		Path configPath = vaultPath.resolve(CONFIG);
+		Path dataDir = vaultPath.resolve(Constants.DATA_DIR_NAME);
+
+		if (createDataDir) {
+			Files.createDirectory(dataDir);
+		}
+		if (createConfig) {
+			Files.createFile(configPath);
+		}
+		if (createKey) {
+			Files.createFile(keyPath);
+		}
+
+		Assertions.assertEquals(expectedResult, DirStructure.checkDirStructure(vaultPath, CONFIG, KEY));
+	}
+
+	private static Stream<Arguments> provideAllCases() {
+		return Stream.of(
+				Arguments.of(true, true, true, DirStructure.VAULT),
+				Arguments.of(true, true, false, DirStructure.VAULT),
+				Arguments.of(true, false, true, DirStructure.MAYBE_LEGACY),
+				Arguments.of(true, false, false, DirStructure.UNRELATED),
+				Arguments.of(false, false, false, DirStructure.UNRELATED),
+				Arguments.of(false, false, true, DirStructure.UNRELATED),
+				Arguments.of(false, true, false, DirStructure.UNRELATED),
+				Arguments.of(false, true, true, DirStructure.UNRELATED)
+		);
+	}
+
+}

From 27b45aad775266c623ba833f12755dab760936ef Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 5 May 2021 13:36:21 +0200
Subject: [PATCH 57/70] change unit test for CryptoFileSystemProvider

---
 .../cryptomator/cryptofs/CryptoFileSystemProviderTest.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index d786d9d0..6105c309 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -228,7 +228,7 @@ public void testContainsVaultReturnsTrueIfDirectoryContainsVaultConfigFileAndDat
 	}
 
 	@Test
-	public void testContainsVaultReturnsTrueIfDirectoryContainsMasterkeyFileAndDataDir() throws IOException {
+	public void testContainsVaultReturnsFalseIfDirectoryContainsMasterkeyFileAndDataDir() throws IOException {
 		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
 
 		String vaultConfigFilename = "vaultconfig.foo.baz";
@@ -240,7 +240,7 @@ public void testContainsVaultReturnsTrueIfDirectoryContainsMasterkeyFileAndDataD
 		Files.createDirectories(dataDir);
 		Files.write(masterkeyFile, new byte[0]);
 
-		Assertions.assertTrue(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
+		Assertions.assertFalse(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
 	}
 
 	@Test

From 1155a7be65ccfcb276c235a3ff77e1e6ac603903 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 5 May 2021 13:44:45 +0200
Subject: [PATCH 58/70] renamed method

---
 .../org/cryptomator/cryptofs/CryptoFileSystemProvider.java    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 33e64236..36ee1bf8 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -172,7 +172,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 	 */
 	public static boolean containsVault(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) {
 		try {
-			return checkDirStructure(pathToAssumedVault, vaultConfigFilename, masterkeyFilename) == DirStructure.VAULT;
+			return checkDirStructureForVault(pathToAssumedVault, vaultConfigFilename, masterkeyFilename) == DirStructure.VAULT;
 		} catch (IOException e) {
 			return false;
 		}
@@ -188,7 +188,7 @@ public static boolean containsVault(Path pathToAssumedVault, String vaultConfigF
 	 * @throws IOException
 	 * @since 2.0.0
 	 */
-	public static DirStructure checkDirStructure(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
+	public static DirStructure checkDirStructureForVault(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) throws IOException {
 		return DirStructure.checkDirStructure(pathToAssumedVault, vaultConfigFilename, masterkeyFilename);
 	}
 

From 2938d45429cc5e637d17036d5f68158f0aa70b5d Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@zoho.eu>
Date: Wed, 5 May 2021 15:32:36 +0200
Subject: [PATCH 59/70] Apply suggestions from code review

Improve doc [ci skip]

Co-authored-by: Sebastian Stenzel <overheadhunter@users.noreply.github.com>
---
 src/main/java/org/cryptomator/cryptofs/DirStructure.java | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/DirStructure.java b/src/main/java/org/cryptomator/cryptofs/DirStructure.java
index 2e33d1c3..daff7fb4 100644
--- a/src/main/java/org/cryptomator/cryptofs/DirStructure.java
+++ b/src/main/java/org/cryptomator/cryptofs/DirStructure.java
@@ -12,10 +12,10 @@
  * Enumeration of the vault directory structure resemblances.
  * <p>
  * A valid vault must contain a `d` directory.
- * If the vault version is 8, it must also contains a vault config file.
- * If the vault version is smaller than 8, it must also contain a masterkey file.
+ * Beginning with vault format 8, it must also contain a vault config file.
+ * If the vault format is lower than 8, it must instead contain a masterkey file.
  * <p>
- * In the latter case, to distinct between a damaged vault 8 directory and a legacy vault the masterkey file must be read.
+ * In the latter case, to distinguish between a damaged vault 8 directory and a legacy vault the masterkey file must be read.
  * For efficiency reasons, this class only checks for existence/readability of the above elements.
  * Hence, if the result of {@link #checkDirStructure(Path, String, String)} is {@link #MAYBE_LEGACY}, one needs to parse
  * the masterkey file and read out the vault version to determine this case.

From c92a45ceeb86c5d934e30a9f590abbc504b44267 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Wed, 5 May 2021 15:44:13 +0200
Subject: [PATCH 60/70] remove CryptoFileSystemProvider.containsVault() method

---
 .../cryptofs/CryptoFileSystemProvider.java    | 21 +-----
 ...yptoFileSystemProviderIntegrationTest.java |  6 +-
 .../CryptoFileSystemProviderTest.java         | 64 -------------------
 3 files changed, 4 insertions(+), 87 deletions(-)

diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 36ee1bf8..19d5be63 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -156,26 +156,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 		} finally {
 			Arrays.fill(rawKey, (byte) 0x00);
 		}
-		assert containsVault(pathToVault, properties.vaultConfigFilename(), properties.masterkeyFilename());
-	}
-
-	/**
-	 * Checks if the folder represented by the given path exists and contains a valid vault structure.
-	 * <p>
-	 * See {@link DirStructure#VAULT} for the criteria of being a valid vault.
-	 *
-	 * @param pathToAssumedVault A directory path
-	 * @param vaultConfigFilename Name of the vault config file
-	 * @param masterkeyFilename Name of the masterkey file
-	 * @return <code>true</code> if the directory seems to contain a vault.
-	 * @since 2.0.0
-	 */
-	public static boolean containsVault(Path pathToAssumedVault, String vaultConfigFilename, String masterkeyFilename) {
-		try {
-			return checkDirStructureForVault(pathToAssumedVault, vaultConfigFilename, masterkeyFilename) == DirStructure.VAULT;
-		} catch (IOException e) {
-			return false;
-		}
+		assert checkDirStructureForVault(pathToVault, properties.vaultConfigFilename(), properties.masterkeyFilename()) == DirStructure.VAULT;
 	}
 
 	/**
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 5ae7a36a..6ae7b903 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -227,9 +227,9 @@ public void initializeVaults() {
 		@Test
 		@Order(2)
 		@DisplayName("get filesystem with incorrect credentials")
-		public void testGetFsWithWrongCredentials() {
-			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault1, "vault.cryptomator", "masterkey.cryptomator"));
-			Assumptions.assumeTrue(CryptoFileSystemProvider.containsVault(pathToVault2, "vault.cryptomator", "masterkey.cryptomator"));
+		public void testGetFsWithWrongCredentials() throws IOException {
+			Assumptions.assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault1, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT);
+			Assumptions.assumeTrue(CryptoFileSystemProvider.checkDirStructureForVault(pathToVault2, "vault.cryptomator", "masterkey.cryptomator") == DirStructure.VAULT);
 			Assertions.assertAll(
 					() -> {
 						URI fsUri = CryptoFileSystemUri.create(pathToVault1);
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 6105c309..64836e78 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -46,7 +46,6 @@
 import static java.nio.file.StandardOpenOption.APPEND;
 import static java.util.Arrays.asList;
 import static org.cryptomator.cryptofs.CryptoFileSystemProperties.cryptoFileSystemProperties;
-import static org.cryptomator.cryptofs.CryptoFileSystemProvider.containsVault;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.mockito.Mockito.mock;
@@ -211,69 +210,6 @@ public void testNewFileSystem() throws IOException, MasterkeyLoadingFailedExcept
 		Mockito.verify(fileSystems).create(Mockito.same(inTest), Mockito.eq(pathToVault.toAbsolutePath()), Mockito.eq(properties));
 	}
 
-	@Test
-	public void testContainsVaultReturnsTrueIfDirectoryContainsVaultConfigFileAndDataDir() throws IOException {
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-
-		String vaultConfigFilename = "vaultconfig.foo.baz";
-		String masterkeyFilename = "masterkey.foo.baz";
-		Path pathToVault = fs.getPath("/vaultDir");
-
-		Path vaultConfigFile = pathToVault.resolve(vaultConfigFilename);
-		Path dataDir = pathToVault.resolve("d");
-		Files.createDirectories(dataDir);
-		Files.write(vaultConfigFile, new byte[0]);
-
-		Assertions.assertTrue(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
-	}
-
-	@Test
-	public void testContainsVaultReturnsFalseIfDirectoryContainsMasterkeyFileAndDataDir() throws IOException {
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-
-		String vaultConfigFilename = "vaultconfig.foo.baz";
-		String masterkeyFilename = "masterkey.foo.baz";
-		Path pathToVault = fs.getPath("/vaultDir");
-
-		Path masterkeyFile = pathToVault.resolve(masterkeyFilename);
-		Path dataDir = pathToVault.resolve("d");
-		Files.createDirectories(dataDir);
-		Files.write(masterkeyFile, new byte[0]);
-
-		Assertions.assertFalse(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
-	}
-
-	@Test
-	public void testContainsVaultReturnsFalseIfDirectoryContainsOnlyDataDir() throws IOException {
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-
-		String vaultConfigFilename = "vaultconfig.foo.baz";
-		String masterkeyFilename = "masterkey.foo.baz";
-		Path pathToVault = fs.getPath("/vaultDir");
-
-		Path dataDir = pathToVault.resolve("d");
-		Files.createDirectories(dataDir);
-
-		Assertions.assertFalse(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
-	}
-
-	@Test
-	public void testContainsVaultReturnsFalseIfDirectoryContainsNoDataDir() throws IOException {
-		FileSystem fs = Jimfs.newFileSystem(Configuration.unix());
-
-		String vaultConfigFilename = "vaultconfig.foo.baz";
-		String masterkeyFilename = "masterkey.foo.baz";
-		Path pathToVault = fs.getPath("/vaultDir");
-
-		Path vaultConfigFile = pathToVault.resolve(vaultConfigFilename);
-		Path masterkeyFile = pathToVault.resolve(masterkeyFilename);
-		Files.createDirectories(pathToVault);
-		Files.write(vaultConfigFile, new byte[0]);
-		Files.write(masterkeyFile, new byte[0]);
-
-		Assertions.assertFalse(containsVault(pathToVault, vaultConfigFilename, masterkeyFilename));
-	}
-
 	@Test
 	public void testGetFileSystemInvokesFileSystemsGetWithPathToVaultFromUri() {
 		Path pathToVault = get("a").toAbsolutePath();

From c7b713b639c67a78d7b735cbeee9b2c1da3c0542 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Thu, 6 May 2021 15:54:13 +0200
Subject: [PATCH 61/70] use release candidate of cryptolib

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index a3edce77..1cb32b17 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-beta7</cryptolib.version>
+		<cryptolib.version>2.0.0-rc1</cryptolib.version>
 		<jwt.version>3.15.0</jwt.version>
 		<dagger.version>2.35.1</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>

From 7b24e848bd0623467138697556f3988834f28e4d Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 21 May 2021 18:52:38 +0200
Subject: [PATCH 62/70] added module-info

---
 pom.xml                                       | 15 ++-----
 src/main/java/module-info.java                | 21 +++++++++
 .../cryptofs/CryptoFileSystems.java           |  1 -
 .../cryptofs/CryptoFileStoreTest.java         |  4 +-
 ...yptoFileSystemProviderIntegrationTest.java | 12 ++---
 .../CryptoFileSystemProviderTest.java         |  4 +-
 .../cryptofs/DirStructureTest.java            |  4 +-
 .../attr/CryptoDosFileAttributesTest.java     |  4 +-
 .../cryptofs/ch/CleartextFileChannelTest.java |  2 +-
 .../cryptofs/ch/CleartextFileLockTest.java    | 41 ++++++-----------
 .../FileSystemCapabilityCheckerTest.java      |  4 +-
 .../common/MasterkeyBackupHelperTest.java     |  4 +-
 .../dir/BrokenDirectoryFilterTest.java        |  2 +-
 .../cryptofs/dir/C9SInflatorTest.java         |  2 +-
 .../cryptofs/dir/C9rConflictResolverTest.java |  2 +-
 .../cryptofs/dir/C9rDecryptorTest.java        |  2 +-
 .../cryptofs/fh/FileHeaderHolderTest.java     |  6 +--
 .../cryptofs/fh/OpenCryptoFileTest.java       |  2 +-
 .../cryptofs/migration/MigratorsTest.java     | 45 +++++++++----------
 ...mpleMigrationContinuationListenerTest.java |  2 +-
 .../migration/v7/FilePathMigrationTest.java   |  4 +-
 21 files changed, 88 insertions(+), 95 deletions(-)
 create mode 100644 src/main/java/module-info.java

diff --git a/pom.xml b/pom.xml
index 1cb32b17..713f05a4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,15 +18,15 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-rc1</cryptolib.version>
+		<cryptolib.version>2.0.0-rc2</cryptolib.version>
 		<jwt.version>3.15.0</jwt.version>
 		<dagger.version>2.35.1</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>
 		<slf4j.version>1.7.30</slf4j.version>
 
 		<!-- test dependencies -->
-		<junit.jupiter.version>5.7.1</junit.jupiter.version>
-		<mockito.version>3.9.0</mockito.version>
+		<junit.jupiter.version>5.7.2</junit.jupiter.version>
+		<mockito.version>3.10.0</mockito.version>
 		<hamcrest.version>2.2</hamcrest.version>
 
 		<!-- build plugin dependencies -->
@@ -140,19 +140,12 @@
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-surefire-plugin</artifactId>
-				<version>2.22.2</version>
+				<version>3.0.0-M5</version>
 			</plugin>
 			<plugin>
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-jar-plugin</artifactId>
 				<version>3.2.0</version>
-				<configuration>
-					<archive>
-						<manifestEntries>
-							<Automatic-Module-Name>org.cryptomator.cryptofs</Automatic-Module-Name>
-						</manifestEntries>
-					</archive>
-				</configuration>
 			</plugin>
 			<plugin>
 				<artifactId>maven-source-plugin</artifactId>
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
new file mode 100644
index 00000000..7782d4bb
--- /dev/null
+++ b/src/main/java/module-info.java
@@ -0,0 +1,21 @@
+import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+
+import java.nio.file.spi.FileSystemProvider;
+
+module org.cryptomator.cryptofs {
+	requires transitive org.cryptomator.cryptolib;
+	requires com.google.common;
+	requires org.slf4j;
+
+	/* TODO: filename-based modules: */
+	requires java.jwt;
+	requires dagger;
+	requires static javax.inject; // probably no longer needed if dagger is an automatic module (but might require --patch-module in case of split packages)
+
+	exports org.cryptomator.cryptofs;
+	exports org.cryptomator.cryptofs.common;
+	exports org.cryptomator.cryptofs.migration;
+	exports org.cryptomator.cryptofs.migration.api;
+
+	provides FileSystemProvider with CryptoFileSystemProvider;
+}
\ No newline at end of file
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index 16c5e2ca..de071d9a 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -18,7 +18,6 @@
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
 import java.security.SecureRandom;
-import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java
index a5b5ba7a..bee34ccb 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileStoreTest.java
@@ -38,7 +38,7 @@ public class CryptoFileStoreTest {
 	
 	@Nested
 	@DisplayName("with delegate present")
-	class DelegatingCryptoFileStoreTest {
+	public class DelegatingCryptoFileStoreTest {
 
 		private final FileStore delegate = mock(FileStore.class);
 		private CryptoFileStore cryptoFileStore;
@@ -128,7 +128,7 @@ public void testGetAttribute() {
 
 	@Nested
 	@DisplayName("with delegate absent")
-	class FallbackCryptoFileStoreTest {
+	public class FallbackCryptoFileStoreTest {
 		
 		private CryptoFileStore cryptoFileStore;
 		
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
index 6ae7b903..549edc6f 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderIntegrationTest.java
@@ -74,7 +74,7 @@ public class CryptoFileSystemProviderIntegrationTest {
 
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-	class WithLimitedPaths {
+	public class WithLimitedPaths {
 
 		private MasterkeyLoader keyLoader = Mockito.mock(MasterkeyLoader.class);
 		private CryptoFileSystem fs;
@@ -170,7 +170,7 @@ public void testCopyExceedingPathLengthLimit(String path) {
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
-	class InMemory {
+	public class InMemory {
 
 		private FileSystem tmpFs;
 		private MasterkeyLoader keyLoader1;
@@ -523,7 +523,7 @@ public void testMoveFileFromOneCryptoFileSystemToAnother() throws IOException {
 	@EnabledOnOs({OS.MAC, OS.LINUX})
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	@DisplayName("On POSIX Systems")
-	class PosixTests {
+	public class PosixTests {
 
 		private FileSystem fs;
 
@@ -540,7 +540,7 @@ public void setup(@TempDir Path tmpDir) throws IOException, MasterkeyLoadingFail
 
 		@Nested
 		@DisplayName("File Locks")
-		class FileLockTests {
+		public class FileLockTests {
 
 			private Path file = fs.getPath("/lock.txt");
 
@@ -615,7 +615,7 @@ public void testOverlappingLocks(boolean shared) throws IOException {
 	@EnabledOnOs(OS.WINDOWS)
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	@DisplayName("On Windows Systems")
-	class WindowsTests {
+	public class WindowsTests {
 
 		private FileSystem fs;
 
@@ -661,7 +661,7 @@ public void testDosFileAttributes() throws IOException {
 
 		@Nested
 		@DisplayName("read-only file")
-		class OnReadOnlyFile {
+		public class OnReadOnlyFile {
 
 			private Path file = fs.getPath("/readonly.txt");
 			private DosFileAttributeView attrView;
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
index 64836e78..cba944b0 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemProviderTest.java
@@ -70,7 +70,7 @@ public class CryptoFileSystemProviderTest {
 
 	private CryptoFileSystemProvider inTest;
 
-	private static final Stream<InvocationWhichShouldFail> shouldFailWithProviderMismatch() {
+	public static Stream<InvocationWhichShouldFail> shouldFailWithProviderMismatch() {
 		return Stream.of( //
 				invocation("newAsynchronousFileChannel", (inTest, path) -> inTest.newAsynchronousFileChannel(path, new HashSet<>(), mock(ExecutorService.class))), //
 				invocation("newFileChannel", (inTest, path) -> inTest.newFileChannel(path, new HashSet<>())), //
@@ -91,7 +91,7 @@ private static final Stream<InvocationWhichShouldFail> shouldFailWithProviderMis
 	}
 
 	@SuppressWarnings("unchecked")
-	private static final Stream<InvocationWhichShouldFail> shouldFailWithRelativePath() {
+	public static Stream<InvocationWhichShouldFail> shouldFailWithRelativePath() {
 		return Stream.of( //
 				invocation("newAsynchronousFileChannel", (inTest, path) -> inTest.newAsynchronousFileChannel(path, new HashSet<>(), mock(ExecutorService.class))), //
 				invocation("newFileChannel", (inTest, path) -> inTest.newFileChannel(path, new HashSet<>())), //
diff --git a/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java b/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java
index f35818bc..2cb27ddb 100644
--- a/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/DirStructureTest.java
@@ -20,7 +20,7 @@ public class DirStructureTest {
 	private static final String CONFIG = "config";
 
 	@TempDir
-	Path vaultPath;
+	public Path vaultPath;
 
 	@Test
 	public void testNonExistingVaultPathThrowsIOException() {
@@ -59,7 +59,7 @@ public void testAllCombosOfDataAndConfigAndKey(boolean createDataDir, boolean cr
 		Assertions.assertEquals(expectedResult, DirStructure.checkDirStructure(vaultPath, CONFIG, KEY));
 	}
 
-	private static Stream<Arguments> provideAllCases() {
+	public static Stream<Arguments> provideAllCases() {
 		return Stream.of(
 				Arguments.of(true, true, true, DirStructure.VAULT),
 				Arguments.of(true, true, false, DirStructure.VAULT),
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java
index 05d6c2e8..31c3ffd5 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoDosFileAttributesTest.java
@@ -45,7 +45,7 @@ public void setup() {
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	@DisplayName("on read-write filesystem")
-	class ReadWriteFileSystem {
+	public class ReadWriteFileSystem {
 
 		private CryptoDosFileAttributes inTest;
 
@@ -96,7 +96,7 @@ public void testIsSystemDelegates(boolean value) {
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	@DisplayName("on read-only filesystem")
-	class ReadOnlyFileSystem {
+	public class ReadOnlyFileSystem {
 
 		private CryptoDosFileAttributes inTest;
 
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
index b38f4458..2856e31f 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileChannelTest.java
@@ -252,7 +252,7 @@ public void testMapThrowsUnsupportedOperationException() throws IOException {
 	}
 
 	@Nested
-	class Locking {
+	public class Locking {
 
 		private FileLock delegate = Mockito.mock(FileLock.class);
 
diff --git a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java
index 9171bead..e439be59 100644
--- a/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/ch/CleartextFileLockTest.java
@@ -22,16 +22,18 @@ public class CleartextFileLockTest {
 	@BeforeEach
 	public void setup() {
 		channel = Mockito.mock(FileChannel.class);
+		delegate = Mockito.mock(FileLock.class);
 		Mockito.when(channel.isOpen()).thenReturn(true);
 	}
 
 	@Nested
 	@DisplayName("Shared Locks")
-	class ValidSharedLockTests {
+	public class SharedLockTests {
 
 		@BeforeEach
 		public void setup() {
-			delegate = Mockito.spy(new FileLockMock(channel, position, size, true));
+			Mockito.when(delegate.isValid()).thenReturn(true);
+			Mockito.when(delegate.isShared()).thenReturn(true);
 			inTest = new CleartextFileLock(channel, delegate, position, size);
 		}
 
@@ -73,11 +75,12 @@ public void testIsValid() {
 
 		@Nested
 		@DisplayName("After releasing the lock")
-		class ReleasedLock {
+		public class ReleasedLock {
 
 			@BeforeEach
 			public void setup() throws IOException {
 				inTest.release();
+				Mockito.when(delegate.isValid()).thenReturn(false);
 			}
 
 			@Test
@@ -96,7 +99,7 @@ public void testReleaseDelegate() throws IOException {
 
 		@Nested
 		@DisplayName("After closing the channel")
-		class ClosedChannel {
+		public class ClosedChannel {
 
 			@BeforeEach
 			public void setup() throws IOException {
@@ -115,11 +118,12 @@ public void testIsValid() {
 
 	@Nested
 	@DisplayName("Exclusive Locks")
-	class InvalidSharedLockTests {
+	public class ExclusiveLockTests {
 
 		@BeforeEach
 		public void setup() {
-			delegate = Mockito.spy(new FileLockMock(channel, position, size, false));
+			Mockito.when(delegate.isValid()).thenReturn(true);
+			Mockito.when(delegate.isShared()).thenReturn(false);
 			inTest = new CleartextFileLock(channel, delegate, position, size);
 		}
 
@@ -161,11 +165,12 @@ public void testIsValid() {
 
 		@Nested
 		@DisplayName("After releasing the lock")
-		class ReleasedLock {
+		public class ReleasedLock {
 
 			@BeforeEach
 			public void setup() throws IOException {
 				inTest.release();
+				Mockito.when(delegate.isValid()).thenReturn(false);
 			}
 
 			@Test
@@ -184,7 +189,7 @@ public void testReleaseDelegate() throws IOException {
 
 		@Nested
 		@DisplayName("After closing the channel")
-		class ClosedChannel {
+		public class ClosedChannel {
 
 			@BeforeEach
 			public void setup() throws IOException {
@@ -201,24 +206,4 @@ public void testIsValid() {
 
 	}
 
-	private static class FileLockMock extends FileLock {
-
-		private boolean valid;
-
-		protected FileLockMock(FileChannel channel, long position, long size, boolean shared) {
-			super(channel, position, size, shared);
-			this.valid = true;
-		}
-
-		@Override
-		public boolean isValid() {
-			return valid;
-		}
-
-		@Override
-		public void release() {
-			valid = false;
-		}
-	}
-
 }
diff --git a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
index 4f7fefd1..1c363eaf 100644
--- a/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/common/FileSystemCapabilityCheckerTest.java
@@ -19,11 +19,11 @@
 import java.nio.file.spi.FileSystemProvider;
 import java.util.Collections;
 
-class FileSystemCapabilityCheckerTest {
+public class FileSystemCapabilityCheckerTest {
 	
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-	class PathLengthLimits {
+	public class PathLengthLimits {
 		
 		private Path pathToVault = Mockito.mock(Path.class);
 		private Path cDir = Mockito.mock(Path.class);
diff --git a/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java b/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java
index f4b4c15a..20467f00 100644
--- a/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/common/MasterkeyBackupHelperTest.java
@@ -15,7 +15,7 @@
 import java.util.Random;
 import java.util.stream.Stream;
 
-class MasterkeyBackupHelperTest {
+public class MasterkeyBackupHelperTest {
 
 	@EnabledOnOs({OS.LINUX, OS.MAC})
 	@ParameterizedTest
@@ -47,7 +47,7 @@ public void testBackupFileWin(byte[] contents, @TempDir Path tmp) throws IOExcep
 		Assertions.assertEquals(backupFile, backupFile2);
 	}
 
-	static Stream<byte[]> createRandomBytes() {
+	public static Stream<byte[]> createRandomBytes() {
 		Random rnd = new Random(42l);
 		return Stream.generate(() -> {
 			byte[] bytes = new byte[100];
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java
index 1741b73b..e18810e1 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/BrokenDirectoryFilterTest.java
@@ -11,7 +11,7 @@
 import java.nio.file.Path;
 import java.util.stream.Stream;
 
-class BrokenDirectoryFilterTest {
+public class BrokenDirectoryFilterTest {
 
 	private CryptoPathMapper cryptoPathMapper = Mockito.mock(CryptoPathMapper.class);
 	private BrokenDirectoryFilter brokenDirectoryFilter = new BrokenDirectoryFilter(cryptoPathMapper);
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java
index 9bdb7f8b..e0ec9c00 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9SInflatorTest.java
@@ -13,7 +13,7 @@
 import java.nio.file.Paths;
 import java.util.stream.Stream;
 
-class C9SInflatorTest {
+public class C9SInflatorTest {
 
 	private LongFileNameProvider longFileNameProvider;
 	private Cryptor cryptor;
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
index 75113ae3..fc868950 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rConflictResolverTest.java
@@ -18,7 +18,7 @@
 import java.nio.file.Paths;
 import java.util.stream.Stream;
 
-class C9rConflictResolverTest {
+public class C9rConflictResolverTest {
 
 	private Cryptor cryptor;
 	private FileNameCryptor fileNameCryptor;
diff --git a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
index 6cf26197..05455820 100644
--- a/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/dir/C9rDecryptorTest.java
@@ -15,7 +15,7 @@
 import java.util.Optional;
 import java.util.stream.Stream;
 
-class C9rDecryptorTest {
+public class C9rDecryptorTest {
 	
 	private Cryptor cryptor;
 	private FileNameCryptor fileNameCryptor;
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
index c8e92b69..bad4f2cd 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/FileHeaderHolderTest.java
@@ -24,7 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-class FileHeaderHolderTest {
+public class FileHeaderHolderTest {
 
 	static {
 		System.setProperty("org.slf4j.simpleLogger.log.org.cryptomator.cryptofs.ch.FileHeaderHolder", "trace");
@@ -46,7 +46,7 @@ public void setup() throws IOException {
 
 	@Nested
 	@DisplayName("existing header")
-	class ExistingHeader {
+	public class ExistingHeader {
 
 		private FileHeader headerToLoad = Mockito.mock(FileHeader.class);
 		private FileChannel channel = Mockito.mock(FileChannel.class);
@@ -81,7 +81,7 @@ public void testLoadExisting() throws IOException, AuthenticationFailedException
 
 	@Nested
 	@DisplayName("new header")
-	class NewHeader {
+	public class NewHeader {
 
 		private FileHeader headerToCreate = Mockito.mock(FileHeader.class);
 
diff --git a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
index f424d770..13873697 100644
--- a/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/fh/OpenCryptoFileTest.java
@@ -90,7 +90,7 @@ public void testCloseImmediatelyIfOpeningFirstChannelFails() {
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
 	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
 	@DisplayName("FileChannels")
-	class FileChannelFactoryTest {
+	public class FileChannelFactoryTest {
 
 		private OpenCryptoFile openCryptoFile;
 		private CleartextFileChannel cleartextFileChannel;
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
index f17acd8b..5cb2295e 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/MigratorsTest.java
@@ -24,46 +24,39 @@
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.FileSystem;
 import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.spi.FileSystemProvider;
 import java.util.Collections;
 import java.util.Map;
 
 public class MigratorsTest {
 
-	private MockedStatic<Files> filesClass;
 	private Path pathToVault;
-	private FileSystemCapabilityChecker fsCapabilityChecker;
 	private Path vaultConfigPath;
 	private Path masterkeyPath;
+	private FileSystemCapabilityChecker fsCapabilityChecker;
 
 	@BeforeEach
-	public void setup() {
-		filesClass = Mockito.mockStatic(Files.class);
-		pathToVault = Mockito.mock(Path.class, "path/to/vault");
+	public void setup(@TempDir Path tmpDir) {
+		pathToVault = tmpDir;
+		vaultConfigPath = tmpDir.resolve("vault.cryptomator");
+		masterkeyPath = tmpDir.resolve("masterkey.cryptomator");
 		fsCapabilityChecker = Mockito.mock(FileSystemCapabilityChecker.class);
-		vaultConfigPath = Mockito.mock(Path.class, "path/to/vault/vault.cryptomator");
-		masterkeyPath = Mockito.mock(Path.class, "path/to/vault/masterkey.cryptomator");
-
-		Mockito.when(pathToVault.resolve("masterkey.cryptomator")).thenReturn(masterkeyPath);
-		Mockito.when(pathToVault.resolve("vault.cryptomator")).thenReturn(vaultConfigPath);
-	}
-
-	@AfterEach
-	public void tearDown() {
-		filesClass.close();
 	}
 
 	@Test
 	@DisplayName("can't determine vault version without masterkey.cryptomator or vault.cryptomator")
 	public void throwsExceptionIfNeitherMasterkeyNorVaultConfigExists() {
-		filesClass.when(() -> Files.exists(vaultConfigPath)).thenReturn(false);
-		filesClass.when(() -> Files.exists(masterkeyPath)).thenReturn(false);
-
 		Migrators migrators = new Migrators(Collections.emptyMap(), fsCapabilityChecker);
 
 		IOException thrown = Assertions.assertThrows(IOException.class, () -> {
@@ -79,13 +72,14 @@ public class WithExistingVaultConfig {
 		private VaultConfig.UnverifiedVaultConfig unverifiedVaultConfig;
 
 		@BeforeEach
-		public void setup() {
-			Assumptions.assumeFalse(Files.exists(masterkeyPath));
+		public void setup() throws IOException {
 			vaultConfigClass = Mockito.mockStatic(VaultConfig.class);
 			unverifiedVaultConfig = Mockito.mock(VaultConfig.UnverifiedVaultConfig.class);
 
-			filesClass.when(() -> Files.exists(vaultConfigPath)).thenReturn(true);
-			filesClass.when(() -> Files.readString(vaultConfigPath)).thenReturn("vault-config");
+			Files.write(vaultConfigPath, "vault-config".getBytes(StandardCharsets.UTF_8));
+			Assumptions.assumeTrue(Files.exists(vaultConfigPath));
+			Assumptions.assumeFalse(Files.exists(masterkeyPath));
+
 			vaultConfigClass.when(() -> VaultConfig.decode("vault-config")).thenReturn(unverifiedVaultConfig);
 		}
 
@@ -164,10 +158,11 @@ public class WithExistingMasterkeyFile {
 		private MockedStatic<MasterkeyFileAccess> masterkeyFileAccessClass;
 
 		@BeforeEach
-		public void setup() {
-			Assumptions.assumeFalse(Files.exists(vaultConfigPath));
+		public void setup() throws IOException {
 			masterkeyFileAccessClass = Mockito.mockStatic(MasterkeyFileAccess.class);
-			filesClass.when(() -> Files.exists(masterkeyPath)).thenReturn(true);
+			Files.createFile(masterkeyPath);
+			Assumptions.assumeFalse(Files.exists(vaultConfigPath));
+			Assumptions.assumeTrue(Files.exists(masterkeyPath));
 		}
 
 		@AfterEach
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java b/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java
index 3e2738d9..7db601f9 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/api/SimpleMigrationContinuationListenerTest.java
@@ -9,7 +9,7 @@
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
-class SimpleMigrationContinuationListenerTest {
+public class SimpleMigrationContinuationListenerTest {
 	
 	@Test
 	public void testConcurrency() {
diff --git a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java
index 120592bb..9d0cb5a0 100644
--- a/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/migration/v7/FilePathMigrationTest.java
@@ -145,7 +145,7 @@ public void testGetTargetPath(String oldCanonicalName, String attemptSuffix, Str
 	@DisplayName("FilePathMigration.parse(...)")
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-	class Parsing {
+	public class Parsing {
 
 		private FileSystem fs;
 		private Path vaultRoot;
@@ -275,7 +275,7 @@ public void testParseShortenedFile(String oldPath, String metadataFilePath, Stri
 	@DisplayName("FilePathMigration.parse(...).get().migrate(...)")
 	@Nested
 	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-	class Migrating {
+	public class Migrating {
 
 		private FileSystem fs;
 		private Path vaultRoot;

From 6445f1ddb06c201dfa6e4e9c81459201c17dec70 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Fri, 21 May 2021 21:32:33 +0200
Subject: [PATCH 63/70] fix for https://issues.sonatype.org/browse/OSSRH-66257
 cherry-picked from 2.1.0 branch

---
 .github/workflows/publish-central.yml | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/.github/workflows/publish-central.yml b/.github/workflows/publish-central.yml
index d9be480e..93b31389 100644
--- a/.github/workflows/publish-central.yml
+++ b/.github/workflows/publish-central.yml
@@ -32,6 +32,11 @@ jobs:
       - name: Deploy
         run: mvn deploy -B -DskipTests -Psign,deploy-central --no-transfer-progress
         env:
+          MAVEN_OPTS: >
+            --add-opens=java.base/java.util=ALL-UNNAMED
+            --add-opens=java.base/java.lang.reflect=ALL-UNNAMED
+            --add-opens=java.base/java.text=ALL-UNNAMED
+            --add-opens=java.desktop/java.awt.font=ALL-UNNAMED
           MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
           MAVEN_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
           MAVEN_GPG_PASSPHRASE: ${{ secrets.RELEASES_GPG_PASSPHRASE }}
\ No newline at end of file

From e32836d8b7594d107cfe1728e95ddbcf81179a42 Mon Sep 17 00:00:00 2001
From: Armin Schrenk <armin.schrenk@skymatic.de>
Date: Tue, 15 Jun 2021 10:57:03 +0200
Subject: [PATCH 64/70] add IDE specific code style

---
 .idea/codeStyles/Project.xml         | 78 ++++++++++++++++++++++++++++
 .idea/codeStyles/codeStyleConfig.xml |  5 ++
 2 files changed, 83 insertions(+)
 create mode 100644 .idea/codeStyles/Project.xml
 create mode 100644 .idea/codeStyles/codeStyleConfig.xml

diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
new file mode 100644
index 00000000..d361191e
--- /dev/null
+++ b/.idea/codeStyles/Project.xml
@@ -0,0 +1,78 @@
+<component name="ProjectCodeStyleConfiguration">
+  <code_scheme name="Project" version="173">
+    <option name="OTHER_INDENT_OPTIONS">
+      <value>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </value>
+    </option>
+    <option name="LINE_SEPARATOR" value="&#10;" />
+    <option name="RIGHT_MARGIN" value="220" />
+    <option name="FORMATTER_TAGS_ENABLED" value="true" />
+    <JavaCodeStyleSettings>
+      <option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
+      <option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="10" />
+      <option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
+        <value />
+      </option>
+      <option name="IMPORT_LAYOUT_TABLE">
+        <value>
+          <package name="" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="javax" withSubpackages="true" static="false" />
+          <package name="javafx" withSubpackages="true" static="false" />
+          <package name="java" withSubpackages="true" static="false" />
+          <emptyLine />
+          <package name="" withSubpackages="true" static="true" />
+        </value>
+      </option>
+      <option name="JD_ALIGN_PARAM_COMMENTS" value="false" />
+      <option name="JD_ALIGN_EXCEPTION_COMMENTS" value="false" />
+    </JavaCodeStyleSettings>
+    <Properties>
+      <option name="KEEP_BLANK_LINES" value="true" />
+    </Properties>
+    <XML>
+      <option name="XML_ATTRIBUTE_WRAP" value="0" />
+    </XML>
+    <codeStyleSettings language="CSS">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="Groovy">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="HTML">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="JAVA">
+      <option name="KEEP_LINE_BREAKS" value="false" />
+      <option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
+      <option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
+      <option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
+      <option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
+      <option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
+      <option name="ENUM_CONSTANTS_WRAP" value="2" />
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+      <arrangement>
+        <rules />
+      </arrangement>
+    </codeStyleSettings>
+    <codeStyleSettings language="JSON">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+    <codeStyleSettings language="XML">
+      <indentOptions>
+        <option name="USE_TAB_CHARACTER" value="true" />
+      </indentOptions>
+    </codeStyleSettings>
+  </code_scheme>
+</component>
\ No newline at end of file
diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..79ee123c
--- /dev/null
+++ b/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+<component name="ProjectCodeStyleConfiguration">
+  <state>
+    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
+  </state>
+</component>
\ No newline at end of file

From fb251f77407160f24cb491d59a535fff53727a04 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Thu, 17 Jun 2021 08:38:06 +0200
Subject: [PATCH 65/70] bumped dependency

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 713f05a4..6480821c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-rc2</cryptolib.version>
+		<cryptolib.version>2.0.0-rc4</cryptolib.version>
 		<jwt.version>3.15.0</jwt.version>
 		<dagger.version>2.35.1</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>

From 59ff24cc4752830c8a51e93ec2839e085c49135e Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Thu, 24 Jun 2021 14:42:04 +0200
Subject: [PATCH 66/70] dependency bump

---
 pom.xml                        | 12 ++++++------
 src/main/java/module-info.java |  9 +++++++--
 2 files changed, 13 insertions(+), 8 deletions(-)

diff --git a/pom.xml b/pom.xml
index 6480821c..b440f7b3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,20 +18,20 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-rc4</cryptolib.version>
+		<cryptolib.version>2.0.0-rc5</cryptolib.version>
 		<jwt.version>3.15.0</jwt.version>
-		<dagger.version>2.35.1</dagger.version>
+		<dagger.version>2.37</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>
-		<slf4j.version>1.7.30</slf4j.version>
+		<slf4j.version>1.7.31</slf4j.version>
 
 		<!-- test dependencies -->
 		<junit.jupiter.version>5.7.2</junit.jupiter.version>
-		<mockito.version>3.10.0</mockito.version>
+		<mockito.version>3.11.2</mockito.version>
 		<hamcrest.version>2.2</hamcrest.version>
 
 		<!-- build plugin dependencies -->
-		<dependency-check.version>6.1.6</dependency-check.version>
-		<jacoco.version>0.8.6</jacoco.version>
+		<dependency-check.version>6.2.2</dependency-check.version>
+		<jacoco.version>0.8.7</jacoco.version>
 		<nexus-staging.version>1.6.8</nexus-staging.version>
 	</properties>
 
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 7782d4bb..49a75a59 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -6,11 +6,16 @@
 	requires transitive org.cryptomator.cryptolib;
 	requires com.google.common;
 	requires org.slf4j;
+	requires dagger;
 
 	/* TODO: filename-based modules: */
 	requires java.jwt;
-	requires dagger;
-	requires static javax.inject; // probably no longer needed if dagger is an automatic module (but might require --patch-module in case of split packages)
+
+	// filename-based module required by dagger
+	// we will probably need to live with this for a while:
+	// https://github.com/javax-inject/javax-inject/issues/33
+	// May be provided by another lib during runtime
+	requires static javax.inject;
 
 	exports org.cryptomator.cryptofs;
 	exports org.cryptomator.cryptofs.common;

From 1178bf1f6c8c3c078a7faf447e3a6b8ec0781807 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Sat, 26 Jun 2021 13:28:59 +0200
Subject: [PATCH 67/70] dependency bump

---
 pom.xml                        | 2 +-
 src/main/java/module-info.java | 4 +---
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index b440f7b3..0d79dafb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
 
 		<!-- dependencies -->
 		<cryptolib.version>2.0.0-rc5</cryptolib.version>
-		<jwt.version>3.15.0</jwt.version>
+		<jwt.version>3.17.0</jwt.version>
 		<dagger.version>2.37</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>
 		<slf4j.version>1.7.31</slf4j.version>
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 49a75a59..c645dcc5 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -7,9 +7,7 @@
 	requires com.google.common;
 	requires org.slf4j;
 	requires dagger;
-
-	/* TODO: filename-based modules: */
-	requires java.jwt;
+	requires com.auth0.jwt;
 
 	// filename-based module required by dagger
 	// we will probably need to live with this for a while:

From 9e6f94f07056f34bfbe3dfac02a914b3ec0b6e31 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 28 Jun 2021 15:39:02 +0200
Subject: [PATCH 68/70] adjusted to latest cryptolib api changes

---
 pom.xml                                       |  2 +-
 .../cryptofs/CryptoFileSystemProperties.java  | 13 +++----
 .../cryptofs/CryptoFileSystemProvider.java    |  3 +-
 .../cryptofs/CryptoFileSystems.java           |  3 +-
 .../cryptofs/VaultCipherCombo.java            | 34 -------------------
 .../org/cryptomator/cryptofs/VaultConfig.java | 11 +++---
 .../attr/CryptoBasicFileAttributes.java       |  4 +--
 .../cryptofs/ch/CleartextFileChannel.java     |  3 +-
 .../cryptofs/fh/OpenCryptoFile.java           |  4 +--
 .../cryptofs/fh/OpenCryptoFileModule.java     |  2 --
 .../cryptofs/migration/MigrationModule.java   |  8 -----
 .../cryptofs/CryptoFileSystemsTest.java       | 10 +++---
 .../cryptomator/cryptofs/VaultConfigTest.java |  7 ++--
 .../attr/CryptoBasicFileAttributesTest.java   |  1 +
 14 files changed, 34 insertions(+), 71 deletions(-)
 delete mode 100644 src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java

diff --git a/pom.xml b/pom.xml
index 0d79dafb..6b6c5c8f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-rc5</cryptolib.version>
+		<cryptolib.version>2.0.0-rc6</cryptolib.version>
 		<jwt.version>3.17.0</jwt.version>
 		<dagger.version>2.37</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
index 39d3c04d..d9e1b944 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProperties.java
@@ -9,6 +9,7 @@
 package org.cryptomator.cryptofs;
 
 import com.google.common.base.Strings;
+import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 
 import java.net.URI;
@@ -91,7 +92,7 @@ public enum FileSystemFlags {
 	 */
 	public static final String PROPERTY_CIPHER_COMBO = "cipherCombo";
 
-	static final VaultCipherCombo DEFAULT_CIPHER_COMBO = VaultCipherCombo.SIV_GCM;
+	static final CryptorProvider.Scheme DEFAULT_CIPHER_COMBO = CryptorProvider.Scheme.SIV_GCM;
 
 	private final Set<Entry<String, Object>> entries;
 
@@ -110,8 +111,8 @@ MasterkeyLoader keyLoader() {
 		return (MasterkeyLoader) get(PROPERTY_KEYLOADER);
 	}
 
-	public VaultCipherCombo cipherCombo() {
-		return (VaultCipherCombo) get(PROPERTY_CIPHER_COMBO);
+	public CryptorProvider.Scheme cipherCombo() {
+		return (CryptorProvider.Scheme) get(PROPERTY_CIPHER_COMBO);
 	}
 
 	@SuppressWarnings("unchecked")
@@ -183,7 +184,7 @@ public static CryptoFileSystemProperties wrap(Map<String, ?> properties) {
 	 */
 	public static class Builder {
 
-		public VaultCipherCombo cipherCombo = DEFAULT_CIPHER_COMBO;
+		public CryptorProvider.Scheme cipherCombo = DEFAULT_CIPHER_COMBO;
 		private MasterkeyLoader keyLoader = null;
 		private final Set<FileSystemFlags> flags = EnumSet.copyOf(DEFAULT_FILESYSTEM_FLAGS);
 		private String vaultConfigFilename = DEFAULT_VAULTCONFIG_FILENAME;
@@ -199,7 +200,7 @@ private Builder(Map<String, ?> properties) {
 			checkedSet(String.class, PROPERTY_MASTERKEY_FILENAME, properties, this::withMasterkeyFilename);
 			checkedSet(Set.class, PROPERTY_FILESYSTEM_FLAGS, properties, this::withFlags);
 			checkedSet(Integer.class, PROPERTY_MAX_CLEARTEXT_NAME_LENGTH, properties, this::withMaxCleartextNameLength);
-			checkedSet(VaultCipherCombo.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo);
+			checkedSet(CryptorProvider.Scheme.class, PROPERTY_CIPHER_COMBO, properties, this::withCipherCombo);
 		}
 
 		private <T> void checkedSet(Class<T> type, String key, Map<String, ?> properties, Consumer<T> setter) {
@@ -235,7 +236,7 @@ public Builder withMaxCleartextNameLength(int maxCleartextNameLength) {
 		 * @return this
 		 * @since 2.0.0
 		 */
-		public Builder withCipherCombo(VaultCipherCombo cipherCombo) {
+		public Builder withCipherCombo(CryptorProvider.Scheme cipherCombo) {
 			this.cipherCombo = cipherCombo;
 			return this;
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
index 19d5be63..d6d32188 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystemProvider.java
@@ -12,6 +12,7 @@
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 
@@ -143,7 +144,7 @@ public static void initialize(Path pathToVault, CryptoFileSystemProperties prope
 		byte[] rawKey = new byte[0];
 		var config = VaultConfig.createNew().cipherCombo(properties.cipherCombo()).shorteningThreshold(Constants.DEFAULT_SHORTENING_THRESHOLD).build();
 		try (Masterkey key = properties.keyLoader().loadKey(keyId);
-			 Cryptor cryptor = config.getCipherCombo().getCryptorProvider(strongSecureRandom()).withKey(key)) {
+			 Cryptor cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key, strongSecureRandom())) {
 			rawKey = key.getEncoded();
 			// save vault config:
 			Path vaultConfigPath = pathToVault.resolve(properties.vaultConfigFilename());
diff --git a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
index de071d9a..81f422bf 100644
--- a/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
+++ b/src/main/java/org/cryptomator/cryptofs/CryptoFileSystems.java
@@ -3,6 +3,7 @@
 import org.cryptomator.cryptofs.common.Constants;
 import org.cryptomator.cryptofs.common.FileSystemCapabilityChecker;
 import org.cryptomator.cryptolib.api.Cryptor;
+import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
 import org.slf4j.Logger;
@@ -51,7 +52,7 @@ public CryptoFileSystemImpl create(CryptoFileSystemProvider provider, Path pathT
 		try (Masterkey key = properties.keyLoader().loadKey(keyId)) {
 			var config = configLoader.verify(key.getEncoded(), Constants.VAULT_VERSION);
 			var adjustedProperties = adjustForCapabilities(pathToVault, properties);
-			var cryptor = config.getCipherCombo().getCryptorProvider(csprng).withKey(key.clone());
+			var cryptor = CryptorProvider.forScheme(config.getCipherCombo()).provide(key.clone(), csprng);
 			try {
 				checkVaultRootExistence(pathToVault, cryptor);
 				return fileSystems.compute(normalizedPathToVault, (path, fs) -> {
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java b/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
deleted file mode 100644
index 101cdd0c..00000000
--- a/src/main/java/org/cryptomator/cryptofs/VaultCipherCombo.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package org.cryptomator.cryptofs;
-
-import org.cryptomator.cryptolib.Cryptors;
-import org.cryptomator.cryptolib.api.CryptorProvider;
-
-import java.security.SecureRandom;
-import java.util.function.Function;
-
-/**
- * A combination of different ciphers and/or cipher modes in a Cryptomator vault.
- */
-public enum VaultCipherCombo {
-	/**
-	 * AES-SIV for file name encryption
-	 * AES-CTR + HMAC for content encryption
-	 */
-	SIV_CTRMAC(Cryptors::version1),
-
-	/**
-	 * AES-SIV for file name encryption
-	 * AES-GCM for content encryption
-	 */
-	SIV_GCM(Cryptors::version2);
-
-	private final Function<SecureRandom, CryptorProvider> cryptorProvider;
-
-	VaultCipherCombo(Function<SecureRandom, CryptorProvider> cryptorProvider) {
-		this.cryptorProvider = cryptorProvider;
-	}
-
-	public CryptorProvider getCryptorProvider(SecureRandom csprng) {
-		return cryptorProvider.apply(csprng);
-	}
-}
diff --git a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
index 18356d0a..95e63312 100644
--- a/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
+++ b/src/main/java/org/cryptomator/cryptofs/VaultConfig.java
@@ -8,6 +8,7 @@
 import com.auth0.jwt.exceptions.SignatureVerificationException;
 import com.auth0.jwt.interfaces.DecodedJWT;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
@@ -36,13 +37,13 @@ public class VaultConfig {
 
 	private final String id;
 	private final int vaultVersion;
-	private final VaultCipherCombo cipherCombo;
+	private final CryptorProvider.Scheme cipherCombo;
 	private final int shorteningThreshold;
 
 	private VaultConfig(DecodedJWT verifiedConfig) {
 		this.id = verifiedConfig.getId();
 		this.vaultVersion = verifiedConfig.getClaim(JSON_KEY_VAULTVERSION).asInt();
-		this.cipherCombo = VaultCipherCombo.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString());
+		this.cipherCombo = CryptorProvider.Scheme.valueOf(verifiedConfig.getClaim(JSON_KEY_CIPHERCONFIG).asString());
 		this.shorteningThreshold = verifiedConfig.getClaim(JSON_KEY_SHORTENING_THRESHOLD).asInt();
 	}
 
@@ -61,7 +62,7 @@ public int getVaultVersion() {
 		return vaultVersion;
 	}
 
-	public VaultCipherCombo getCipherCombo() {
+	public CryptorProvider.Scheme getCipherCombo() {
 		return cipherCombo;
 	}
 
@@ -184,10 +185,10 @@ public static class VaultConfigBuilder {
 
 		private final String id = UUID.randomUUID().toString();
 		private final int vaultVersion = Constants.VAULT_VERSION;
-		private VaultCipherCombo cipherCombo;
+		private CryptorProvider.Scheme cipherCombo;
 		private int shorteningThreshold;
 
-		public VaultConfigBuilder cipherCombo(VaultCipherCombo cipherCombo) {
+		public VaultConfigBuilder cipherCombo(CryptorProvider.Scheme cipherCombo) {
 			this.cipherCombo = cipherCombo;
 			return this;
 		}
diff --git a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java
index 92a8d320..252a9a3f 100644
--- a/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java
+++ b/src/main/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributes.java
@@ -10,7 +10,6 @@
 
 import org.cryptomator.cryptofs.common.CiphertextFileType;
 import org.cryptomator.cryptofs.fh.OpenCryptoFile;
-import org.cryptomator.cryptolib.Cryptors;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -53,7 +52,8 @@ private static long getPlaintextFileSize(Path ciphertextPath, long size, Optiona
 
 	private static long calculatePlaintextFileSize(Path ciphertextPath, long size, Cryptor cryptor) {
 		try {
-			return Cryptors.cleartextSize(size - cryptor.fileHeaderCryptor().headerSize(), cryptor);
+			long payloadSize = size - cryptor.fileHeaderCryptor().headerSize();
+			return cryptor.fileContentCryptor().cleartextSize(payloadSize);
 		} catch (IllegalArgumentException e) {
 			LOG.warn("Unable to calculate cleartext file size for {}. Ciphertext size (including header): {}", ciphertextPath, size);
 			return 0l;
diff --git a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
index 75abe860..b41b626d 100644
--- a/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
+++ b/src/main/java/org/cryptomator/cryptofs/ch/CleartextFileChannel.java
@@ -32,7 +32,6 @@
 
 import static java.lang.Math.max;
 import static java.lang.Math.min;
-import static org.cryptomator.cryptolib.Cryptors.ciphertextSize;
 
 @ChannelScoped
 public class CleartextFileChannel extends AbstractFileChannel {
@@ -192,7 +191,7 @@ protected void truncateLocked(long newSize) throws IOException {
 			if (sizeOfIncompleteChunk > 0) {
 				chunkCache.get(indexOfLastChunk).truncate(sizeOfIncompleteChunk);
 			}
-			long ciphertextFileSize = cryptor.fileHeaderCryptor().headerSize() + ciphertextSize(newSize, cryptor);
+			long ciphertextFileSize = cryptor.fileHeaderCryptor().headerSize() + cryptor.fileContentCryptor().ciphertextSize(newSize);
 			chunkCache.invalidateAll(); // make sure no chunks _after_ newSize exist that would otherwise be written during the next cache eviction
 			ciphertextFileChannel.truncate(ciphertextFileSize);
 			position = min(newSize, position);
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
index d9352fa6..98b22756 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFile.java
@@ -11,7 +11,6 @@
 import org.cryptomator.cryptofs.EffectiveOpenOptions;
 import org.cryptomator.cryptofs.ch.ChannelComponent;
 import org.cryptomator.cryptofs.ch.CleartextFileChannel;
-import org.cryptomator.cryptolib.Cryptors;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.FileHeader;
 import org.slf4j.Logger;
@@ -135,7 +134,8 @@ private void initFileSize(FileChannel ciphertextFileChannel) throws IOException
 			try {
 				long ciphertextSize = ciphertextFileChannel.size();
 				if (ciphertextSize > 0l) {
-					cleartextSize = Cryptors.cleartextSize(ciphertextSize - cryptor.fileHeaderCryptor().headerSize(), cryptor);
+					long payloadSize = ciphertextSize - cryptor.fileHeaderCryptor().headerSize();
+					cleartextSize = cryptor.fileContentCryptor().cleartextSize(payloadSize);
 				}
 			} catch (IllegalArgumentException e) {
 				LOG.warn("Invalid cipher text file size. Assuming empty file.", e);
diff --git a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java
index 281d8e7f..81cac714 100644
--- a/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/fh/OpenCryptoFileModule.java
@@ -18,8 +18,6 @@
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.function.Supplier;
 
-import static org.cryptomator.cryptolib.Cryptors.cleartextSize;
-
 @Module
 public class OpenCryptoFileModule {
 
diff --git a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
index 8b777654..fcc9ba7e 100644
--- a/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
+++ b/src/main/java/org/cryptomator/cryptofs/migration/MigrationModule.java
@@ -14,13 +14,10 @@
 import org.cryptomator.cryptofs.migration.v6.Version6Migrator;
 import org.cryptomator.cryptofs.migration.v7.Version7Migrator;
 import org.cryptomator.cryptofs.migration.v8.Version8Migrator;
-import org.cryptomator.cryptolib.Cryptors;
-import org.cryptomator.cryptolib.api.CryptorProvider;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
-import java.security.SecureRandom;
 
 import static java.lang.annotation.ElementType.METHOD;
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
@@ -28,11 +25,6 @@
 @Module
 class MigrationModule {
 
-	@Provides
-	CryptorProvider provideVersion1CryptorProvider(SecureRandom csprng) {
-		return Cryptors.version1(csprng);
-	}
-
 	@Provides
 	FileSystemCapabilityChecker provideFileSystemCapabilityChecker() {
 		return new FileSystemCapabilityChecker();
diff --git a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
index fd2e6bca..37dd143b 100644
--- a/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/CryptoFileSystemsTest.java
@@ -50,7 +50,7 @@ public class CryptoFileSystemsTest {
 	private final Masterkey clonedMasterkey = Mockito.mock(Masterkey.class);
 	private final byte[] rawKey = new byte[64];
 	private final VaultConfig vaultConfig = mock(VaultConfig.class);
-	private final VaultCipherCombo cipherCombo = mock(VaultCipherCombo.class);
+	private final CryptorProvider.Scheme cipherCombo = mock(CryptorProvider.Scheme.class);
 	private final SecureRandom csprng = Mockito.mock(SecureRandom.class);
 	private final CryptorProvider cryptorProvider = mock(CryptorProvider.class);
 	private final Cryptor cryptor = mock(Cryptor.class);
@@ -60,6 +60,7 @@ public class CryptoFileSystemsTest {
 
 	private MockedStatic<VaultConfig> vaultConficClass;
 	private MockedStatic<Files> filesClass;
+	private MockedStatic<CryptorProvider> cryptorProviderClass;
 
 	private final CryptoFileSystems inTest = new CryptoFileSystems(cryptoFileSystemComponentBuilder, capabilityChecker, csprng);
 
@@ -67,6 +68,7 @@ public class CryptoFileSystemsTest {
 	public void setup() throws IOException, MasterkeyLoadingFailedException {
 		vaultConficClass = Mockito.mockStatic(VaultConfig.class);
 		filesClass = Mockito.mockStatic(Files.class);
+		cryptorProviderClass = Mockito.mockStatic(CryptorProvider.class);
 
 		when(pathToVault.normalize()).thenReturn(normalizedPathToVault);
 		when(normalizedPathToVault.resolve("vault.cryptomator")).thenReturn(configFilePath);
@@ -74,16 +76,15 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 		when(properties.keyLoader()).thenReturn(keyLoader);
 		filesClass.when(() -> Files.readString(configFilePath, StandardCharsets.US_ASCII)).thenReturn("jwt-vault-config");
 		vaultConficClass.when(() -> VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
+		cryptorProviderClass.when(() -> CryptorProvider.forScheme(cipherCombo)).thenReturn(cryptorProvider);
 		when(VaultConfig.decode("jwt-vault-config")).thenReturn(configLoader);
 		when(configLoader.getKeyId()).thenReturn(URI.create("test:key"));
 		when(keyLoader.loadKey(Mockito.any())).thenReturn(masterkey);
 		when(masterkey.getEncoded()).thenReturn(rawKey);
 		when(masterkey.clone()).thenReturn(clonedMasterkey);
 		when(configLoader.verify(rawKey, Constants.VAULT_VERSION)).thenReturn(vaultConfig);
-		when(cryptorProvider.withKey(clonedMasterkey)).thenReturn(cryptor);
+		when(cryptorProvider.provide(clonedMasterkey, csprng)).thenReturn(cryptor);
 		when(vaultConfig.getCipherCombo()).thenReturn(cipherCombo);
-		when(cipherCombo.getCryptorProvider(csprng)).thenReturn(cryptorProvider);
-		when(cryptorProvider.withKey(masterkey)).thenReturn(cryptor);
 		when(cryptor.fileNameCryptor()).thenReturn(fileNameCryptor);
 		when(fileNameCryptor.hashDirectoryId("")).thenReturn("ABCDEFGHIJKLMNOP");
 		when(pathToVault.resolve(Constants.DATA_DIR_NAME)).thenReturn(dataDirPath);
@@ -103,6 +104,7 @@ public void setup() throws IOException, MasterkeyLoadingFailedException {
 	public void tearDown() {
 		vaultConficClass.close();
 		filesClass.close();
+		cryptorProviderClass.close();
 	}
 
 	@Test
diff --git a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
index 546c0aba..a7abbda7 100644
--- a/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/VaultConfigTest.java
@@ -6,6 +6,7 @@
 import com.auth0.jwt.interfaces.DecodedJWT;
 import com.auth0.jwt.interfaces.Verification;
 import org.cryptomator.cryptofs.common.Constants;
+import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
 import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
@@ -47,7 +48,7 @@ public class WithValidToken {
 
 		@BeforeEach
 		public void setup() throws MasterkeyLoadingFailedException {
-			originalConfig = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).shorteningThreshold(220).build();
+			originalConfig = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).build();
 			token = originalConfig.toToken("TEST_KEY", rawKey);
 		}
 
@@ -76,11 +77,11 @@ public void testLoadWithInvalidKey(int pos) {
 
 	@Test
 	public void testCreateNew() {
-		var config = VaultConfig.createNew().cipherCombo(VaultCipherCombo.SIV_CTRMAC).shorteningThreshold(220).build();
+		var config = VaultConfig.createNew().cipherCombo(CryptorProvider.Scheme.SIV_CTRMAC).shorteningThreshold(220).build();
 
 		Assertions.assertNotNull(config.getId());
 		Assertions.assertEquals(Constants.VAULT_VERSION, config.getVaultVersion());
-		Assertions.assertEquals(VaultCipherCombo.SIV_CTRMAC, config.getCipherCombo());
+		Assertions.assertEquals(CryptorProvider.Scheme.SIV_CTRMAC, config.getCipherCombo());
 		Assertions.assertEquals(220, config.getShorteningThreshold());
 	}
 
diff --git a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java
index 87af1128..abda39d7 100644
--- a/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java
+++ b/src/test/java/org/cryptomator/cryptofs/attr/CryptoBasicFileAttributesTest.java
@@ -40,6 +40,7 @@ public void setup() {
 		Mockito.when(cryptor.fileContentCryptor()).thenReturn(contentCryptor);
 		Mockito.when(contentCryptor.cleartextChunkSize()).thenReturn(32 * 1024);
 		Mockito.when(contentCryptor.ciphertextChunkSize()).thenReturn(16 + 32 * 1024 + 32);
+		Mockito.doCallRealMethod().when(contentCryptor).cleartextSize(Mockito.anyLong());
 		ciphertextFilePath = Mockito.mock(Path.class, "ciphertextFile");
 		delegateAttr = Mockito.mock(BasicFileAttributes.class);
 

From 2a623cfadd4685497dde9a0f2b119e4472a4bd35 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 19 Jul 2021 13:01:51 +0200
Subject: [PATCH 69/70] dependency update

---
 pom.xml | 16 ++++------------
 1 file changed, 4 insertions(+), 12 deletions(-)

diff --git a/pom.xml b/pom.xml
index 6b6c5c8f..e847712d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,8 +18,8 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-rc6</cryptolib.version>
-		<jwt.version>3.17.0</jwt.version>
+		<cryptolib.version>2.0.0-rc7</cryptolib.version>
+		<jwt.version>3.18.1</jwt.version>
 		<dagger.version>2.37</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>
 		<slf4j.version>1.7.31</slf4j.version>
@@ -161,7 +161,7 @@
 			</plugin>
 			<plugin>
 				<artifactId>maven-javadoc-plugin</artifactId>
-				<version>3.2.0</version>
+				<version>3.3.0</version>
 				<executions>
 					<execution>
 						<id>attach-javadocs</id>
@@ -196,14 +196,6 @@
 						<tag><name>serialData</name></tag>
 						<tag><name>see</name></tag>
 					</tags>
-					<!-- Used for javax.annotation.Generated in dagger-generated code. Can be removed when using JDK 11+ -->
-					<additionalDependencies>
-						<additionalDependency>
-							<groupId>javax.annotation</groupId>
-							<artifactId>jsr250-api</artifactId>
-							<version>1.0</version>
-						</additionalDependency>
-					</additionalDependencies>
 				</configuration>
 			</plugin>
 		</plugins>
@@ -270,7 +262,7 @@
 				<plugins>
 					<plugin>
 						<artifactId>maven-gpg-plugin</artifactId>
-						<version>1.6</version>
+						<version>3.0.1</version>
 						<executions>
 							<execution>
 								<id>sign-artifacts</id>

From c982beedec1cc061b8a2fd919cffdd1d088c6ff0 Mon Sep 17 00:00:00 2001
From: Sebastian Stenzel <sebastian.stenzel@gmail.com>
Date: Mon, 19 Jul 2021 14:53:52 +0200
Subject: [PATCH 70/70] updated cryptolib

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index e847712d..62515786 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,7 +18,7 @@
 		<maven.compiler.release>16</maven.compiler.release>
 
 		<!-- dependencies -->
-		<cryptolib.version>2.0.0-rc7</cryptolib.version>
+		<cryptolib.version>2.0.0</cryptolib.version>
 		<jwt.version>3.18.1</jwt.version>
 		<dagger.version>2.37</dagger.version>
 		<guava.version>30.1.1-jre</guava.version>