diff --git a/.gitignore b/.gitignore index 7051b0cd0..52f9a6ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,15 +9,13 @@ .settings .project .classpath + +# Maven # target/ -test-output/ -# IntelliJ Settings Files # -.idea/ -out/ -.idea_modules/ -*.iws -*.iml - -# Temporary file created by test launcher -main/launcher/.ipcPort.tmp +# IntelliJ Settings Files (https://intellij-support.jetbrains.com/hc/en-us/articles/206544839-How-to-manage-projects-under-Version-Control-Systems) # +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries +.idea/**/libraries/ +*.iml \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 000000000..f9944b4aa --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +Cryptomator \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 000000000..d6cb2c866 --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,51 @@ + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 000000000..79ee123c2 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 000000000..3eb6376b7 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 000000000..40b0d6477 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..146ab09b7 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..fd8e48573 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1f205b58b..c04747c60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ cache: - $HOME/.m2 env: global: - - secure: "lV9OwUbHMrMpLUH1CY+Z4puLDdFXytudyPlG1eGRsesdpuG6KM3uQVz6uAtf6lrU8DRbMM/T7ML+PmvQ4UoPPYLdLxESLLBat2qUPOIVBOhTSlCc7I0DmGy04CSvkeMy8dPaQC0ukgNiR7zwoNzfcpGRN/U9S8tziDruuHoZSrg=" # BINTRAY_API_KEY + - secure: "HftEaabMmWn5GwKFKksUkOcelc3Mn7xazwAEy+4d4gL1+F8VhID/6DCK7nas+afUymWnxTano8Rv4Ci5MWryNkNkTH+FUPWmF3xWezc3hajSyS7RB92IZ8VPetl4Fo8UI1WwM5apDEaugalPxkIf8a7N+lpG5X/Gpumwzo3Be3w=" # BINTRAY_API_KEY - secure: "oWFgRTVP6lyTa7qVxlvkpm20MtVc3BtmsNXQJS6bfg2A0o/iCQMNx7OD59BaafCLGRKvCcJVESiC8FlSylVMS7CDSyYu0gg70NUiIuHp4NBM5inFWYCy/PdQsCTzr5uvNG+rMFQpMFRaCV0FrfM3tLondcVkhsHL68l93Xoexx4=" # CODACY_PROJECT_TOKEN - secure: "zJxgytA2Ks5Xzv+7kUaUq+EBFNQw9Qec63lcMJVuXVWczjL16nKW1EzzV515ag+OWL46z3lEPForDhufw0VtFnNmaX68jkO0mp01eLrHApc1llN2Y/U8GBXfNNazN4+Kom4H+z/AO+wJr8EsKMMUczCdQ3APgd9uVI0hzXw/Z3M=" # GITHUB_API_KEY addons: diff --git a/main/ant-kit/.gitignore b/main/ant-kit/.gitignore deleted file mode 100644 index b83d22266..000000000 --- a/main/ant-kit/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/target/ diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 1f8510d99..41c91efcf 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettings.java @@ -5,16 +5,7 @@ *******************************************************************************/ package org.cryptomator.common.settings; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Base64; -import java.util.Objects; -import java.util.UUID; - -import org.apache.commons.lang3.StringUtils; -import org.fxmisc.easybind.EasyBind; - +import com.google.common.base.Strings; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -22,12 +13,27 @@ import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import org.apache.commons.lang3.StringUtils; +import org.fxmisc.easybind.EasyBind; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +/** + * The settings specific to a single vault. + * TODO: Change the name of individualMountPath and its derivatives to customMountPath + */ public class VaultSettings { public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false; public static final boolean DEFAULT_REAVEAL_AFTER_MOUNT = true; public static final boolean DEFAULT_USES_INDIVIDUAL_MOUNTPATH = false; + public static final boolean DEFAULT_USES_READONLY_MODE = false; private final String id; private final ObjectProperty path = new SimpleObjectProperty<>(); @@ -37,6 +43,7 @@ public class VaultSettings { private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REAVEAL_AFTER_MOUNT); private final BooleanProperty usesIndividualMountPath = new SimpleBooleanProperty(DEFAULT_USES_INDIVIDUAL_MOUNTPATH); private final StringProperty individualMountPath = new SimpleStringProperty(); + private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE); public VaultSettings(String id) { this.id = Objects.requireNonNull(id); @@ -45,7 +52,7 @@ public class VaultSettings { } Observable[] observables() { - return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath}; + return new Observable[]{path, mountName, winDriveLetter, unlockAfterStartup, revealAfterMount, usesIndividualMountPath, individualMountPath, usesReadOnlyMode}; } private void deriveMountNameFromPath(Path path) { @@ -128,6 +135,18 @@ public class VaultSettings { return individualMountPath; } + public Optional getIndividualMountPath() { + if (usesIndividualMountPath.get()) { + return Optional.ofNullable(Strings.emptyToNull(individualMountPath.get())); + } else { + return Optional.empty(); + } + } + + public BooleanProperty usesReadOnlyMode() { + return usesReadOnlyMode; + } + /* Hashcode/Equals */ @Override diff --git a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java index 5fb30ef88..996c9a358 100644 --- a/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ b/main/commons/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java @@ -5,14 +5,13 @@ *******************************************************************************/ package org.cryptomator.common.settings; -import java.io.IOException; -import java.nio.file.Paths; - +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.nio.file.Paths; class VaultSettingsJsonAdapter { @@ -27,8 +26,8 @@ class VaultSettingsJsonAdapter { out.name("unlockAfterStartup").value(value.unlockAfterStartup().get()); out.name("revealAfterMount").value(value.revealAfterMount().get()); out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().get()); - //TODO: should this always be written? ( because it could contain metadata, which the user does not want to save!) - out.name("individualMountPath").value(value.individualMountPath().get()); + out.name("individualMountPath").value(value.individualMountPath().get()); //TODO: should this always be written? ( because it could contain metadata, which the user may not want to save!) + out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get()); out.endObject(); } @@ -41,6 +40,7 @@ class VaultSettingsJsonAdapter { boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP; boolean revealAfterMount = VaultSettings.DEFAULT_REAVEAL_AFTER_MOUNT; boolean usesIndividualMountPath = VaultSettings.DEFAULT_USES_INDIVIDUAL_MOUNTPATH; + boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE; in.beginObject(); while (in.hasNext()) { @@ -70,6 +70,9 @@ class VaultSettingsJsonAdapter { case "individualMountPath": individualMountPath = in.nextString(); break; + case "usesReadOnlyMode": + usesReadOnlyMode = in.nextBoolean(); + break; default: LOG.warn("Unsupported vault setting found in JSON: " + name); in.skipValue(); @@ -85,6 +88,7 @@ class VaultSettingsJsonAdapter { vaultSettings.revealAfterMount().set(revealAfterMount); vaultSettings.usesIndividualMountPath().set(usesIndividualMountPath); vaultSettings.individualMountPath().set(individualMountPath); + vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode); return vaultSettings; } diff --git a/main/pom.xml b/main/pom.xml index 06b447a40..d5dce5671 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -25,11 +25,11 @@ 1.2.1 - 1.6.1 + 1.7.0 2.0.0 - 1.0.2 - 1.0.0 - 1.0.5 + 1.1.0 + 1.1.3 + 1.0.6 11 @@ -39,7 +39,7 @@ 1.0.3 27.0-jre - 2.19 + 2.20 2.8.5 1.7.25 @@ -331,8 +331,6 @@ maven-compiler-plugin 3.8.0 - 11 - 11 11 diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java index 12a838139..2965d5dbd 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -16,6 +16,7 @@ import java.util.Optional; import javax.inject.Inject; +import javafx.beans.Observable; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; import org.cryptomator.ui.controls.SecPasswordField; @@ -48,7 +49,7 @@ public class ChangePasswordController implements ViewController { private final Application app; private final PasswordStrengthUtil strengthRater; private final Localization localization; - private final IntegerProperty passwordStrength = new SimpleIntegerProperty(); // 0-4 + private final IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // 0-4 private Optional listener = Optional.empty(); private Vault vault; @@ -100,11 +101,9 @@ public class ChangePasswordController implements ViewController { @Override public void initialize() { - BooleanBinding oldPasswordIsEmpty = oldPasswordField.textProperty().isEmpty(); - BooleanBinding newPasswordIsEmpty = newPasswordField.textProperty().isEmpty(); - BooleanBinding passwordsDiffer = newPasswordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); - changePasswordButton.disableProperty().bind(oldPasswordIsEmpty.or(newPasswordIsEmpty.or(passwordsDiffer))); - passwordStrength.bind(EasyBind.map(newPasswordField.textProperty(), strengthRater::computeRate)); + oldPasswordField.textProperty().addListener(this::passwordsChanged); + newPasswordField.textProperty().addListener(this::passwordsChanged); + retypePasswordField.textProperty().addListener(this::passwordsChanged); passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor)); passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor)); @@ -114,6 +113,14 @@ public class ChangePasswordController implements ViewController { passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); } + private void passwordsChanged(Observable observable) { + boolean oldPasswordEmpty = oldPasswordField.getCharacters().length() == 0; + boolean newPasswordEmpty = newPasswordField.getCharacters().length() == 0; + boolean passwordsEqual = newPasswordField.getCharacters().equals(retypePasswordField.getCharacters()); + changePasswordButton.setDisable(oldPasswordEmpty || newPasswordEmpty || !passwordsEqual); + passwordStrength.set(strengthRater.computeRate(newPasswordField.getCharacters().toString())); + } + @Override public Parent getRoot() { return root; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index 5c7c0dc5f..5e727d5f1 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -16,6 +16,9 @@ import java.util.Optional; import javax.inject.Inject; +import javafx.beans.Observable; +import javafx.beans.property.IntegerProperty; +import javafx.beans.value.ObservableIntegerValue; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.l10n.Localization; import org.cryptomator.ui.model.Vault; @@ -42,7 +45,7 @@ public class InitializeController implements ViewController { private final Localization localization; private final PasswordStrengthUtil strengthRater; - private ObservableValue passwordStrength; // 0-4 + private IntegerProperty passwordStrength = new SimpleIntegerProperty(-1); // strengths: 0-4 private Optional listener = Optional.empty(); private Vault vault; @@ -87,10 +90,8 @@ public class InitializeController implements ViewController { @Override public void initialize() { - BooleanBinding passwordIsEmpty = passwordField.textProperty().isEmpty(); - BooleanBinding passwordsDiffer = passwordField.textProperty().isNotEqualTo(retypePasswordField.textProperty()); - okButton.disableProperty().bind(passwordIsEmpty.or(passwordsDiffer)); - passwordStrength = EasyBind.map(passwordField.textProperty(), strengthRater::computeRate); + passwordField.textProperty().addListener(this::passwordsChanged); + retypePasswordField.textProperty().addListener(this::passwordsChanged); passwordStrengthLevel0.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(0), strengthRater::getBackgroundWithStrengthColor)); passwordStrengthLevel1.backgroundProperty().bind(EasyBind.combine(passwordStrength, new SimpleIntegerProperty(1), strengthRater::getBackgroundWithStrengthColor)); @@ -100,6 +101,13 @@ public class InitializeController implements ViewController { passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription)); } + private void passwordsChanged(Observable observable) { + boolean passwordsEmpty = passwordField.getCharacters().length() == 0; + boolean passwordsEqual = passwordField.getCharacters().equals(retypePasswordField.getCharacters()); + okButton.setDisable(passwordsEmpty || !passwordsEqual); + passwordStrength.set(strengthRater.computeRate(passwordField.getCharacters().toString())); + } + @Override public Parent getRoot() { return root; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index c401da43a..b1a823178 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -222,7 +222,7 @@ public class MainController implements ViewController { ButtonType forceShutdownButtonType = new ButtonType(localization.getString("main.gracefulShutdown.button.forceShutdown")); Alert gracefulShutdownDialog = DialogBuilderUtil.buildGracefulShutdownDialog( localization.getString("main.gracefulShutdown.dialog.title"), localization.getString("main.gracefulShutdown.dialog.header"), localization.getString("main.gracefulShutdown.dialog.content"), - forceShutdownButtonType, forceShutdownButtonType, tryAgainButtonType); + forceShutdownButtonType, ButtonType.CANCEL, forceShutdownButtonType, tryAgainButtonType); Optional choice = gracefulShutdownDialog.showAndWait(); choice.ifPresent(btnType -> { @@ -230,6 +230,8 @@ public class MainController implements ViewController { gracefulShutdown(); } else if (forceShutdownButtonType.equals(btnType)) { Platform.runLater(Platform::exit); + } else { + return; } }); } else { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 9584bfc81..cc6da7214 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -8,13 +8,6 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; -import javax.inject.Inject; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.ExecutorService; - import com.google.common.base.CharMatcher; import com.google.common.base.Strings; import javafx.application.Application; @@ -34,7 +27,10 @@ import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import javafx.scene.text.Text; +import javafx.stage.DirectoryChooser; +import javafx.stage.Stage; import javafx.util.StringConverter; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.SystemUtils; @@ -43,9 +39,7 @@ import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.common.settings.VolumeImpl; import org.cryptomator.cryptolib.api.InvalidPassphraseException; import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.frontend.webdav.ServerLifecycleException; import org.cryptomator.keychain.KeychainAccess; -import org.cryptomator.ui.model.InvalidSettingsException; import org.cryptomator.ui.controls.SecPasswordField; import org.cryptomator.ui.l10n.Localization; import org.cryptomator.ui.model.Vault; @@ -57,6 +51,19 @@ import org.fxmisc.easybind.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; +import javax.inject.Named; +import java.io.File; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.ExecutorService; + public class UnlockController implements ViewController { private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class); @@ -67,6 +74,7 @@ public class UnlockController implements ViewController { .precomputed(); private final Application app; + private final Stage mainWindow; private final Localization localization; private final WindowsDriveLetters driveLetters; private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; @@ -78,8 +86,9 @@ public class UnlockController implements ViewController { private Subscription vaultSubs = Subscription.EMPTY; @Inject - public UnlockController(Application app, Localization localization, WindowsDriveLetters driveLetters, Optional keychainAccess, Settings settings, ExecutorService executor) { + public UnlockController(Application app, @Named("mainWindow") Stage mainWindow, Localization localization, WindowsDriveLetters driveLetters, Optional keychainAccess, Settings settings, ExecutorService executor) { this.app = app; + this.mainWindow = mainWindow; this.localization = localization; this.driveLetters = driveLetters; this.keychainAccess = keychainAccess; @@ -115,13 +124,13 @@ public class UnlockController implements ViewController { private ChoiceBox winDriveLetter; @FXML - private CheckBox useOwnMountPath; + private CheckBox useCustomMountPoint; @FXML - private Label mountPathLabel; + private HBox customMountPoint; @FXML - private TextField mountPath; + private Label customMountPointLabel; @FXML private ProgressIndicator progressIndicator; @@ -141,6 +150,9 @@ public class UnlockController implements ViewController { @FXML private CheckBox unlockAfterStartup; + @FXML + private CheckBox useReadOnlyMode; + @Override public void initialize() { advancedOptions.managedProperty().bind(advancedOptions.visibleProperty()); @@ -150,28 +162,15 @@ public class UnlockController implements ViewController { savePassword.setDisable(!keychainAccess.isPresent()); unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not())); - mountPathLabel.visibleProperty().bind(useOwnMountPath.selectedProperty()); - mountPath.visibleProperty().bind(useOwnMountPath.selectedProperty()); - mountPath.managedProperty().bind(useOwnMountPath.selectedProperty()); - mountPath.textProperty().addListener(this::mountPathDidChange); + customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty()); + customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty()); + winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); - if (SystemUtils.IS_OS_WINDOWS) { - winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); - useOwnMountPath.setVisible(false); - useOwnMountPath.setManaged(false); - mountPathLabel.setManaged(false); - //dirty cheat - mountPath.setMouseTransparent(true); - } else { + if (!SystemUtils.IS_OS_WINDOWS) { winDriveLetterLabel.setVisible(false); winDriveLetterLabel.setManaged(false); winDriveLetter.setVisible(false); winDriveLetter.setManaged(false); - if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) { - useOwnMountPath.setVisible(false); - useOwnMountPath.setManaged(false); - mountPathLabel.setManaged(false); - } } } @@ -210,20 +209,18 @@ public class UnlockController implements ViewController { winDriveLetter.getItems().addAll(driveLetters.getAvailableDriveLetters()); winDriveLetter.getItems().sort(new WinDriveLetterComparator()); winDriveLetter.valueProperty().addListener(driveLetterChangeListener); + chooseSelectedDriveLetter(); } downloadsPageLink.setVisible(false); messageText.setText(null); mountName.setText(vault.getMountName()); - if (SystemUtils.IS_OS_WINDOWS) { - chooseSelectedDriveLetter(); - } savePassword.setSelected(false); // auto-fill pw from keychain: if (keychainAccess.isPresent()) { char[] storedPw = keychainAccess.get().loadPassphrase(vault.getId()); if (storedPw != null) { savePassword.setSelected(true); - passwordField.setText(new String(storedPw)); + passwordField.setPassword(storedPw); passwordField.selectRange(storedPw.length, storedPw.length); Arrays.fill(storedPw, ' '); } @@ -231,15 +228,59 @@ public class UnlockController implements ViewController { VaultSettings vaultSettings = vault.getVaultSettings(); unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get()); revealAfterMount.setSelected(vaultSettings.revealAfterMount().get()); - useOwnMountPath.setSelected(vaultSettings.usesIndividualMountPath().get()); + + // WEBDAV-dependent controls: + if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) { + useCustomMountPoint.setVisible(false); + useCustomMountPoint.setManaged(false); + } else { + useCustomMountPoint.setVisible(true); + useCustomMountPoint.setSelected(vaultSettings.usesIndividualMountPath().get()); + if (Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) { + customMountPointLabel.setText(localization.getString("unlock.label.chooseMountPath")); + } else { + customMountPointLabel.setText(displayablePath(vaultSettings.individualMountPath().getValueSafe())); + } + } + + // DOKANY-dependent controls: + if (VolumeImpl.DOKANY.equals(settings.preferredVolumeImpl().get())) { + winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); + winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); + winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); + winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); + // readonly not yet supported by dokany + useReadOnlyMode.setSelected(false); + useReadOnlyMode.setVisible(false); + useReadOnlyMode.setManaged(false); + } else { + useReadOnlyMode.setSelected(vaultSettings.usesReadOnlyMode().get()); + } + + // OS-dependent controls: + if (SystemUtils.IS_OS_WINDOWS) { + winDriveLetter.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); + winDriveLetter.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); + winDriveLetterLabel.visibleProperty().bind(useCustomMountPoint.selectedProperty().not()); + winDriveLetterLabel.managedProperty().bind(useCustomMountPoint.selectedProperty().not()); + } vaultSubs = vaultSubs.and(EasyBind.subscribe(unlockAfterStartup.selectedProperty(), vaultSettings.unlockAfterStartup()::set)); vaultSubs = vaultSubs.and(EasyBind.subscribe(revealAfterMount.selectedProperty(), vaultSettings.revealAfterMount()::set)); - vaultSubs = vaultSubs.and(EasyBind.subscribe(useOwnMountPath.selectedProperty(), vaultSettings.usesIndividualMountPath()::set)); - - - mountPath.textProperty().setValue(vaultSettings.individualMountPath().getValueSafe()); + vaultSubs = vaultSubs.and(EasyBind.subscribe(useCustomMountPoint.selectedProperty(), vaultSettings.usesIndividualMountPath()::set)); + vaultSubs = vaultSubs.and(EasyBind.subscribe(useReadOnlyMode.selectedProperty(), vaultSettings.usesReadOnlyMode()::set)); + } + private String displayablePath(String path) { + Path homeDir = Paths.get(SystemUtils.USER_HOME); + Path p = Paths.get(path); + if (p.startsWith(homeDir)) { + Path relativePath = homeDir.relativize(p); + String homePrefix = SystemUtils.IS_OS_WINDOWS ? "~\\" : "~/"; + return homePrefix + relativePath.toString(); + } else { + return p.toString(); + } } // **************************************** @@ -281,8 +322,13 @@ public class UnlockController implements ViewController { } } - private void mountPathDidChange(ObservableValue property, String oldValue, String newValue) { - vault.setIndividualMountPath(newValue); + public void didClickChooseCustomMountPoint(ActionEvent actionEvent) { + DirectoryChooser dirChooser = new DirectoryChooser(); + File file = dirChooser.showDialog(mainWindow); + if (file != null) { + customMountPointLabel.setText(displayablePath(file.toString())); + vault.setCustomMountPath(file.toString()); + } } /** @@ -295,7 +341,7 @@ public class UnlockController implements ViewController { if (letter == null) { return localization.getString("unlock.choicebox.winDriveLetter.auto"); } else { - return Character.toString(letter) + ":"; + return letter + ":"; } } @@ -389,6 +435,7 @@ public class UnlockController implements ViewController { CharSequence password = passwordField.getCharacters(); Tasks.create(() -> { + messageText.setText(localization.getString("unlock.pendingMessage.unlocking")); vault.unlock(password); if (keychainAccess.isPresent() && savePassword.isSelected()) { keychainAccess.get().storePassphrase(vault.getId(), password); @@ -397,10 +444,6 @@ public class UnlockController implements ViewController { messageText.setText(null); downloadsPageLink.setVisible(false); listener.ifPresent(lstnr -> lstnr.didUnlock(vault)); - }).onError(InvalidSettingsException.class, e -> { - messageText.setText(localization.getString("unlock.errorMessage.invalidMountPath")); - advancedOptions.setVisible(true); - mountPath.setStyle("-fx-border-color: red;"); }).onError(InvalidPassphraseException.class, e -> { messageText.setText(localization.getString("unlock.errorMessage.wrongPassword")); passwordField.selectAll(); @@ -416,10 +459,17 @@ public class UnlockController implements ViewController { } else if (e.getDetectedVersion() == Integer.MAX_VALUE) { messageText.setText(localization.getString("unlock.errorMessage.unauthenticVersionMac")); } - }).onError(ServerLifecycleException.class, e -> { - LOG.error("Unlock failed for technical reasons.", e); - messageText.setText(localization.getString("unlock.errorMessage.unlockFailed")); - }).onError(Exception.class, e -> { + }).onError(NotDirectoryException.class, e -> { + LOG.error("Unlock failed. Mount point not a directory: {}", e.getMessage()); + advancedOptions.setVisible(true); + messageText.setText(null); + showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNonExisting"); + }).onError(DirectoryNotEmptyException.class, e -> { + LOG.error("Unlock failed. Mount point not empty: {}", e.getMessage()); + advancedOptions.setVisible(true); + messageText.setText(null); + showUnlockFailedErrorDialog("unlock.failedDialog.content.mountPathNotEmpty"); + }).onError(Exception.class, e -> { // including RuntimeExceptions LOG.error("Unlock failed for technical reasons.", e); messageText.setText(localization.getString("unlock.errorMessage.unlockFailed")); }).andFinally(() -> { @@ -428,12 +478,17 @@ public class UnlockController implements ViewController { } advancedOptions.setDisable(false); progressIndicator.setVisible(false); - if (advancedOptions.isVisible()) { //dirty programming, but otherwise the focus is wrong - mountPath.requestFocus(); - } }).runOnce(executor); } + private void showUnlockFailedErrorDialog(String localizableContentKey) { + String title = localization.getString("unlock.failedDialog.title"); + String header = localization.getString("unlock.failedDialog.header"); + String content = localization.getString(localizableContentKey); + Alert alert = DialogBuilderUtil.buildErrorDialog(title, header, content, ButtonType.OK); + alert.show(); + } + /* callback */ public void setListener(UnlockListener listener) { @@ -449,9 +504,9 @@ public class UnlockController implements ViewController { /* state */ public enum State { - UNLOCKING(null), - INITIALIZED("unlock.successLabel.vaultCreated"), - PASSWORD_CHANGED("unlock.successLabel.passwordChanged"), + UNLOCKING(null), // + INITIALIZED("unlock.successLabel.vaultCreated"), // + PASSWORD_CHANGED("unlock.successLabel.passwordChanged"), // UPGRADED("unlock.successLabel.upgraded"); private Optional successMessage; diff --git a/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java b/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java index 19d01cfbd..c2774c345 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java @@ -2,25 +2,35 @@ * Copyright (c) 2014, 2017 Sebastian Stenzel * All rights reserved. * This program and the accompanying materials are made available under the terms of the accompanying LICENSE file. - * + * * Contributors: * Sebastian Stenzel - initial API and implementation ******************************************************************************/ package org.cryptomator.ui.controls; -import java.util.Arrays; - +import com.google.common.base.Strings; import javafx.scene.control.PasswordField; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; +import java.nio.CharBuffer; +import java.util.Arrays; + /** - * Compromise in security. While the text can be swiped, any access to the {@link #getText()} method will create a copy of the String in the heap. + * Patched PasswordField that doesn't create String copies of the password in memory. Instead the password is stored in a char[] that can be swiped. + * + * @implNote Since {@link #setText(String)} is final, we can not override its behaviour. For that reason you should not use the {@link #textProperty()} for anything else than display purposes. */ public class SecPasswordField extends PasswordField { private static final char SWIPE_CHAR = ' '; + private static final int INITIAL_BUFFER_SIZE = 50; + private static final int GROW_BUFFER_SIZE = 50; + private static final String PLACEHOLDER = "*"; + + private char[] content = new char[INITIAL_BUFFER_SIZE]; + private int length = 0; public SecPasswordField() { this.onDragOverProperty().set(this::handleDragOver); @@ -43,26 +53,54 @@ public class SecPasswordField extends PasswordField { event.consume(); } + @Override + public void replaceText(int start, int end, String text) { + int removed = end - start; + int added = text.length(); + this.length += added - removed; + growContentIfNeeded(); + text.getChars(0, text.length(), content, start); + + String placeholderString = Strings.repeat(PLACEHOLDER, text.length()); + super.replaceText(start, end, placeholderString); + } + + private void growContentIfNeeded() { + if (this.length > content.length) { + char[] newContent = new char[content.length + GROW_BUFFER_SIZE]; + System.arraycopy(content, 0, newContent, 0, content.length); + swipe(); + this.content = newContent; + } + } + /** - * {@link #getContent()} uses a StringBuilder, which in turn is backed by a char[]. - * The delete operation of AbstractStringBuilder closes the gap, that forms by deleting chars, by moving up the following chars. - *
- * Imagine the following example with pass being the password, x being the swipe char and ' being the offset of the char array: - *
    - *
  1. Append filling chars to the end of the password: passxxxx'
  2. - *
  3. Delete first 4 chars. Internal implementation will then copy subsequent chars to the position, where the deletion occured: xxxx'xxxx
  4. - *
  5. Delete first 4 chars again, as we appended 4 chars in step 1: 'xxxxxx
  6. - *
+ * Creates a CharSequence by wrapping the password characters. + * + * @return A character sequence backed by the SecPasswordField's buffer (not a copy). + * @implNote The CharSequence will not copy the backing char[]. + * Therefore any mutation to the SecPasswordField's content will mutate or eventually swipe the returned CharSequence. + * @see #swipe() + */ + @Override + public CharSequence getCharacters() { + return CharBuffer.wrap(content, 0, length); + } + + public void setPassword(char[] password) { + swipe(); + content = Arrays.copyOf(password, password.length); + length = password.length; + + String placeholderString = Strings.repeat(PLACEHOLDER, password.length); + setText(placeholderString); + } + + /** + * Destroys the stored password by overriding each character with a different character. */ public void swipe() { - final int pwLength = this.getContent().length(); - final char[] fillingChars = new char[pwLength]; - Arrays.fill(fillingChars, SWIPE_CHAR); - this.getContent().insert(pwLength, new String(fillingChars), false); - this.getContent().delete(0, pwLength, true); - this.getContent().delete(0, pwLength, true); - // previous text has now been overwritten. but we still need to update the text to trigger some property bindings: - this.clear(); + Arrays.fill(content, SWIPE_CHAR); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java b/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java index f986b6e3f..b9200a981 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/DokanyVolume.java @@ -1,19 +1,29 @@ package org.cryptomator.ui.model; -import javax.inject.Inject; -import java.nio.file.Paths; -import java.util.Set; -import java.util.concurrent.ExecutorService; - -import com.google.common.collect.Sets; +import com.google.common.base.Strings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.frontend.dokany.Mount; import org.cryptomator.frontend.dokany.MountFactory; import org.cryptomator.frontend.dokany.MountFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; +import java.util.concurrent.ExecutorService; public class DokanyVolume implements Volume { + private static final Logger LOG = LoggerFactory.getLogger(DokanyVolume.class); + private static final String FS_TYPE_NAME = "Cryptomator File System"; private final VaultSettings vaultSettings; @@ -28,34 +38,51 @@ public class DokanyVolume implements Volume { this.windowsDriveLetters = windowsDriveLetters; } - @Override public boolean isSupported() { return DokanyVolume.isSupportedStatic(); } - //TODO: Drive letter 'A' as mount point is invalid in dokany. maybe we should do already here something against it @Override - public void mount(CryptoFileSystem fs) throws VolumeException { - char driveLetter; - if (!vaultSettings.winDriveLetter().getValueSafe().equals("")) { - driveLetter = vaultSettings.winDriveLetter().get().charAt(0); + public void mount(CryptoFileSystem fs) throws VolumeException, IOException { + Path mountPath = getMountPoint(); + String mountName = vaultSettings.mountName().get(); + try { + this.mount = mountFactory.mount(fs.getPath("/"), mountPath, mountName, FS_TYPE_NAME); + } catch (MountFailedException e) { + if (vaultSettings.getIndividualMountPath().isPresent()) { + LOG.warn("Failed to mount vault into {}. Is this directory currently accessed by another process (e.g. Windows Explorer)?", mountPath); + } + throw new VolumeException("Unable to mount Filesystem", e); + } + } + + private Path getMountPoint() throws VolumeException, IOException { + Optional optionalCustomMountPoint = vaultSettings.getIndividualMountPath(); + if (optionalCustomMountPoint.isPresent()) { + Path customMountPoint = Paths.get(optionalCustomMountPoint.get()); + checkProvidedMountPoint(customMountPoint); + return customMountPoint; + } else if (!Strings.isNullOrEmpty(vaultSettings.winDriveLetter().get())) { + return Paths.get(vaultSettings.winDriveLetter().get().charAt(0) + ":\\"); } else { //auto assign drive letter if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) { - //this is a temporary fix for 'A' being an invalid drive letter - Set availableLettersWithoutA = Sets.difference(windowsDriveLetters.getAvailableDriveLetters(), Set.of('A')); - driveLetter = availableLettersWithoutA.iterator().next(); -// driveLetter = windowsDriveLetters.getAvailableDriveLetters().iterator().next(); + return Paths.get(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\"); } else { throw new VolumeException("No free drive letter available."); } } - String mountName = vaultSettings.mountName().get(); - try { - this.mount = mountFactory.mount(fs.getPath("/"), Paths.get(driveLetter + ":\\") , mountName, FS_TYPE_NAME); - } catch (MountFailedException e) { - throw new VolumeException("Unable to mount Filesystem", e); + } + + private void checkProvidedMountPoint(Path mountPoint) throws IOException { + if (!Files.isDirectory(mountPoint)) { + throw new NotDirectoryException(mountPoint.toString()); + } + try (DirectoryStream ds = Files.newDirectoryStream(mountPoint)) { + if (ds.iterator().hasNext()) { + throw new DirectoryNotEmptyException(mountPoint.toString()); + } } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java b/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java index ec63da443..b1d07894b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/FuseVolume.java @@ -1,14 +1,5 @@ package org.cryptomator.ui.model; -import java.io.IOException; -import java.nio.file.DirectoryNotEmptyException; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import javax.inject.Inject; - import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystem; @@ -20,65 +11,86 @@ import org.cryptomator.frontend.fuse.mount.Mount; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.DirectoryNotEmptyException; +import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Optional; + public class FuseVolume implements Volume { private static final Logger LOG = LoggerFactory.getLogger(FuseVolume.class); - /** - * TODO: dont use fixed Strings and rather set them in some system environment variables in the cryptomator installer and load those! - */ + // TODO: dont use fixed Strings and rather set them in some system environment variables in the cryptomator installer and load those! private static final String DEFAULT_MOUNTROOTPATH_MAC = System.getProperty("user.home") + "/Library/Application Support/Cryptomator"; private static final String DEFAULT_MOUNTROOTPATH_LINUX = System.getProperty("user.home") + "/.Cryptomator"; + private static final int MAX_TMPMOUNTPOINT_CREATION_RETRIES = 10; private final VaultSettings vaultSettings; private Mount fuseMnt; - private Path mountPath; - private boolean extraDirCreated; + private Path mountPoint; + private boolean createdTemporaryMountPoint; @Inject public FuseVolume(VaultSettings vaultSettings) { this.vaultSettings = vaultSettings; - this.extraDirCreated = false; } @Override public void mount(CryptoFileSystem fs) throws IOException, FuseNotSupportedException, VolumeException { - String mountPath; - if (vaultSettings.usesIndividualMountPath().get()) { - //specific path given - mountPath = vaultSettings.individualMountPath().get(); + Optional optionalCustomMountPoint = vaultSettings.getIndividualMountPath(); + if (optionalCustomMountPoint.isPresent()) { + Path customMountPoint = Paths.get(optionalCustomMountPoint.get()); + checkProvidedMountPoint(customMountPoint); + this.mountPoint = customMountPoint; + this.createdTemporaryMountPoint = false; + LOG.debug("Successfully checked custom mount point: {}", mountPoint); } else { - //choose default path & create extra directory - mountPath = createDirIfNotExist(SystemUtils.IS_OS_MAC ? DEFAULT_MOUNTROOTPATH_MAC : DEFAULT_MOUNTROOTPATH_LINUX, vaultSettings.mountName().get()); - extraDirCreated = true; + this.mountPoint = createTemporaryMountPoint(); + this.createdTemporaryMountPoint = true; + LOG.debug("Successfully created mount point: {}", mountPoint); } - this.mountPath = Paths.get(mountPath).toAbsolutePath(); mount(fs.getPath("/")); } - private String createDirIfNotExist(String prefix, String dirName) throws IOException { - Path p = Paths.get(prefix, dirName + vaultSettings.getId()); - if (Files.isDirectory(p)) { - try (DirectoryStream emptyCheck = Files.newDirectoryStream(p)) { - if (emptyCheck.iterator().hasNext()) { - throw new DirectoryNotEmptyException("Mount point is not empty."); - } else { - LOG.info("Directory already exists and is empty. Using it as mount point."); - return p.toString(); - } - } - } else { - Files.createDirectory(p); - return p.toString(); + private void checkProvidedMountPoint(Path mountPoint) throws IOException { + if (!Files.isDirectory(mountPoint)) { + throw new NotDirectoryException(mountPoint.toString()); } + try (DirectoryStream ds = Files.newDirectoryStream(mountPoint)) { + if (ds.iterator().hasNext()) { + throw new DirectoryNotEmptyException(mountPoint.toString()); + } + } + } + + private Path createTemporaryMountPoint() throws IOException { + Path parent = Paths.get(SystemUtils.IS_OS_MAC ? DEFAULT_MOUNTROOTPATH_MAC : DEFAULT_MOUNTROOTPATH_LINUX); + String basename = vaultSettings.getId(); + for (int i = 0; i < MAX_TMPMOUNTPOINT_CREATION_RETRIES; i++) { + try { + Path mountPath = parent.resolve(basename + "_" + i); + Files.createDirectory(mountPath); + return mountPath; + } catch (FileAlreadyExistsException e) { + continue; + } + } + LOG.error("Failed to create mount path at {}/{}_x. Giving up after {} attempts.", parent, basename, MAX_TMPMOUNTPOINT_CREATION_RETRIES); + throw new FileAlreadyExistsException(parent.toString() + "/" + basename); } private void mount(Path root) throws VolumeException { try { - EnvironmentVariables envVars = EnvironmentVariables.create() - .withMountName(vaultSettings.mountName().getValue()) - .withMountPath(mountPath) + EnvironmentVariables envVars = EnvironmentVariables.create() // + .withMountName(vaultSettings.mountName().getValue()) // + .withMountPath(mountPoint) // .build(); this.fuseMnt = FuseMountFactory.getMounter().mount(root, envVars); } catch (CommandFailedException e) { @@ -91,27 +103,45 @@ public class FuseVolume implements Volume { try { fuseMnt.revealInFileManager(); } catch (CommandFailedException e) { - LOG.info("Revealing the vault in file manger failed: " + e.getMessage()); + LOG.debug("Revealing the vault in file manger failed: " + e.getMessage()); throw new VolumeException(e); } } @Override - public synchronized void unmount() throws VolumeException { + public boolean supportsForcedUnmount() { + return true; + } + + @Override + public synchronized void unmountForced() throws VolumeException { try { + fuseMnt.unmountForced(); fuseMnt.close(); } catch (CommandFailedException e) { throw new VolumeException(e); } - cleanup(); + deleteTemporaryMountPoint(); } - private void cleanup() { - if (extraDirCreated) { + @Override + public synchronized void unmount() throws VolumeException { + try { + fuseMnt.unmount(); + fuseMnt.close(); + } catch (CommandFailedException e) { + throw new VolumeException(e); + } + deleteTemporaryMountPoint(); + } + + private void deleteTemporaryMountPoint() { + if (createdTemporaryMountPoint) { try { - Files.delete(mountPath); + Files.delete(mountPoint); + LOG.debug("Successfully deleted mount point: {}", mountPoint); } catch (IOException e) { - LOG.warn("Could not delete mounting directory:" + e.getMessage()); + LOG.warn("Could not delete mount point: {}", e.getMessage()); } } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/InvalidSettingsException.java b/main/ui/src/main/java/org/cryptomator/ui/model/InvalidSettingsException.java deleted file mode 100644 index ed9b39040..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/model/InvalidSettingsException.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.cryptomator.ui.model; - -public class InvalidSettingsException extends RuntimeException { - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java index cf0e6d7cc..acaa33fdf 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/Vault.java @@ -8,19 +8,7 @@ *******************************************************************************/ package org.cryptomator.ui.model; -import java.io.IOException; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.Files; -import java.nio.file.NoSuchFileException; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Predicate; - -import javax.inject.Inject; -import javax.inject.Provider; - +import com.google.common.base.Strings; import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Binding; @@ -34,6 +22,7 @@ import org.cryptomator.common.settings.Settings; import org.cryptomator.common.settings.VaultSettings; import org.cryptomator.cryptofs.CryptoFileSystem; import org.cryptomator.cryptofs.CryptoFileSystemProperties; +import org.cryptomator.cryptofs.CryptoFileSystemProperties.FileSystemFlags; import org.cryptomator.cryptofs.CryptoFileSystemProvider; import org.cryptomator.cryptolib.api.CryptoException; import org.cryptomator.cryptolib.api.InvalidPassphraseException; @@ -42,6 +31,21 @@ import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.inject.Inject; +import javax.inject.Provider; +import java.io.IOException; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.NotDirectoryException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Predicate; + @PerVault public class Vault { @@ -78,9 +82,13 @@ public class Vault { } private CryptoFileSystem unlockCryptoFileSystem(CharSequence passphrase) throws NoSuchFileException, IOException, InvalidPassphraseException, CryptoException { + List flags = new ArrayList<>(); + if (vaultSettings.usesReadOnlyMode().get()) { + flags.add(FileSystemFlags.READONLY); + } CryptoFileSystemProperties fsProps = CryptoFileSystemProperties.cryptoFileSystemProperties() // .withPassphrase(passphrase) // - .withFlags() // + .withFlags(flags) // .withMasterkeyFilename(MASTERKEY_FILENAME) // .build(); return CryptoFileSystemProvider.newFileSystem(getPath(), fsProps); @@ -98,11 +106,11 @@ public class Vault { CryptoFileSystemProvider.changePassphrase(getPath(), MASTERKEY_FILENAME, oldPassphrase, newPassphrase); } - public synchronized void unlock(CharSequence passphrase) throws InvalidSettingsException, CryptoException, IOException, Volume.VolumeException { + public synchronized void unlock(CharSequence passphrase) throws CryptoException, IOException, Volume.VolumeException { Platform.runLater(() -> state.set(State.PROCESSING)); try { - if (vaultSettings.usesIndividualMountPath().and(vaultSettings.individualMountPath().isEmpty()).get()) { - throw new InvalidSettingsException(); + if (vaultSettings.usesIndividualMountPath().get() && Strings.isNullOrEmpty(vaultSettings.individualMountPath().get())) { + throw new NotDirectoryException(""); } CryptoFileSystem fs = getCryptoFileSystem(passphrase); volume = volumeProvider.get(); @@ -241,11 +249,11 @@ public class Vault { return vaultSettings.mountName().get(); } - public String getIndividualMountPath() { + public String getCustomMountPath() { return vaultSettings.individualMountPath().getValueSafe(); } - public void setIndividualMountPath(String mountPath) { + public void setCustomMountPath(String mountPath) { vaultSettings.individualMountPath().set(mountPath); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java b/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java index 41b6799ba..4c2c3111e 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java +++ b/main/ui/src/main/java/org/cryptomator/ui/model/WindowsDriveLetters.java @@ -22,11 +22,11 @@ import org.apache.commons.lang3.SystemUtils; @Singleton public final class WindowsDriveLetters { - private static final Set A_TO_Z; + private static final Set D_TO_Z; static { - try (IntStream stream = IntStream.rangeClosed('A', 'Z')) { - A_TO_Z = stream.mapToObj(i -> (char) i).collect(Collectors.toSet()); + try (IntStream stream = IntStream.rangeClosed('D', 'Z')) { + D_TO_Z = stream.mapToObj(i -> (char) i).collect(Collectors.toSet()); } } @@ -45,7 +45,7 @@ public final class WindowsDriveLetters { public Set getAvailableDriveLetters() { Set occupiedDriveLetters = getOccupiedDriveLetters(); Predicate isOccupiedDriveLetter = occupiedDriveLetters::contains; - return A_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet()); + return D_TO_Z.stream().filter(isOccupiedDriveLetter.negate()).collect(Collectors.toSet()); } } diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml index 5675556a1..6439514ad 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock.fxml @@ -7,29 +7,25 @@ Contributors: Sebastian Stenzel - initial API and implementation --> - - - + + + + + + - - - - - - - - - - - - + + + + + - + @@ -45,7 +41,7 @@