Browse Source

improved vault upgrading, preparation for migration to vault version 4

Sebastian Stenzel 9 năm trước cách đây
mục cha
commit
1468c6ec90

+ 8 - 8
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/UnsupportedVaultFormatException.java

@@ -11,28 +11,28 @@ package org.cryptomator.crypto.engine;
 public class UnsupportedVaultFormatException extends CryptoException {
 
 	private final Integer detectedVersion;
-	private final Integer supportedVersion;
+	private final Integer latestSupportedVersion;
 
-	public UnsupportedVaultFormatException(Integer detectedVersion, Integer supportedVersion) {
-		super("Tried to open vault of version " + detectedVersion + ", but can only handle version " + supportedVersion);
+	public UnsupportedVaultFormatException(Integer detectedVersion, Integer latestSupportedVersion) {
+		super("Tried to open vault of version " + detectedVersion + ", latest supported version is " + latestSupportedVersion);
 		this.detectedVersion = detectedVersion;
-		this.supportedVersion = supportedVersion;
+		this.latestSupportedVersion = latestSupportedVersion;
 	}
 
 	public Integer getDetectedVersion() {
 		return detectedVersion;
 	}
 
-	public Integer getSupportedVersion() {
-		return supportedVersion;
+	public Integer getLatestSupportedVersion() {
+		return latestSupportedVersion;
 	}
 
 	public boolean isVaultOlderThanSoftware() {
-		return detectedVersion == null || detectedVersion < supportedVersion;
+		return detectedVersion == null || detectedVersion < latestSupportedVersion;
 	}
 
 	public boolean isSoftwareOlderThanVault() {
-		return detectedVersion > supportedVersion;
+		return detectedVersion > latestSupportedVersion;
 	}
 
 }

+ 5 - 0
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/Constants.java

@@ -1,10 +1,15 @@
 package org.cryptomator.crypto.engine.impl;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
 public final class Constants {
 
 	private Constants() {
 	}
 
+	static final Collection<Integer> SUPPORTED_VAULT_VERSIONS = Collections.unmodifiableCollection(Arrays.asList(3, 4));
 	static final Integer CURRENT_VAULT_VERSION = 4;
 
 	public static final int PAYLOAD_SIZE = 32 * 1024;

+ 2 - 1
main/filesystem-crypto/src/main/java/org/cryptomator/crypto/engine/impl/CryptorImpl.java

@@ -9,6 +9,7 @@
 package org.cryptomator.crypto.engine.impl;
 
 import static org.cryptomator.crypto.engine.impl.Constants.CURRENT_VAULT_VERSION;
+import static org.cryptomator.crypto.engine.impl.Constants.SUPPORTED_VAULT_VERSIONS;
 
 import java.io.IOException;
 import java.nio.ByteBuffer;
@@ -109,7 +110,7 @@ class CryptorImpl implements Cryptor {
 		assert keyFile != null;
 
 		// check version
-		if (!CURRENT_VAULT_VERSION.equals(keyFile.getVersion())) {
+		if (!SUPPORTED_VAULT_VERSIONS.contains(keyFile.getVersion())) {
 			throw new UnsupportedVaultFormatException(keyFile.getVersion(), CURRENT_VAULT_VERSION);
 		}
 

+ 2 - 2
main/filesystem-crypto/src/main/java/org/cryptomator/filesystem/crypto/Constants.java

@@ -8,8 +8,8 @@ public final class Constants {
 	static final String DATA_ROOT_DIR = "d";
 	static final String ROOT_DIRECOTRY_ID = "";
 
-	static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
-	static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
+	public static final String MASTERKEY_FILENAME = "masterkey.cryptomator";
+	public static final String MASTERKEY_BACKUP_FILENAME = "masterkey.cryptomator.bkup";
 
 	static final String DIR_PREFIX = "0";
 

+ 5 - 2
main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java

@@ -26,6 +26,7 @@ import javax.inject.Singleton;
 
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.controls.DirectoryListCell;
+import org.cryptomator.ui.model.UpgradeStrategies;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.model.VaultFactory;
 import org.cryptomator.ui.settings.Localization;
@@ -78,6 +79,7 @@ public class MainController extends LocalizedFXMLViewController {
 	private final Provider<UnlockedController> unlockedControllerProvider;
 	private final Lazy<ChangePasswordController> changePasswordController;
 	private final Lazy<SettingsController> settingsController;
+	private final Lazy<UpgradeStrategies> upgradeStrategies;
 	private final ObjectProperty<AbstractFXMLViewController> activeController = new SimpleObjectProperty<>();
 	private final ObservableList<Vault> vaults;
 	private final ObjectProperty<Vault> selectedVault = new SimpleObjectProperty<>();
@@ -90,7 +92,7 @@ public class MainController extends LocalizedFXMLViewController {
 	@Inject
 	public MainController(@Named("mainWindow") Stage mainWindow, Localization localization, Settings settings, VaultFactory vaultFactoy, Lazy<WelcomeController> welcomeController,
 			Lazy<InitializeController> initializeController, Lazy<NotFoundController> notFoundController, Lazy<UpgradeController> upgradeController, Lazy<UnlockController> unlockController,
-			Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController) {
+			Provider<UnlockedController> unlockedControllerProvider, Lazy<ChangePasswordController> changePasswordController, Lazy<SettingsController> settingsController, Lazy<UpgradeStrategies> upgradeStrategies) {
 		super(localization);
 		this.mainWindow = mainWindow;
 		this.vaultFactoy = vaultFactoy;
@@ -102,6 +104,7 @@ public class MainController extends LocalizedFXMLViewController {
 		this.unlockedControllerProvider = unlockedControllerProvider;
 		this.changePasswordController = changePasswordController;
 		this.settingsController = settingsController;
+		this.upgradeStrategies = upgradeStrategies;
 		this.vaults = FXCollections.observableList(settings.getDirectories());
 		this.vaults.addListener((Change<? extends Vault> c) -> {
 			settings.save();
@@ -288,7 +291,7 @@ public class MainController extends LocalizedFXMLViewController {
 			this.showUnlockedView(newValue);
 		} else if (!newValue.doesVaultDirectoryExist()) {
 			this.showNotFoundView();
-		} else if (newValue.isValidVaultDirectory() && newValue.needsUpgrade()) {
+		} else if (newValue.isValidVaultDirectory() && upgradeStrategies.get().getUpgradeStrategy(newValue).isPresent()) {
 			this.showUpgradeView();
 		} else if (newValue.isValidVaultDirectory()) {
 			this.showUnlockView();

+ 38 - 20
main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java

@@ -7,8 +7,10 @@ import java.util.concurrent.ExecutorService;
 
 import javax.inject.Inject;
 
-import org.cryptomator.ui.model.UpgradeInstruction;
-import org.cryptomator.ui.model.UpgradeInstruction.UpgradeFailedException;
+import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.model.UpgradeStrategies;
+import org.cryptomator.ui.model.UpgradeStrategy;
+import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.settings.Localization;
 import org.fxmisc.easybind.EasyBind;
@@ -16,7 +18,6 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javafx.application.Platform;
-import javafx.beans.binding.Binding;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.event.ActionEvent;
@@ -30,19 +31,24 @@ public class UpgradeController extends LocalizedFXMLViewController {
 	private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
 
 	final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
+	final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
+	private final UpgradeStrategies strategies;
 	private final ExecutorService exec;
-	private final Binding<Optional<UpgradeInstruction>> upgradeInstruction = EasyBind.monadic(vault).map(Vault::availableUpgrade);
 	private Optional<UpgradeListener> listener = Optional.empty();
 
 	@Inject
-	public UpgradeController(Localization localization, ExecutorService exec) {
+	public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
 		super(localization);
+		this.strategies = strategies;
 		this.exec = exec;
 	}
 
 	@FXML
 	private Label upgradeLabel;
 
+	@FXML
+	private SecPasswordField passwordField;
+
 	@FXML
 	private Button upgradeButton;
 
@@ -54,10 +60,12 @@ public class UpgradeController extends LocalizedFXMLViewController {
 
 	@Override
 	protected void initialize() {
-		upgradeLabel.textProperty().bind(EasyBind.monadic(upgradeInstruction).map(instruction -> {
+		upgradeLabel.textProperty().bind(EasyBind.monadic(strategy).map(instruction -> {
 			return instruction.map(this::upgradeNotification).orElse("");
 		}).orElse(""));
 
+		upgradeButton.disableProperty().bind(passwordField.textProperty().isEmpty().or(passwordField.disabledProperty()));
+
 		EasyBind.subscribe(vault, this::vaultDidChange);
 	}
 
@@ -68,14 +76,15 @@ public class UpgradeController extends LocalizedFXMLViewController {
 
 	private void vaultDidChange(Vault newVault) {
 		errorLabel.setText(null);
+		strategy.set(strategies.getUpgradeStrategy(newVault));
 	}
 
 	// ****************************************
 	// Upgrade label
 	// ****************************************
 
-	private String upgradeNotification(UpgradeInstruction instruction) {
-		return instruction.getNotification(vault.get(), localization);
+	private String upgradeNotification(UpgradeStrategy instruction) {
+		return instruction.getNotification(vault.get());
 	}
 
 	// ****************************************
@@ -84,36 +93,45 @@ public class UpgradeController extends LocalizedFXMLViewController {
 
 	@FXML
 	private void didClickUpgradeButton(ActionEvent event) {
-		upgradeInstruction.getValue().ifPresent(this::upgrade);
+		strategy.getValue().ifPresent(this::upgrade);
 	}
 
-	private void upgrade(UpgradeInstruction instruction) {
-		Vault v = vault.getValue();
-		Objects.requireNonNull(v);
+	private void upgrade(UpgradeStrategy instruction) {
+		Vault v = Objects.requireNonNull(vault.getValue());
+		passwordField.setDisable(true);
 		progressIndicator.setVisible(true);
-		upgradeButton.setDisable(true);
 		exec.submit(() -> {
 			if (!instruction.isApplicable(v)) {
 				LOG.error("No upgrade needed for " + v.path().getValue());
 				throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
 			}
 			try {
-				instruction.upgrade(v, localization);
-				Platform.runLater(() -> {
-					progressIndicator.setVisible(false);
-					upgradeButton.setDisable(false);
-					listener.ifPresent(UpgradeListener::didUpgrade);
-				});
+				instruction.upgrade(v, passwordField.getCharacters());
+				Platform.runLater(this::showNextUpgrade);
 			} catch (UpgradeFailedException e) {
 				Platform.runLater(() -> {
 					errorLabel.setText(e.getLocalizedMessage());
+				});
+			} finally {
+				Platform.runLater(() -> {
 					progressIndicator.setVisible(false);
-					upgradeButton.setDisable(false);
+					passwordField.setDisable(false);
+					passwordField.swipe();
 				});
 			}
 		});
 	}
 
+	private void showNextUpgrade() {
+		errorLabel.setText(null);
+		Optional<UpgradeStrategy> nextStrategy = strategies.getUpgradeStrategy(vault.getValue());
+		if (nextStrategy.isPresent()) {
+			strategy.set(nextStrategy);
+		} else {
+			listener.ifPresent(UpgradeListener::didUpgrade);
+		}
+	}
+
 	/* callback */
 
 	public void setListener(UpgradeListener listener) {

+ 0 - 40
main/ui/src/main/java/org/cryptomator/ui/model/UpgradeInstruction.java

@@ -1,40 +0,0 @@
-package org.cryptomator.ui.model;
-
-import org.cryptomator.ui.settings.Localization;
-
-public interface UpgradeInstruction {
-
-	static UpgradeInstruction[] AVAILABLE_INSTRUCTIONS = {new UpgradeVersion3DropBundleExtension()};
-
-	/**
-	 * @return Localized string to display to the user when an upgrade is needed.
-	 */
-	String getNotification(Vault vault, Localization localization);
-
-	/**
-	 * Upgrades a vault. Might take a moment, should be run in a background thread.
-	 */
-	void upgrade(Vault vault, Localization localization) throws UpgradeFailedException;
-
-	/**
-	 * Determines in O(1), if an upgrade can be applied to a vault.
-	 * 
-	 * @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
-	 */
-	boolean isApplicable(Vault vault);
-
-	/**
-	 * Thrown when data migration failed.
-	 */
-	public class UpgradeFailedException extends Exception {
-
-		UpgradeFailedException() {
-		}
-
-		UpgradeFailedException(String message) {
-			super(message);
-		}
-
-	}
-
-}

+ 27 - 0
main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategies.java

@@ -0,0 +1,27 @@
+package org.cryptomator.ui.model;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Optional;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class UpgradeStrategies {
+
+	private final Collection<UpgradeStrategy> strategies;
+
+	@Inject
+	public UpgradeStrategies(UpgradeVersion3DropBundleExtension upgrader1, UpgradeVersion3to4 upgrader2) {
+		strategies = Collections.unmodifiableList(Arrays.asList(upgrader1, upgrader2));
+	}
+
+	public Optional<UpgradeStrategy> getUpgradeStrategy(Vault vault) {
+		return strategies.stream().filter(strategy -> {
+			return strategy.isApplicable(vault);
+		}).findFirst();
+	}
+
+}

+ 87 - 0
main/ui/src/main/java/org/cryptomator/ui/model/UpgradeStrategy.java

@@ -0,0 +1,87 @@
+package org.cryptomator.ui.model;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.StandardOpenOption;
+
+import javax.inject.Provider;
+
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
+import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
+import org.cryptomator.filesystem.crypto.Constants;
+import org.cryptomator.ui.settings.Localization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public abstract class UpgradeStrategy {
+
+	private static final Logger LOG = LoggerFactory.getLogger(UpgradeStrategy.class);
+
+	protected final Provider<Cryptor> cryptorProvider;
+	protected final Localization localization;
+
+	UpgradeStrategy(Provider<Cryptor> cryptorProvider, Localization localization) {
+		this.cryptorProvider = cryptorProvider;
+		this.localization = localization;
+	}
+
+	/**
+	 * @return Localized string to display to the user when an upgrade is needed.
+	 */
+	public abstract String getNotification(Vault vault);
+
+	/**
+	 * Upgrades a vault. Might take a moment, should be run in a background thread.
+	 */
+	public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
+		final Cryptor cryptor = cryptorProvider.get();
+		try {
+			final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
+			final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
+			cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
+			// create backup, as soon as we know the password was correct:
+			final Path masterkeyBackupFile = vault.path().getValue().resolve(Constants.MASTERKEY_BACKUP_FILENAME);
+			Files.copy(masterkeyFile, masterkeyBackupFile, StandardCopyOption.REPLACE_EXISTING);
+			// do stuff:
+			upgrade(vault, cryptor);
+			// write updated masterkey file:
+			final byte[] upgradedMasterkeyFileContents = cryptor.writeKeysToMasterkeyFile(passphrase);
+			final Path masterkeyFileAfterUpgrading = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME); // path may have changed
+			Files.write(masterkeyFileAfterUpgrading, upgradedMasterkeyFileContents, StandardOpenOption.TRUNCATE_EXISTING);
+		} catch (InvalidPassphraseException e) {
+			throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
+		} catch (IOException | UnsupportedVaultFormatException e) {
+			LOG.warn("Upgrade failed.", e);
+			throw new UpgradeFailedException("Upgrade failed. Details in log message.");
+		} finally {
+			cryptor.destroy();
+		}
+	}
+
+	protected abstract void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException;
+
+	/**
+	 * Determines in O(1), if an upgrade can be applied to a vault.
+	 * 
+	 * @return <code>true</code> if and only if the vault can be migrated to a newer version without the risk of data losses.
+	 */
+	public abstract boolean isApplicable(Vault vault);
+
+	/**
+	 * Thrown when data migration failed.
+	 */
+	public class UpgradeFailedException extends Exception {
+
+		UpgradeFailedException() {
+		}
+
+		UpgradeFailedException(String message) {
+			super(message);
+		}
+
+	}
+
+}

+ 40 - 3
main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3DropBundleExtension.java

@@ -4,19 +4,36 @@ import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
 import org.apache.commons.lang3.StringUtils;
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.crypto.engine.InvalidPassphraseException;
+import org.cryptomator.crypto.engine.UnsupportedVaultFormatException;
+import org.cryptomator.filesystem.crypto.Constants;
 import org.cryptomator.ui.settings.Localization;
+import org.cryptomator.ui.settings.Settings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javafx.application.Platform;
 
-class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
+@Singleton
+class UpgradeVersion3DropBundleExtension extends UpgradeStrategy {
 
 	private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3DropBundleExtension.class);
+	private final Settings settings;
+
+	@Inject
+	public UpgradeVersion3DropBundleExtension(Provider<Cryptor> cryptorProvider, Localization localization, Settings settings) {
+		super(cryptorProvider, localization);
+		this.settings = settings;
+	}
 
 	@Override
-	public String getNotification(Vault vault, Localization localization) {
+	public String getNotification(Vault vault) {
 		String fmt = localization.getString("upgrade.version3dropBundleExtension.msg");
 		Path path = vault.path().getValue();
 		String oldVaultName = path.getFileName().toString();
@@ -25,7 +42,26 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
 	}
 
 	@Override
-	public void upgrade(Vault vault, Localization localization) throws UpgradeFailedException {
+	public void upgrade(Vault vault, CharSequence passphrase) throws UpgradeFailedException {
+		final Cryptor cryptor = cryptorProvider.get();
+		try {
+			final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
+			final byte[] masterkeyFileContents = Files.readAllBytes(masterkeyFile);
+			cryptor.readKeysFromMasterkeyFile(masterkeyFileContents, passphrase);
+			upgrade(vault, cryptor);
+			// don't write new masterkey. this is a special case, as we were stupid and didn't increase the vault version with this upgrade...
+		} catch (InvalidPassphraseException e) {
+			throw new UpgradeFailedException(localization.getString("unlock.errorMessage.wrongPassword"));
+		} catch (IOException | UnsupportedVaultFormatException e) {
+			LOG.warn("Upgrade failed.", e);
+			throw new UpgradeFailedException("Upgrade failed. Details in log message.");
+		} finally {
+			cryptor.destroy();
+		}
+	}
+
+	@Override
+	protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
 		Path path = vault.path().getValue();
 		String oldVaultName = path.getFileName().toString();
 		String newVaultName = StringUtils.removeEnd(oldVaultName, Vault.VAULT_FILE_EXTENSION);
@@ -39,6 +75,7 @@ class UpgradeVersion3DropBundleExtension implements UpgradeInstruction {
 				Files.move(path, path.resolveSibling(newVaultName));
 				Platform.runLater(() -> {
 					vault.setPath(newPath);
+					settings.save();
 				});
 			} catch (IOException e) {
 				LOG.error("Vault migration failed", e);

+ 55 - 0
main/ui/src/main/java/org/cryptomator/ui/model/UpgradeVersion3to4.java

@@ -0,0 +1,55 @@
+package org.cryptomator.ui.model;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import org.cryptomator.crypto.engine.Cryptor;
+import org.cryptomator.filesystem.crypto.Constants;
+import org.cryptomator.ui.settings.Localization;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Singleton
+class UpgradeVersion3to4 extends UpgradeStrategy {
+
+	private static final Logger LOG = LoggerFactory.getLogger(UpgradeVersion3to4.class);
+
+	@Inject
+	public UpgradeVersion3to4(Provider<Cryptor> cryptorProvider, Localization localization) {
+		super(cryptorProvider, localization);
+	}
+
+	@Override
+	public String getNotification(Vault vault) {
+		return localization.getString("upgrade.version3to4.msg");
+	}
+
+	@Override
+	protected void upgrade(Vault vault, Cryptor cryptor) throws UpgradeFailedException {
+		throw new UpgradeFailedException("not yet implemented");
+	}
+
+	@Override
+	public boolean isApplicable(Vault vault) {
+		final Path masterkeyFile = vault.path().getValue().resolve(Constants.MASTERKEY_FILENAME);
+		try {
+			if (Files.isRegularFile(masterkeyFile)) {
+				final String keyContents = new String(Files.readAllBytes(masterkeyFile), StandardCharsets.UTF_8);
+				return keyContents.contains("\"version\":3") || keyContents.contains("\"version\": 3");
+			} else {
+				LOG.warn("Not a file: {}", masterkeyFile);
+				return false;
+			}
+		} catch (IOException e) {
+			LOG.warn("Could not determine, whether upgrade is applicable.", e);
+			return false;
+		}
+	}
+
+}

+ 2 - 13
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -16,7 +16,6 @@ import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
@@ -64,9 +63,9 @@ public class Vault implements CryptoFileSystemDelegate {
 	public static final String VAULT_FILE_EXTENSION = ".cryptomator";
 
 	private final ObjectProperty<Path> path;
-	private final DeferredCloser closer;
 	private final ShorteningFileSystemFactory shorteningFileSystemFactory;
 	private final CryptoFileSystemFactory cryptoFileSystemFactory;
+	private final DeferredCloser closer;
 	private final BooleanProperty unlocked = new SimpleBooleanProperty();
 	private final ObservableList<String> namesOfResourcesWithInvalidMac = FXThreads.observableListOnMainThread(FXCollections.observableArrayList());
 	private final Set<String> whitelistedResourcesWithInvalidMac = new HashSet<>();
@@ -82,9 +81,9 @@ public class Vault implements CryptoFileSystemDelegate {
 	 */
 	Vault(Path vaultDirectoryPath, ShorteningFileSystemFactory shorteningFileSystemFactory, CryptoFileSystemFactory cryptoFileSystemFactory, DeferredCloser closer) {
 		this.path = new SimpleObjectProperty<Path>(vaultDirectoryPath);
-		this.closer = closer;
 		this.shorteningFileSystemFactory = shorteningFileSystemFactory;
 		this.cryptoFileSystemFactory = cryptoFileSystemFactory;
+		this.closer = closer;
 
 		try {
 			setMountName(name().getValue());
@@ -167,16 +166,6 @@ public class Vault implements CryptoFileSystemDelegate {
 		Optionals.ifPresent(filesystemFrontend.get(), Frontend::unmount);
 	}
 
-	public boolean needsUpgrade() {
-		return availableUpgrade().isPresent();
-	}
-
-	public Optional<UpgradeInstruction> availableUpgrade() {
-		return Arrays.stream(UpgradeInstruction.AVAILABLE_INSTRUCTIONS).filter(instruction -> {
-			return instruction.isApplicable(this);
-		}).findAny();
-	}
-
 	// ******************************************************************************
 	// Delegate methods
 	// ********************************************************************************/

+ 3 - 0
main/ui/src/main/resources/fxml/upgrade.fxml

@@ -12,10 +12,13 @@
 <?import javafx.scene.control.ProgressIndicator?>
 <?import javafx.scene.control.Button?>
 <?import javafx.scene.layout.VBox?>
+<?import org.cryptomator.ui.controls.SecPasswordField?>
 
 <VBox prefWidth="400.0" prefHeight="400.0" spacing="24.0" alignment="CENTER" xmlns:fx="http://javafx.com/fxml" cacheShape="true" cache="true">
 
 	<Label fx:id="upgradeLabel" textAlignment="CENTER" wrapText="true"/>
+	
+	<SecPasswordField fx:id="passwordField" prefWidth="300.0" cacheShape="true" cache="true" />
 
 	<Button fx:id="upgradeButton" text="%upgrade.button" prefWidth="150.0" onAction="#didClickUpgradeButton" cacheShape="true" cache="true" />
 

+ 3 - 0
main/ui/src/main/resources/localization/en.txt

@@ -43,6 +43,9 @@ upgrade.button=Upgrade vault
 upgrade.version3dropBundleExtension.msg=This vault needs to be migrated to a newer format.\n"%1$s" will be renamed to "%2$s".\nPlease make sure synchronization has finished before proceeding.
 upgrade.version3dropBundleExtension.err.alreadyExists=Automatic migration failed.\n"%s" already exists.
 
+upgrade.version3to4.msg=This vault needs to be migrated to a newer format.\nEncrypted folder names will be updated.\nPlease make sure synchronization has finished before proceeding.
+
+
 # unlock.fxml
 unlock.label.password=Password
 unlock.label.mountName=Drive name