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/main/ant-kit/pom.xml b/main/ant-kit/pom.xml index 7b9301ebf..fc0c72190 100644 --- a/main/ant-kit/pom.xml +++ b/main/ant-kit/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.4.2 + 1.4.3 ant-kit pom diff --git a/main/commons/pom.xml b/main/commons/pom.xml index 002de7da5..64477feee 100644 --- a/main/commons/pom.xml +++ b/main/commons/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.4.2 + 1.4.3 commons Cryptomator Commons 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 5b33a9765..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,6 +5,7 @@ *******************************************************************************/ package org.cryptomator.common.settings; +import com.google.common.base.Strings; import javafx.beans.Observable; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -20,6 +21,7 @@ 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; /** @@ -31,6 +33,7 @@ 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<>(); @@ -40,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); @@ -48,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) { @@ -131,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 866b78bc9..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 @@ -27,6 +27,7 @@ class VaultSettingsJsonAdapter { out.name("revealAfterMount").value(value.revealAfterMount().get()); out.name("usesIndividualMountPath").value(value.usesIndividualMountPath().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(); } @@ -39,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()) { @@ -68,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(); @@ -83,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/keychain/pom.xml b/main/keychain/pom.xml index 5e5e28e22..cf9d7942f 100644 --- a/main/keychain/pom.xml +++ b/main/keychain/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.4.2 + 1.4.3 keychain System Keychain Access diff --git a/main/launcher/pom.xml b/main/launcher/pom.xml index e98029baa..299cfcf5e 100644 --- a/main/launcher/pom.xml +++ b/main/launcher/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.4.2 + 1.4.3 launcher Cryptomator Launcher diff --git a/main/pom.xml b/main/pom.xml index 1f792c630..ca956f46d 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.cryptomator main - 1.4.2 + 1.4.3 pom Cryptomator @@ -25,11 +25,11 @@ 1.2.1 - 1.6.2 + 1.7.0 2.0.0 - 1.0.3 - 1.1.1 - 1.0.5 + 1.1.0 + 1.1.3 + 1.0.6 2.6 3.8.1 diff --git a/main/uber-jar/pom.xml b/main/uber-jar/pom.xml index 8b33a18f4..59d5c5f0c 100644 --- a/main/uber-jar/pom.xml +++ b/main/uber-jar/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.4.2 + 1.4.3 uber-jar Single über jar with all dependencies diff --git a/main/ui/pom.xml b/main/ui/pom.xml index b7a12ea50..21fc93ef4 100644 --- a/main/ui/pom.xml +++ b/main/ui/pom.xml @@ -4,7 +4,7 @@ org.cryptomator main - 1.4.2 + 1.4.3 ui Cryptomator GUI 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/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index 6c102d6cc..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 @@ -27,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; @@ -36,11 +39,9 @@ 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.controls.SecPasswordField; import org.cryptomator.ui.l10n.Localization; -import org.cryptomator.ui.model.InvalidSettingsException; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.WindowsDriveLetters; import org.cryptomator.ui.util.DialogBuilderUtil; @@ -51,6 +52,12 @@ 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; @@ -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 useCustomMountPath; + private CheckBox useCustomMountPoint; @FXML - private Label customMountPathLabel; + private HBox customMountPoint; @FXML - private TextField customMountPathField; + 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,11 +162,8 @@ public class UnlockController implements ViewController { savePassword.setDisable(!keychainAccess.isPresent()); unlockAfterStartup.disableProperty().bind(savePassword.disabledProperty().or(savePassword.selectedProperty().not())); - customMountPathLabel.visibleProperty().bind(useCustomMountPath.selectedProperty()); - customMountPathLabel.managedProperty().bind(useCustomMountPath.selectedProperty()); - customMountPathField.visibleProperty().bind(useCustomMountPath.selectedProperty()); - customMountPathField.managedProperty().bind(useCustomMountPath.selectedProperty()); - customMountPathField.textProperty().addListener(this::mountPathDidChange); + customMountPoint.visibleProperty().bind(useCustomMountPoint.selectedProperty()); + customMountPoint.managedProperty().bind(useCustomMountPoint.selectedProperty()); winDriveLetter.setConverter(new WinDriveLetterLabelConverter()); if (!SystemUtils.IS_OS_WINDOWS) { @@ -163,20 +172,6 @@ public class UnlockController implements ViewController { winDriveLetter.setVisible(false); winDriveLetter.setManaged(false); } - - if (VolumeImpl.WEBDAV.equals(settings.preferredVolumeImpl().get())) { - useCustomMountPath.setVisible(false); - useCustomMountPath.setManaged(false); - customMountPathField.setMouseTransparent(true); - } else { - useCustomMountPath.setVisible(true); - if (SystemUtils.IS_OS_WINDOWS) { - winDriveLetter.visibleProperty().bind(useCustomMountPath.selectedProperty().not()); - winDriveLetter.managedProperty().bind(useCustomMountPath.selectedProperty().not()); - winDriveLetterLabel.visibleProperty().bind(useCustomMountPath.selectedProperty().not()); - winDriveLetterLabel.managedProperty().bind(useCustomMountPath.selectedProperty().not()); - } - } } @@ -225,7 +220,7 @@ public class UnlockController implements ViewController { 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, ' '); } @@ -234,15 +229,58 @@ public class UnlockController implements ViewController { unlockAfterStartup.setSelected(savePassword.isSelected() && vaultSettings.unlockAfterStartup().get()); revealAfterMount.setSelected(vaultSettings.revealAfterMount().get()); - if (!settings.preferredVolumeImpl().get().equals(VolumeImpl.WEBDAV)) { - useCustomMountPath.setSelected(vaultSettings.usesIndividualMountPath().get()); - customMountPathField.textProperty().setValue(vaultSettings.individualMountPath().getValueSafe()); + // 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(useCustomMountPath.selectedProperty(), vaultSettings.usesIndividualMountPath()::set)); + 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(); + } } // **************************************** @@ -284,8 +322,13 @@ public class UnlockController implements ViewController { } } - private void mountPathDidChange(ObservableValue property, String oldValue, String newValue) { - vault.setCustomMountPath(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()); + } } /** @@ -298,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 + ":"; } } @@ -401,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); - customMountPathField.setStyle("-fx-border-color: red;"); }).onError(InvalidPassphraseException.class, e -> { messageText.setText(localization.getString("unlock.errorMessage.wrongPassword")); passwordField.selectAll(); @@ -420,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(() -> { @@ -432,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 - customMountPathField.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) { @@ -453,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 26a4b7993..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 @@ -6,14 +6,24 @@ 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,37 +38,52 @@ public class DokanyVolume implements Volume { this.windowsDriveLetters = windowsDriveLetters; } - @Override public boolean isSupported() { return DokanyVolume.isSupportedStatic(); } @Override - public void mount(CryptoFileSystem fs) throws VolumeException { - Path mountPath = Paths.get(getMountPathString()); + 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 String getMountPathString() throws VolumeException { - if (vaultSettings.usesIndividualMountPath().get()) { - return vaultSettings.individualMountPath().get(); + 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 vaultSettings.winDriveLetter().get().charAt(0) + ":\\"; + return Paths.get(vaultSettings.winDriveLetter().get().charAt(0) + ":\\"); } else { //auto assign drive letter if (!windowsDriveLetters.getAvailableDriveLetters().isEmpty()) { - return windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\"; + return Paths.get(windowsDriveLetters.getAvailableDriveLetters().iterator().next() + ":\\"); } else { throw new VolumeException("No free drive letter available."); } } + } + 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()); + } + } } @Override 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 ec1b889b3..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,6 +8,7 @@ *******************************************************************************/ package org.cryptomator.ui.model; +import com.google.common.base.Strings; import javafx.application.Platform; import javafx.beans.Observable; import javafx.beans.binding.Binding; @@ -21,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; @@ -35,8 +37,11 @@ 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; @@ -77,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); @@ -97,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(); diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml index 76d08a98a..6439514ad 100644 --- a/main/ui/src/main/resources/fxml/unlock.fxml +++ b/main/ui/src/main/resources/fxml/unlock.fxml @@ -7,23 +7,20 @@ Contributors: Sebastian Stenzel - initial API and implementation --> - + + + + + + - - - - - - - - - - - - - + + + + + @@ -83,16 +80,23 @@ - + - -