From fa7421b1b0674fc1b2e914a3e2d1b80cee95cef4 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Fri, 6 Sep 2019 18:55:47 +0200 Subject: [PATCH] Added migration workflow --- .../AddVaultSuccessController.java | 7 +- .../org/cryptomator/ui/common/FxmlFile.java | 2 + .../ui/migration/MigrationModule.java | 62 +++++- .../ui/migration/MigrationRunController.java | 183 ++++++++++++++++++ .../migration/MigrationStartController.java | 8 +- .../migration/MigrationSuccessController.java | 45 +++++ .../ui/unlock/UnlockSuccessController.java | 4 +- .../main/resources/fxml/migration_run.fxml | 40 ++++ .../main/resources/fxml/migration_start.fxml | 8 +- .../resources/fxml/migration_success.fxml | 40 ++++ .../main/resources/i18n/strings.properties | 12 ++ 11 files changed, 390 insertions(+), 21 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java create mode 100644 main/ui/src/main/resources/fxml/migration_run.fxml create mode 100644 main/ui/src/main/resources/fxml/migration_success.fxml diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java index 79db525f0..c28f4602e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultSuccessController.java @@ -25,13 +25,14 @@ public class AddVaultSuccessController implements FxController { this.vault = vault; } - public void unlockAndClose(ActionEvent actionEvent) { - close(actionEvent); + @FXML + public void unlockAndClose() { + close(); fxApplication.showUnlockWindow(vault.get()); } @FXML - public void close(ActionEvent actionEvent) { + public void close() { window.close(); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java index 4a7c07faa..6fef2b405 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/FxmlFile.java @@ -9,7 +9,9 @@ public enum FxmlFile { ADDVAULT_SUCCESS("/fxml/addvault_success.fxml"), // CHANGEPASSWORD("/fxml/changepassword.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), // + MIGRATION_RUN("/fxml/migration_run.fxml"), // MIGRATION_START("/fxml/migration_start.fxml"), // + MIGRATION_SUCCESS("/fxml/migration_success.fxml"), // PREFERENCES("/fxml/preferences.fxml"), // QUIT("/fxml/quit.fxml"), // REMOVE_VAULT("/fxml/remove_vault.fxml"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java index f2fea79a2..25cc31206 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationModule.java @@ -1,9 +1,11 @@ package org.cryptomator.ui.migration; import dagger.Binds; +import dagger.Lazy; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; +import dagger.multibindings.IntoSet; import javafx.scene.Scene; import javafx.scene.image.Image; import javafx.scene.input.KeyCode; @@ -11,6 +13,7 @@ import javafx.scene.input.KeyCodeCombination; import javafx.scene.input.KeyCombination; import javafx.stage.Modality; import javafx.stage.Stage; +import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.common.FXMLLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; @@ -23,6 +26,7 @@ import javax.inject.Provider; import java.util.Map; import java.util.Optional; import java.util.ResourceBundle; +import java.util.Set; @Module abstract class MigrationModule { @@ -33,16 +37,26 @@ abstract class MigrationModule { static FXMLLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, ResourceBundle resourceBundle) { return new FXMLLoaderFactory(factories, resourceBundle); } + + @Provides + @MigrationWindow + @MigrationScoped + static Map provideDefaultAccellerators(@MigrationWindow Set> accelerators) { + return Map.ofEntries(accelerators.toArray(Map.Entry[]::new)); + } @Provides @MigrationWindow @MigrationScoped - static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon) { + static Stage provideStage(@MainWindow Stage owner, ResourceBundle resourceBundle, @Named("windowIcon") Optional windowIcon, @MigrationWindow Lazy> accelerators) { Stage stage = new Stage(); - stage.setTitle(resourceBundle.getString("unlock.title")); + stage.setTitle(resourceBundle.getString("migration.title")); stage.setResizable(false); stage.initModality(Modality.WINDOW_MODAL); stage.initOwner(owner); + stage.sceneProperty().addListener(observable -> { + stage.getScene().getAccelerators().putAll(accelerators.get()); + }); windowIcon.ifPresent(stage.getIcons()::add); return stage; } @@ -50,15 +64,36 @@ abstract class MigrationModule { @Provides @FxmlScene(FxmlFile.MIGRATION_START) @MigrationScoped - static Scene provideMigrationStartScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders, @MigrationWindow Stage window) { - Scene scene = fxmlLoaders.createScene("/fxml/migration_start.fxml"); // TODO rename fxml file - - KeyCombination cmdW = new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN); - scene.getAccelerators().put(cmdW, window::close); - - return scene; + static Scene provideMigrationStartScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/migration_start.fxml"); } + @Provides + @FxmlScene(FxmlFile.MIGRATION_RUN) + @MigrationScoped + static Scene provideMigrationRunScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/migration_run.fxml"); + } + + @Provides + @FxmlScene(FxmlFile.MIGRATION_SUCCESS) + @MigrationScoped + static Scene provideMigrationSuccessScene(@MigrationWindow FXMLLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene("/fxml/migration_success.fxml"); + } + + // ------------------ + + @Provides + @IntoSet + @MigrationWindow + static Map.Entry provideCloseWindowShortcut(@MigrationWindow Stage window) { + if (SystemUtils.IS_OS_WINDOWS) { + return Map.entry(new KeyCodeCombination(KeyCode.F4, KeyCombination.ALT_DOWN), window::close); + } else { + return Map.entry(new KeyCodeCombination(KeyCode.W, KeyCombination.SHORTCUT_DOWN), window::close); + } + } // ------------------ @@ -67,5 +102,14 @@ abstract class MigrationModule { @FxControllerKey(MigrationStartController.class) abstract FxController bindMigrationStartController(MigrationStartController controller); + @Binds + @IntoMap + @FxControllerKey(MigrationRunController.class) + abstract FxController bindMigrationRunController(MigrationRunController controller); + + @Binds + @IntoMap + @FxControllerKey(MigrationSuccessController.class) + abstract FxController bindMigrationSuccessController(MigrationSuccessController controller); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java new file mode 100644 index 000000000..b7e0d6eaa --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationRunController.java @@ -0,0 +1,183 @@ +package org.cryptomator.ui.migration; + +import dagger.Lazy; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.value.WritableValue; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.scene.control.ContentDisplay; +import javafx.stage.Stage; +import javafx.util.Duration; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.common.vaults.VaultState; +import org.cryptomator.cryptofs.migration.Migrators; +import org.cryptomator.cryptofs.migration.api.NoApplicableMigratorException; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.keychain.KeychainAccess; +import org.cryptomator.keychain.KeychainAccessException; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.Tasks; +import org.cryptomator.ui.controls.NiceSecurePasswordField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + +@MigrationScoped +public class MigrationRunController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(MigrationRunController.class); + private static final String MASTERKEY_FILENAME = "masterkey.cryptomator"; // TODO: deduplicate constant declared in multiple classes + + private final Stage window; + private final Vault vault; + private final ExecutorService executor; + private final Optional keychainAccess; + private final Lazy startScene; + private final Lazy successScene; + private final ObjectBinding migrateButtonContentDisplay; + private final BooleanProperty migrationButtonDisabled; + public NiceSecurePasswordField passwordField; + + @Inject + public MigrationRunController(@MigrationWindow Stage window, @MigrationWindow Vault vault, ExecutorService executor, Optional keychainAccess, @FxmlScene(FxmlFile.MIGRATION_START) Lazy startScene, @FxmlScene(FxmlFile.MIGRATION_SUCCESS) Lazy successScene) { + this.window = window; + this.vault = vault; + this.executor = executor; + this.keychainAccess = keychainAccess; + this.startScene = startScene; + this.successScene = successScene; + this.migrateButtonContentDisplay = Bindings.createObjectBinding(this::getMigrateButtonContentDisplay, vault.stateProperty()); + this.migrationButtonDisabled = new SimpleBooleanProperty(); + } + + public void initialize() { + if (keychainAccess.isPresent()) { + loadStoredPassword(); + } + migrationButtonDisabled.bind(vault.stateProperty().isNotEqualTo(VaultState.NEEDS_MIGRATION).or(passwordField.textProperty().isEmpty())); + } + + @FXML + public void back() { + window.setScene(startScene.get()); + } + + @FXML + public void migrate() { + LOG.info("Migrating vault {}", vault.getPath()); + CharSequence password = passwordField.getCharacters(); + vault.setState(VaultState.PROCESSING); + Tasks.create(() -> { + Migrators migrators = Migrators.get(); + migrators.migrate(vault.getPath(), MASTERKEY_FILENAME, password); + return migrators.needsMigration(vault.getPath(), MASTERKEY_FILENAME); + }).onSuccess(needsAnotherMigration -> { + if (needsAnotherMigration) { + vault.setState(VaultState.NEEDS_MIGRATION); + } else { + vault.setState(VaultState.LOCKED); + passwordField.swipe(); + LOG.info("Migration of '{}' succeeded.", vault.getDisplayableName()); + window.setScene(successScene.get()); + } + }).onError(InvalidPassphraseException.class, e -> { + shakeWindow(); + passwordField.selectAll(); + passwordField.requestFocus(); + vault.setState(VaultState.NEEDS_MIGRATION); + }).onError(NoApplicableMigratorException.class, e -> { + LOG.error("Can not migrate vault.", e); + vault.setState(VaultState.ERROR); + // TODO show specific error screen + }).onError(Exception.class, e -> { // including RuntimeExceptions + LOG.error("Migration failed for technical reasons.", e); + vault.setState(VaultState.ERROR); + // TODO show generic error screen + }).runOnce(executor); + } + + private void loadStoredPassword() { + assert keychainAccess.isPresent(); + char[] storedPw = null; + try { + storedPw = keychainAccess.get().loadPassphrase(vault.getId()); + if (storedPw != null) { + passwordField.setPassword(storedPw); + passwordField.selectRange(storedPw.length, storedPw.length); + } + } catch (KeychainAccessException e) { + LOG.error("Failed to load entry from system keychain.", e); + } finally { + if (storedPw != null) { + Arrays.fill(storedPw, ' '); + } + } + } + + /* Animations */ + + private void shakeWindow() { + WritableValue writableWindowX = new WritableValue<>() { + @Override + public Double getValue() { + return window.getX(); + } + + @Override + public void setValue(Double value) { + window.setX(value); + } + }; + Timeline timeline = new Timeline( // + new KeyFrame(Duration.ZERO, new KeyValue(writableWindowX, window.getX())), // + new KeyFrame(new Duration(100), new KeyValue(writableWindowX, window.getX() - 22.0)), // + new KeyFrame(new Duration(200), new KeyValue(writableWindowX, window.getX() + 18.0)), // + new KeyFrame(new Duration(300), new KeyValue(writableWindowX, window.getX() - 14.0)), // + new KeyFrame(new Duration(400), new KeyValue(writableWindowX, window.getX() + 10.0)), // + new KeyFrame(new Duration(500), new KeyValue(writableWindowX, window.getX() - 6.0)), // + new KeyFrame(new Duration(600), new KeyValue(writableWindowX, window.getX() + 2.0)), // + new KeyFrame(new Duration(700), new KeyValue(writableWindowX, window.getX())) // + ); + timeline.play(); + } + + /* Getter/Setter */ + + public Vault getVault() { + return vault; + } + + public BooleanProperty migrationButtonDisabledProperty() { + return migrationButtonDisabled; + } + + public boolean isMigrationButtonDisabled() { + return migrationButtonDisabled.get(); + } + + public ObjectBinding migrateButtonContentDisplayProperty() { + return migrateButtonContentDisplay; + } + + public ContentDisplay getMigrateButtonContentDisplay() { + switch (vault.getState()) { + case PROCESSING: + return ContentDisplay.LEFT; + default: + return ContentDisplay.TEXT_ONLY; + } + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationStartController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationStartController.java index 082477653..260b43823 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationStartController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationStartController.java @@ -16,13 +16,13 @@ public class MigrationStartController implements FxController { private final Stage window; private final Vault vault; - private final Lazy successScene; + private final Lazy runMigrationScene; @Inject - public MigrationStartController(@MigrationWindow Stage window, @MigrationWindow Vault vault, @FxmlScene(FxmlFile.MIGRATION_START) Lazy successScene) { + public MigrationStartController(@MigrationWindow Stage window, @MigrationWindow Vault vault, @FxmlScene(FxmlFile.MIGRATION_RUN) Lazy runMigrationScene) { this.window = window; this.vault = vault; - this.successScene = successScene; + this.runMigrationScene = runMigrationScene; } public void initialize() { @@ -35,7 +35,7 @@ public class MigrationStartController implements FxController { @FXML public void proceed() { - + window.setScene(runMigrationScene.get()); } /* Getter/Setter */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java new file mode 100644 index 000000000..4d31c377f --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/migration/MigrationSuccessController.java @@ -0,0 +1,45 @@ +package org.cryptomator.ui.migration; + +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyObjectProperty; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.stage.Stage; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.fxapp.FxApplication; + +import javax.inject.Inject; + +@MigrationScoped +public class MigrationSuccessController implements FxController { + + private final FxApplication fxApplication; + private final Stage window; + private final Vault vault; + + @Inject + MigrationSuccessController(FxApplication fxApplication, @MigrationWindow Stage window, @MigrationWindow Vault vault) { + this.fxApplication = fxApplication; + this.window = window; + this.vault = vault; + } + + @FXML + public void unlockAndClose() { + close(); + fxApplication.showUnlockWindow(vault); + } + + @FXML + public void close() { + window.close(); + } + + /* Getter/Setters */ + + public Vault getVault() { + return vault; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java index fa5bbb895..44e1c0010 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockSuccessController.java @@ -5,6 +5,7 @@ import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; import javafx.scene.control.ContentDisplay; import javafx.stage.Stage; import org.cryptomator.common.vaults.Vault; @@ -37,12 +38,13 @@ public class UnlockSuccessController implements FxController { this.revealButtonDisabled = new SimpleBooleanProperty(); } - + @FXML public void close() { LOG.trace("UnlockSuccessController.close()"); window.close(); } + @FXML public void revealAndClose() { LOG.trace("UnlockSuccessController.revealAndClose()"); revealButtonState.set(ContentDisplay.LEFT); diff --git a/main/ui/src/main/resources/fxml/migration_run.fxml b/main/ui/src/main/resources/fxml/migration_run.fxml new file mode 100644 index 000000000..16cf87611 --- /dev/null +++ b/main/ui/src/main/resources/fxml/migration_run.fxml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/main/ui/src/main/resources/fxml/migration_start.fxml b/main/ui/src/main/resources/fxml/migration_start.fxml index 4e5e6d462..c568ae9cb 100644 --- a/main/ui/src/main/resources/fxml/migration_start.fxml +++ b/main/ui/src/main/resources/fxml/migration_start.fxml @@ -28,15 +28,15 @@ - - + + - + -