From 7edd303f2edef69358e46e8572d0dfa3d10f782a Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sun, 22 Feb 2015 16:10:17 +0100 Subject: [PATCH] Added change password functionality (fixes #20) Moved controllers to new package Small UI improvements --- .../org/cryptomator/ui/MainApplication.java | 1 + .../controllers/ChangePasswordController.java | 175 ++++++++++++++++++ .../InitializeController.java | 16 +- .../ui/{ => controllers}/MainController.java | 44 +++-- .../{ => controllers}/UnlockController.java | 21 ++- .../{ => controllers}/UnlockedController.java | 2 +- .../cryptomator/ui/util/DeferredCloser.java | 2 +- main/ui/src/main/resources/css/mac_theme.css | 3 +- .../main/resources/fxml/change_password.fxml | 53 ++++++ .../src/main/resources/fxml/initialize.fxml | 2 +- main/ui/src/main/resources/fxml/main.fxml | 5 +- main/ui/src/main/resources/fxml/unlock.fxml | 4 +- main/ui/src/main/resources/fxml/unlocked.fxml | 2 +- .../main/resources/localization.properties | 14 +- 14 files changed, 302 insertions(+), 42 deletions(-) create mode 100644 main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java rename main/ui/src/main/java/org/cryptomator/ui/{ => controllers}/InitializeController.java (91%) rename main/ui/src/main/java/org/cryptomator/ui/{ => controllers}/MainController.java (86%) rename main/ui/src/main/java/org/cryptomator/ui/{ => controllers}/UnlockController.java (89%) rename main/ui/src/main/java/org/cryptomator/ui/{ => controllers}/UnlockedController.java (99%) create mode 100644 main/ui/src/main/resources/fxml/change_password.fxml diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java index 21a751db1..2df84fc49 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java +++ b/main/ui/src/main/java/org/cryptomator/ui/MainApplication.java @@ -24,6 +24,7 @@ import javafx.stage.Stage; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.ui.MainModule.ControllerFactory; +import org.cryptomator.ui.controllers.MainController; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.util.ActiveWindowStyleSupport; import org.cryptomator.ui.util.DeferredCloser; 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 new file mode 100644 index 000000000..91fa9df66 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java @@ -0,0 +1,175 @@ +package org.cryptomator.ui.controllers; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ResourceBundle; + +import javafx.application.Platform; +import javafx.beans.value.ObservableValue; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.Initializable; +import javafx.scene.control.Button; +import javafx.scene.control.Label; + +import org.cryptomator.crypto.exceptions.DecryptFailedException; +import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; +import org.cryptomator.crypto.exceptions.WrongPasswordException; +import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.model.Vault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.inject.Inject; + +public class ChangePasswordController implements Initializable { + + private static final Logger LOG = LoggerFactory.getLogger(ChangePasswordController.class); + + private ResourceBundle rb; + private ChangePasswordListener listener; + private Vault vault; + + @FXML + private SecPasswordField oldPasswordField; + + @FXML + private SecPasswordField newPasswordField; + + @FXML + private SecPasswordField retypePasswordField; + + @FXML + private Button changePasswordButton; + + @FXML + private Label messageLabel; + + @Inject + public ChangePasswordController() { + super(); + } + + @Override + public void initialize(URL location, ResourceBundle rb) { + this.rb = rb; + + oldPasswordField.textProperty().addListener(this::passwordFieldsDidChange); + newPasswordField.textProperty().addListener(this::passwordFieldsDidChange); + retypePasswordField.textProperty().addListener(this::passwordFieldsDidChange); + } + + // **************************************** + // Password fields + // **************************************** + + private void passwordFieldsDidChange(ObservableValue property, String oldValue, String newValue) { + boolean oldPasswordIsEmpty = oldPasswordField.getText().isEmpty(); + boolean newPasswordIsEmpty = newPasswordField.getText().isEmpty(); + boolean passwordsAreEqual = newPasswordField.getText().equals(retypePasswordField.getText()); + changePasswordButton.setDisable(oldPasswordIsEmpty || newPasswordIsEmpty || !passwordsAreEqual); + } + + // **************************************** + // Change password button + // **************************************** + + @FXML + private void didClickChangePasswordButton(ActionEvent event) { + final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); + final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE); + + // decrypt with old password: + final CharSequence oldPassword = oldPasswordField.getCharacters(); + try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) { + vault.getCryptor().decryptMasterKey(masterKeyInputStream, oldPassword); + Files.copy(masterKeyPath, masterKeyBackupPath, StandardCopyOption.REPLACE_EXISTING); + } catch (DecryptFailedException | IOException ex) { + messageLabel.setText(rb.getString("changePassword.errorMessage.decryptionFailed")); + LOG.error("Decryption failed for technical reasons.", ex); + newPasswordField.swipe(); + retypePasswordField.swipe(); + return; + } catch (WrongPasswordException e) { + messageLabel.setText(rb.getString("changePassword.errorMessage.wrongPassword")); + newPasswordField.swipe(); + retypePasswordField.swipe(); + Platform.runLater(oldPasswordField::requestFocus); + return; + } catch (UnsupportedKeyLengthException ex) { + messageLabel.setText(rb.getString("changePassword.errorMessage.unsupportedKeyLengthInstallJCE")); + LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); + newPasswordField.swipe(); + retypePasswordField.swipe(); + return; + } finally { + oldPasswordField.swipe(); + } + + // when we reach this line, decryption was successful. + + // encrypt with new password: + final CharSequence newPassword = newPasswordField.getCharacters(); + try (final OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.SYNC)) { + vault.getCryptor().encryptMasterKey(masterKeyOutputStream, newPassword); + messageLabel.setText(rb.getString("changePassword.infoMessage.success")); + Platform.runLater(this::didChangePassword); + // At this point the backup is still using the old password. + // It will be changed as soon as the user unlocks the vault the next time. + // This way he can still restore the old password, if he doesn't remember the new one. + } catch (IOException ex) { + LOG.error("Re-encryption failed for technical reasons. Restoring Backup.", ex); + this.restoreBackupQuietly(); + } finally { + newPasswordField.swipe(); + retypePasswordField.swipe(); + } + } + + private void restoreBackupQuietly() { + final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); + final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE); + try { + Files.copy(masterKeyBackupPath, masterKeyPath, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException ex) { + LOG.error("Restoring Backup failed.", ex); + } + } + + private void didChangePassword() { + if (listener != null) { + listener.didChangePassword(this); + } + } + + /* Getter/Setter */ + + public Vault getVault() { + return vault; + } + + public void setVault(Vault vault) { + this.vault = vault; + } + + public ChangePasswordListener getListener() { + return listener; + } + + public void setListener(ChangePasswordListener listener) { + this.listener = listener; + } + + /* callback */ + + interface ChangePasswordListener { + void didChangePassword(ChangePasswordController ctrl); + } + +} diff --git a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java similarity index 91% rename from main/ui/src/main/java/org/cryptomator/ui/InitializeController.java rename to main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java index e15421cfa..0d392b4db 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/InitializeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation ******************************************************************************/ -package org.cryptomator.ui; +package org.cryptomator.ui.controllers; import java.io.IOException; import java.io.OutputStream; @@ -35,7 +35,7 @@ public class InitializeController implements Initializable { private static final Logger LOG = LoggerFactory.getLogger(InitializeController.class); private ResourceBundle localization; - private Vault directory; + private Vault vault; private InitializationListener listener; @FXML @@ -74,10 +74,10 @@ public class InitializeController implements Initializable { @FXML protected void initializeVault(ActionEvent event) { setControlsDisabled(true); - final Path masterKeyPath = directory.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); + final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); final CharSequence password = passwordField.getCharacters(); try (OutputStream masterKeyOutputStream = Files.newOutputStream(masterKeyPath, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW)) { - directory.getCryptor().encryptMasterKey(masterKeyOutputStream, password); + vault.getCryptor().encryptMasterKey(masterKeyOutputStream, password); if (listener != null) { listener.didInitialize(this); } @@ -102,12 +102,12 @@ public class InitializeController implements Initializable { /* Getter/Setter */ - public Vault getDirectory() { - return directory; + public Vault getVault() { + return vault; } - public void setDirectory(Vault directory) { - this.directory = directory; + public void setVault(Vault vault) { + this.vault = vault; } public InitializationListener getListener() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/MainController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java similarity index 86% rename from main/ui/src/main/java/org/cryptomator/ui/MainController.java rename to main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java index 99b0dc643..441d7fc1a 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/MainController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/MainController.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation ******************************************************************************/ -package org.cryptomator.ui; +package org.cryptomator.ui.controllers; import java.io.File; import java.io.IOException; @@ -38,10 +38,11 @@ import javafx.stage.FileChooser; import javafx.stage.Stage; import javafx.stage.WindowEvent; -import org.cryptomator.ui.InitializeController.InitializationListener; import org.cryptomator.ui.MainModule.ControllerFactory; -import org.cryptomator.ui.UnlockController.UnlockListener; -import org.cryptomator.ui.UnlockedController.LockListener; +import org.cryptomator.ui.controllers.ChangePasswordController.ChangePasswordListener; +import org.cryptomator.ui.controllers.InitializeController.InitializationListener; +import org.cryptomator.ui.controllers.UnlockController.UnlockListener; +import org.cryptomator.ui.controllers.UnlockedController.LockListener; import org.cryptomator.ui.controls.DirectoryListCell; import org.cryptomator.ui.model.Vault; import org.cryptomator.ui.model.VaultFactory; @@ -51,7 +52,7 @@ import org.slf4j.LoggerFactory; import com.google.inject.Inject; -public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener { +public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener, ChangePasswordListener { private static final Logger LOG = LoggerFactory.getLogger(MainController.class); @@ -152,7 +153,7 @@ public class MainController implements Initializable, InitializationListener, Un * * @param path non-null, writable, existing directory */ - void addVault(final Path path, boolean select) { + public void addVault(final Path path, boolean select) { if (path == null || !Files.isWritable(path)) { return; } @@ -199,11 +200,17 @@ public class MainController implements Initializable, InitializationListener, Un @FXML private void didClickRemoveSelectedEntry(ActionEvent e) { - final Vault selectedDir = vaultList.getSelectionModel().getSelectedItem(); - vaultList.getItems().remove(selectedDir); + final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem(); + vaultList.getItems().remove(selectedVault); vaultList.getSelectionModel().clearSelection(); } + @FXML + private void didClickChangePassword(ActionEvent e) { + final Vault selectedVault = vaultList.getSelectionModel().getSelectedItem(); + showChangePasswordView(selectedVault); + } + // **************************************** // Subcontroller for right panel // **************************************** @@ -239,20 +246,20 @@ public class MainController implements Initializable, InitializationListener, Un this.showView("/fxml/welcome.fxml"); } - private void showInitializeView(Vault directory) { + private void showInitializeView(Vault vault) { final InitializeController ctrl = showView("/fxml/initialize.fxml"); - ctrl.setDirectory(directory); + ctrl.setVault(vault); ctrl.setListener(this); } @Override public void didInitialize(InitializeController ctrl) { - showUnlockView(ctrl.getDirectory()); + showUnlockView(ctrl.getVault()); } - private void showUnlockView(Vault directory) { + private void showUnlockView(Vault vault) { final UnlockController ctrl = showView("/fxml/unlock.fxml"); - ctrl.setVault(directory); + ctrl.setVault(vault); ctrl.setListener(this); } @@ -276,6 +283,17 @@ public class MainController implements Initializable, InitializationListener, Un } } + private void showChangePasswordView(Vault vault) { + final ChangePasswordController ctrl = showView("/fxml/change_password.fxml"); + ctrl.setVault(vault); + ctrl.setListener(this); + } + + @Override + public void didChangePassword(ChangePasswordController ctrl) { + showUnlockView(ctrl.getVault()); + } + /* Convenience */ public Collection getDirectories() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java similarity index 89% rename from main/ui/src/main/java/org/cryptomator/ui/UnlockController.java rename to main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index d3f1dbd01..2c666ece7 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation ******************************************************************************/ -package org.cryptomator.ui; +package org.cryptomator.ui.controllers; import java.io.IOException; import java.io.InputStream; @@ -30,7 +30,6 @@ import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharUtils; import org.cryptomator.crypto.exceptions.DecryptFailedException; import org.cryptomator.crypto.exceptions.UnsupportedKeyLengthException; @@ -78,10 +77,20 @@ public class UnlockController implements Initializable { public void initialize(URL url, ResourceBundle rb) { this.rb = rb; + passwordField.textProperty().addListener(this::passwordFieldsDidChange); mountName.addEventFilter(KeyEvent.KEY_TYPED, this::filterAlphanumericKeyEvents); mountName.textProperty().addListener(this::mountNameDidChange); } + // **************************************** + // Password field + // **************************************** + + private void passwordFieldsDidChange(ObservableValue property, String oldValue, String newValue) { + boolean passwordIsEmpty = passwordField.getText().isEmpty(); + unlockButton.setDisable(passwordIsEmpty); + } + // **************************************** // Unlock button // **************************************** @@ -89,13 +98,11 @@ public class UnlockController implements Initializable { @FXML private void didClickUnlockButton(ActionEvent event) { setControlsDisabled(true); + progressIndicator.setVisible(true); final Path masterKeyPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_FILE); final Path masterKeyBackupPath = vault.getPath().resolve(Vault.VAULT_MASTERKEY_BACKUP_FILE); final CharSequence password = passwordField.getCharacters(); - InputStream masterKeyInputStream = null; - try { - progressIndicator.setVisible(true); - masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ); + try (final InputStream masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ)) { vault.getCryptor().decryptMasterKey(masterKeyInputStream, password); if (!vault.startServer()) { messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed")); @@ -127,12 +134,12 @@ public class UnlockController implements Initializable { LOG.warn("Unsupported Key-Length. Please install Oracle Java Cryptography Extension (JCE).", ex); } finally { passwordField.swipe(); - IOUtils.closeQuietly(masterKeyInputStream); } } private void setControlsDisabled(boolean disable) { passwordField.setDisable(disable); + mountName.setDisable(disable); unlockButton.setDisable(disable); } diff --git a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java similarity index 99% rename from main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java rename to main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 3b51dc017..2f625317b 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -6,7 +6,7 @@ * Contributors: * Sebastian Stenzel - initial API and implementation ******************************************************************************/ -package org.cryptomator.ui; +package org.cryptomator.ui.controllers; import java.net.URL; import java.util.ResourceBundle; diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java b/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java index ef0df455a..aaeae93c5 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java +++ b/main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import org.cryptomator.ui.MainController; +import org.cryptomator.ui.controllers.MainController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/main/ui/src/main/resources/css/mac_theme.css b/main/ui/src/main/resources/css/mac_theme.css index 040ef3b6d..7a070830f 100644 --- a/main/ui/src/main/resources/css/mac_theme.css +++ b/main/ui/src/main/resources/css/mac_theme.css @@ -305,7 +305,8 @@ -fx-background-color: linear-gradient(to bottom, #4AA0F9 0%, #045FFF 100%), linear-gradient(to bottom, #69B2FA 0%, #0D81FF 100%); -fx-text-fill: -fx-light-text-color; } -.button:default:disabled { +.button:default:disabled, +.root.active-window .button:default:disabled { -fx-background-color: linear-gradient(to bottom, #D2D2D2 0%, #C4C4C4 100%), #F2F2F2; -fx-background-insets: 0, 1; -fx-text-fill: -fx-mid-text-color; diff --git a/main/ui/src/main/resources/fxml/change_password.fxml b/main/ui/src/main/resources/fxml/change_password.fxml new file mode 100644 index 000000000..ebf2aa1de --- /dev/null +++ b/main/ui/src/main/resources/fxml/change_password.fxml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +