Added change password functionality (fixes #20)

Moved controllers to new package
Small UI improvements
This commit is contained in:
Sebastian Stenzel 2015-02-22 16:10:17 +01:00
parent ea3384d189
commit 7edd303f2e
14 changed files with 302 additions and 42 deletions

View File

@ -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;

View File

@ -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<? extends String> 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);
}
}

View File

@ -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() {

View File

@ -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<Vault> getDirectories() {

View File

@ -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<? extends String> 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);
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2014 Sebastian Stenzel
This file is licensed under the terms of the MIT license.
See the LICENSE.txt file for more info.
Contributors:
Sebastian Stenzel - initial API and implementation
-->
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import java.lang.String?>
<?import org.cryptomator.ui.controls.SecPasswordField?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.ChangePasswordController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
<columnConstraints>
<ColumnConstraints percentWidth="38.2"/>
<ColumnConstraints percentWidth="61.8"/>
</columnConstraints>
<children>
<!-- Row 0 -->
<Label text="%changePassword.label.oldPassword" GridPane.rowIndex="0" GridPane.columnIndex="0" />
<SecPasswordField fx:id="oldPasswordField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 1 -->
<Label text="%changePassword.label.newPassword" GridPane.rowIndex="1" GridPane.columnIndex="0" />
<SecPasswordField fx:id="newPasswordField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Label text="%changePassword.label.retypePassword" GridPane.rowIndex="2" GridPane.columnIndex="0" />
<SecPasswordField fx:id="retypePasswordField" GridPane.rowIndex="2" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 3 -->
<Button fx:id="changePasswordButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickChangePasswordButton" disable="true"/>
<!-- Row 4 -->
<Label fx:id="messageLabel" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" />
</children>
</GridPane>

View File

@ -18,7 +18,7 @@
<?import javafx.scene.control.TextField?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.InitializeController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.InitializeController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>

View File

@ -19,7 +19,7 @@
<?import javafx.scene.control.Separator?>
<?import javafx.geometry.Insets?>
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.MainController" xmlns:fx="http://javafx.com/fxml">
<HBox fx:id="rootPane" prefHeight="440.0" prefWidth="640.0" fx:controller="org.cryptomator.ui.controllers.MainController" xmlns:fx="http://javafx.com/fxml">
<padding><Insets top="20" right="20" bottom="20" left="20.0"/></padding>
@ -28,8 +28,7 @@
<ContextMenu fx:id="vaultListCellContextMenu">
<items>
<MenuItem text="%main.directoryList.contextMenu.remove" onAction="#didClickRemoveSelectedEntry" />
<!-- TODO: -->
<MenuItem text="%main.directoryList.contextMenu.changePassword" disable="true" />
<MenuItem text="%main.directoryList.contextMenu.changePassword" onAction="#didClickChangePassword" />
</items>
</ContextMenu>
<ContextMenu fx:id="addVaultContextMenu" onShowing="#willShowAddVaultContextMenu" onHidden="#didHideAddVaultContextMenu">

View File

@ -19,7 +19,7 @@
<?import javafx.scene.control.CheckBox?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>
@ -39,7 +39,7 @@
<TextField fx:id="mountName" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" maxWidth="Infinity" />
<!-- Row 2 -->
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton"/>
<Button fx:id="unlockButton" text="%unlock.button.unlock" defaultButton="true" GridPane.rowIndex="2" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="RIGHT" prefWidth="150.0" onAction="#didClickUnlockButton" disable="true"/>
<!-- Row 3-->
<ProgressIndicator progress="-1" fx:id="progressIndicator" GridPane.rowIndex="3" GridPane.columnIndex="0" GridPane.columnSpan="2" GridPane.halignment="CENTER" visible="false"/>

View File

@ -18,7 +18,7 @@
<?import javafx.scene.chart.NumberAxis?>
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<GridPane vgap="12.0" hgap="12.0" prefWidth="400.0" fx:controller="org.cryptomator.ui.controllers.UnlockedController" xmlns:fx="http://javafx.com/fxml">
<padding>
<Insets top="24.0" right="24.0" bottom="24.0" left="24.0" />
</padding>

View File

@ -25,8 +25,6 @@ welcome.addButtonInstructionLabel=Start by adding a new vault :-)
initialize.label.password=Password
initialize.label.retypePassword=Retype password
initialize.button.ok=Create vault
initialize.alert.directoryIsNotEmpty.title=The chosen directory is not empty
initialize.alert.directoryIsNotEmpty.content=All existing files inside this directory will get encrypted. Continue?
# unlock.fxml
@ -35,12 +33,20 @@ unlock.label.mountName=Drive name
unlock.button.unlock=Unlock vault
unlock.errorMessage.wrongPassword=Wrong password.
unlock.errorMessage.decryptionFailed=Decryption failed.
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE.
unlock.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
unlock.messageLabel.startServerFailed=Starting WebDAV server failed.
# change_password.fxml
changePassword.label.oldPassword=Old password
changePassword.label.newPassword=New password
changePassword.label.retypePassword=Retype password
changePassword.button.unlock=Change password
changePassword.errorMessage.wrongPassword=Wrong password.
changePassword.errorMessage.decryptionFailed=Decryption failed.
changePassword.errorMessage.unsupportedKeyLengthInstallJCE=Decryption failed. Please install Oracle JCE Unlimited Strength Policy.
changePassword.infoMessage.success=Password changed.
# unlocked.fxml
unlocked.messageLabel.runningOnPort=Vault is accessible via WebDAV on local port %d.
unlocked.button.lock=Lock vault
unlocked.ioGraph.yAxis.label=Throughput (MiB/s)