From efd73e0d3ea6d1d90260413746b76ada11577401 Mon Sep 17 00:00:00 2001 From: Jan-Peter Klein Date: Wed, 14 Jan 2026 17:14:23 +0100 Subject: [PATCH 1/3] move recovery IO operations to background --- .../RecoveryKeyCreationController.java | 83 +++++++++++++------ .../RecoveryKeyResetPasswordController.java | 70 +++++++++++----- 2 files changed, 107 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 456de11f4..ac26fabd8 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -138,38 +138,41 @@ public class RecoveryKeyCreationController implements FxController { @FXML public void restoreWithPassword() { + Task task = new RestoreWithPasswordTask(); - try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { - Path recoveryPath = recoveryDirectory.getRecoveryPath(); + task.setOnScheduled(_ -> { + LOG.debug("Restoring vault configuration with password for {}.", vault.getDisplayablePath()); + }); - Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME); - - try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) { - var combo = MasterkeyService.detect(masterkey, vault.getPath()) - .orElseThrow(() -> new IllegalStateException("Could not detect combo for vault path: " + vault.getPath())); - - CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), combo); + task.setOnSucceeded(_ -> { + LOG.debug("Restored vault configuration for {}.", vault.getDisplayablePath()); + try { + if (!vaultListManager.isAlreadyAdded(vault.getPath())) { + vaultListManager.add(vault.getPath()); + } + window.close(); + dialogs.prepareRecoverPasswordSuccess((Stage) window.getOwner()) // + .setTitleKey("recover.recoverVaultConfig.title") // + .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // + .setDescriptionKey("recoveryKey.recover.resetMasterkeyFileSuccess.description") + .build().showAndWait(); + } catch (IOException e) { + LOG.error("Failed to add vault to list.", e); + appWindows.showErrorWindow(e, window, null); } + }); - recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); - - if (!vaultListManager.isAlreadyAdded(vault.getPath())) { - vaultListManager.add(vault.getPath()); + task.setOnFailed(_ -> { + if (task.getException() instanceof InvalidPassphraseException e) { + LOG.info("Password invalid", e); + Animations.createShakeWindowAnimation(window).play(); + } else { + LOG.error("Recovery process failed.", task.getException()); + appWindows.showErrorWindow(task.getException(), window, null); } - window.close(); - dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) // - .setTitleKey("recover.recoverVaultConfig.title") // - .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // - .setDescriptionKey("recoveryKey.recover.resetMasterkeyFileSuccess.description") - .build().showAndWait(); + }); - } catch (InvalidPassphraseException e) { - LOG.info("Password invalid", e); - Animations.createShakeWindowAnimation(window).play(); - } catch (IOException | CryptoException | IllegalStateException e) { - LOG.error("Recovery process failed", e); - appWindows.showErrorWindow(e, window, null); - } + executor.submit(task); } @FXML @@ -190,6 +193,34 @@ public class RecoveryKeyCreationController implements FxController { } + private class RestoreWithPasswordTask extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(RestoreWithPasswordTask.class); + + private RestoreWithPasswordTask() { + setOnFailed(_ -> LOG.error("Failed to restore vault configuration with password", getException())); + } + + @Override + protected Void call() throws IOException, CryptoException { + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) { + var combo = MasterkeyService.detect(masterkey, vault.getPath()) + .orElseThrow(() -> new IllegalStateException("Could not detect combo for vault path: " + vault.getPath())); + + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), combo); + } + + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + } + return null; + } + + } + /* Getter/Setter */ public Vault getVault() { diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index 0c06ba9b2..d904ea707 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -125,30 +125,35 @@ public class RecoveryKeyResetPasswordController implements FxController { @FXML public void restorePassword() { - try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { - Path recoveryPath = recoveryDirectory.getRecoveryPath(); - MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters()); + Task task = new RestorePasswordTask(); - try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, recoveryPath.resolve(MASTERKEY_FILENAME), newPasswordController.passwordField.getCharacters())) { - CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), cipherCombo.get()); + task.setOnScheduled(_ -> { + LOG.debug("Restoring vault configuration for {}.", vault.getDisplayablePath()); + }); + + task.setOnSucceeded(_ -> { + LOG.debug("Restored vault configuration for {}.", vault.getDisplayablePath()); + try { + if (!vaultListManager.isAlreadyAdded(vault.getPath())) { + vaultListManager.add(vault.getPath()); + } + window.close(); + dialogs.prepareRecoverPasswordSuccess((Stage) window.getOwner()) // + .setTitleKey("recover.recoverVaultConfig.title") // + .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // + .build().showAndWait(); + } catch (IOException e) { + LOG.error("Failed to add vault to list.", e); + appWindows.showErrorWindow(e, window, null); } + }); - recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME); - recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + task.setOnFailed(_ -> { + LOG.error("Recovery process failed.", task.getException()); + appWindows.showErrorWindow(task.getException(), window, null); + }); - if (!vaultListManager.isAlreadyAdded(vault.getPath())) { - vaultListManager.add(vault.getPath()); - } - window.close(); - dialogs.prepareRecoverPasswordSuccess((Stage)window.getOwner()) // - .setTitleKey("recover.recoverVaultConfig.title") // - .setMessageKey("recoveryKey.recover.resetVaultConfigSuccess.message") // - .build().showAndWait(); - - } catch (IOException | CryptoException e) { - LOG.error("Recovery process failed", e); - appWindows.showErrorWindow(e, window, null); - } + executor.submit(task); } @FXML @@ -192,6 +197,31 @@ public class RecoveryKeyResetPasswordController implements FxController { } } + private class RestorePasswordTask extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(RestorePasswordTask.class); + + public RestorePasswordTask() { + setOnFailed(_ -> LOG.error("Failed to restore vault configuration", getException())); + } + + @Override + protected Void call() throws IOException, CryptoException { + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters()); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, recoveryPath.resolve(MASTERKEY_FILENAME), newPasswordController.passwordField.getCharacters())) { + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), cipherCombo.get()); + } + + recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME); + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + } + return null; + } + } + /* Getter/Setter */ public ReadOnlyBooleanProperty passwordSufficientAndMatchingProperty() { From aa898c634f7366c931e689a2861c2d30aacbc41e Mon Sep 17 00:00:00 2001 From: Jan-Peter Klein Date: Fri, 23 Jan 2026 13:24:58 +0100 Subject: [PATCH 2/3] refactor recovery restore to sync logic with async task wrapper for testability --- .../RecoveryKeyCreationController.java | 65 ++++++++++--------- .../RecoveryKeyResetPasswordController.java | 60 +++++++++-------- 2 files changed, 66 insertions(+), 59 deletions(-) diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index ac26fabd8..8b0e5f416 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -104,7 +104,7 @@ public class RecoveryKeyCreationController implements FxController { descriptionLabel.formatProperty().set(resourceBundle.getString("recoveryKey.recover.description")); cancelButton.setOnAction((_) -> back()); cancelButton.setText(resourceBundle.getString("generic.button.back")); - nextButton.setOnAction((_) -> restoreWithPassword()); + nextButton.setOnAction((_) -> restoreWithPasswordAsync()); } } @@ -137,8 +137,8 @@ public class RecoveryKeyCreationController implements FxController { } @FXML - public void restoreWithPassword() { - Task task = new RestoreWithPasswordTask(); + public void restoreWithPasswordAsync() { + Task task = createTask(this::restoreWithPassword); task.setOnScheduled(_ -> { LOG.debug("Restoring vault configuration with password for {}.", vault.getDisplayablePath()); @@ -175,11 +175,42 @@ public class RecoveryKeyCreationController implements FxController { executor.submit(task); } + void restoreWithPassword() throws IOException, CryptoException { + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) { + var combo = MasterkeyService.detect(masterkey, vault.getPath()) + .orElseThrow(() -> new IllegalStateException("Could not detect combo for vault path: " + vault.getPath())); + + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), combo); + } + + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + } + } + @FXML public void close() { window.close(); } + @FunctionalInterface + private interface TaskAction { + void run() throws Exception; + } + + private Task createTask(TaskAction action) { + return new Task() { + @Override + protected Void call() throws Exception { + action.run(); + return null; + } + }; + } + private class RecoveryKeyCreationTask extends Task { private RecoveryKeyCreationTask() { @@ -193,34 +224,6 @@ public class RecoveryKeyCreationController implements FxController { } - private class RestoreWithPasswordTask extends Task { - - private static final Logger LOG = LoggerFactory.getLogger(RestoreWithPasswordTask.class); - - private RestoreWithPasswordTask() { - setOnFailed(_ -> LOG.error("Failed to restore vault configuration with password", getException())); - } - - @Override - protected Void call() throws IOException, CryptoException { - try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { - Path recoveryPath = recoveryDirectory.getRecoveryPath(); - Path masterkeyFilePath = vault.getPath().resolve(MASTERKEY_FILENAME); - - try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, masterkeyFilePath, passwordField.getCharacters())) { - var combo = MasterkeyService.detect(masterkey, vault.getPath()) - .orElseThrow(() -> new IllegalStateException("Could not detect combo for vault path: " + vault.getPath())); - - CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), combo); - } - - recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); - } - return null; - } - - } - /* Getter/Setter */ public Vault getVault() { diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index d904ea707..ccc7df0e3 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -117,15 +117,15 @@ public class RecoveryKeyResetPasswordController implements FxController { @FXML public void next() { switch (recoverType.get()) { - case RESTORE_ALL -> restorePassword(); + case RESTORE_ALL -> restorePasswordAsync(); case RESTORE_MASTERKEY, RESET_PASSWORD -> resetPassword(); default -> resetPassword(); // Fallback } } @FXML - public void restorePassword() { - Task task = new RestorePasswordTask(); + public void restorePasswordAsync() { + Task task = createTask(this::restorePassword); task.setOnScheduled(_ -> { LOG.debug("Restoring vault configuration for {}.", vault.getDisplayablePath()); @@ -156,6 +156,20 @@ public class RecoveryKeyResetPasswordController implements FxController { executor.submit(task); } + void restorePassword() throws IOException, CryptoException { + try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { + Path recoveryPath = recoveryDirectory.getRecoveryPath(); + MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters()); + + try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, recoveryPath.resolve(MASTERKEY_FILENAME), newPasswordController.passwordField.getCharacters())) { + CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), cipherCombo.get()); + } + + recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME); + recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); + } + } + @FXML public void resetPassword() { Task task = new ResetPasswordTask(); @@ -182,6 +196,21 @@ public class RecoveryKeyResetPasswordController implements FxController { executor.submit(task); } + @FunctionalInterface + private interface TaskAction { + void run() throws Exception; + } + + private Task createTask(TaskAction action) { + return new Task() { + @Override + protected Void call() throws Exception { + action.run(); + return null; + } + }; + } + private class ResetPasswordTask extends Task { private static final Logger LOG = LoggerFactory.getLogger(ResetPasswordTask.class); @@ -197,31 +226,6 @@ public class RecoveryKeyResetPasswordController implements FxController { } } - private class RestorePasswordTask extends Task { - - private static final Logger LOG = LoggerFactory.getLogger(RestorePasswordTask.class); - - public RestorePasswordTask() { - setOnFailed(_ -> LOG.error("Failed to restore vault configuration", getException())); - } - - @Override - protected Void call() throws IOException, CryptoException { - try (RecoveryDirectory recoveryDirectory = RecoveryDirectory.create(vault.getPath())) { - Path recoveryPath = recoveryDirectory.getRecoveryPath(); - MasterkeyService.recoverFromRecoveryKey(recoveryKey.get(), recoveryKeyFactory, recoveryPath, newPasswordController.passwordField.getCharacters()); - - try (Masterkey masterkey = MasterkeyService.load(masterkeyFileAccess, recoveryPath.resolve(MASTERKEY_FILENAME), newPasswordController.passwordField.getCharacters())) { - CryptoFsInitializer.init(recoveryPath, masterkey, shorteningThreshold.get(), cipherCombo.get()); - } - - recoveryDirectory.moveRecoveredFile(MASTERKEY_FILENAME); - recoveryDirectory.moveRecoveredFile(VAULTCONFIG_FILENAME); - } - return null; - } - } - /* Getter/Setter */ public ReadOnlyBooleanProperty passwordSufficientAndMatchingProperty() { From 8b05ae0a542d5588adfc9faf4b82458d552224ea Mon Sep 17 00:00:00 2001 From: Jan-Peter Klein Date: Fri, 23 Jan 2026 16:09:06 +0100 Subject: [PATCH 3/3] dedup createTask --- .../RecoveryKeyCreationController.java | 17 +------------ .../RecoveryKeyResetPasswordController.java | 17 +------------ .../ui/recoverykey/RecoveryKeyTasks.java | 25 +++++++++++++++++++ 3 files changed, 27 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyTasks.java diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java index 8b0e5f416..e67ca87c6 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyCreationController.java @@ -138,7 +138,7 @@ public class RecoveryKeyCreationController implements FxController { @FXML public void restoreWithPasswordAsync() { - Task task = createTask(this::restoreWithPassword); + Task task = RecoveryKeyTasks.createTask(this::restoreWithPassword); task.setOnScheduled(_ -> { LOG.debug("Restoring vault configuration with password for {}.", vault.getDisplayablePath()); @@ -196,21 +196,6 @@ public class RecoveryKeyCreationController implements FxController { window.close(); } - @FunctionalInterface - private interface TaskAction { - void run() throws Exception; - } - - private Task createTask(TaskAction action) { - return new Task() { - @Override - protected Void call() throws Exception { - action.run(); - return null; - } - }; - } - private class RecoveryKeyCreationTask extends Task { private RecoveryKeyCreationTask() { diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index ccc7df0e3..b57758b67 100644 --- a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -125,7 +125,7 @@ public class RecoveryKeyResetPasswordController implements FxController { @FXML public void restorePasswordAsync() { - Task task = createTask(this::restorePassword); + Task task = RecoveryKeyTasks.createTask(this::restorePassword); task.setOnScheduled(_ -> { LOG.debug("Restoring vault configuration for {}.", vault.getDisplayablePath()); @@ -196,21 +196,6 @@ public class RecoveryKeyResetPasswordController implements FxController { executor.submit(task); } - @FunctionalInterface - private interface TaskAction { - void run() throws Exception; - } - - private Task createTask(TaskAction action) { - return new Task() { - @Override - protected Void call() throws Exception { - action.run(); - return null; - } - }; - } - private class ResetPasswordTask extends Task { private static final Logger LOG = LoggerFactory.getLogger(ResetPasswordTask.class); diff --git a/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyTasks.java b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyTasks.java new file mode 100644 index 000000000..ba6caf249 --- /dev/null +++ b/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyTasks.java @@ -0,0 +1,25 @@ +package org.cryptomator.ui.recoverykey; + +import javafx.concurrent.Task; + +final class RecoveryKeyTasks { + + private RecoveryKeyTasks() { + } + + @FunctionalInterface + interface TaskAction { + void run() throws Exception; + } + + static Task createTask(TaskAction action) { + return new Task() { + @Override + protected Void call() throws Exception { + action.run(); + return null; + } + }; + } + +}