Browse Source

created more static methods in RecoverUtil, added first flow for choose dir and restore

Jan-Peter Klein 6 months ago
parent
commit
8c5325511c

+ 170 - 11
src/main/java/org/cryptomator/common/RecoverUtil.java

@@ -1,30 +1,60 @@
 package org.cryptomator.common;
 
+import dagger.Lazy;
+import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.settings.VaultSettings;
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultComponent;
+import org.cryptomator.common.vaults.VaultConfigCache;
+import org.cryptomator.common.vaults.VaultListManager;
 import org.cryptomator.common.vaults.VaultState;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties;
+import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptolib.api.CryptoException;
 import org.cryptomator.cryptolib.api.Cryptor;
 import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
+import org.cryptomator.integrations.mount.MountService;
+import org.cryptomator.ui.changepassword.NewPasswordController;
+import org.cryptomator.ui.dialogs.Dialogs;
+import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.StringProperty;
+import javafx.concurrent.Task;
+import javafx.scene.Scene;
+import javafx.stage.DirectoryChooser;
+import javafx.stage.Stage;
+import java.io.File;
 import java.io.IOException;
 import java.nio.ByteBuffer;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardCopyOption;
 import java.security.SecureRandom;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
 import java.util.stream.Stream;
 
+import static org.cryptomator.common.Constants.DEFAULT_KEY_ID;
+import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
+import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING;
 import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME;
 import static org.cryptomator.cryptolib.api.CryptorProvider.Scheme.SIV_CTRMAC;
 import static org.cryptomator.cryptolib.api.CryptorProvider.Scheme.SIV_GCM;
 
 public class RecoverUtil {
 
+	private static final Logger LOG = LoggerFactory.getLogger(RecoverUtil.class);
+
 	public static CryptorProvider.Scheme detectCipherCombo(byte[] masterkey, Path pathToVault) {
 		try (Stream<Path> paths = Files.walk(pathToVault.resolve(DATA_DIR_NAME))) {
-			return paths.filter(path -> path.toString().endsWith(".c9r"))
-					.findFirst()
-					.map(c9rFile -> determineScheme(c9rFile, masterkey))
-					.orElseThrow(() -> new IllegalStateException("No .c9r file found."));
+			return paths.filter(path -> path.toString().endsWith(".c9r")).findFirst().map(c9rFile -> determineScheme(c9rFile, masterkey)).orElseThrow(() -> new IllegalStateException("No .c9r file found."));
 		} catch (IOException e) {
 			throw new IllegalStateException("Failed to detect cipher combo.", e);
 		}
@@ -33,8 +63,7 @@ public class RecoverUtil {
 	private static CryptorProvider.Scheme determineScheme(Path c9rFile, byte[] masterkey) {
 		try {
 			ByteBuffer header = ByteBuffer.wrap(Files.readAllBytes(c9rFile));
-			return tryDecrypt(header, new Masterkey(masterkey), SIV_GCM) ? SIV_GCM :
-					tryDecrypt(header, new Masterkey(masterkey), SIV_CTRMAC) ? SIV_CTRMAC : null;
+			return tryDecrypt(header, new Masterkey(masterkey), SIV_GCM) ? SIV_GCM : tryDecrypt(header, new Masterkey(masterkey), SIV_CTRMAC) ? SIV_CTRMAC : null;
 		} catch (IOException e) {
 			return null;
 		}
@@ -51,11 +80,7 @@ public class RecoverUtil {
 
 	public static boolean restoreBackupIfAvailable(Path configPath, VaultState.Value vaultState) {
 		try (Stream<Path> files = Files.list(configPath.getParent())) {
-			return files
-					.filter(file -> matchesBackupFile(file.getFileName().toString(), vaultState))
-					.findFirst()
-					.map(backupFile -> copyBackupFile(backupFile, configPath))
-					.orElse(false);
+			return files.filter(file -> matchesBackupFile(file.getFileName().toString(), vaultState)).findFirst().map(backupFile -> copyBackupFile(backupFile, configPath)).orElse(false);
 		} catch (IOException e) {
 			return false;
 		}
@@ -78,4 +103,138 @@ public class RecoverUtil {
 		}
 	}
 
+	public static Path createRecoveryDirectory(Path vaultPath) throws IOException {
+		Path recoveryPath = vaultPath.resolve("r");
+		Files.createDirectory(recoveryPath);
+		return recoveryPath;
+	}
+
+	public static void createNewMasterkeyFile(RecoveryKeyFactory recoveryKeyFactory, Path recoveryPath, String recoveryKey, CharSequence newPassword) throws IOException {
+		recoveryKeyFactory.newMasterkeyFileWithPassphrase(recoveryPath, recoveryKey, newPassword);
+	}
+
+	public static Masterkey loadMasterkey(org.cryptomator.cryptolib.common.MasterkeyFileAccess masterkeyFileAccess, Path masterkeyFilePath, CharSequence password) throws IOException {
+		return masterkeyFileAccess.load(masterkeyFilePath, password);
+	}
+
+	public static void initializeCryptoFileSystem(Path recoveryPath, Path vaultPath, Masterkey masterkey, IntegerProperty shorteningThreshold) throws IOException, CryptoException {
+		var combo = RecoverUtil.detectCipherCombo(masterkey.getEncoded(), vaultPath);
+		org.cryptomator.cryptolib.api.MasterkeyLoader loader = ignored -> masterkey.copy();
+		CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties().withCipherCombo(combo).withKeyLoader(loader).withShorteningThreshold(shorteningThreshold.get()).build();
+		CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID);
+	}
+
+	public static void moveRecoveredFiles(Path recoveryPath, Path vaultPath) throws IOException {
+		Files.move(recoveryPath.resolve(MASTERKEY_FILENAME), vaultPath.resolve(MASTERKEY_FILENAME), StandardCopyOption.REPLACE_EXISTING);
+		Files.move(recoveryPath.resolve(VAULTCONFIG_FILENAME), vaultPath.resolve(VAULTCONFIG_FILENAME));
+	}
+
+	public static void deleteRecoveryDirectory(Path recoveryPath) {
+		try (var paths = Files.walk(recoveryPath)) {
+			paths.sorted(Comparator.reverseOrder()).forEach(p -> {
+				try {
+					Files.delete(p);
+				} catch (IOException e) {
+					LOG.info("Unable to delete {}. Please delete it manually.", p);
+				}
+			});
+		} catch (IOException e) {
+			LOG.error("Failed to clean up recovery directory", e);
+		}
+	}
+
+	public static void addVaultToList(VaultListManager vaultListManager, Path vaultPath) throws IOException {
+		if (!vaultListManager.containsVault(vaultPath)) {
+			vaultListManager.add(vaultPath);
+		}
+	}
+
+	public static Task<Void> createResetPasswordTask(RecoveryKeyFactory recoveryKeyFactory, Vault vault, StringProperty recoveryKey, NewPasswordController newPasswordController, Stage window, Lazy<Scene> recoverResetPasswordSuccessScene, Lazy<Scene> recoverResetVaultConfigSuccessScene, FxApplicationWindows appWindows) {
+
+		Task<Void> task = new ResetPasswordTask(recoveryKeyFactory, vault, recoveryKey, newPasswordController);
+
+		task.setOnScheduled(_ -> {
+			LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath());
+		});
+
+		task.setOnSucceeded(_ -> {
+			LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath());
+			if (vault.getState().equals(VAULT_CONFIG_MISSING)) {
+				window.setScene(recoverResetVaultConfigSuccessScene.get());
+			} else {
+				window.setScene(recoverResetPasswordSuccessScene.get());
+			}
+		});
+
+		task.setOnFailed(_ -> {
+			LOG.error("Resetting password failed.", task.getException());
+			appWindows.showErrorWindow(task.getException(), window, null);
+		});
+
+		return task;
+	}
+
+
+	public static class ResetPasswordTask extends Task<Void> {
+
+		private static final Logger LOG = LoggerFactory.getLogger(ResetPasswordTask.class);
+		private final RecoveryKeyFactory recoveryKeyFactory;
+		private final Vault vault;
+		private final StringProperty recoveryKey;
+		private final NewPasswordController newPasswordController;
+
+		public ResetPasswordTask(RecoveryKeyFactory recoveryKeyFactory, Vault vault, StringProperty recoveryKey, NewPasswordController newPasswordController) {
+			this.recoveryKeyFactory = recoveryKeyFactory;
+			this.vault = vault;
+			this.recoveryKey = recoveryKey;
+			this.newPasswordController = newPasswordController;
+
+			setOnFailed(event -> LOG.error("Failed to reset password", getException()));
+		}
+
+		@Override
+		protected Void call() throws IOException, IllegalArgumentException {
+			recoveryKeyFactory.newMasterkeyFileWithPassphrase(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters());
+			return null;
+		}
+	}
+
+	public static Optional<Vault> prepareVaultFromDirectory(DirectoryChooser directoryChooser, Stage window, Dialogs dialogs, VaultComponent.Factory vaultComponentFactory, List<MountService> mountServices) {
+
+		File selectedDirectory;
+		do {
+			selectedDirectory = directoryChooser.showDialog(window);
+			if (selectedDirectory == null) {
+				return Optional.empty();
+			}
+			boolean hasSubfolderD = new File(selectedDirectory, "d").isDirectory();
+
+			if (!hasSubfolderD) {
+				dialogs.prepareNoDDirectorySelectedDialog(window).build().showAndWait();
+				selectedDirectory = null;
+			}
+		} while (selectedDirectory == null);
+
+		Path selectedPath = selectedDirectory.toPath();
+		VaultSettings vaultSettings = VaultSettings.withRandomId();
+		vaultSettings.path.set(selectedPath);
+		if (selectedPath.getFileName() != null) {
+			vaultSettings.displayName.set(selectedPath.getFileName().toString());
+		} else {
+			vaultSettings.displayName.set("defaultVaultName");
+		}
+
+		var wrapper = new VaultConfigCache(vaultSettings);
+		Vault vault = vaultComponentFactory.create(vaultSettings, wrapper, VAULT_CONFIG_MISSING, null).vault();
+
+		// Spezialbehandlung für Windows + Dropbox + WinFsp
+		var nameOfWinfspLocalMounter = "org.cryptomator.frontend.fuse.mount.WinFspMountProvider";
+		if (SystemUtils.IS_OS_WINDOWS && vaultSettings.path.get().toString().contains("Dropbox") && mountServices.stream().anyMatch(s -> s.getClass().getName().equals(nameOfWinfspLocalMounter))) {
+			vaultSettings.mountService.setValue(nameOfWinfspLocalMounter);
+		}
+
+		return Optional.of(vault);
+	}
+
+
 }

+ 1 - 1
src/main/java/org/cryptomator/common/vaults/VaultConfigCache.java

@@ -20,7 +20,7 @@ public class VaultConfigCache {
 	private final VaultSettings settings;
 	private final AtomicReference<VaultConfig.UnverifiedVaultConfig> config;
 
-	VaultConfigCache(VaultSettings settings) {
+	public VaultConfigCache(VaultSettings settings) {
 		this.settings = settings;
 		this.config = new AtomicReference<>(null);
 	}

+ 6 - 0
src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -69,6 +69,12 @@ public class VaultListManager {
 		autoLocker.init();
 	}
 
+	public boolean containsVault(Path vaultPath) {
+		assert vaultPath.isAbsolute();
+		assert vaultPath.normalize().equals(vaultPath);
+		return vaultList.stream().anyMatch(v -> vaultPath.equals(v.getPath()));
+	}
+
 	public Vault add(Path pathToVault) throws IOException {
 		Path normalizedPathToVault = pathToVault.normalize().toAbsolutePath();
 		if (CryptoFileSystemProvider.checkDirStructureForVault(normalizedPathToVault, VAULTCONFIG_FILENAME, MASTERKEY_FILENAME) == DirStructure.UNRELATED) {

+ 40 - 3
src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java

@@ -2,14 +2,19 @@ package org.cryptomator.ui.addvaultwizard;
 
 import dagger.Lazy;
 import org.apache.commons.lang3.SystemUtils;
+import org.cryptomator.common.RecoverUtil;
 import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.common.vaults.VaultComponent;
 import org.cryptomator.common.vaults.VaultListManager;
+import org.cryptomator.integrations.mount.MountService;
 import org.cryptomator.integrations.uiappearance.Theme;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.dialogs.Dialogs;
 import org.cryptomator.ui.fxapp.FxApplicationStyle;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
+import org.cryptomator.ui.recoverykey.RecoveryKeyComponent;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -19,12 +24,15 @@ import javafx.beans.value.ObservableValue;
 import javafx.fxml.FXML;
 import javafx.scene.Scene;
 import javafx.scene.image.Image;
+import javafx.stage.DirectoryChooser;
 import javafx.stage.FileChooser;
 import javafx.stage.Stage;
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.ResourceBundle;
 
 import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
@@ -42,6 +50,11 @@ public class ChooseExistingVaultController implements FxController {
 	private final VaultListManager vaultListManager;
 	private final ResourceBundle resourceBundle;
 	private final ObservableValue<Image> screenshot;
+	private final Dialogs dialogs;
+	private final VaultComponent.Factory vaultComponentFactory;
+	private final RecoveryKeyComponent.Factory recoveryKeyWindow;
+	private final List<MountService> mountServices;
+
 
 	@Inject
 	ChooseExistingVaultController(@AddVaultWizardWindow Stage window, //
@@ -51,7 +64,11 @@ public class ChooseExistingVaultController implements FxController {
 								  @AddVaultWizardWindow ObjectProperty<Vault> vault, //
 								  VaultListManager vaultListManager, //
 								  ResourceBundle resourceBundle, //
-								  FxApplicationStyle applicationStyle) {
+								  FxApplicationStyle applicationStyle, //
+								  RecoveryKeyComponent.Factory recoveryKeyWindow, //
+								  VaultComponent.Factory vaultComponentFactory, //
+								  List<MountService> mountServices, //
+								  Dialogs dialogs) {
 		this.window = window;
 		this.successScene = successScene;
 		this.appWindows = appWindows;
@@ -60,6 +77,10 @@ public class ChooseExistingVaultController implements FxController {
 		this.vaultListManager = vaultListManager;
 		this.resourceBundle = resourceBundle;
 		this.screenshot = applicationStyle.appliedThemeProperty().map(this::selectScreenshot);
+		this.recoveryKeyWindow = recoveryKeyWindow;
+		this.vaultComponentFactory = vaultComponentFactory;
+		this.mountServices = mountServices;
+		this.dialogs = dialogs;
 	}
 
 	private Image selectScreenshot(Theme theme) {
@@ -96,7 +117,24 @@ public class ChooseExistingVaultController implements FxController {
 
 	@FXML
 	public void restoreVaultConfigWithRecoveryKey() {
-		//appWindows.showErrorWindow(e, window, window.getScene());
+		DirectoryChooser directoryChooser = new DirectoryChooser();
+		directoryChooser.setTitle(resourceBundle.getString("generic.button.cancel"));
+
+		Optional<Vault> optionalVault = RecoverUtil.prepareVaultFromDirectory(directoryChooser, window, dialogs, vaultComponentFactory, mountServices);
+
+		optionalVault.ifPresent(vault -> {
+			dialogs.prepareContactHubAdmin(window) //
+					.setTitleKey("a.title", vault.getVaultSettings().displayName.get() + " " + vault.getState()) //
+					.setDescriptionKey("a.description") //
+					.setMessageKey("a.message") //
+					.setCancelButtonKey("generic.button.cancel") //
+					.setOkButtonKey("generic.button.next") //
+					.setOkAction(stage -> {
+						recoveryKeyWindow.create(vault, window).showIsHubVaultDialogWindow();
+						stage.close();
+					}) //
+					.build().showAndWait();
+		});
 	}
 
 	/* Getter */
@@ -109,5 +147,4 @@ public class ChooseExistingVaultController implements FxController {
 		return screenshot.getValue();
 	}
 
-
 }

+ 1 - 0
src/main/java/org/cryptomator/ui/common/FxmlFile.java

@@ -41,6 +41,7 @@ public enum FxmlFile {
 	QUIT_FORCED("/fxml/quit_forced.fxml"), //
 	RECOVERYKEY_CREATE("/fxml/recoverykey_create.fxml"), //
 	RECOVERYKEY_IS_HUB_VAULT("/fxml/recoverykey_is_hub_vault.fxml"), //
+	RECOVERYKEY_EXPERT_SETTINGS("/fxml/recoverykey_expert_settings.fxml"), //
 	RECOVERYKEY_RECOVER("/fxml/recoverykey_recover.fxml"), //
 	RECOVERYKEY_RESET_PASSWORD("/fxml/recoverykey_reset_password.fxml"), //
 	RECOVERYKEY_RESET_PASSWORD_SUCCESS("/fxml/recoverykey_reset_password_success.fxml"), //

+ 14 - 0
src/main/java/org/cryptomator/ui/dialogs/Dialogs.java

@@ -97,4 +97,18 @@ public class Dialogs {
 				.setOkAction(okAction) //
 				.setCancelAction(Stage::close);
 	}
+
+	public SimpleDialog.Builder prepareNoDDirectorySelectedDialog(Stage window) {
+		return createDialogBuilder() //
+				.setOwner(window) //
+				.setTitleKey("recoveryKey.noDDirDetected.title") //
+				.setMessageKey("recoveryKey.noDDirDetected.message") //
+				.setDescriptionKey("recoveryKey.noDDirDetected.description") //
+				.setIcon(FontAwesome5Icon.EXCLAMATION) //
+				.setOkButtonKey("generic.button.change") //
+				.setCancelButtonKey("generic.button.close") //
+				.setOkAction(Stage::close) //
+				.setCancelAction(Stage::close);
+	}
+
 }

+ 3 - 3
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java

@@ -43,16 +43,16 @@ public interface RecoveryKeyComponent {
 
 	default void showRecoveryKeyRecoverWindow(String title) {
 		Stage stage = window();
-		stage.setScene(recoverScene().get());
 		stage.setTitle(title);
+		stage.setScene(recoverScene().get());
 		stage.sizeToScene();
 		stage.show();
 	}
 
-	default void showIsHubVaultDialogWindow(){
+	default void showIsHubVaultDialogWindow() {
 		Stage stage = window();
 		stage.setScene(recoverIsHubVaultScene().get());
-		stage.setTitle("Recover Vault Config");
+		stage.setTitle("Recover Config");
 		stage.sizeToScene();
 		stage.show();
 	}

+ 97 - 0
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyExpertSettingsController.java

@@ -0,0 +1,97 @@
+package org.cryptomator.ui.recoverykey;
+
+import dagger.Lazy;
+import org.cryptomator.ui.addvaultwizard.CreateNewVaultExpertSettingsController;
+import org.cryptomator.ui.common.FxController;
+import org.cryptomator.ui.common.FxmlFile;
+import org.cryptomator.ui.common.FxmlScene;
+import org.cryptomator.ui.controls.NumericTextField;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javafx.application.Application;
+import javafx.beans.binding.Bindings;
+import javafx.beans.binding.BooleanBinding;
+import javafx.beans.property.IntegerProperty;
+import javafx.fxml.FXML;
+import javafx.scene.Scene;
+import javafx.scene.control.CheckBox;
+import javafx.stage.Stage;
+
+@RecoveryKeyScoped
+public class RecoveryKeyExpertSettingsController implements FxController {
+
+	public static final int MAX_SHORTENING_THRESHOLD = 220;
+	public static final int MIN_SHORTENING_THRESHOLD = 36;
+	private static final String DOCS_NAME_SHORTENING_URL = "https://docs.cryptomator.org/security/architecture/#name-shortening";
+
+	private final Stage window;
+	private final Lazy<Application> application;
+	private final Lazy<Scene> resetPasswordScene;
+	private final Lazy<Scene> recoverScene;
+
+	public CheckBox expertSettingsCheckBox;
+	public NumericTextField shorteningThresholdTextField;
+	private final IntegerProperty shorteningThreshold;
+	private final BooleanBinding validShorteningThreshold;
+
+
+	@Inject
+	public RecoveryKeyExpertSettingsController(@RecoveryKeyWindow Stage window, //
+											   Lazy<Application> application, //
+											   @Named("shorteningThreshold") IntegerProperty shorteningThreshold, //
+											   @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, //
+											   @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverScene) {
+		this.window = window;
+		this.application = application;
+		this.resetPasswordScene = resetPasswordScene;
+		this.recoverScene = recoverScene;
+		this.shorteningThreshold = shorteningThreshold;
+		this.validShorteningThreshold = Bindings.createBooleanBinding(this::isValidShorteningThreshold, shorteningThreshold);
+
+	}
+
+	@FXML
+	public void initialize() {
+		shorteningThresholdTextField.setPromptText(MIN_SHORTENING_THRESHOLD + "-" + MAX_SHORTENING_THRESHOLD);
+		shorteningThresholdTextField.setText(Integer.toString(MAX_SHORTENING_THRESHOLD));
+		shorteningThresholdTextField.textProperty().addListener((_, _, newValue) -> {
+			try {
+				int intValue = Integer.parseInt(newValue);
+				shorteningThreshold.set(intValue);
+			} catch (NumberFormatException e) {
+				shorteningThreshold.set(0); //the value is set to 0 to ensure that an invalid value assignment is detected during a NumberFormatException
+			}
+		});
+	}
+
+	@FXML
+	public void toggleUseExpertSettings() {
+		if (!expertSettingsCheckBox.isSelected()) {
+			shorteningThresholdTextField.setText(Integer.toString(CreateNewVaultExpertSettingsController.MAX_SHORTENING_THRESHOLD));
+		}
+	}
+
+	public void openDocs() {
+		application.get().getHostServices().showDocument(DOCS_NAME_SHORTENING_URL);
+	}
+
+	public BooleanBinding validShorteningThresholdProperty() {
+		return validShorteningThreshold;
+	}
+
+	public boolean isValidShorteningThreshold() {
+		var value = shorteningThreshold.get();
+		return value >= MIN_SHORTENING_THRESHOLD && value <= MAX_SHORTENING_THRESHOLD;
+	}
+
+	@FXML
+	public void back() {
+		window.setScene(recoverScene.get());
+	}
+
+	@FXML
+	public void next() {
+		window.setScene(resetPasswordScene.get());
+	}
+}

+ 2 - 8
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyIsHubVaultController.java

@@ -1,7 +1,6 @@
 package org.cryptomator.ui.recoverykey;
 
 import dagger.Lazy;
-import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
@@ -9,11 +8,9 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
-import javafx.beans.property.StringProperty;
 import javafx.fxml.FXML;
 import javafx.scene.Scene;
 import javafx.stage.Stage;
-import java.util.ResourceBundle;
 
 @RecoveryKeyScoped
 public class RecoveryKeyIsHubVaultController implements FxController {
@@ -25,12 +22,8 @@ public class RecoveryKeyIsHubVaultController implements FxController {
 
 	@Inject
 	public RecoveryKeyIsHubVaultController(@RecoveryKeyWindow Stage window,
-										   @RecoveryKeyWindow Vault vault,
-										   @RecoveryKeyWindow StringProperty recoveryKey,
-										   @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverykeyRecoverScene,
-										   ResourceBundle resourceBundle) {
+										   @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverykeyRecoverScene) {
 		this.window = window;
-		//window.setTitle("Is it a hub vault? Huh?");
 		this.recoverykeyRecoverScene = recoverykeyRecoverScene;
 	}
 
@@ -45,6 +38,7 @@ public class RecoveryKeyIsHubVaultController implements FxController {
 
 	@FXML
 	public void recover() {
+		window.setTitle("Recover Config");
 		window.setScene(recoverykeyRecoverScene.get());
 	}
 }

+ 23 - 0
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java

@@ -7,6 +7,7 @@ import dagger.multibindings.IntoMap;
 import org.cryptomator.common.Nullable;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptofs.VaultConfig;
+import org.cryptomator.ui.addvaultwizard.CreateNewVaultExpertSettingsController;
 import org.cryptomator.ui.common.DefaultSceneFactory;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxControllerKey;
@@ -19,6 +20,8 @@ import org.cryptomator.ui.common.StageFactory;
 
 import javax.inject.Named;
 import javax.inject.Provider;
+import javafx.beans.property.IntegerProperty;
+import javafx.beans.property.SimpleIntegerProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 import javafx.scene.Scene;
@@ -119,6 +122,13 @@ abstract class RecoveryKeyModule {
 		return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_IS_HUB_VAULT);
 	}
 
+	@Provides
+	@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS)
+	@RecoveryKeyScoped
+	static Scene provideRecoveryKeyExpertSettingsScene(@RecoveryKeyWindow FxmlLoaderFactory fxmlLoaders) {
+		return fxmlLoaders.createScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS);
+	}
+
 	// ------------------
 
 	@Binds
@@ -133,6 +143,19 @@ abstract class RecoveryKeyModule {
 		return new RecoveryKeyDisplayController(window, vault.getDisplayName(), recoveryKey.get(), localization);
 	}
 
+	@Provides
+	@Named("shorteningThreshold")
+	@RecoveryKeyScoped
+	static IntegerProperty provideShorteningThreshold() {
+		return new SimpleIntegerProperty(CreateNewVaultExpertSettingsController.MAX_SHORTENING_THRESHOLD);
+	}
+
+
+	@Binds
+	@IntoMap
+	@FxControllerKey(RecoveryKeyExpertSettingsController.class)
+	abstract FxController provideRecoveryKeyExpertSettingsController(RecoveryKeyExpertSettingsController controller);
+
 	@Binds
 	@IntoMap
 	@FxControllerKey(RecoveryKeyIsHubVaultController.class)

+ 10 - 8
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java

@@ -4,8 +4,6 @@ import dagger.Lazy;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
 import javafx.fxml.FXML;
@@ -16,10 +14,8 @@ import java.util.ResourceBundle;
 @RecoveryKeyScoped
 public class RecoveryKeyRecoverController implements FxController {
 
-	private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
-
 	private final Stage window;
-	private final Lazy<Scene> resetPasswordScene;
+	private final Lazy<Scene> nextScene;
 
 	@FXML
 	RecoveryKeyValidateController recoveryKeyValidateController;
@@ -27,10 +23,16 @@ public class RecoveryKeyRecoverController implements FxController {
 	@Inject
 	public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, //
 										@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, //
+										@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> expertSettingsScene, //
 										ResourceBundle resourceBundle) {
 		this.window = window;
-		window.setTitle(resourceBundle.getString("recoveryKey.recover.title"));
-		this.resetPasswordScene = resetPasswordScene;
+		if (window.getTitle().equals("Recover Config")) {
+			this.nextScene = expertSettingsScene;
+		} else if (window.getTitle().equals(resourceBundle.getString("recoveryKey.recover.title"))) {
+			this.nextScene = resetPasswordScene;
+		} else {
+			this.nextScene = resetPasswordScene;
+		}
 	}
 
 	@FXML
@@ -44,7 +46,7 @@ public class RecoveryKeyRecoverController implements FxController {
 
 	@FXML
 	public void recover() {
-		window.setScene(resetPasswordScene.get());
+		window.setScene(nextScene.get());
 	}
 
 	/* Getter/Setter */

+ 34 - 80
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java

@@ -3,23 +3,21 @@ package org.cryptomator.ui.recoverykey;
 import dagger.Lazy;
 import org.cryptomator.common.RecoverUtil;
 import org.cryptomator.common.vaults.Vault;
-import org.cryptomator.common.vaults.VaultState;
-import org.cryptomator.cryptofs.CryptoFileSystemProperties;
-import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.common.vaults.VaultListManager;
 import org.cryptomator.cryptolib.api.CryptoException;
-import org.cryptomator.cryptolib.api.CryptorProvider;
 import org.cryptomator.cryptolib.api.Masterkey;
-import org.cryptomator.cryptolib.api.MasterkeyLoader;
 import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.cryptomator.ui.changepassword.NewPasswordController;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
-import org.cryptomator.ui.changepassword.NewPasswordController;
 import org.cryptomator.ui.fxapp.FxApplicationWindows;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import javax.inject.Inject;
+import javax.inject.Named;
+import javafx.beans.property.IntegerProperty;
 import javafx.beans.property.ReadOnlyBooleanProperty;
 import javafx.beans.property.StringProperty;
 import javafx.concurrent.Task;
@@ -27,16 +25,10 @@ import javafx.fxml.FXML;
 import javafx.scene.Scene;
 import javafx.stage.Stage;
 import java.io.IOException;
-import java.nio.file.CopyOption;
-import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.StandardCopyOption;
-import java.util.Comparator;
 import java.util.concurrent.ExecutorService;
 
-import static org.cryptomator.common.Constants.DEFAULT_KEY_ID;
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
-import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 
 @RecoveryKeyScoped
 public class RecoveryKeyResetPasswordController implements FxController {
@@ -52,6 +44,8 @@ public class RecoveryKeyResetPasswordController implements FxController {
 	private final Lazy<Scene> recoverResetVaultConfigSuccessScene;
 	private final FxApplicationWindows appWindows;
 	private final MasterkeyFileAccess masterkeyFileAccess;
+	private final VaultListManager vaultListManager;
+	private final IntegerProperty shorteningThreshold;
 
 	public NewPasswordController newPasswordController;
 
@@ -64,7 +58,9 @@ public class RecoveryKeyResetPasswordController implements FxController {
 											  @FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD_SUCCESS) Lazy<Scene> recoverResetPasswordSuccessScene, //
 											  @FxmlScene(FxmlFile.RECOVERYKEY_RESET_VAULT_CONFIG_SUCCESS) Lazy<Scene> recoverResetVaultConfigSuccessScene, //
 											  FxApplicationWindows appWindows, //
-											  MasterkeyFileAccess masterkeyFileAccess) {
+											  MasterkeyFileAccess masterkeyFileAccess, //
+											  VaultListManager vaultListManager, //
+											  @Named("shorteningThreshold") IntegerProperty shorteningThreshold) {
 		this.window = window;
 		this.vault = vault;
 		this.recoveryKeyFactory = recoveryKeyFactory;
@@ -74,6 +70,8 @@ public class RecoveryKeyResetPasswordController implements FxController {
 		this.recoverResetVaultConfigSuccessScene = recoverResetVaultConfigSuccessScene;
 		this.appWindows = appWindows;
 		this.masterkeyFileAccess = masterkeyFileAccess;
+		this.vaultListManager = vaultListManager;
+		this.shorteningThreshold = shorteningThreshold;
 	}
 
 	@FXML
@@ -83,82 +81,38 @@ public class RecoveryKeyResetPasswordController implements FxController {
 
 	@FXML
 	public void resetPassword() {
-		if(vault.isMissingVaultConfig()){
-			Path vaultPath = vault.getPath();
-			Path recoveryPath = vaultPath.resolve("r");
+		if (vault.isMissingVaultConfig()) {
 			try {
-				Files.createDirectory(recoveryPath);
-				recoveryKeyFactory.newMasterkeyFileWithPassphrase(recoveryPath, recoveryKey.get(), newPasswordController.passwordField.getCharacters());
-			} catch (IOException e) {
-				LOG.error("Creating directory or recovering masterkey failed", e);
-			}
+				Path recoveryPath = RecoverUtil.createRecoveryDirectory(vault.getPath());
+				RecoverUtil.createNewMasterkeyFile(recoveryKeyFactory, recoveryPath, recoveryKey.get(), newPasswordController.passwordField.getCharacters());
+				Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
 
-			Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
-			try (Masterkey masterkey = masterkeyFileAccess.load(masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
-				try {
-					var combo = RecoverUtil.detectCipherCombo(masterkey.getEncoded(),vaultPath);
-					MasterkeyLoader loader = ignored -> masterkey.copy();
-					CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() //
-							.withCipherCombo(combo) //
-							.withKeyLoader(loader) //
-							.withShorteningThreshold(220) //
-							.build();
-					CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID);
-				} catch (CryptoException | IOException e) {
-					LOG.error("Recovering vault failed", e);
-				}
-				Files.move(masterkeyFilePath, vaultPath.resolve(MASTERKEY_FILENAME), StandardCopyOption.REPLACE_EXISTING);
-				Files.move(recoveryPath.resolve(VAULTCONFIG_FILENAME), vaultPath.resolve(VAULTCONFIG_FILENAME));
-				try (var paths = Files.walk(recoveryPath)) {
-					paths.sorted(Comparator.reverseOrder()).forEach(p -> {
-						try {
-							Files.delete(p);
-						} catch (IOException e) {
-							LOG.info("Unable to delete {}. Please delete it manually.", p);
-						}
-					});
+				try (Masterkey masterkey = RecoverUtil.loadMasterkey(masterkeyFileAccess, masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
+					RecoverUtil.initializeCryptoFileSystem(recoveryPath, vault.getPath(), masterkey, shorteningThreshold);
 				}
+
+				RecoverUtil.moveRecoveredFiles(recoveryPath, vault.getPath());
+				RecoverUtil.deleteRecoveryDirectory(recoveryPath);
+				RecoverUtil.addVaultToList(vaultListManager, vault.getPath());
+
 				window.setScene(recoverResetVaultConfigSuccessScene.get());
-			} catch (IOException e) {
-				LOG.error("Moving recovered files failed", e);
+			} catch (IOException | CryptoException e) {
+				LOG.error("Recovery process failed", e);
 			}
-		}
-		else {
-			Task<Void> task = new ResetPasswordTask();
-			task.setOnScheduled(event -> {
-				LOG.debug("Using recovery key to reset password for {}.", vault.getDisplayablePath());
-			});
-			task.setOnSucceeded(event -> {
-				LOG.info("Used recovery key to reset password for {}.", vault.getDisplayablePath());
-				if(vault.getState().equals(VaultState.Value.VAULT_CONFIG_MISSING)){
-					window.setScene(recoverResetVaultConfigSuccessScene.get());
-				}
-				else {
-					window.setScene(recoverResetPasswordSuccessScene.get());
-				}
-			});
-			task.setOnFailed(event -> {
-				LOG.error("Resetting password failed.", task.getException());
-				appWindows.showErrorWindow(task.getException(), window, null);
-			});
+		} else {
+			Task<Void> task = RecoverUtil.createResetPasswordTask( //
+					recoveryKeyFactory, //
+					vault, //
+					recoveryKey, //
+					newPasswordController, //
+					window, //
+					recoverResetPasswordSuccessScene, //
+					recoverResetVaultConfigSuccessScene, //
+					appWindows);
 			executor.submit(task);
 		}
 	}
 
-	private class ResetPasswordTask extends Task<Void> {
-
-		private ResetPasswordTask() {
-			setOnFailed(event -> LOG.error("Failed to reset password", getException()));
-		}
-
-		@Override
-		protected Void call() throws IOException, IllegalArgumentException {
-			recoveryKeyFactory.newMasterkeyFileWithPassphrase(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters());
-			return null;
-		}
-
-	}
-
 	/* Getter/Setter */
 
 	public ReadOnlyBooleanProperty passwordSufficientAndMatchingProperty() {

+ 10 - 2
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordSuccessController.java

@@ -3,21 +3,29 @@ package org.cryptomator.ui.recoverykey;
 import org.cryptomator.ui.common.FxController;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javafx.fxml.FXML;
 import javafx.stage.Stage;
 
 @RecoveryKeyScoped
-public class RecoveryKeyResetPasswordSuccessController  implements FxController {
+public class RecoveryKeyResetPasswordSuccessController implements FxController {
 
 	private final Stage window;
+	private final Stage owner;
 
 	@Inject
-	public RecoveryKeyResetPasswordSuccessController(@RecoveryKeyWindow Stage window) {
+	public RecoveryKeyResetPasswordSuccessController(@RecoveryKeyWindow Stage window, //
+													 @Named("keyRecoveryOwner") Stage owner) {
+
 		this.window = window;
+		this.owner = owner;
 	}
 
 	@FXML
 	public void close() {
+		if (!owner.getTitle().equals("Cryptomator")) {
+			owner.close();
+		}
 		window.close();
 	}
 

+ 6 - 2
src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java

@@ -14,6 +14,7 @@ import javafx.beans.property.SimpleBooleanProperty;
 import javafx.beans.value.ObservableValue;
 import javafx.fxml.FXML;
 import javafx.stage.Stage;
+import java.util.ResourceBundle;
 
 @VaultOptionsScoped
 public class MasterkeyOptionsController implements FxController {
@@ -27,16 +28,19 @@ public class MasterkeyOptionsController implements FxController {
 	private final ForgetPasswordComponent.Builder forgetPasswordWindow;
 	private final KeychainManager keychain;
 	private final ObservableValue<Boolean> passwordSaved;
+	private final ResourceBundle resourceBundle;
 
 
 	@Inject
-	MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Factory recoveryKeyWindow, ForgetPasswordComponent.Builder forgetPasswordWindow, KeychainManager keychain) {
+	MasterkeyOptionsController(@VaultOptionsWindow Vault vault, @VaultOptionsWindow Stage window, ChangePasswordComponent.Builder changePasswordWindow, RecoveryKeyComponent.Factory recoveryKeyWindow, ForgetPasswordComponent.Builder forgetPasswordWindow, KeychainManager keychain, //
+							   ResourceBundle resourceBundle) {
 		this.vault = vault;
 		this.window = window;
 		this.changePasswordWindow = changePasswordWindow;
 		this.recoveryKeyWindow = recoveryKeyWindow;
 		this.forgetPasswordWindow = forgetPasswordWindow;
 		this.keychain = keychain;
+		this.resourceBundle = resourceBundle;
 		if (keychain.isSupported() && !keychain.isLocked()) {
 			this.passwordSaved = keychain.getPassphraseStoredProperty(vault.getId()).orElse(false);
 		} else {
@@ -56,7 +60,7 @@ public class MasterkeyOptionsController implements FxController {
 
 	@FXML
 	public void showRecoverVaultDialog() {
-		recoveryKeyWindow.create(vault, window).showRecoveryKeyRecoverWindow();
+		recoveryKeyWindow.create(vault, window).showRecoveryKeyRecoverWindow(resourceBundle.getString("recoveryKey.recover.title"));
 	}
 
 	@FXML

+ 85 - 0
src/main/resources/fxml/recoverykey_expert_settings.fxml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import org.cryptomator.ui.controls.FontAwesome5IconView?>
+<?import org.cryptomator.ui.controls.NumericTextField?>
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.CheckBox?>
+<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.Tooltip?>
+<?import javafx.scene.Group?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.Region?>
+<?import javafx.scene.layout.StackPane?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.shape.Circle?>
+<HBox xmlns:fx="http://javafx.com/fxml"
+	  xmlns="http://javafx.com/javafx"
+	  fx:controller="org.cryptomator.ui.recoverykey.RecoveryKeyExpertSettingsController"
+	  minWidth="400"
+	  maxWidth="400"
+	  minHeight="145"
+	  spacing="12"
+	  alignment="TOP_CENTER">
+	<padding>
+		<Insets topRightBottomLeft="12"/>
+	</padding>
+	<Group>
+		<StackPane>
+			<padding>
+				<Insets topRightBottomLeft="6"/>
+			</padding>
+			<Circle styleClass="glyph-icon-primary" radius="24"/>
+			<FontAwesome5IconView styleClass="glyph-icon-white" glyph="QUESTION" glyphSize="24"/>
+		</StackPane>
+	</Group>
+
+	<VBox HBox.hgrow="ALWAYS">
+		<Label styleClass="label-large" text="Expert Settings" wrapText="true">
+			<padding>
+				<Insets bottom="6" top="6"/>
+			</padding>
+		</Label>
+
+		<CheckBox fx:id="expertSettingsCheckBox" text="%addvaultwizard.new.expertSettings.enableExpertSettingsCheckbox" onAction="#toggleUseExpertSettings"/>
+		<VBox spacing="6" visible="${expertSettingsCheckBox.selected}">
+			<HBox spacing="2" HBox.hgrow="NEVER">
+				<Label text="%addvaultwizard.new.expertSettings.shorteningThreshold.title"/>
+				<Region prefWidth="2"/>
+				<Hyperlink contentDisplay="GRAPHIC_ONLY" onAction="#openDocs">
+					<graphic>
+						<FontAwesome5IconView glyph="QUESTION_CIRCLE" styleClass="glyph-icon-muted"/>
+					</graphic>
+					<tooltip>
+						<Tooltip text="%addvaultwizard.new.expertSettings.shorteningThreshold.tooltip" showDelay="10ms"/>
+					</tooltip>
+				</Hyperlink>
+			</HBox>
+			<NumericTextField fx:id="shorteningThresholdTextField"/>
+			<HBox alignment="TOP_RIGHT">
+				<Region minWidth="4" prefWidth="4" HBox.hgrow="NEVER"/>
+				<StackPane>
+					<Label styleClass="label-muted" text="%addvaultwizard.new.expertSettings.shorteningThreshold.invalid" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${!controller.validShorteningThreshold}" managed="${!controller.validShorteningThreshold}" graphicTextGap="6">
+						<graphic>
+							<FontAwesome5IconView styleClass="glyph-icon-red" glyph="TIMES"/>
+						</graphic>
+					</Label>
+					<Label styleClass="label-muted" text="%addvaultwizard.new.expertSettings.shorteningThreshold.valid" textAlignment="RIGHT" alignment="CENTER_RIGHT" visible="${controller.validShorteningThreshold}" managed="${controller.validShorteningThreshold}" graphicTextGap="6">
+						<graphic>
+							<FontAwesome5IconView styleClass="glyph-icon-primary" glyph="CHECK"/>
+						</graphic>
+					</Label>
+				</StackPane>
+			</HBox>
+		</VBox>
+		<Region VBox.vgrow="ALWAYS" minHeight="18"/>
+		<ButtonBar buttonMinWidth="120" buttonOrder="+C">
+			<buttons>
+				<Button text="%generic.button.back" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#back"/>
+				<Button text="%generic.button.next" ButtonBar.buttonData="CANCEL_CLOSE" defaultButton="true" onAction="#next"/>
+			</buttons>
+		</ButtonBar>
+	</VBox>
+</HBox>

+ 10 - 0
src/main/resources/i18n/strings.properties

@@ -525,6 +525,16 @@ recoveryKey.recover.resetSuccess.description=You can unlock your vault with the
 ### Recovery Key Vault Config Reset Success
 recoveryKey.recover.resetVaultConfigSuccess.message=Vault config reset successful
 
+### Recover Kram
+
+recoveryKey.noDDirDetected.title=Invalid Selection
+recoveryKey.noDDirDetected.description=The selected folder must contain a subfolder named "d".
+recoveryKey.noDDirDetected.message=Please choose a different folder that includes a subfolder named "d".
+
+a.title=Title %s
+a.description=Description
+a.message=Message
+
 # Convert Vault
 convertVault.title=Convert Vault
 convertVault.convert.convertBtn.before=Convert