Przeglądaj źródła

new recovery package in common

Jan-Peter Klein 4 tygodni temu
rodzic
commit
90cbb0c5f6
18 zmienionych plików z 455 dodań i 93 usunięć
  1. 52 0
      src/main/java/org/cryptomator/common/recovery/BackupRestorer.java
  2. 33 0
      src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java
  3. 102 0
      src/main/java/org/cryptomator/common/recovery/MasterkeyService.java
  4. 9 0
      src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java
  5. 65 0
      src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java
  6. 33 38
      src/main/java/org/cryptomator/common/vaults/VaultListManager.java
  7. 42 3
      src/main/java/org/cryptomator/ui/addvaultwizard/ChooseExistingVaultController.java
  8. 2 2
      src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java
  9. 3 3
      src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java
  10. 2 2
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyComponent.java
  11. 4 4
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyIsHubVaultController.java
  12. 2 2
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java
  13. 2 2
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyRecoverController.java
  14. 92 29
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java
  15. 6 4
      src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java
  16. 3 3
      src/main/java/org/cryptomator/ui/vaultoptions/MasterkeyOptionsController.java
  17. 2 1
      src/main/resources/fxml/recoverykey_reset_password.fxml
  18. 1 0
      src/main/resources/i18n/strings.properties

+ 52 - 0
src/main/java/org/cryptomator/common/recovery/BackupRestorer.java

@@ -0,0 +1,52 @@
+package org.cryptomator.common.recovery;
+
+import static org.cryptomator.common.vaults.VaultState.Value.*;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.stream.Stream;
+
+import org.cryptomator.common.vaults.VaultState.Value;
+
+public final class BackupRestorer {
+
+	private BackupRestorer() {}
+
+	public static boolean restoreIfPresent(Path vaultPath, Value vaultState) {
+		Path targetFile;
+		switch (vaultState) {
+			case VAULT_CONFIG_MISSING -> targetFile = vaultPath.resolve("vault.cryptomator");
+			case MASTERKEY_MISSING -> targetFile = vaultPath.resolve("masterkey.cryptomator");
+			default -> {
+				return false;
+			}
+		}
+
+		try (Stream<Path> files = Files.list(vaultPath)) {
+			return files
+					.filter(file -> isValidBackupFileForState(file.getFileName().toString(), vaultState))
+					.findFirst()
+					.map(backupFile -> copyBackupFile(backupFile, targetFile))
+					.orElse(false);
+		} catch (IOException e) {
+			return false;
+		}
+	}
+
+	private static boolean isValidBackupFileForState(String fileName, Value vaultState) {
+		return switch (vaultState) {
+			case VAULT_CONFIG_MISSING -> fileName.startsWith("vault.cryptomator") && fileName.endsWith(".bkup");
+			case MASTERKEY_MISSING -> fileName.startsWith("masterkey.cryptomator") && fileName.endsWith(".bkup");
+			default -> false;
+		};
+	}
+
+	private static boolean copyBackupFile(Path backupFile, Path configPath) {
+		try {
+			Files.copy(backupFile, configPath, StandardCopyOption.REPLACE_EXISTING);
+			return true;
+		} catch (IOException e) {
+			return false;
+		}
+	}
+}

+ 33 - 0
src/main/java/org/cryptomator/common/recovery/CryptoFsInitializer.java

@@ -0,0 +1,33 @@
+package org.cryptomator.common.recovery;
+
+import java.io.IOException;
+import java.nio.file.Path;
+
+import javafx.beans.property.IntegerProperty;
+
+import org.cryptomator.common.Constants;
+import org.cryptomator.cryptofs.CryptoFileSystemProperties;
+import org.cryptomator.cryptofs.CryptoFileSystemProvider;
+import org.cryptomator.cryptolib.api.*;
+
+import static org.cryptomator.common.Constants.DEFAULT_KEY_ID;
+
+public final class CryptoFsInitializer {
+
+	private CryptoFsInitializer() {}
+
+	public static void init(Path recoveryPath,
+							Masterkey masterkey,
+							IntegerProperty shorteningThreshold,
+							CryptorProvider.Scheme scheme) throws IOException, CryptoException {
+
+		MasterkeyLoader loader = ignored -> masterkey.copy();
+		CryptoFileSystemProperties fsProps = CryptoFileSystemProperties //
+				.cryptoFileSystemProperties() //
+				.withCipherCombo(scheme) //
+				.withKeyLoader(loader) //
+				.withShorteningThreshold(shorteningThreshold.get()) //
+				.build();
+		CryptoFileSystemProvider.initialize(recoveryPath, fsProps, DEFAULT_KEY_ID);
+	}
+}

+ 102 - 0
src/main/java/org/cryptomator/common/recovery/MasterkeyService.java

@@ -0,0 +1,102 @@
+package org.cryptomator.common.recovery;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Stream;
+
+import org.cryptomator.common.vaults.Vault;
+import org.cryptomator.cryptolib.api.*;
+import org.cryptomator.cryptolib.common.MasterkeyFileAccess;
+import org.cryptomator.ui.recoverykey.RecoveryKeyFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javafx.beans.property.StringProperty;
+
+import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.cryptofs.common.Constants.DATA_DIR_NAME;
+
+public final class MasterkeyService {
+
+	private static final Logger LOG = LoggerFactory.getLogger(MasterkeyService.class);
+
+	private MasterkeyService() {}
+
+	public static void recoverFromRecoveryKey(String recoveryKey, RecoveryKeyFactory recoveryKeyFactory, Path recoveryPath, CharSequence newPassword) throws IOException {
+		recoveryKeyFactory.newMasterkeyFileWithPassphrase(recoveryPath, recoveryKey, newPassword);
+	}
+
+	public static Masterkey load(MasterkeyFileAccess masterkeyFileAccess, Path masterkeyFilePath, CharSequence password) throws IOException {
+		return masterkeyFileAccess.load(masterkeyFilePath, password);
+	}
+
+	public static Optional<CryptorProvider.Scheme> validateRecoveryKeyAndDetectCombo(RecoveryKeyFactory recoveryKeyFactory, Vault vault, StringProperty recoveryKey, MasterkeyFileAccess masterkeyFileAccess, AtomicBoolean illegalArgumentExceptionOccurred) {
+
+		var tmpPass = UUID.randomUUID().toString();
+		try(RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) {
+			var tempRecoveryPath = recoveryDirectory.getRecoveryPath();
+			recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, tempRecoveryPath, tmpPass);
+			var masterkeyFilePath = tempRecoveryPath.resolve(MASTERKEY_FILENAME);
+
+			try (Masterkey mk = load(masterkeyFileAccess, masterkeyFilePath, tmpPass)) {
+				return detect(mk.getEncoded(), vault.getPath());
+			} catch (IOException | CryptoException e) {
+				LOG.info("Recovery key validation failed", e);
+				return Optional.empty();
+			} catch (IllegalArgumentException e) {
+				illegalArgumentExceptionOccurred.set(true);
+				return Optional.empty();
+			}
+		} catch (IOException | CryptoException e) {
+			LOG.info("Recovery key validation failed");
+		} catch (IllegalArgumentException e) {
+			LOG.info("Recovery key has an illegal argument");
+			illegalArgumentExceptionOccurred.set(true);
+		}
+		return Optional.empty();
+
+	}
+	public static Optional<CryptorProvider.Scheme> detect(byte[] masterkey, Path vaultPath) {
+		try (Stream<Path> paths = Files.walk(vaultPath.resolve(DATA_DIR_NAME))) {
+			Path c9rFile = paths.filter(p -> p.toString().endsWith(".c9r"))
+					.findFirst().orElse(null);
+			if (c9rFile == null) {
+				LOG.info("No *.c9r file found in {}", vaultPath);
+				return Optional.empty();
+			}
+			return determineScheme(c9rFile, masterkey); // jetzt auch ein Optional
+		} catch (IOException e) {
+			LOG.debug("Failed to inspect vault", e);
+			return Optional.empty();
+		}
+	}
+
+	private static Optional<CryptorProvider.Scheme> determineScheme(Path c9rFile, byte[] masterkey) {
+		try {
+			ByteBuffer header = ByteBuffer.wrap(Files.readAllBytes(c9rFile));
+			return Arrays.stream(CryptorProvider.Scheme.values())
+					.filter(s -> tryDecrypt(header, new Masterkey(masterkey), s))
+					.findFirst();
+		} catch (IOException e) {
+			LOG.info("Failed to decrypt .c9r file", e);
+			return Optional.empty();
+		}
+	}
+
+	private static boolean tryDecrypt(ByteBuffer header, Masterkey masterkey, CryptorProvider.Scheme scheme) {
+		try (Cryptor cryptor = CryptorProvider.forScheme(scheme).provide(masterkey, SecureRandom.getInstanceStrong())) {
+			cryptor.fileHeaderCryptor().decryptHeader(header.duplicate());
+			return true;
+		} catch (Exception e) {
+			return false;
+		}
+	}
+}

+ 9 - 0
src/main/java/org/cryptomator/common/recovery/RecoveryActionType.java

@@ -0,0 +1,9 @@
+package org.cryptomator.common.recovery;
+
+public enum RecoveryActionType {
+	RESTORE_VAULT_CONFIG,
+	RESTORE_MASTERKEY,
+	RESET_PASSWORD,
+	SHOW_KEY,
+	CONVERT_VAULT
+}

+ 65 - 0
src/main/java/org/cryptomator/common/recovery/RecoveryDirectory.java

@@ -0,0 +1,65 @@
+package org.cryptomator.common.recovery;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.util.Comparator;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
+import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
+
+public final class RecoveryDirectory implements AutoCloseable {
+
+	private static final Logger LOG = LoggerFactory.getLogger(RecoveryDirectory.class);
+
+	private final Path recoveryPath;
+	private final Path vaultPath;
+
+
+	private static Path addR(Path p){
+		return p.resolve("r");
+	}
+
+	private RecoveryDirectory(Path vaultPath) {
+		this.vaultPath = vaultPath;
+		this.recoveryPath = addR(vaultPath);
+	}
+
+	public static RecoveryDirectory create(Path vaultPath) throws IOException {
+		//TODO: Files.createTmpDirectory Doku lesen und ggf nutzen
+		Path recovery = addR(vaultPath);
+		Files.createDirectory(recovery);
+		return new RecoveryDirectory(vaultPath);
+	}
+
+	public void moveRecoveredFiles() 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)); //TODO: ? StandardCopyOption.REPLACE_EXISTING
+	}
+
+	private void deleteRecoveryDirectory() {
+		try (var paths = Files.walk(recoveryPath)) {
+			paths.sorted(Comparator.reverseOrder()).forEach(p -> { //TODO: wieso reverseOrder
+				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);
+		}
+	}
+
+	@Override
+	public void close() {
+		deleteRecoveryDirectory();
+	}
+
+	public Path getRecoveryPath() {
+		return recoveryPath;
+	}
+
+}

+ 33 - 38
src/main/java/org/cryptomator/common/vaults/VaultListManager.java

@@ -26,9 +26,11 @@ import static org.cryptomator.common.Constants.VAULTCONFIG_FILENAME;
 import static org.cryptomator.common.vaults.VaultState.Value.ERROR;
 import static org.cryptomator.common.vaults.VaultState.Value.LOCKED;
 import static org.cryptomator.common.vaults.VaultState.Value.MASTERKEY_MISSING;
+import static org.cryptomator.common.vaults.VaultState.Value.PROCESSING;
+import static org.cryptomator.common.vaults.VaultState.Value.UNLOCKED;
 
 import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.BackupRestorer;
 import org.cryptomator.common.settings.Settings;
 import org.cryptomator.common.settings.VaultSettings;
 import org.cryptomator.cryptofs.CryptoFileSystemProvider;
@@ -146,45 +148,35 @@ public class VaultListManager {
 			return vaultComponentFactory.create(vaultSettings, wrapper, ERROR, e).vault();
 		}
 	}
-
 	public static VaultState.Value redetermineVaultState(Vault vault) {
-		VaultState state = vault.stateProperty();
-		VaultState.Value previousState = state.getValue();
-		return switch (previousState) {
-			case LOCKED, NEEDS_MIGRATION, MISSING, VAULT_CONFIG_MISSING, MASTERKEY_MISSING -> {
-				try {
-					var determinedState = determineVaultState(vault.getPath(), vault.getVaultSettings());
-					if (determinedState == MASTERKEY_MISSING) {
-						var vaultScheme = vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme();
-						if (KeyLoadingStrategy.isHubVault(vaultScheme)) {
-							determinedState = LOCKED;
-						}
-					}
-					if (determinedState == LOCKED) {
-						vault.getVaultConfigCache().reloadConfig();
-					}
-					state.set(determinedState);
-					yield determinedState;
-				} catch (IOException e) {
-					LOG.warn("Failed to determine vault state for {}", vault.getPath(), e);
-					state.set(ERROR);
-					vault.setLastKnownException(e);
-					yield ERROR;
+		VaultState state  = vault.stateProperty();
+		VaultState.Value previous = state.getValue();
+
+		if (previous.equals(UNLOCKED)||previous.equals(PROCESSING)) {
+			return previous;
+		}
+
+		try {
+			VaultState.Value determined = determineVaultState(vault.getPath(), vault.getVaultSettings());
+
+			if (determined == MASTERKEY_MISSING) {
+				if (KeyLoadingStrategy.isHubVault(vault.getVaultConfigCache().getUnchecked().getKeyId().getScheme())) {
+					determined = LOCKED;
 				}
 			}
-			case ERROR -> {
-				try {
-					var determinedState = determineVaultState(vault.getPath(), vault.getVaultSettings());
-					state.set(determinedState);
-					yield determinedState;
-				} catch (IOException e) {
-					LOG.warn("Failed to redetermine vault state for {}", vault.getPath(), e);
-					vault.setLastKnownException(e);
-					yield ERROR;
-				}
+
+			if (determined == LOCKED) {
+				vault.getVaultConfigCache().reloadConfig();
 			}
-			case UNLOCKED, PROCESSING -> previousState;
-		};
+
+			state.set(determined);
+			return determined;
+		} catch (IOException e) {
+			LOG.warn("Failed to (re)determine vault state for {}", vault.getPath(), e);
+			vault.setLastKnownException(e);
+			state.set(ERROR);
+			return ERROR;
+		}
 	}
 
 	private static VaultState.Value determineVaultState(Path pathToVault, VaultSettings vaultSettings) throws IOException {
@@ -195,9 +187,12 @@ public class VaultListManager {
 			return VaultState.Value.MISSING;
 		}
 
-		boolean vaultConfigRestored = Files.notExists(pathToVaultConfig) && RecoverUtil.restoreBackupIfAvailable(pathToVaultConfig, VaultState.Value.VAULT_CONFIG_MISSING);
+		boolean vaultConfigRestored = Files.notExists(pathToVaultConfig)
+				&& BackupRestorer.restoreIfPresent(pathToVaultConfig.getParent(), VaultState.Value.VAULT_CONFIG_MISSING);
 
-		boolean masterkeyRestored = Files.notExists(pathToMasterkey) && KeyLoadingStrategy.isMasterkeyFileVault(vaultSettings.lastKnownKeyLoader.get()) && RecoverUtil.restoreBackupIfAvailable(pathToMasterkey, VaultState.Value.MASTERKEY_MISSING);
+		boolean masterkeyRestored = Files.notExists(pathToMasterkey)
+				&& KeyLoadingStrategy.isMasterkeyFileVault(vaultSettings.lastKnownKeyLoader.get())
+				&& BackupRestorer.restoreIfPresent(pathToMasterkey.getParent(), VaultState.Value.MASTERKEY_MISSING);
 
 		if (vaultConfigRestored || masterkeyRestored) {
 			return LOCKED;

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

@@ -22,12 +22,15 @@ import java.util.Optional;
 import java.util.ResourceBundle;
 
 import static org.cryptomator.common.Constants.CRYPTOMATOR_FILENAME_GLOB;
+import static org.cryptomator.common.vaults.VaultState.Value.VAULT_CONFIG_MISSING;
 
 import dagger.Lazy;
 import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
+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.integrations.mount.MountService;
 import org.cryptomator.integrations.uiappearance.Theme;
@@ -131,14 +134,50 @@ public class ChooseExistingVaultController implements FxController {
 	@FXML
 	public void restoreVaultConfigWithRecoveryKey() {
 		DirectoryChooser directoryChooser = new DirectoryChooser();
-		Optional<Vault> optionalVault = RecoverUtil.checkAndPrepareVaultFromDirectory(directoryChooser, window, dialogs, vaultComponentFactory, mountServices);
 
+		File selectedDirectory;
+		do {
+			selectedDirectory = directoryChooser.showDialog(window);
+			boolean hasSubfolderD = new File(selectedDirectory, "d").isDirectory();
+
+			if (!hasSubfolderD) {
+				dialogs.prepareNoDDirectorySelectedDialog(window).build().showAndWait();
+				selectedDirectory = null;
+			}
+		} while (selectedDirectory == null);
+
+		Optional<Vault> optionalVault = prepareVault(selectedDirectory,vaultComponentFactory,
+				mountServices);
+		//TODO: optional raus, und mit error dialog arbeiten (UI kram in UI package!) hier nur fehler werfen
 		optionalVault.ifPresent(vault -> {
-			ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESTORE_VAULT_CONFIG);
+			ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG);
 			recoveryKeyWindow.create(vault, window, recoverTypeProperty).showIsHubVaultDialogWindow();
 		});
 	}
 
+	public static Optional<Vault> prepareVault(File selectedDirectory, VaultComponent.Factory vaultComponentFactory, List<MountService> mountServices) {
+
+		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(); //TODO: VAULT_CONFIG_MISSING nicht sicher, stand nochmal überprüfen
+
+		//due to https://github.com/cryptomator/cryptomator/issues/2880#issuecomment-1680313498
+		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);
+	}
+
 	/* Getter */
 
 	public ObservableValue<Image> screenshotProperty() {

+ 2 - 2
src/main/java/org/cryptomator/ui/convertvault/ConvertVaultModule.java

@@ -4,7 +4,7 @@ import dagger.Binds;
 import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.IntoMap;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.ui.changepassword.NewPasswordController;
@@ -122,7 +122,7 @@ abstract class ConvertVaultModule {
 	@IntoMap
 	@FxControllerKey(RecoveryKeyValidateController.class)
 	static FxController bindRecoveryKeyValidateController(@ConvertVaultWindow Vault vault, @ConvertVaultWindow VaultConfig.UnverifiedVaultConfig vaultConfig, @ConvertVaultWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory) {
-		return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, new SimpleObjectProperty<>(RecoverUtil.Type.CONVERT_VAULT), null, null);
+		return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, new SimpleObjectProperty<>(RecoveryActionType.CONVERT_VAULT), null, null);
 	}
 
 }

+ 3 - 3
src/main/java/org/cryptomator/ui/mainwindow/VaultDetailMissingVaultController.java

@@ -1,6 +1,6 @@
 package org.cryptomator.ui.mainwindow;
 
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultListManager;
 import org.cryptomator.ui.common.FxController;
@@ -61,14 +61,14 @@ public class VaultDetailMissingVaultController implements FxController {
 			dialogs.prepareContactHubAdmin(window).build().showAndWait();
 		}
 		else {
-			ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESTORE_VAULT_CONFIG);
+			ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESTORE_VAULT_CONFIG);
 			recoveryKeyWindow.create(vault.get(), window, recoverTypeProperty).showIsHubVaultDialogWindow();
 		}
 	}
 
 	@FXML
 	void restoreMasterkey() {
-		ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESTORE_MASTERKEY);
+		ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESTORE_MASTERKEY);
 		recoveryKeyWindow.create(vault.get(), window, recoverTypeProperty).showRecoveryKeyRecoverWindow();
 	}
 

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

@@ -3,7 +3,7 @@ package org.cryptomator.ui.recoverykey;
 import dagger.BindsInstance;
 import dagger.Lazy;
 import dagger.Subcomponent;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
@@ -55,7 +55,7 @@ public interface RecoveryKeyComponent {
 
 		RecoveryKeyComponent create(@BindsInstance @RecoveryKeyWindow Vault vault, //
 									@BindsInstance @Named("keyRecoveryOwner") Stage owner, //
-									@BindsInstance @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType);
+									@BindsInstance @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType);
 	}
 
 }

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

@@ -9,7 +9,7 @@ import javafx.stage.Stage;
 import java.util.ResourceBundle;
 
 import dagger.Lazy;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
@@ -19,12 +19,12 @@ public class RecoveryKeyIsHubVaultController implements FxController {
 
 	private final Stage window;
 	private final Lazy<Scene> recoverykeyRecoverScene;
-	private final ObjectProperty<RecoverUtil.Type> recoverType;
+	private final ObjectProperty<RecoveryActionType> recoverType;
 
 	@Inject
 	public RecoveryKeyIsHubVaultController(@RecoveryKeyWindow Stage window, //
 										   @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy<Scene> recoverykeyRecoverScene, //
-										   @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, //
+										   @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
 										   ResourceBundle resourceBundle) {
 		this.window = window;
 		window.setTitle(resourceBundle.getString("recoveryKey.recoverVaultConfig.title"));
@@ -40,7 +40,7 @@ public class RecoveryKeyIsHubVaultController implements FxController {
 
 	@FXML
 	public void recover() {
-		recoverType.set(RecoverUtil.Type.RESTORE_VAULT_CONFIG);
+		recoverType.set(RecoveryActionType.RESTORE_VAULT_CONFIG);
 		window.setScene(recoverykeyRecoverScene.get());
 	}
 }

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

@@ -5,7 +5,7 @@ import dagger.Module;
 import dagger.Provides;
 import dagger.multibindings.IntoMap;
 import org.cryptomator.common.Nullable;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptolib.api.CryptorProvider;
@@ -177,7 +177,7 @@ abstract class RecoveryKeyModule {
 	@Provides
 	@IntoMap
 	@FxControllerKey(RecoveryKeyValidateController.class)
-	static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo, @Nullable MasterkeyFileAccess masterkeyFileAccess) {
+	static FxController bindRecoveryKeyValidateController(@RecoveryKeyWindow Vault vault, @RecoveryKeyWindow @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, @RecoveryKeyWindow StringProperty recoveryKey, RecoveryKeyFactory recoveryKeyFactory, @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo, @Nullable MasterkeyFileAccess masterkeyFileAccess) {
 		return new RecoveryKeyValidateController(vault, vaultConfig, recoveryKey, recoveryKeyFactory, recoverType, cipherCombo, masterkeyFileAccess);
 	}
 

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

@@ -1,7 +1,7 @@
 package org.cryptomator.ui.recoverykey;
 
 import dagger.Lazy;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
@@ -27,7 +27,7 @@ public class RecoveryKeyRecoverController implements FxController {
 	public RecoveryKeyRecoverController(@RecoveryKeyWindow Stage window, //
 										@FxmlScene(FxmlFile.RECOVERYKEY_RESET_PASSWORD) Lazy<Scene> resetPasswordScene, //
 										@FxmlScene(FxmlFile.RECOVERYKEY_EXPERT_SETTINGS) Lazy<Scene> expertSettingsScene, //
-										ResourceBundle resourceBundle, @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType) {
+										ResourceBundle resourceBundle, @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType) {
 		this.window = window;
 
 		this.nextScene = switch (recoverType.get()) {

+ 92 - 29
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java

@@ -5,6 +5,7 @@ import javax.inject.Named;
 import javafx.beans.property.IntegerProperty;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanWrapper;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 import javafx.concurrent.Task;
@@ -19,7 +20,10 @@ import java.util.concurrent.ExecutorService;
 import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
 
 import dagger.Lazy;
-import org.cryptomator.common.RecoverUtil;
+import org.cryptomator.common.recovery.CryptoFsInitializer;
+import org.cryptomator.common.recovery.MasterkeyService;
+import org.cryptomator.common.recovery.RecoveryActionType;
+import org.cryptomator.common.recovery.RecoveryDirectory;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.VaultListManager;
 import org.cryptomator.cryptolib.api.CryptoException;
@@ -50,7 +54,7 @@ public class RecoveryKeyResetPasswordController implements FxController {
 	private final MasterkeyFileAccess masterkeyFileAccess;
 	private final VaultListManager vaultListManager;
 	private final IntegerProperty shorteningThreshold;
-	private final ObjectProperty<RecoverUtil.Type> recoverType;
+	private final ObjectProperty<RecoveryActionType> recoverType;
 	private final ObjectProperty<CryptorProvider.Scheme> cipherCombo;
 	private final ResourceBundle resourceBundle;
 	private final StringProperty buttonText = new SimpleStringProperty();
@@ -70,7 +74,8 @@ public class RecoveryKeyResetPasswordController implements FxController {
 											  MasterkeyFileAccess masterkeyFileAccess, //
 											  VaultListManager vaultListManager, //
 											  @Named("shorteningThreshold") IntegerProperty shorteningThreshold, //
-											  @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo,//
+											  @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
+											  @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo,//
 											  ResourceBundle resourceBundle, Dialogs dialogs) {
 		this.window = window;
 		this.vault = vault;
@@ -90,8 +95,8 @@ public class RecoveryKeyResetPasswordController implements FxController {
 		initButtonText(recoverType.get());
 	}
 
-	private void initButtonText(RecoverUtil.Type type) {
-		if (type == RecoverUtil.Type.RESTORE_MASTERKEY) {
+	private void initButtonText(RecoveryActionType type) {
+		if (type == RecoveryActionType.RESTORE_MASTERKEY) {
 			buttonText.set(resourceBundle.getString("generic.button.close"));
 		} else {
 			buttonText.set(resourceBundle.getString("generic.button.back"));
@@ -100,44 +105,70 @@ public class RecoveryKeyResetPasswordController implements FxController {
 
 	@FXML
 	public void close() {
-		if (recoverType.getValue().equals(RecoverUtil.Type.RESTORE_MASTERKEY)) {
+		if (recoverType.getValue().equals(RecoveryActionType.RESTORE_MASTERKEY)) {
 			window.close();
 		} else {
 			window.setScene(recoverExpertSettingsScene.get());
 		}
 	}
-
 	@FXML
-	public void resetPassword() {
-		if (vault.isMissingVaultConfig()) {
-			try {
-				Path recoveryPath = RecoverUtil.createRecoveryDirectory(vault.getPath());
-				RecoverUtil.createNewMasterkeyFile(recoveryKeyFactory, recoveryPath, recoveryKey.get(), newPasswordController.passwordField.getCharacters());
-				Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
+	public void restorePassword() {
+		try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) {
+			Path recoveryPath = recoveryDirectory.getRecoveryPath();
+			MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters());
 
-				try (Masterkey masterkey = RecoverUtil.loadMasterkey(masterkeyFileAccess, masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
-					RecoverUtil.initializeCryptoFileSystem(recoveryPath, masterkey, shorteningThreshold, cipherCombo.get());
-				}
+			Path masterkeyFilePath = recoveryPath.resolve(MASTERKEY_FILENAME);
 
-				RecoverUtil.moveRecoveredFiles(recoveryPath, vault.getPath());
-				RecoverUtil.deleteRecoveryDirectory(recoveryPath);
-				RecoverUtil.addVaultToList(vaultListManager, vault.getPath());
+			try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, newPasswordController.passwordField.getCharacters())) {
+				CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold, cipherCombo.get());
+			}
 
-				dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle).setTitleKey("recoveryKey.recoverVaultConfig.title").setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message").build().showAndWait();
-				window.close();
+			recoveryDirectory.moveRecoveredFiles();
 
-			} catch (IOException | CryptoException e) {
-				LOG.error("Recovery process failed", e);
+			if (!vaultListManager.containsVault(vault.getPath())) {
+				vaultListManager.add(vault.getPath());
 			}
-		} else {
-			Task<Void> task = RecoverUtil.createResetPasswordTask( //
-					resourceBundle, owner, recoveryKeyFactory, //
-					vault, recoveryKey, newPasswordController, //
-					window, appWindows, dialogs);
-			executor.submit(task);
+
+			dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle)
+					.setTitleKey("recoveryKey.recoverVaultConfig.title")
+					.setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message")
+					.build().showAndWait();
+			window.close();
+
+		} catch (IOException | CryptoException e) {
+			LOG.error("Recovery process failed", e);
+			appWindows.showErrorWindow(e, window, null);
 		}
 	}
 
+	@FXML
+	public void resetPassword() {
+		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(org.cryptomator.common.vaults.VaultState.Value.MASTERKEY_MISSING)) {
+				dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle)
+						.setTitleKey("recoveryKey.recoverMasterkey.title")
+						.setMessageKey("recoveryKey.recover.resetMasterkeyFileSuccess.message")
+						.build().showAndWait();
+			} else {
+				dialogs.prepareRecoverPasswordSuccess(window, owner, resourceBundle)
+						.build().showAndWait();
+			}
+			window.close();
+		});
+
+		task.setOnFailed(_ -> {
+			LOG.error("Resetting password failed.", task.getException());
+			appWindows.showErrorWindow(task.getException(), window, null);
+		});
+
+		executor.submit(task);
+	}
+
 	/* Getter/Setter */
 
 	public StringProperty buttonTextProperty() {
@@ -155,5 +186,37 @@ public class RecoveryKeyResetPasswordController implements FxController {
 	public boolean isPasswordSufficientAndMatching() {
 		return newPasswordController.isGoodPassword();
 	}
+	private final ReadOnlyBooleanWrapper vaultConfigMissing = new ReadOnlyBooleanWrapper();
+
+	public ReadOnlyBooleanProperty vaultConfigMissingProperty() {
+		return vaultConfigMissing.getReadOnlyProperty();
+	}
+
+	public boolean isVaultConfigMissing() {
+		return vault.isMissingVaultConfig();
+	}
+
+	private 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(_ -> 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;
+		}
+	}
 }

+ 6 - 4
src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyValidateController.java

@@ -5,7 +5,6 @@ import com.google.common.base.CharMatcher;
 import com.google.common.base.Strings;
 import org.cryptomator.common.Nullable;
 import org.cryptomator.common.ObservableUtil;
-import org.cryptomator.common.RecoverUtil;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptofs.VaultConfig;
 import org.cryptomator.cryptofs.VaultConfigLoadException;
@@ -28,6 +27,9 @@ import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import org.cryptomator.common.recovery.MasterkeyService;
+import org.cryptomator.common.recovery.RecoveryActionType;
+
 public class RecoveryKeyValidateController implements FxController {
 
 	private static final Logger LOG = LoggerFactory.getLogger(RecoveryKeyCreationController.class);
@@ -43,7 +45,7 @@ public class RecoveryKeyValidateController implements FxController {
 	private final ObjectProperty<RecoveryKeyState> recoveryKeyState;
 	private final ObjectProperty<CryptorProvider.Scheme> cipherCombo;
 	private final AutoCompleter autoCompleter;
-	private final ObjectProperty<RecoverUtil.Type> recoverType;
+	private final ObjectProperty<RecoveryActionType> recoverType;
 	private final MasterkeyFileAccess masterkeyFileAccess;
 
 	private volatile boolean isWrongKey;
@@ -54,7 +56,7 @@ public class RecoveryKeyValidateController implements FxController {
 										 @Nullable VaultConfig.UnverifiedVaultConfig vaultConfig, //
 										 StringProperty recoveryKey, //
 										 RecoveryKeyFactory recoveryKeyFactory, //
-										 @Named("recoverType") ObjectProperty<RecoverUtil.Type> recoverType, //
+										 @Named("recoverType") ObjectProperty<RecoveryActionType> recoverType, //
 										 @Named("cipherCombo") ObjectProperty<CryptorProvider.Scheme> cipherCombo,//
 										 MasterkeyFileAccess masterkeyFileAccess) {
 		this.vault = vault;
@@ -137,7 +139,7 @@ public class RecoveryKeyValidateController implements FxController {
 		switch (recoverType.get()) {
 			case RESTORE_VAULT_CONFIG -> {
 				AtomicBoolean illegalArgumentExceptionOccurred = new AtomicBoolean(false);
-				var combo = RecoverUtil.validateRecoveryKeyAndGetCombo(
+				var combo = MasterkeyService.validateRecoveryKeyAndDetectCombo(
 						recoveryKeyFactory, vault, recoveryKey, masterkeyFileAccess, illegalArgumentExceptionOccurred);
 				combo.ifPresent(cipherCombo::set);
 				if (illegalArgumentExceptionOccurred.get()) {

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

@@ -1,7 +1,7 @@
 package org.cryptomator.ui.vaultoptions;
 
-import org.cryptomator.common.RecoverUtil;
 import org.cryptomator.common.keychain.KeychainManager;
+import org.cryptomator.common.recovery.RecoveryActionType;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.changepassword.ChangePasswordComponent;
 import org.cryptomator.ui.common.FxController;
@@ -50,13 +50,13 @@ public class MasterkeyOptionsController implements FxController {
 
 	@FXML
 	public void showRecoveryKey() {
-		ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.SHOW_KEY);
+		ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.SHOW_KEY);
 		recoveryKeyWindow.create(vault, window, recoverTypeProperty).showRecoveryKeyCreationWindow();
 	}
 
 	@FXML
 	public void showRecoverVaultDialog() {
-		ObjectProperty<RecoverUtil.Type> recoverTypeProperty = new SimpleObjectProperty<>(RecoverUtil.Type.RESET_PASSWORD);
+		ObjectProperty<RecoveryActionType> recoverTypeProperty = new SimpleObjectProperty<>(RecoveryActionType.RESET_PASSWORD);
 		recoveryKeyWindow.create(vault, window, recoverTypeProperty).showRecoveryKeyRecoverWindow();
 	}
 

+ 2 - 1
src/main/resources/fxml/recoverykey_reset_password.fxml

@@ -25,7 +25,8 @@
 			<ButtonBar buttonMinWidth="120" buttonOrder="+CI">
 				<buttons>
 					<Button text="${controller.buttonText}" ButtonBar.buttonData="CANCEL_CLOSE" cancelButton="true" onAction="#close"/>
-					<Button text="%recoveryKey.recover.resetBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#resetPassword" disable="${!controller.passwordSufficientAndMatching}"/>
+					<Button text="%recoveryKey.recover.resetBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#resetPassword" managed="${!controller.vaultConfigMissing}" disable="${!controller.passwordSufficientAndMatching}"/>
+					<Button text="%recoveryKey.recover.recoverBtn" ButtonBar.buttonData="FINISH" defaultButton="true" onAction="#restorePassword" managed="${controller.vaultConfigMissing}" disable="${!controller.passwordSufficientAndMatching}"/>
 				</buttons>
 			</ButtonBar>
 		</VBox>

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

@@ -519,6 +519,7 @@ recoveryKey.recover.invalidKey=This recovery key is not valid
 recoveryKey.printout.heading=Cryptomator Recovery Key\n"%s"\n
 ### Reset Password
 recoveryKey.recover.resetBtn=Reset
+recoveryKey.recover.recoverBtn=Recover
 ### Recovery Key Password Reset Success
 recoveryKey.recover.resetSuccess.message=Password reset successful
 recoveryKey.recover.resetSuccess.description=You can unlock your vault with the new password.