diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java index e36369c35..02fd03600 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/Vault.java @@ -109,7 +109,7 @@ public class Vault { } else if(vaultSettings.maxCleartextFilenameLength().get() == -1) { LOG.debug("Determining cleartext filename length limitations..."); var checker = new FileSystemCapabilityChecker(); - int shorteningThreshold = getUnverifiedVaultConfig().orElseThrow().allegedShorteningThreshold(); + int shorteningThreshold = getUnverifiedVaultConfig().allegedShorteningThreshold(); int ciphertextLimit = checker.determineSupportedCiphertextFileNameLength(getPath()); if (ciphertextLimit < shorteningThreshold) { int cleartextLimit = checker.determineSupportedCleartextFileNameLength(getPath()); @@ -327,14 +327,10 @@ public class Vault { return stats; } - public Optional getUnverifiedVaultConfig() { + public UnverifiedVaultConfig getUnverifiedVaultConfig() throws IOException { Path configPath = getPath().resolve(org.cryptomator.common.Constants.VAULTCONFIG_FILENAME); - try { - String token = Files.readString(configPath, StandardCharsets.US_ASCII); - return Optional.of(VaultConfig.decode(token)); - } catch (IOException e) { - return Optional.empty(); - } + String token = Files.readString(configPath, StandardCharsets.US_ASCII); + return VaultConfig.decode(token); } public Observable[] observables() { diff --git a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java index 19d577975..901ee7f42 100644 --- a/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java +++ b/main/commons/src/main/java/org/cryptomator/common/vaults/VaultModule.java @@ -98,7 +98,6 @@ public class VaultModule { flags.append(" -oatomic_o_trunc"); flags.append(" -oauto_xattr"); flags.append(" -oauto_cache"); - flags.append(" -omodules=iconv,from_code=UTF-8,to_code=UTF-8-MAC"); // show files names in Unicode NFD encoding flags.append(" -onoappledouble"); // vastly impacts performance for some reason... flags.append(" -odefault_permissions"); // let the kernel assume permissions based on file attributes etc diff --git a/main/pom.xml b/main/pom.xml index a21b0140d..eee540365 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -25,14 +25,14 @@ 16 - 2.0.0-rc2 + 2.1.0-beta5 1.0.0-beta2 1.0.0-beta2 1.0.0-beta2 1.0.0-beta1 1.3.1 1.3.1 - 1.2.0 + 1.2.2 16 @@ -76,12 +76,6 @@ cryptofs ${cryptomator.cryptofs.version} - - - org.cryptomator - cryptolib - 2.0.0-rc1 - org.cryptomator fuse-nio-adapter diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java index 42d243e80..8a5a776ea 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/AddVaultModule.java @@ -6,10 +6,10 @@ import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; -import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.common.PasswordStrengthUtil; @@ -33,13 +33,6 @@ import java.util.ResourceBundle; @Module public abstract class AddVaultModule { - @Provides - @AddVaultWizardScoped - @Named("newPassword") - static ObjectProperty provideNewPasswordProperty() { - return new SimpleObjectProperty<>(""); - } - @Provides @AddVaultWizardWindow @AddVaultWizardScoped @@ -167,8 +160,8 @@ public abstract class AddVaultModule { @Provides @IntoMap @FxControllerKey(NewPasswordController.class) - static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty password) { - return new NewPasswordController(resourceBundle, strengthRater, password); + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + return new NewPasswordController(resourceBundle, strengthRater); } @Binds diff --git a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java index 4be978956..4b4e02ed2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java @@ -14,6 +14,7 @@ import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.common.Tasks; import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingStrategy; import org.cryptomator.ui.recoverykey.RecoveryKeyFactory; @@ -23,7 +24,6 @@ import org.slf4j.LoggerFactory; import javax.inject.Inject; import javax.inject.Named; import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; import javafx.beans.binding.ObjectBinding; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -41,7 +41,6 @@ import java.net.URI; import java.nio.channels.WritableByteChannel; import java.nio.file.FileSystem; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.security.SecureRandom; @@ -70,7 +69,6 @@ public class CreateNewVaultPasswordController implements FxController { private final StringProperty recoveryKeyProperty; private final VaultListManager vaultListManager; private final ResourceBundle resourceBundle; - private final ObjectProperty password; private final ReadmeGenerator readmeGenerator; private final SecureRandom csprng; private final MasterkeyFileAccess masterkeyFileAccess; @@ -81,9 +79,10 @@ public class CreateNewVaultPasswordController implements FxController { public ToggleGroup recoveryKeyChoice; public Toggle showRecoveryKey; public Toggle skipRecoveryKey; + public NewPasswordController newPasswordSceneController; @Inject - CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, @Named("newPassword") ObjectProperty password, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { + CreateNewVaultPasswordController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_LOCATION) Lazy chooseLocationScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_RECOVERYKEY) Lazy recoveryKeyScene, @FxmlScene(FxmlFile.ADDVAULT_SUCCESS) Lazy successScene, ErrorComponent.Builder errorComponent, ExecutorService executor, RecoveryKeyFactory recoveryKeyFactory, @Named("vaultName") StringProperty vaultName, ObjectProperty vaultPath, @AddVaultWizardWindow ObjectProperty vault, @Named("recoveryKey") StringProperty recoveryKey, VaultListManager vaultListManager, ResourceBundle resourceBundle, ReadmeGenerator readmeGenerator, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { this.window = window; this.chooseLocationScene = chooseLocationScene; this.recoveryKeyScene = recoveryKeyScene; @@ -97,7 +96,6 @@ public class CreateNewVaultPasswordController implements FxController { this.recoveryKeyProperty = recoveryKey; this.vaultListManager = vaultListManager; this.resourceBundle = resourceBundle; - this.password = password; this.readmeGenerator = readmeGenerator; this.csprng = csprng; this.masterkeyFileAccess = masterkeyFileAccess; @@ -108,8 +106,11 @@ public class CreateNewVaultPasswordController implements FxController { @FXML public void initialize() { - BooleanBinding isValidNewPassword = Bindings.createBooleanBinding(() -> password.get() != null && password.get().length() > 0, password); - readyToCreateVault.bind(isValidNewPassword.and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not())); + readyToCreateVault.bind(newPasswordSceneController.passwordsMatchAndSufficientProperty().and(recoveryKeyChoice.selectedToggleProperty().isNotNull()).and(processing.not())); + window.setOnHiding(event -> { + newPasswordSceneController.passwordField.wipe(); + newPasswordSceneController.reenterField.wipe(); + }); } @FXML @@ -142,8 +143,8 @@ public class CreateNewVaultPasswordController implements FxController { Path pathToVault = vaultPathProperty.get(); processing.set(true); Tasks.create(() -> { - initializeVault(pathToVault, password.get()); - return recoveryKeyFactory.createRecoveryKey(pathToVault, password.get()); + initializeVault(pathToVault); + return recoveryKeyFactory.createRecoveryKey(pathToVault, newPasswordSceneController.passwordField.getCharacters()); }).onSuccess(recoveryKey -> { initializationSucceeded(pathToVault); recoveryKeyProperty.set(recoveryKey); @@ -160,7 +161,7 @@ public class CreateNewVaultPasswordController implements FxController { Path pathToVault = vaultPathProperty.get(); processing.set(true); Tasks.create(() -> { - initializeVault(pathToVault, password.get()); + initializeVault(pathToVault); }).onSuccess(() -> { initializationSucceeded(pathToVault); window.setScene(successScene.get()); @@ -172,11 +173,11 @@ public class CreateNewVaultPasswordController implements FxController { }).runOnce(executor); } - private void initializeVault(Path path, CharSequence passphrase) throws IOException { + private void initializeVault(Path path) throws IOException { // 1. write masterkey: Path masterkeyFilePath = path.resolve(MASTERKEY_FILENAME); try (Masterkey masterkey = Masterkey.generate(csprng)) { - masterkeyFileAccess.persist(masterkey, masterkeyFilePath, passphrase); + masterkeyFileAccess.persist(masterkey, masterkeyFilePath, newPasswordSceneController.passwordField.getCharacters()); // 2. initialize vault: try { diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java index b0ad164e2..808375ecd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java @@ -10,21 +10,18 @@ import org.cryptomator.integrations.keychain.KeychainAccessException; import org.cryptomator.ui.common.Animations; import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.controls.NiceSecurePasswordField; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; -import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.ObjectProperty; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.stage.Stage; import java.io.IOException; -import java.nio.CharBuffer; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; @@ -41,7 +38,6 @@ public class ChangePasswordController implements FxController { private final Stage window; private final Vault vault; - private final ObjectProperty newPassword; private final ErrorComponent.Builder errorComponent; private final KeychainManager keychain; private final SecureRandom csprng; @@ -50,12 +46,12 @@ public class ChangePasswordController implements FxController { public NiceSecurePasswordField oldPasswordField; public CheckBox finalConfirmationCheckbox; public Button finishButton; + public NewPasswordController newPasswordController; @Inject - public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, @Named("newPassword") ObjectProperty newPassword, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { + public ChangePasswordController(@ChangePasswordWindow Stage window, @ChangePasswordWindow Vault vault, ErrorComponent.Builder errorComponent, KeychainManager keychain, SecureRandom csprng, MasterkeyFileAccess masterkeyFileAccess) { this.window = window; this.vault = vault; - this.newPassword = newPassword; this.errorComponent = errorComponent; this.keychain = keychain; this.csprng = csprng; @@ -66,8 +62,12 @@ public class ChangePasswordController implements FxController { public void initialize() { BooleanBinding checkboxNotConfirmed = finalConfirmationCheckbox.selectedProperty().not(); BooleanBinding oldPasswordFieldEmpty = oldPasswordField.textProperty().isEmpty(); - BooleanBinding newPasswordInvalid = Bindings.createBooleanBinding(() -> newPassword.get() == null || newPassword.get().length() == 0, newPassword); - finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordInvalid)); + finishButton.disableProperty().bind(checkboxNotConfirmed.or(oldPasswordFieldEmpty).or(newPasswordController.passwordsMatchAndSufficientProperty().not())); + window.setOnHiding(event -> { + oldPasswordField.wipe(); + newPasswordController.passwordField.wipe(); + newPasswordController.reenterField.wipe(); + }); } @FXML @@ -78,10 +78,8 @@ public class ChangePasswordController implements FxController { @FXML public void finish() { try { - //String normalizedOldPassphrase = Normalizer.normalize(oldPasswordField.getCharacters(), Normalizer.Form.NFC); - //String normalizedNewPassphrase = Normalizer.normalize(newPassword.get(), Normalizer.Form.NFC); - CharSequence oldPassphrase = oldPasswordField.getCharacters(); // TODO verify: is this already NFC-normalized? - CharSequence newPassphrase = newPassword.get(); // TODO verify: is this already NFC-normalized? + CharSequence oldPassphrase = oldPasswordField.getCharacters(); + CharSequence newPassphrase = newPasswordController.passwordField.getCharacters(); Path masterkeyPath = vault.getPath().resolve(MASTERKEY_FILENAME); byte[] oldMasterkeyBytes = Files.readAllBytes(masterkeyPath); byte[] newMasterkeyBytes = masterkeyFileAccess.changePassphrase(oldMasterkeyBytes, oldPassphrase, newPassphrase); @@ -89,8 +87,8 @@ public class ChangePasswordController implements FxController { Files.move(masterkeyPath, backupKeyPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); Files.write(masterkeyPath, newMasterkeyBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE); LOG.info("Successfully changed password for {}", vault.getDisplayName()); - window.close(); updatePasswordInSystemkeychain(); + window.close(); } catch (InvalidPassphraseException e) { Animations.createShakeWindowAnimation(window).play(); oldPasswordField.selectAll(); @@ -104,7 +102,7 @@ public class ChangePasswordController implements FxController { private void updatePasswordInSystemkeychain() { if (keychain.isSupported()) { try { - keychain.changePassphrase(vault.getId(), CharBuffer.wrap(newPassword.get())); + keychain.changePassphrase(vault.getId(), newPasswordController.passwordField.getCharacters()); LOG.info("Successfully updated password in system keychain for {}", vault.getDisplayName()); } catch (KeychainAccessException e) { LOG.error("Failed to update password in system keychain.", e); diff --git a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java index e80871208..d95b19410 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordModule.java @@ -5,10 +5,10 @@ import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.ui.common.DefaultSceneFactory; -import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.common.PasswordStrengthUtil; @@ -16,8 +16,6 @@ import org.cryptomator.ui.common.StageFactory; import javax.inject.Named; import javax.inject.Provider; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.scene.Scene; import javafx.stage.Modality; import javafx.stage.Stage; @@ -27,13 +25,6 @@ import java.util.ResourceBundle; @Module abstract class ChangePasswordModule { - @Provides - @ChangePasswordScoped - @Named("newPassword") - static ObjectProperty provideNewPasswordProperty() { - return new SimpleObjectProperty<>(""); - } - @Provides @ChangePasswordWindow @ChangePasswordScoped @@ -71,8 +62,8 @@ abstract class ChangePasswordModule { @Provides @IntoMap @FxControllerKey(NewPasswordController.class) - static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty password) { - return new NewPasswordController(resourceBundle, strengthRater, password); + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + return new NewPasswordController(resourceBundle, strengthRater); } } 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 32dc94f14..b8d5bbff0 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 @@ -11,6 +11,8 @@ public enum FxmlFile { CHANGEPASSWORD("/fxml/changepassword.fxml"), // ERROR("/fxml/error.fxml"), // FORGET_PASSWORD("/fxml/forget_password.fxml"), // + HEALTH_START("/fxml/health_start.fxml"), // + HEALTH_CHECK_LIST("/fxml/health_check_list.fxml"), // LOCK_FORCED("/fxml/lock_forced.fxml"), // LOCK_FAILED("/fxml/lock_failed.fxml"), // MAIN_WINDOW("/fxml/main_window.fxml"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java index b7bf48870..13e59f2cd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/common/NewPasswordController.java @@ -8,7 +8,8 @@ import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.beans.property.IntegerProperty; -import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.ReadOnlyBooleanWrapper; import javafx.beans.property.SimpleIntegerProperty; import javafx.fxml.FXML; import javafx.scene.control.Label; @@ -18,8 +19,8 @@ public class NewPasswordController implements FxController { private final ResourceBundle resourceBundle; private final PasswordStrengthUtil strengthRater; - private final ObjectProperty password; private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); + private final ReadOnlyBooleanWrapper passwordsMatchAndSufficient = new ReadOnlyBooleanWrapper(); public NiceSecurePasswordField passwordField; public NiceSecurePasswordField reenterField; @@ -31,10 +32,9 @@ public class NewPasswordController implements FxController { public FontAwesome5IconView passwordMatchCheckmark; public FontAwesome5IconView passwordMatchCross; - public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, ObjectProperty password) { + public NewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { this.resourceBundle = resourceBundle; this.strengthRater = strengthRater; - this.password = password; } @FXML @@ -44,7 +44,7 @@ public class NewPasswordController implements FxController { passwordStrengthLabel.graphicProperty().bind(Bindings.createObjectBinding(this::getIconViewForPasswordStrengthLabel, passwordField.textProperty(), passwordStrength)); passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); - BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::hasSamePasswordInBothFields, passwordField.textProperty(), reenterField.textProperty()); + BooleanBinding passwordsMatch = Bindings.createBooleanBinding(this::passwordFieldsMatch, passwordField.textProperty(), reenterField.textProperty()); BooleanBinding reenterFieldNotEmpty = reenterField.textProperty().isNotEmpty(); passwordMatchLabel.visibleProperty().bind(reenterFieldNotEmpty); passwordMatchLabel.graphicProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(passwordMatchCheckmark).otherwise(passwordMatchCross)); @@ -54,6 +54,7 @@ public class NewPasswordController implements FxController { reenterField.textProperty().addListener(this::passwordsDidChange); } + private FontAwesome5IconView getIconViewForPasswordStrengthLabel() { if (passwordField.getCharacters().length() == 0) { return null; @@ -67,17 +68,19 @@ public class NewPasswordController implements FxController { } private void passwordsDidChange(@SuppressWarnings("unused") Observable observable) { - if (hasSamePasswordInBothFields() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) { - password.set(passwordField.getCharacters()); - } else { - password.set(""); + if (passwordFieldsMatch() && strengthRater.fulfillsMinimumRequirements(passwordField.getCharacters())) { + passwordsMatchAndSufficient.setValue(true); } } - private boolean hasSamePasswordInBothFields() { + private boolean passwordFieldsMatch() { return CharSequence.compare(passwordField.getCharacters(), reenterField.getCharacters()) == 0; } + public ReadOnlyBooleanProperty passwordsMatchAndSufficientProperty() { + return passwordsMatchAndSufficient.getReadOnlyProperty(); + } + /* Getter/Setter */ public IntegerProperty passwordStrengthProperty() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java index f4d0d058a..13c583c0e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java @@ -6,8 +6,10 @@ package org.cryptomator.ui.controls; public enum FontAwesome5Icon { ANCHOR("\uF13D"), // ARROW_UP("\uF062"), // + BAN("\uF05E"), // BUG("\uF188"), // CHECK("\uF00C"), // + CLOCK("\uF017"), // COG("\uF013"), // COGS("\uF085"), // COPY("\uF0C5"), // diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java b/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java index c99cc62ea..04ed7e477 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/FormattedLabel.java @@ -12,6 +12,7 @@ public class FormattedLabel extends Label { private final StringProperty format = new SimpleStringProperty(""); private final ObjectProperty arg1 = new SimpleObjectProperty<>(); + private final ObjectProperty arg2 = new SimpleObjectProperty<>(); // add arg2, arg3, ... on demand public FormattedLabel() { @@ -19,11 +20,11 @@ public class FormattedLabel extends Label { } protected StringBinding createStringBinding() { - return Bindings.createStringBinding(this::updateText, format, arg1); + return Bindings.createStringBinding(this::updateText, format, arg1, arg2); } private String updateText() { - return String.format(format.get(), arg1.get()); + return String.format(format.get(), arg1.get(), arg2.get()); } /* Observables */ @@ -51,4 +52,16 @@ public class FormattedLabel extends Label { public void setArg1(Object arg1) { this.arg1.set(arg1); } + + public ObjectProperty arg2Property() { + return arg2; + } + + public Object getArg2() { + return arg2.get(); + } + + public void setArg2(Object arg2) { + this.arg2.set(arg2); + } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java index 0fde886f6..3689b7e6e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/SecurePasswordField.java @@ -123,7 +123,7 @@ public class SecurePasswordField extends TextField { } private void updateCapsLocked() { - // AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed: + //TODO: fixed in JavaFX 17. AWT code needed until update (see https://bugs.openjdk.java.net/browse/JDK-8259680) capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/BatchService.java b/main/ui/src/main/java/org/cryptomator/ui/health/BatchService.java new file mode 100644 index 000000000..f3968c27d --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/BatchService.java @@ -0,0 +1,36 @@ +package org.cryptomator.ui.health; + +import com.google.common.base.Preconditions; +import com.google.common.base.Suppliers; +import dagger.Lazy; + +import javax.inject.Inject; +import javafx.concurrent.Service; +import javafx.concurrent.Task; +import java.util.Collection; +import java.util.Iterator; +import java.util.concurrent.ExecutorService; +import java.util.function.Supplier; + +public class BatchService extends Service { + + private final Iterator remainingTasks; + + @Inject + public BatchService(Iterable tasks) { + this.remainingTasks = tasks.iterator(); + } + + @Override + protected Task createTask() { + Preconditions.checkState(remainingTasks.hasNext(), "No remaining tasks"); + return remainingTasks.next(); + } + + @Override + protected void succeeded() { + if (remainingTasks.hasNext()) { + this.restart(); + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/CheckDetailController.java b/main/ui/src/main/java/org/cryptomator/ui/health/CheckDetailController.java new file mode 100644 index 000000000..d579ff709 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/CheckDetailController.java @@ -0,0 +1,170 @@ +package org.cryptomator.ui.health; + +import com.tobiasdiez.easybind.EasyBind; +import com.tobiasdiez.easybind.EasyObservableList; +import com.tobiasdiez.easybind.Subscription; +import com.tobiasdiez.easybind.optional.OptionalBinding; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.ui.common.FxController; + +import javax.inject.Inject; +import javafx.beans.binding.Binding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.concurrent.Worker; +import javafx.fxml.FXML; +import javafx.scene.control.ListView; +import java.util.function.Function; +import java.util.stream.Stream; + +@HealthCheckScoped +public class CheckDetailController implements FxController { + + private final EasyObservableList results; + private final OptionalBinding taskState; + private final Binding taskName; + private final Binding taskDuration; + private final ResultListCellFactory resultListCellFactory; + private final Binding taskRunning; + private final Binding taskScheduled; + private final Binding taskFinished; + private final Binding taskNotStarted; + private final Binding taskSucceeded; + private final Binding taskFailed; + private final Binding taskCancelled; + private final Binding countOfWarnSeverity; + private final Binding countOfCritSeverity; + + public ListView resultsListView; + private Subscription resultSubscription; + + @Inject + public CheckDetailController(ObjectProperty selectedTask, ResultListCellFactory resultListCellFactory) { + this.results = EasyBind.wrapList(FXCollections.observableArrayList()); + this.taskState = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::stateProperty); + this.taskName = EasyBind.wrapNullable(selectedTask).map(HealthCheckTask::getTitle).orElse(""); + this.taskDuration = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::durationInMillisProperty).orElse(-1L); + this.resultListCellFactory = resultListCellFactory; + this.taskRunning = EasyBind.wrapNullable(selectedTask).mapObservable(HealthCheckTask::runningProperty).orElse(false); //TODO: DOES NOT WORK + this.taskScheduled = taskState.map(Worker.State.SCHEDULED::equals).orElse(false); + this.taskNotStarted = taskState.map(Worker.State.READY::equals).orElse(false); + this.taskSucceeded = taskState.map(Worker.State.SUCCEEDED::equals).orElse(false); + this.taskFailed = taskState.map(Worker.State.FAILED::equals).orElse(false); + this.taskCancelled = taskState.map(Worker.State.CANCELLED::equals).orElse(false); + this.taskFinished = EasyBind.combine(taskSucceeded, taskFailed, taskCancelled, (a, b, c) -> a || b || c); + this.countOfWarnSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.WARN)); + this.countOfCritSeverity = results.reduce(countSeverity(DiagnosticResult.Severity.CRITICAL)); + selectedTask.addListener(this::selectedTaskChanged); + } + + private void selectedTaskChanged(ObservableValue observable, HealthCheckTask oldValue, HealthCheckTask newValue) { + if (resultSubscription != null) { + resultSubscription.unsubscribe(); + } + if (newValue != null) { + resultSubscription = EasyBind.bindContent(results, newValue.results()); + } + } + + private Function, Long> countSeverity(DiagnosticResult.Severity severity) { + return stream -> stream.filter(item -> severity.equals(item.getServerity())).count(); + } + + @FXML + public void initialize() { + resultsListView.setItems(results); + resultsListView.setCellFactory(resultListCellFactory); + } + + /* Getter/Setter */ + + public String getTaskName() { + return taskName.getValue(); + } + + public Binding taskNameProperty() { + return taskName; + } + + public Number getTaskDuration() { + return taskDuration.getValue(); + } + + public Binding taskDurationProperty() { + return taskDuration; + } + + public long getCountOfWarnSeverity() { + return countOfWarnSeverity.getValue().longValue(); + } + + public Binding countOfWarnSeverityProperty() { + return countOfWarnSeverity; + } + + public long getCountOfCritSeverity() { + return countOfCritSeverity.getValue().longValue(); + } + + public Binding countOfCritSeverityProperty() { + return countOfCritSeverity; + } + + public boolean isTaskRunning() { + return taskRunning.getValue(); + } + + public Binding taskRunningProperty() { + return taskRunning; + } + + public boolean isTaskFinished() { + return taskFinished.getValue(); + } + + public Binding taskFinishedProperty() { + return taskFinished; + } + + public boolean isTaskScheduled() { + return taskScheduled.getValue(); + } + + public Binding taskScheduledProperty() { + return taskScheduled; + } + + public boolean isTaskNotStarted() { + return taskNotStarted.getValue(); + } + + public Binding taskNotStartedProperty() { + return taskNotStarted; + } + + public boolean isTaskSucceeded() { + return taskSucceeded.getValue(); + } + + public Binding taskSucceededProperty() { + return taskSucceeded; + } + + public boolean isTaskFailed() { + return taskFailed.getValue(); + } + + public Binding taskFailedProperty() { + return taskFailed; + } + + public boolean isTaskCancelled() { + return taskCancelled.getValue(); + } + + public Binding taskCancelledProperty() { + return taskCancelled; + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java new file mode 100644 index 000000000..78f8b1b33 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListCell.java @@ -0,0 +1,64 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.cryptomator.ui.controls.FontAwesome5IconView; + +import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Worker; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.Node; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ListCell; + +class CheckListCell extends ListCell { + + private final FontAwesome5IconView stateIcon = new FontAwesome5IconView(); + + CheckListCell() { + setPadding(new Insets(6)); + setAlignment(Pos.CENTER_LEFT); + setContentDisplay(ContentDisplay.LEFT); + } + + @Override + protected void updateItem(HealthCheckTask item, boolean empty) { + super.updateItem(item, empty); + + if (item != null) { + textProperty().bind(item.titleProperty()); + item.stateProperty().addListener(this::stateChanged); + graphicProperty().bind(Bindings.createObjectBinding(() -> graphicForState(item.getState()),item.stateProperty())); + stateIcon.setGlyph(glyphForState(item.getState())); + } else { + textProperty().unbind(); + graphicProperty().unbind(); + setGraphic(null); + setText(null); + } + } + + private void stateChanged(ObservableValue observable, Worker.State oldState, Worker.State newState) { + stateIcon.setGlyph(glyphForState(newState)); + stateIcon.setVisible(true); + } + + private Node graphicForState(Worker.State state) { + return switch (state) { + case READY -> null; + case SCHEDULED, RUNNING, FAILED, CANCELLED, SUCCEEDED -> stateIcon; + }; + } + + private FontAwesome5Icon glyphForState(Worker.State state) { + return switch (state) { + case READY -> FontAwesome5Icon.COG; //just a placeholder + case SCHEDULED -> FontAwesome5Icon.CLOCK; + case RUNNING -> FontAwesome5Icon.SPINNER; + case FAILED -> FontAwesome5Icon.EXCLAMATION_TRIANGLE; + case CANCELLED -> FontAwesome5Icon.BAN; + case SUCCEEDED -> FontAwesome5Icon.CHECK; + }; + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/CheckListController.java b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListController.java new file mode 100644 index 000000000..ccb41d56b --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/CheckListController.java @@ -0,0 +1,180 @@ +package org.cryptomator.ui.health; + +import com.google.common.base.Preconditions; +import com.tobiasdiez.easybind.EasyBind; +import dagger.Lazy; +import org.cryptomator.ui.common.ErrorComponent; +import org.cryptomator.ui.common.FxController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.beans.binding.Binding; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Worker; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ListView; +import javafx.scene.control.cell.CheckBoxListCell; +import javafx.stage.Stage; +import javafx.util.StringConverter; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +@HealthCheckScoped +public class CheckListController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(CheckListController.class); + private static final Set END_STATES = Set.of(Worker.State.FAILED, Worker.State.CANCELLED, Worker.State.SUCCEEDED); + + private final Stage window; + private final ObservableList tasks; + private final ReportWriter reportWriter; + private final ExecutorService executorService; + private final ObjectProperty selectedTask; + private final Lazy errorComponenBuilder; + private final SimpleObjectProperty> runningTask; + private final Binding running; + private final Binding finished; + private final Map listPickIndicators; + private final IntegerProperty numberOfPickedChecks; + private final BooleanBinding anyCheckSelected; + private final BooleanProperty showResultScreen; + + /* FXML */ + public ListView checksListView; + + + @Inject + public CheckListController(@HealthCheckWindow Stage window, Lazy> tasks, ReportWriter reportWriteTask, ObjectProperty selectedTask, ExecutorService executorService, Lazy errorComponenBuilder) { + this.window = window; + this.tasks = FXCollections.observableArrayList(tasks.get()); + this.reportWriter = reportWriteTask; + this.executorService = executorService; + this.selectedTask = selectedTask; + this.errorComponenBuilder = errorComponenBuilder; + this.runningTask = new SimpleObjectProperty<>(); + this.running = EasyBind.wrapNullable(runningTask).mapObservable(Worker::runningProperty).orElse(false); + this.finished = EasyBind.wrapNullable(runningTask).mapObservable(Worker::stateProperty).map(END_STATES::contains).orElse(false); + this.listPickIndicators = new HashMap<>(); + this.numberOfPickedChecks = new SimpleIntegerProperty(0); + this.tasks.forEach(task -> { + var entrySelectedProp = new SimpleBooleanProperty(false); + entrySelectedProp.addListener((observable, oldValue, newValue) -> numberOfPickedChecks.set(numberOfPickedChecks.get() + (newValue ? 1 : -1))); + listPickIndicators.put(task, entrySelectedProp); + }); + this.anyCheckSelected = selectedTask.isNotNull(); + this.showResultScreen = new SimpleBooleanProperty(false); + } + + @FXML + public void initialize() { + checksListView.setItems(tasks); + checksListView.setCellFactory(CheckBoxListCell.forListView(listPickIndicators::get, new StringConverter() { + @Override + public String toString(HealthCheckTask object) { + return object.getTitle(); + } + + @Override + public HealthCheckTask fromString(String string) { + return null; + } + })); + selectedTask.bind(checksListView.getSelectionModel().selectedItemProperty()); + } + + @FXML + public void toggleSelectAll(ActionEvent event) { + if (event.getSource() instanceof CheckBox c) { + listPickIndicators.forEach( (task, pickProperty) -> pickProperty.set(c.isSelected())); + } + } + + @FXML + public void runSelectedChecks() { + Preconditions.checkState(runningTask.get() == null); + var batch = checksListView.getItems().filtered(item -> listPickIndicators.get(item).get()); + var batchService = new BatchService(batch); + batchService.setExecutor(executorService); + batchService.start(); + runningTask.set(batchService); + showResultScreen.set(true); + checksListView.getSelectionModel().select(batch.get(0)); + checksListView.setCellFactory(view -> new CheckListCell()); + window.sizeToScene(); + } + + @FXML + public synchronized void cancelCheck() { + Preconditions.checkState(runningTask.get() != null); + runningTask.get().cancel(); + } + + @FXML + public void exportResults() { + try { + reportWriter.writeReport(tasks); + } catch (IOException e) { + LOG.error("Failed to write health check report.", e); + errorComponenBuilder.get().cause(e).window(window).returnToScene(window.getScene()).build().showErrorScene(); + } + } + + /* Getter/Setter */ + public boolean isRunning() { + return running.getValue(); + } + + public Binding runningProperty() { + return running; + } + + public boolean isFinished() { + return finished.getValue(); + } + + public Binding finishedProperty() { + return finished; + } + + public boolean isAnyCheckSelected() { + return anyCheckSelected.get(); + } + + public BooleanBinding anyCheckSelectedProperty() { + return anyCheckSelected; + } + + public boolean getShowResultScreen() { + return showResultScreen.get(); + } + + public BooleanProperty showResultScreenProperty() { + return showResultScreen; + } + + public int getNumberOfPickedChecks() { + return numberOfPickedChecks.get(); + } + + public IntegerProperty numberOfPickedChecksProperty() { + return numberOfPickedChecks; + } + + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java new file mode 100644 index 000000000..48b16f694 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckComponent.java @@ -0,0 +1,39 @@ +package org.cryptomator.ui.health; + +import dagger.BindsInstance; +import dagger.Lazy; +import dagger.Subcomponent; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; + +import javafx.scene.Scene; +import javafx.stage.Stage; + +@HealthCheckScoped +@Subcomponent(modules = {HealthCheckModule.class}) +public interface HealthCheckComponent { + + @HealthCheckWindow + Stage window(); + + @FxmlScene(FxmlFile.HEALTH_START) + Lazy scene(); + + default Stage showHealthCheckWindow() { + Stage stage = window(); + stage.setScene(scene().get()); + stage.show(); + return stage; + } + + @Subcomponent.Builder + interface Builder { + + @BindsInstance + Builder vault(@HealthCheckWindow Vault vault); + + HealthCheckComponent build(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java new file mode 100644 index 000000000..e33a9f2f1 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckModule.java @@ -0,0 +1,141 @@ +package org.cryptomator.ui.health; + +import dagger.Binds; +import dagger.Module; +import dagger.Provides; +import dagger.multibindings.IntoMap; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.HealthCheck; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.ui.common.DefaultSceneFactory; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxControllerKey; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.StageFactory; +import org.cryptomator.ui.keyloading.KeyLoadingComponent; +import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import org.cryptomator.ui.mainwindow.MainWindow; + +import javax.inject.Provider; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ChangeListener; +import javafx.scene.Scene; +import javafx.stage.Modality; +import javafx.stage.Stage; +import java.security.SecureRandom; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; + +@Module(subcomponents = {KeyLoadingComponent.class}) +abstract class HealthCheckModule { + + @Provides + @HealthCheckScoped + static AtomicReference provideMasterkeyRef() { + return new AtomicReference<>(); + } + + @Provides + @HealthCheckScoped + static AtomicReference provideVaultConfigRef() { + return new AtomicReference<>(); + } + + @Provides + @HealthCheckScoped + static Collection provideAvailableHealthChecks() { + return HealthCheck.allChecks(); + } + + @Provides + @HealthCheckScoped + static ObjectProperty provideSelectedHealthCheckTask() { + return new SimpleObjectProperty<>(); + } + + /* Only inject with Lazy-Wrapper!*/ + @Provides + @HealthCheckScoped + static Collection provideAvailableHealthCheckTasks(Collection availableHealthChecks, @HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng, ResourceBundle resourceBundle) { + return availableHealthChecks.stream().map(check -> new HealthCheckTask(vault.getPath(), vaultConfigRef.get(), masterkeyRef.get(), csprng, check, resourceBundle)).toList(); + } + + @Provides + @HealthCheckWindow + @HealthCheckScoped + static KeyLoadingStrategy provideKeyLoadingStrategy(KeyLoadingComponent.Builder compBuilder, @HealthCheckWindow Vault vault, @HealthCheckWindow Stage window) { + return compBuilder.vault(vault).window(window).build().keyloadingStrategy(); + } + + @Provides + @HealthCheckWindow + @HealthCheckScoped + static FxmlLoaderFactory provideFxmlLoaderFactory(Map, Provider> factories, DefaultSceneFactory sceneFactory, ResourceBundle resourceBundle) { + return new FxmlLoaderFactory(factories, sceneFactory, resourceBundle); + } + + @Provides + @HealthCheckWindow + @HealthCheckScoped + static Stage provideStage(StageFactory factory, @MainWindow Stage owner, ResourceBundle resourceBundle, ChangeListener showingListener) { + Stage stage = factory.create(); + stage.setTitle(resourceBundle.getString("health.title")); + stage.setResizable(true); + stage.initModality(Modality.WINDOW_MODAL); + stage.initOwner(owner); + stage.showingProperty().addListener(showingListener); // bind masterkey lifecycle to window + return stage; + } + + @Provides + @HealthCheckScoped + static ChangeListener provideWindowShowingChangeListener(AtomicReference masterkey) { + return (observable, wasShowing, isShowing) -> { + if (!isShowing) { + Optional.ofNullable(masterkey.getAndSet(null)).ifPresent(Masterkey::destroy); + } + }; + } + + @Provides + @FxmlScene(FxmlFile.HEALTH_START) + @HealthCheckScoped + static Scene provideHealthStartScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HEALTH_START); + } + + @Provides + @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) + @HealthCheckScoped + static Scene provideHealthCheckListScene(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + return fxmlLoaders.createScene(FxmlFile.HEALTH_CHECK_LIST); + } + + @Binds + @IntoMap + @FxControllerKey(StartController.class) + abstract FxController bindStartController(StartController controller); + + @Binds + @IntoMap + @FxControllerKey(CheckListController.class) + abstract FxController bindCheckController(CheckListController controller); + + @Binds + @IntoMap + @FxControllerKey(CheckDetailController.class) + abstract FxController bindCheckDetailController(CheckDetailController controller); + + @Binds + @IntoMap + @FxControllerKey(ResultListCellController.class) + abstract FxController bindResultListCellController(ResultListCellController controller); + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckScoped.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckScoped.java new file mode 100644 index 000000000..af563d737 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckScoped.java @@ -0,0 +1,13 @@ +package org.cryptomator.ui.health; + +import javax.inject.Scope; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Scope +@Documented +@Retention(RetentionPolicy.RUNTIME) +@interface HealthCheckScoped { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java new file mode 100644 index 000000000..7acbfc1c2 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckTask.java @@ -0,0 +1,108 @@ +package org.cryptomator.ui.health; + +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptofs.health.api.HealthCheck; +import org.cryptomator.cryptolib.api.Masterkey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javafx.application.Platform; +import javafx.beans.property.LongProperty; +import javafx.beans.property.SimpleLongProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.concurrent.Task; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.Instant; +import java.util.MissingResourceException; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.concurrent.CancellationException; + +class HealthCheckTask extends Task { + + private static final Logger LOG = LoggerFactory.getLogger(HealthCheckTask.class); + + private final Path vaultPath; + private final VaultConfig vaultConfig; + private final Masterkey masterkey; + private final SecureRandom csprng; + private final HealthCheck check; + private final ObservableList results; + private final LongProperty durationInMillis; + + public HealthCheckTask(Path vaultPath, VaultConfig vaultConfig, Masterkey masterkey, SecureRandom csprng, HealthCheck check, ResourceBundle resourceBundle) { + this.vaultPath = Objects.requireNonNull(vaultPath); + this.vaultConfig = Objects.requireNonNull(vaultConfig); + this.masterkey = Objects.requireNonNull(masterkey); + this.csprng = Objects.requireNonNull(csprng); + this.check = Objects.requireNonNull(check); + this.results = FXCollections.observableArrayList(); + try { + updateTitle(resourceBundle.getString("health." + check.identifier())); + } catch (MissingResourceException e) { + LOG.warn("Missing proper name for health check {}, falling back to default.", check.identifier()); + updateTitle(check.identifier()); + } + this.durationInMillis = new SimpleLongProperty(-1); + } + + @Override + protected Void call() { + Instant start = Instant.now(); + try (var masterkeyClone = masterkey.clone(); // + var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) { + check.check(vaultPath, vaultConfig, masterkeyClone, cryptor, result -> { + if (isCancelled()) { + throw new CancellationException(); + } + // FIXME: slowdown for demonstration purposes only: + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + if (isCancelled()) { + return; + } else { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + Platform.runLater(() -> results.add(result)); + }); + } + Platform.runLater(() ->durationInMillis.set(Duration.between(start, Instant.now()).toMillis())); + return null; + } + + @Override + protected void scheduled() { + LOG.info("starting {}", check.identifier()); + } + + @Override + protected void done() { + LOG.info("finished {}", check.identifier()); + } + + /* Getter */ + + public ObservableList results() { + return results; + } + + public HealthCheck getCheck() { + return check; + } + + public LongProperty durationInMillisProperty() { + return durationInMillis; + } + + public long getDurationInMillis() { + return durationInMillis.get(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckWindow.java b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckWindow.java new file mode 100644 index 000000000..50243c07a --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/HealthCheckWindow.java @@ -0,0 +1,14 @@ +package org.cryptomator.ui.health; + +import javax.inject.Qualifier; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Qualifier +@Documented +@Retention(RUNTIME) +@interface HealthCheckWindow { + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ReportWriter.java b/main/ui/src/main/java/org/cryptomator/ui/health/ReportWriter.java new file mode 100644 index 000000000..fb74cbd51 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ReportWriter.java @@ -0,0 +1,105 @@ +package org.cryptomator.ui.health; + +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.cryptomator.common.Environment; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Application; +import javafx.concurrent.Worker; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collection; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; + +@HealthCheckScoped +public class ReportWriter { + + private static final Logger LOG = LoggerFactory.getLogger(ReportWriter.class); + private static final String REPORT_HEADER = """ + ************************************** + * Cryptomator Vault Health Report * + ************************************** + Analyzed vault: %s (Current name "%s") + Vault storage path: %s + """; + private static final String REPORT_CHECK_HEADER = """ + + + Check %s + ------------------------------ + """; + private static final String REPORT_CHECK_RESULT = "%8s - %s\n"; + private static final DateTimeFormatter TIME_STAMP = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss").withZone(ZoneId.systemDefault()); + + private final Vault vault; + private final VaultConfig vaultConfig; + private final Application application; + private final Path exportDestination; + + @Inject + public ReportWriter(@HealthCheckWindow Vault vault, AtomicReference vaultConfigRef, Application application, Environment env) { + this.vault = vault; + this.vaultConfig = Objects.requireNonNull(vaultConfigRef.get()); + this.application = application; + this.exportDestination = env.getLogDir().orElse(Path.of(System.getProperty("user.home"))).resolve("healthReport_" + vault.getDisplayName() + "_" + TIME_STAMP.format(Instant.now()) + ".log"); + } + + protected void writeReport(Collection tasks) throws IOException { + try (var out = Files.newOutputStream(exportDestination, StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); // + var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) { + writer.write(REPORT_HEADER.formatted(vaultConfig.getId(), vault.getDisplayName(), vault.getPath())); + for (var task : tasks) { + if (task.getState() == Worker.State.READY) { + LOG.debug("Skipping not performed check {}.", task.getCheck().identifier()); + continue; + } + writer.write(REPORT_CHECK_HEADER.formatted(task.getCheck().identifier())); + switch (task.getState()) { + case SUCCEEDED -> { + writer.write("STATUS: SUCCESS\nRESULTS:\n"); + for (var result : task.results()) { + writer.write(REPORT_CHECK_RESULT.formatted(result.getServerity(), result.toString())); + } + } + case CANCELLED -> writer.write("STATUS: CANCELED\n"); + case FAILED -> { + writer.write("STATUS: FAILED\nREASON:\n" + task.getCheck().identifier()); + writer.write(prepareFailureMsg(task)); + } + case RUNNING, SCHEDULED -> throw new IllegalStateException("Checks are still running."); + } + } + } + reveal(); + } + + private String prepareFailureMsg(HealthCheckTask task) { + if (task.getException() != null) { + return ExceptionUtils.getStackTrace(task.getException()) // + .lines() // + .map(line -> "\t\t" + line + "\n") // + .collect(Collectors.joining()); + } else { + return "Unknown reason of failure."; + } + } + + private void reveal() { + application.getHostServices().showDocument(exportDestination.getParent().toUri().toString()); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java b/main/ui/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java new file mode 100644 index 000000000..9a639e8a0 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ResultFixApplier.java @@ -0,0 +1,47 @@ +package org.cryptomator.ui.health; + +import com.google.common.base.Preconditions; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.cryptolib.api.Masterkey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.scene.control.Alert; +import java.nio.file.Path; +import java.security.SecureRandom; +import java.util.concurrent.atomic.AtomicReference; + +@HealthCheckScoped +class ResultFixApplier { + + private static final Logger LOG = LoggerFactory.getLogger(ResultFixApplier.class); + + private final Path vaultPath; + private final SecureRandom csprng; + private final Masterkey masterkey; + private final VaultConfig vaultConfig; + + @Inject + public ResultFixApplier(@HealthCheckWindow Vault vault, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, SecureRandom csprng) { + this.vaultPath = vault.getPath(); + this.masterkey = masterkeyRef.get(); + this.vaultConfig = vaultConfigRef.get(); + this.csprng = csprng; + } + + public void fix(DiagnosticResult result) { + Preconditions.checkArgument(result.getServerity() == DiagnosticResult.Severity.WARN, "Unfixable result"); + try (var masterkeyClone = masterkey.clone(); // + var cryptor = vaultConfig.getCipherCombo().getCryptorProvider(csprng).withKey(masterkeyClone)) { + result.fix(vaultPath, vaultConfig, masterkeyClone, cryptor); + } catch (Exception e) { + LOG.error("Failed to apply fix", e); + Alert alert = new Alert(Alert.AlertType.ERROR, e.getMessage()); + alert.showAndWait(); + //TODO: real error/not supported handling + } + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellController.java b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellController.java new file mode 100644 index 000000000..f2bca059a --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellController.java @@ -0,0 +1,92 @@ +package org.cryptomator.ui.health; + +import com.tobiasdiez.easybind.EasyBind; +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.controls.FontAwesome5Icon; +import org.cryptomator.ui.controls.FontAwesome5IconView; + +import javax.inject.Inject; +import javafx.beans.binding.Binding; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableValue; +import javafx.fxml.FXML; +import javafx.scene.control.Button; + +// unscoped because each cell needs its own controller +public class ResultListCellController implements FxController { + + private final ResultFixApplier fixApplier; + private final ObjectProperty result; + private final Binding description; + + public FontAwesome5IconView iconView; + public Button actionButton; + + @Inject + public ResultListCellController(ResultFixApplier fixApplier) { + this.result = new SimpleObjectProperty<>(null); + this.description = EasyBind.wrapNullable(result).map(DiagnosticResult::toString).orElse(""); + this.fixApplier = fixApplier; + result.addListener(this::updateCellContent); + } + + private void updateCellContent(ObservableValue observable, DiagnosticResult oldVal, DiagnosticResult newVal) { + iconView.getStyleClass().clear(); + actionButton.setVisible(false); + //TODO: see comment in case WARN + actionButton.setManaged(false); + switch (newVal.getServerity()) { + case INFO -> { + iconView.setGlyph(FontAwesome5Icon.INFO_CIRCLE); + iconView.getStyleClass().add("glyph-icon-muted"); + } + case GOOD -> { + iconView.setGlyph(FontAwesome5Icon.CHECK); + iconView.getStyleClass().add("glyph-icon-primary"); + } + case WARN -> { + iconView.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE); + iconView.getStyleClass().add("glyph-icon-orange"); + //TODO: Neither is any fix implemented, nor it is ensured, that only fix is executed at a time with good ui indication + // before both are not fix, do not show the button + //actionButton.setVisible(true); + } + case CRITICAL -> { + iconView.setGlyph(FontAwesome5Icon.TIMES); + iconView.getStyleClass().add("glyph-icon-red"); + } + } + } + + @FXML + public void runResultAction() { + final var realResult = result.get(); + if (realResult != null) { + fixApplier.fix(realResult); + } + } + /* Getter & Setter */ + + + public DiagnosticResult getResult() { + return result.get(); + } + + public void setResult(DiagnosticResult result) { + this.result.set(result); + } + + public ObjectProperty resultProperty() { + return result; + } + + public String getDescription() { + return description.getValue(); + } + + public Binding descriptionProperty() { + return description; + } +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java new file mode 100644 index 000000000..7acada487 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/ResultListCellFactory.java @@ -0,0 +1,60 @@ +package org.cryptomator.ui.health; + + +import org.cryptomator.cryptofs.health.api.DiagnosticResult; +import org.cryptomator.ui.common.FxmlLoaderFactory; + +import javax.inject.Inject; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.control.ContentDisplay; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.util.Callback; +import java.io.IOException; +import java.io.UncheckedIOException; + +@HealthCheckScoped +public class ResultListCellFactory implements Callback, ListCell> { + + private final FxmlLoaderFactory fxmlLoaders; + + @Inject + ResultListCellFactory(@HealthCheckWindow FxmlLoaderFactory fxmlLoaders) { + this.fxmlLoaders = fxmlLoaders; + } + + @Override + public ListCell call(ListView param) { + try { + FXMLLoader fxmlLoader = fxmlLoaders.load("/fxml/health_result_listcell.fxml"); + return new ResultListCellFactory.Cell(fxmlLoader.getRoot(), fxmlLoader.getController()); + } catch (IOException e) { + throw new UncheckedIOException("Failed to load /fxml/health_result_listcell.fxml.", e); + } + } + + private static class Cell extends ListCell { + + private final Parent node; + private final ResultListCellController controller; + + public Cell(Parent node, ResultListCellController controller) { + this.node = node; + this.controller = controller; + } + + @Override + protected void updateItem(DiagnosticResult item, boolean empty) { + super.updateItem(item, empty); + if (item == null || empty) { + setText(null); + setGraphic(null); + } else { + controller.setResult(item); + setContentDisplay(ContentDisplay.GRAPHIC_ONLY); + setGraphic(node); + } + } + } +} \ No newline at end of file diff --git a/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java b/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java new file mode 100644 index 000000000..3e9cc5b07 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/health/StartController.java @@ -0,0 +1,127 @@ +package org.cryptomator.ui.health; + +import dagger.Lazy; +import org.cryptomator.common.vaults.Vault; +import org.cryptomator.cryptofs.VaultConfig; +import org.cryptomator.cryptofs.VaultConfigLoadException; +import org.cryptomator.cryptofs.VaultKeyInvalidException; +import org.cryptomator.cryptolib.api.Masterkey; +import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException; +import org.cryptomator.ui.common.ErrorComponent; +import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.keyloading.KeyLoadingStrategy; +import org.cryptomator.ui.unlock.UnlockCancelledException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.Scene; +import javafx.stage.Stage; +import java.io.IOException; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicReference; + +@HealthCheckScoped +public class StartController implements FxController { + + private static final Logger LOG = LoggerFactory.getLogger(StartController.class); + + private final Stage window; + private final Optional unverifiedVaultConfig; + private final KeyLoadingStrategy keyLoadingStrategy; + private final ExecutorService executor; + private final AtomicReference masterkeyRef; + private final AtomicReference vaultConfigRef; + private final Lazy checkScene; + private final Lazy errorComponent; + + /* FXML */ + + @Inject + public StartController(@HealthCheckWindow Vault vault, @HealthCheckWindow Stage window, @HealthCheckWindow KeyLoadingStrategy keyLoadingStrategy, ExecutorService executor, AtomicReference masterkeyRef, AtomicReference vaultConfigRef, @FxmlScene(FxmlFile.HEALTH_CHECK_LIST) Lazy checkScene, Lazy errorComponent) { + this.window = window; + this.keyLoadingStrategy = keyLoadingStrategy; + this.executor = executor; + this.masterkeyRef = masterkeyRef; + this.vaultConfigRef = vaultConfigRef; + this.checkScene = checkScene; + this.errorComponent = errorComponent; + + //TODO: this is ugly + //idea: delay the loading of the vault config and show a spinner (something like "check/load config") and react to the result of the loading + //or: load vault config in a previous step to see if it is loadable. + VaultConfig.UnverifiedVaultConfig tmp; + try { + tmp = vault.getUnverifiedVaultConfig(); + } catch (IOException e) { + e.printStackTrace(); + tmp = null; + } + this.unverifiedVaultConfig = Optional.ofNullable(tmp); + } + + @FXML + public void close() { + LOG.trace("StartController.close()"); + window.close(); + } + + @FXML + public void next() { + LOG.trace("StartController.next()"); + executor.submit(this::loadKey); + } + + private void loadKey() { + assert !Platform.isFxApplicationThread(); + assert unverifiedVaultConfig.isPresent(); + try (var masterkey = keyLoadingStrategy.loadKey(unverifiedVaultConfig.orElseThrow().getKeyId())) { + var unverifiedCfg = unverifiedVaultConfig.get(); + var verifiedCfg = unverifiedCfg.verify(masterkey.getEncoded(), unverifiedCfg.allegedVaultVersion()); + vaultConfigRef.set(verifiedCfg); + var old = masterkeyRef.getAndSet(masterkey.clone()); + if (old != null) { + old.destroy(); + } + Platform.runLater(this::loadedKey); + } catch (MasterkeyLoadingFailedException e) { + if (keyLoadingStrategy.recoverFromException(e)) { + // retry + loadKey(); + } else { + Platform.runLater(() -> loadingKeyFailed(e)); + } + } catch (VaultKeyInvalidException e) { + Platform.runLater(() -> loadingKeyFailed(e)); + } catch (VaultConfigLoadException e) { + Platform.runLater(() -> loadingKeyFailed(e)); + } + } + + private void loadedKey() { + LOG.debug("Loaded valid key"); + window.setScene(checkScene.get()); + } + + private void loadingKeyFailed(Exception e) { + if (e instanceof UnlockCancelledException) { + // ok + } else if (e instanceof VaultKeyInvalidException) { + LOG.error("Invalid key"); //TODO: specific error screen + errorComponent.get().window(window).cause(e).build().showErrorScene(); + } else { + LOG.error("Failed to load key.", e); + errorComponent.get().window(window).cause(e).build().showErrorScene(); + } + } + + public boolean isInvalidConfig() { + return unverifiedVaultConfig.isEmpty(); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java index 15a5d27b8..f7eb8922f 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/KeyLoadingModule.java @@ -10,6 +10,7 @@ import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.keyloading.masterkeyfile.MasterkeyFileLoadingModule; import javax.inject.Provider; +import java.io.IOException; import java.net.URI; import java.util.Map; import java.util.Optional; @@ -28,20 +29,13 @@ abstract class KeyLoadingModule { @Provides @KeyLoading @KeyLoadingScoped - static Optional provideKeyId(@KeyLoading Vault vault) { - return vault.getUnverifiedVaultConfig().map(UnverifiedVaultConfig::getKeyId); - } - - @Provides - @KeyLoading - @KeyLoadingScoped - static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Optional keyId, Map> strategies) { - if (keyId.isEmpty()) { - return KeyLoadingStrategy.failed(new IllegalArgumentException("No key id provided")); - } else { - String scheme = keyId.get().getScheme(); + static KeyLoadingStrategy provideKeyLoaderProvider(@KeyLoading Vault vault, Map> strategies) { + try { + String scheme = vault.getUnverifiedVaultConfig().getKeyId().getScheme(); var fallback = KeyLoadingStrategy.failed(new IllegalArgumentException("Unsupported key id " + scheme)); return strategies.getOrDefault(scheme, () -> fallback).get(); + } catch (IOException e) { + return KeyLoadingStrategy.failed(e); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java index 464671929..f8fbdd720 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java +++ b/main/ui/src/main/java/org/cryptomator/ui/keyloading/masterkeyfile/MasterkeyFileLoadingStrategy.java @@ -92,7 +92,7 @@ public class MasterkeyFileLoadingStrategy implements KeyLoadingStrategy { } private Path getAlternateMasterkeyFilePath() throws UnlockCancelledException, InterruptedException { - if (filePath == null) { + if (filePath.get() == null) { return switch (askUserForMasterkeyFilePath()) { case MASTERKEYFILE_PROVIDED -> filePath.get(); case CANCELED -> throw new UnlockCancelledException("Choosing masterkey file cancelled."); diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java index fad1b3c85..90311bd5b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/MainWindowModule.java @@ -12,6 +12,7 @@ import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.StageFactory; +import org.cryptomator.ui.health.HealthCheckComponent; import org.cryptomator.ui.migration.MigrationComponent; import org.cryptomator.ui.removevault.RemoveVaultComponent; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; @@ -27,7 +28,7 @@ import javafx.stage.StageStyle; import java.util.Map; import java.util.ResourceBundle; -@Module(subcomponents = {AddVaultWizardComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class}) +@Module(subcomponents = {AddVaultWizardComponent.class, HealthCheckComponent.class, MigrationComponent.class, RemoveVaultComponent.class, VaultOptionsComponent.class, VaultStatisticsComponent.class, WrongFileAlertComponent.class}) abstract class MainWindowModule { @Provides diff --git a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java index bc2f2100a..20f0d916d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/mainwindow/VaultDetailLockedController.java @@ -5,6 +5,7 @@ import org.cryptomator.common.keychain.KeychainManager; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.fxapp.FxApplication; +import org.cryptomator.ui.health.HealthCheckComponent; import org.cryptomator.ui.vaultoptions.SelectedVaultOptionsTab; import org.cryptomator.ui.vaultoptions.VaultOptionsComponent; @@ -13,6 +14,7 @@ import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyObjectProperty; import javafx.beans.property.SimpleBooleanProperty; +import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.stage.Stage; import java.util.Optional; @@ -28,7 +30,7 @@ public class VaultDetailLockedController implements FxController { private final BooleanExpression passwordSaved; @Inject - VaultDetailLockedController(ObjectProperty vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) { + VaultDetailLockedController(ObjectProperty vault, FxApplication application, VaultOptionsComponent.Builder vaultOptionsWindow, KeychainManager keychain, @MainWindow Stage mainWindow) { this.vault = vault; this.application = application; this.vaultOptionsWindow = vaultOptionsWindow; diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java index 58d9fbde9..b30167e73 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyModule.java @@ -6,10 +6,10 @@ import dagger.Provides; import dagger.multibindings.IntoMap; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.DefaultSceneFactory; -import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxControllerKey; import org.cryptomator.ui.common.FxmlFile; +import org.cryptomator.ui.common.FxmlLoaderFactory; import org.cryptomator.ui.common.FxmlScene; import org.cryptomator.ui.common.NewPasswordController; import org.cryptomator.ui.common.PasswordStrengthUtil; @@ -17,8 +17,6 @@ import org.cryptomator.ui.common.StageFactory; import javax.inject.Named; import javax.inject.Provider; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.Scene; @@ -56,14 +54,6 @@ abstract class RecoveryKeyModule { return new SimpleStringProperty(); } - @Provides - @RecoveryKeyScoped - @Named("newPassword") - static ObjectProperty provideNewPasswordProperty() { - return new SimpleObjectProperty<>(""); - } - - // ------------------ @Provides @@ -126,8 +116,8 @@ abstract class RecoveryKeyModule { @Provides @IntoMap @FxControllerKey(NewPasswordController.class) - static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater, @Named("newPassword") ObjectProperty password) { - return new NewPasswordController(resourceBundle, strengthRater, password); + static FxController provideNewPasswordController(ResourceBundle resourceBundle, PasswordStrengthUtil strengthRater) { + return new NewPasswordController(resourceBundle, strengthRater); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java index cdf54990f..a2319ba3c 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/recoverykey/RecoveryKeyResetPasswordController.java @@ -6,14 +6,12 @@ import org.cryptomator.ui.common.ErrorComponent; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; +import org.cryptomator.ui.common.NewPasswordController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import javax.inject.Named; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.StringProperty; import javafx.concurrent.Task; import javafx.fxml.FXML; @@ -32,21 +30,19 @@ public class RecoveryKeyResetPasswordController implements FxController { private final RecoveryKeyFactory recoveryKeyFactory; private final ExecutorService executor; private final StringProperty recoveryKey; - private final ObjectProperty newPassword; private final Lazy recoverScene; - private final BooleanBinding invalidNewPassword; private final ErrorComponent.Builder errorComponent; + public NewPasswordController newPasswordController; + @Inject - public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @Named("newPassword") ObjectProperty newPassword, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene, ErrorComponent.Builder errorComponent) { + public RecoveryKeyResetPasswordController(@RecoveryKeyWindow Stage window, @RecoveryKeyWindow Vault vault, RecoveryKeyFactory recoveryKeyFactory, ExecutorService executor, @RecoveryKeyWindow StringProperty recoveryKey, @FxmlScene(FxmlFile.RECOVERYKEY_RECOVER) Lazy recoverScene, ErrorComponent.Builder errorComponent) { this.window = window; this.vault = vault; this.recoveryKeyFactory = recoveryKeyFactory; this.executor = executor; this.recoveryKey = recoveryKey; - this.newPassword = newPassword; this.recoverScene = recoverScene; - this.invalidNewPassword = Bindings.createBooleanBinding(this::isInvalidNewPassword, newPassword); this.errorComponent = errorComponent; } @@ -81,7 +77,7 @@ public class RecoveryKeyResetPasswordController implements FxController { @Override protected Void call() throws IOException, IllegalArgumentException { - recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPassword.get()); + recoveryKeyFactory.resetPasswordWithRecoveryKey(vault.getPath(), recoveryKey.get(), newPasswordController.passwordField.getCharacters()); return null; } @@ -89,11 +85,12 @@ public class RecoveryKeyResetPasswordController implements FxController { /* Getter/Setter */ - public BooleanBinding invalidNewPasswordProperty() { - return invalidNewPassword; + public ReadOnlyBooleanProperty validPasswordProperty() { + return newPasswordController.passwordsMatchAndSufficientProperty(); } - public boolean isInvalidNewPassword() { - return newPassword.get() == null || newPassword.get().length() == 0; + public boolean isValidPassword() { + return newPasswordController.passwordsMatchAndSufficientProperty().get(); } + } diff --git a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java index e860ee811..79d3b53ad 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/vaultoptions/GeneralVaultOptionsController.java @@ -3,6 +3,7 @@ package org.cryptomator.ui.vaultoptions; import org.cryptomator.common.settings.WhenUnlocked; import org.cryptomator.common.vaults.Vault; import org.cryptomator.ui.common.FxController; +import org.cryptomator.ui.health.HealthCheckComponent; import javax.inject.Inject; import javafx.beans.Observable; @@ -22,6 +23,7 @@ public class GeneralVaultOptionsController implements FxController { private final Stage window; private final Vault vault; + private final HealthCheckComponent.Builder healthCheckWindow; private final ResourceBundle resourceBundle; public TextField vaultName; @@ -29,9 +31,10 @@ public class GeneralVaultOptionsController implements FxController { public ChoiceBox actionAfterUnlockChoiceBox; @Inject - GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, ResourceBundle resourceBundle) { + GeneralVaultOptionsController(@VaultOptionsWindow Stage window, @VaultOptionsWindow Vault vault, HealthCheckComponent.Builder healthCheckWindow, ResourceBundle resourceBundle) { this.window = window; this.vault = vault; + this.healthCheckWindow = healthCheckWindow; this.resourceBundle = resourceBundle; } @@ -61,6 +64,12 @@ public class GeneralVaultOptionsController implements FxController { } } + @FXML + public void showHealthCheck() { + healthCheckWindow.vault(vault).build().showHealthCheckWindow(); + } + + private static class WhenUnlockedConverter extends StringConverter { private final ResourceBundle resourceBundle; diff --git a/main/ui/src/main/resources/fxml/addvault_new_password.fxml b/main/ui/src/main/resources/fxml/addvault_new_password.fxml index 7b87bd636..4b62f9b78 100644 --- a/main/ui/src/main/resources/fxml/addvault_new_password.fxml +++ b/main/ui/src/main/resources/fxml/addvault_new_password.fxml @@ -23,7 +23,7 @@ - + diff --git a/main/ui/src/main/resources/fxml/changepassword.fxml b/main/ui/src/main/resources/fxml/changepassword.fxml index 5e8727574..3f077df5d 100644 --- a/main/ui/src/main/resources/fxml/changepassword.fxml +++ b/main/ui/src/main/resources/fxml/changepassword.fxml @@ -25,7 +25,7 @@ - + diff --git a/main/ui/src/main/resources/fxml/health_check_details.fxml b/main/ui/src/main/resources/fxml/health_check_details.fxml new file mode 100644 index 000000000..51d9b22ae --- /dev/null +++ b/main/ui/src/main/resources/fxml/health_check_details.fxml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/main/ui/src/main/resources/fxml/health_check_list.fxml b/main/ui/src/main/resources/fxml/health_check_list.fxml new file mode 100644 index 000000000..a3a9c2f93 --- /dev/null +++ b/main/ui/src/main/resources/fxml/health_check_list.fxml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +