Selaa lähdekoodia

Pimped Password Field

Added reveal icon as well, capslock warnings as well a warning for unprintable chars (fixes #458)
Sebastian Stenzel 5 vuotta sitten
vanhempi
commit
efaf5a1553
20 muutettua tiedostoa jossa 242 lisäystä ja 108 poistoa
  1. 1 0
      main/commons/src/main/java/org/cryptomator/common/Environment.java
  2. 3 3
      main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java
  3. 4 5
      main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java
  4. 4 4
      main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java
  5. 3 3
      main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java
  6. 2 2
      main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java
  7. 2 2
      main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java
  8. 2 0
      main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java
  9. 89 0
      main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java
  10. 92 69
      main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java
  11. 3 2
      main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java
  12. 9 0
      main/ui/src/main/resources/css/dark_theme.css
  13. 9 0
      main/ui/src/main/resources/css/light_theme.css
  14. 4 3
      main/ui/src/main/resources/fxml/addvault_new_password.fxml
  15. 4 4
      main/ui/src/main/resources/fxml/changepassword.fxml
  16. 3 3
      main/ui/src/main/resources/fxml/initialize.fxml
  17. 2 2
      main/ui/src/main/resources/fxml/unlock.fxml
  18. 2 2
      main/ui/src/main/resources/fxml/unlock2.fxml
  19. 2 2
      main/ui/src/main/resources/fxml/upgrade.fxml
  20. 2 2
      main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java

+ 1 - 0
main/commons/src/main/java/org/cryptomator/common/Environment.java

@@ -28,6 +28,7 @@ public class Environment {
 
 	@Inject
 	public Environment() {
+		LOG.debug("java.library.path: {}", System.getProperty("java.library.path"));
 		LOG.debug("user.language: {}", System.getProperty("user.language"));
 		LOG.debug("user.region: {}", System.getProperty("user.region"));
 		LOG.debug("logback.configurationFile: {}", System.getProperty("logback.configurationFile"));

+ 3 - 3
main/ui/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultPasswordController.java

@@ -28,7 +28,7 @@ import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.common.Tasks;
 import org.cryptomator.ui.controls.FontAwesome5IconView;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.NiceSecurePasswordField;
 import org.cryptomator.ui.util.PasswordStrengthUtil;
 import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
@@ -71,8 +71,8 @@ public class CreateNewVaultPasswordController implements FxController {
 	private final BooleanProperty readyToCreateVault;
 	private final ObjectBinding<ContentDisplay> createVaultButtonState;
 
-	public SecPasswordField passwordField;
-	public SecPasswordField reenterField;
+	public NiceSecurePasswordField passwordField;
+	public NiceSecurePasswordField reenterField;
 	public Label passwordStrengthLabel;
 	public HBox passwordMatchBox;
 	public FontAwesome5IconView checkmark;

+ 4 - 5
main/ui/src/main/java/org/cryptomator/ui/changepassword/ChangePasswordController.java

@@ -14,7 +14,7 @@ import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.controls.FontAwesome5IconView;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.NiceSecurePasswordField;
 import org.cryptomator.ui.util.PasswordStrengthUtil;
 import org.fxmisc.easybind.EasyBind;
 import org.slf4j.Logger;
@@ -35,9 +35,9 @@ public class ChangePasswordController implements FxController {
 	private final PasswordStrengthUtil strengthRater;
 	private final IntegerProperty passwordStrength;
 
-	public SecPasswordField oldPasswordField;
-	public SecPasswordField newPasswordField;
-	public SecPasswordField reenterPasswordField;
+	public NiceSecurePasswordField oldPasswordField;
+	public NiceSecurePasswordField newPasswordField;
+	public NiceSecurePasswordField reenterPasswordField;
 	public Label passwordStrengthLabel;
 	public HBox passwordMatchBox;
 	public FontAwesome5IconView checkmark;
@@ -71,7 +71,6 @@ public class ChangePasswordController implements FxController {
 		cross.visibleProperty().bind(passwordsMatch.not().and(reenterFieldNotEmpty));
 		cross.managedProperty().bind(cross.visibleProperty());
 		passwordMatchLabel.textProperty().bind(Bindings.when(passwordsMatch.and(reenterFieldNotEmpty)).then(resourceBundle.getString("changepassword.passwordsMatch")).otherwise(resourceBundle.getString("changepassword.passwordsDoNotMatch")));
-
 		passwordStrengthLabel.textProperty().bind(EasyBind.map(passwordStrength, strengthRater::getStrengthDescription));
 	}
 

+ 4 - 4
main/ui/src/main/java/org/cryptomator/ui/controllers/ChangePasswordController.java

@@ -25,7 +25,7 @@ import javafx.scene.layout.Region;
 import javafx.scene.text.Text;
 import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.SecurePasswordField;
 import org.cryptomator.ui.l10n.Localization;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.util.PasswordStrengthUtil;
@@ -58,13 +58,13 @@ public class ChangePasswordController implements ViewController {
 	}
 
 	@FXML
-	private SecPasswordField oldPasswordField;
+	private SecurePasswordField oldPasswordField;
 
 	@FXML
-	private SecPasswordField newPasswordField;
+	private SecurePasswordField newPasswordField;
 
 	@FXML
-	private SecPasswordField retypePasswordField;
+	private SecurePasswordField retypePasswordField;
 
 	@FXML
 	private Button changePasswordButton;

+ 3 - 3
main/ui/src/main/java/org/cryptomator/ui/controllers/InitializeController.java

@@ -20,7 +20,7 @@ import javafx.scene.control.Button;
 import javafx.scene.control.Label;
 import javafx.scene.layout.GridPane;
 import javafx.scene.layout.Region;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.SecurePasswordField;
 import org.cryptomator.ui.l10n.Localization;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.ui.util.PasswordStrengthUtil;
@@ -51,10 +51,10 @@ public class InitializeController implements ViewController {
 	}
 
 	@FXML
-	private SecPasswordField passwordField;
+	private SecurePasswordField passwordField;
 
 	@FXML
-	private SecPasswordField retypePasswordField;
+	private SecurePasswordField retypePasswordField;
 
 	@FXML
 	private Button okButton;

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java

@@ -41,7 +41,7 @@ import org.cryptomator.cryptolib.api.InvalidPassphraseException;
 import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException;
 import org.cryptomator.keychain.KeychainAccess;
 import org.cryptomator.keychain.KeychainAccessException;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.SecurePasswordField;
 import org.cryptomator.ui.l10n.Localization;
 import org.cryptomator.common.vaults.Vault;
 import org.cryptomator.common.vaults.WindowsDriveLetters;
@@ -99,7 +99,7 @@ public class UnlockController implements ViewController {
 	}
 
 	@FXML
-	private SecPasswordField passwordField;
+	private SecurePasswordField passwordField;
 
 	@FXML
 	private Button advancedOptionsButton;

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java

@@ -21,7 +21,7 @@ import javafx.scene.control.CheckBox;
 import javafx.scene.control.Label;
 import javafx.scene.control.ProgressIndicator;
 import javafx.scene.layout.GridPane;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.SecurePasswordField;
 import org.cryptomator.ui.model.upgrade.UpgradeStrategies;
 import org.cryptomator.ui.model.upgrade.UpgradeStrategy;
 import org.cryptomator.ui.model.upgrade.UpgradeStrategy.UpgradeFailedException;
@@ -50,7 +50,7 @@ public class UpgradeController implements ViewController {
 	private Label upgradeMsgLabel;
 
 	@FXML
-	private SecPasswordField passwordField;
+	private SecurePasswordField passwordField;
 
 	@FXML
 	private CheckBox confirmationCheckbox;

+ 2 - 0
main/ui/src/main/java/org/cryptomator/ui/controls/FontAwesome5Icon.java

@@ -5,11 +5,13 @@ package org.cryptomator.ui.controls;
  */
 public enum FontAwesome5Icon {
 	ANCHOR("\uF13D"), //
+	ARROW_ALT_UP("\uF357"), //
 	CHECK("\uF00C"), //
 	COG("\uF013"), //
 	COGS("\uF085"), //
 	EXCLAMATION_TRIANGLE("\uF071"), //
 	EYE("\uF06E"), //
+	EYE_SLASH("\uF070"), //
 	FOLDER_OPEN("\uF07C"), //
 	HDD("\uF0A0"), //
 	KEY("\uF084"), //

+ 89 - 0
main/ui/src/main/java/org/cryptomator/ui/controls/NiceSecurePasswordField.java

@@ -0,0 +1,89 @@
+package org.cryptomator.ui.controls;
+
+import javafx.beans.binding.Bindings;
+import javafx.beans.property.StringProperty;
+import javafx.geometry.Pos;
+import javafx.scene.control.ContentDisplay;
+import javafx.scene.control.ToggleButton;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.StackPane;
+
+public class NiceSecurePasswordField extends StackPane {
+
+	private static final String STYLE_CLASS = "nice-secure-password-field";
+	private static final String ICONS_STLYE_CLASS = "icons";
+	private static final String REVEAL_BUTTON_STLYE_CLASS = "reveal-button";
+	private static final int ICON_SPACING = 6;
+	private static final double ICON_SIZE = 14.0;
+
+	private final SecurePasswordField passwordField = new SecurePasswordField();
+	private final FontAwesome5IconView capsLockedIcon = new FontAwesome5IconView();
+	private final FontAwesome5IconView nonPrintableCharsIcon = new FontAwesome5IconView();
+	private final FontAwesome5IconView revealPasswordIcon = new FontAwesome5IconView();
+	private final ToggleButton revealPasswordButton = new ToggleButton(null, revealPasswordIcon);
+	private final HBox iconContainer = new HBox(ICON_SPACING, nonPrintableCharsIcon, capsLockedIcon, revealPasswordButton);
+
+	public NiceSecurePasswordField() {
+		getStyleClass().add(STYLE_CLASS);
+
+		iconContainer.setAlignment(Pos.CENTER_RIGHT);
+		iconContainer.setMaxWidth(Double.NEGATIVE_INFINITY);
+		iconContainer.setPrefWidth(42); // TODO
+		iconContainer.getStyleClass().add(ICONS_STLYE_CLASS);
+		StackPane.setAlignment(iconContainer, Pos.CENTER_RIGHT);
+
+		capsLockedIcon.setGlyph(FontAwesome5Icon.ARROW_ALT_UP);
+		capsLockedIcon.setGlyphSize(ICON_SIZE);
+		capsLockedIcon.visibleProperty().bind(passwordField.capsLockedProperty());
+		capsLockedIcon.managedProperty().bind(passwordField.capsLockedProperty());
+
+		nonPrintableCharsIcon.setGlyph(FontAwesome5Icon.EXCLAMATION_TRIANGLE);
+		nonPrintableCharsIcon.setGlyphSize(ICON_SIZE);
+		nonPrintableCharsIcon.visibleProperty().bind(passwordField.containingNonPrintableCharsProperty());
+		nonPrintableCharsIcon.managedProperty().bind(passwordField.containingNonPrintableCharsProperty());
+
+		revealPasswordIcon.setGlyph(FontAwesome5Icon.EYE);
+		revealPasswordIcon.glyphProperty().bind(Bindings.createObjectBinding(this::getRevealPasswordGlyph, revealPasswordButton.selectedProperty()));
+		revealPasswordIcon.setGlyphSize(ICON_SIZE);
+
+		revealPasswordButton.setContentDisplay(ContentDisplay.LEFT);
+		revealPasswordButton.setFocusTraversable(false);
+		revealPasswordButton.visibleProperty().bind(passwordField.focusedProperty());
+		revealPasswordButton.managedProperty().bind(passwordField.focusedProperty());
+		revealPasswordButton.getStyleClass().add(REVEAL_BUTTON_STLYE_CLASS);
+
+		passwordField.revealPasswordProperty().bind(revealPasswordButton.selectedProperty());
+
+		getChildren().addAll(passwordField, iconContainer);
+	}
+
+	private FontAwesome5Icon getRevealPasswordGlyph() {
+		return revealPasswordButton.isSelected() ? FontAwesome5Icon.EYE_SLASH : FontAwesome5Icon.EYE;
+	}
+
+	/* Passthrough */
+
+	public StringProperty textProperty() {
+		return passwordField.textProperty();
+	}
+
+	public CharSequence getCharacters() {
+		return passwordField.getCharacters();
+	}
+
+	public void setPassword(char[] password) {
+		passwordField.setPassword(password);
+	}
+
+	public void swipe() {
+		passwordField.swipe();;
+	}
+
+	public void selectAll() {
+		passwordField.selectAll();
+	}
+
+	public void selectRange(int anchor, int caretPosition) {
+		passwordField.selectRange(anchor, caretPosition);
+	}
+}

+ 92 - 69
main/ui/src/main/java/org/cryptomator/ui/controls/SecPasswordField.java

@@ -11,20 +11,19 @@ package org.cryptomator.ui.controls;
 import com.google.common.base.Strings;
 import javafx.beans.NamedArg;
 import javafx.beans.Observable;
-import javafx.geometry.Insets;
-import javafx.geometry.Pos;
-import javafx.scene.control.Label;
-import javafx.scene.control.OverrunStyle;
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.ReadOnlyBooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+import javafx.scene.AccessibleAttribute;
+import javafx.scene.AccessibleRole;
+import javafx.scene.control.IndexRange;
 import javafx.scene.control.PasswordField;
-import javafx.scene.control.Tooltip;
+import javafx.scene.control.TextField;
 import javafx.scene.input.DragEvent;
 import javafx.scene.input.Dragboard;
 import javafx.scene.input.KeyCode;
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.TransferMode;
-import javafx.scene.paint.Color;
-import javafx.scene.text.Font;
-import javafx.scene.text.Text;
 
 import java.awt.Toolkit;
 import java.nio.CharBuffer;
@@ -33,53 +32,54 @@ import java.text.Normalizer.Form;
 import java.util.Arrays;
 
 /**
- * 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.
+ * Patched PasswordField that doesn't create String copies of the password in memory (unless explicitly revealed). 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 {
+public class SecurePasswordField extends TextField {
 
 	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 static final double PADDING = 2.0;
-	private static final double INDICATOR_PADDING = 4.0;
-	private static final Color INDICATOR_COLOR = new Color(0.901, 0.494, 0.133, 1.0);
+	private static final String DEFAULT_PLACEHOLDER = "●";
+	private static final String STYLE_CLASS = "secure-password-field";
 
-	private final Tooltip tooltip = new Tooltip();
-	private final Label indicator = new Label();
-	private final String nonPrintableCharsWarning;
-	private final String capslockWarning;
+	private final String placeholderChar;
+	private final BooleanProperty capsLocked = new SimpleBooleanProperty();
+	private final BooleanProperty containingNonPrintableChars = new SimpleBooleanProperty();
+	private final BooleanProperty revealPassword = new SimpleBooleanProperty();
 
 	private char[] content = new char[INITIAL_BUFFER_SIZE];
 	private int length = 0;
 
-	public SecPasswordField() {
-		this("", "");
+	public SecurePasswordField() {
+		this(DEFAULT_PLACEHOLDER);
 	}
 
-	public SecPasswordField(@NamedArg("nonPrintableCharsWarning") String nonPrintableCharsWarning, @NamedArg("capslockWarning") String capslockWarning) {
-		this.nonPrintableCharsWarning = nonPrintableCharsWarning;
-		this.capslockWarning = capslockWarning;
-		indicator.setPadding(new Insets(PADDING, INDICATOR_PADDING, PADDING, INDICATOR_PADDING));
-		indicator.setAlignment(Pos.CENTER_RIGHT);
-		indicator.setMouseTransparent(true);
-		indicator.setTextOverrun(OverrunStyle.CLIP);
-		indicator.setTextFill(INDICATOR_COLOR);
-		indicator.setFont(Font.font(indicator.getFont().getFamily(), 15.0));
-		this.getChildren().add(indicator);
+	public SecurePasswordField(@NamedArg("placeholderChar") String placeholderChar) {
+		this.getStyleClass().add(STYLE_CLASS);
+		this.placeholderChar = placeholderChar;
+		this.setAccessibleRole(AccessibleRole.PASSWORD_FIELD);
 		this.addEventHandler(DragEvent.DRAG_OVER, this::handleDragOver);
 		this.addEventHandler(DragEvent.DRAG_DROPPED, this::handleDragDropped);
 		this.addEventHandler(KeyEvent.ANY, this::handleKeyEvent);
+		this.revealPasswordProperty().addListener(this::revealPasswordChanged);
 		this.focusedProperty().addListener(this::focusedChanged);
 	}
 
-	@Override
-	protected void layoutChildren() {
-		super.layoutChildren();
-		indicator.relocate(0.0, 0.0);
-		indicator.resize(getWidth(), getHeight());
+	public void cut() {
+	}
+
+	public void copy() {
+	}
+
+	public Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters) {
+		switch(attribute) {
+			case TEXT:
+				return null;
+			default:
+				return super.queryAccessibleAttribute(attribute, parameters);
+		}
 	}
 
 	private void handleDragOver(DragEvent event) {
@@ -100,42 +100,32 @@ public class SecPasswordField extends PasswordField {
 
 	private void handleKeyEvent(KeyEvent e) {
 		if (e.getCode() == KeyCode.CAPS) {
-			updateVisualHints(true);
+			updateCapsLocked();
+		}
+	}
+
+	private void revealPasswordChanged(@SuppressWarnings("unused") Observable observable) {
+		IndexRange selection = getSelection();
+		if (isRevealPassword()) {
+			super.setText(this.getCharacters().toString());
+		} else {
+			String placeholderText = Strings.repeat(placeholderChar, length);
+			super.setText(placeholderText);
 		}
+		selectRange(selection.getStart(), selection.getEnd());
 	}
 
 	private void focusedChanged(@SuppressWarnings("unused") Observable observable) {
-		updateVisualHints(isFocused());
+		updateCapsLocked();
 	}
 
-	private void updateVisualHints(boolean focused) {
-		StringBuilder tooltipSb = new StringBuilder();
-		StringBuilder indicatorSb = new StringBuilder();
-		if (containsNonPrintableCharacters()) {
-			indicatorSb.append('⚠');
-			tooltipSb.append("- ").append(nonPrintableCharsWarning).append('\n');
-		}
+	private void updateCapsLocked() {
 		// AWT code needed until https://bugs.openjdk.java.net/browse/JDK-8090882 is closed:
-		if (focused && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)) {
-			indicatorSb.append('⇪');
-			tooltipSb.append("- ").append(capslockWarning).append('\n');
-		}
-		indicator.setText(indicatorSb.toString());
-		if (!indicator.getText().isEmpty()) {
-			setPadding(new Insets(PADDING, getIndicatorWidth(), PADDING, PADDING));
-		} else {
-			setPadding(new Insets(PADDING));
-		}
-		tooltip.setText(tooltipSb.toString());
-		if (tooltip.getText().isEmpty()) {
-			setTooltip(null);
-		} else {
-			setTooltip(tooltip);
-		}
+		capsLocked.set(isFocused() && Toolkit.getDefaultToolkit().getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK));
 	}
 
-	private double getIndicatorWidth() {
-		return new Text(indicator.getText()).getLayoutBounds().getWidth() + INDICATOR_PADDING * 2.0;
+	private void updateContainingNonPrintableChars() {
+		containingNonPrintableChars.set(containsNonPrintableCharacters());
 	}
 
 	/**
@@ -144,7 +134,7 @@ public class SecPasswordField extends PasswordField {
 	 */
 	boolean containsNonPrintableCharacters() {
 		for (int i = 0; i < length; i++) {
-			if (Character.isISOControl(content[i])) {
+			if (Character.isDigit(content[i])) {
 				return true;
 			}
 		}
@@ -183,9 +173,13 @@ public class SecPasswordField extends PasswordField {
 		normalizedText.getChars(0, normalizedText.length(), content, start);
 
 		// trigger visual hints
-		updateVisualHints(true);
-		String placeholderString = Strings.repeat(PLACEHOLDER, normalizedText.length());
-		super.replaceText(start, end, placeholderString);
+		updateContainingNonPrintableChars();
+		if (isRevealPassword()) {
+			super.replaceText(start, end, text);
+		} else {
+			String placeholderString = Strings.repeat(placeholderChar, normalizedText.length());
+			super.replaceText(start, end, placeholderString);
+		}
 	}
 
 	private void growContentIfNeeded() {
@@ -200,9 +194,9 @@ public class SecPasswordField extends PasswordField {
 	/**
 	 * Creates a CharSequence by wrapping the password characters.
 	 *
-	 * @return A character sequence backed by the SecPasswordField's buffer (not a copy).
+	 * @return A character sequence backed by the SecurePasswordField'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.
+	 * Therefore any mutation to the SecurePasswordField's content will mutate or eventually swipe the returned CharSequence.
 	 * @implSpec The CharSequence is usually in <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a> representation (unless NFD-encoded char[] is set via {@link #setPassword(char[])}).
 	 * @see #swipe()
 	 */
@@ -238,7 +232,7 @@ public class SecPasswordField extends PasswordField {
 		content = Arrays.copyOf(password, password.length);
 		length = password.length;
 
-		String placeholderString = Strings.repeat(PLACEHOLDER, password.length);
+		String placeholderString = Strings.repeat(placeholderChar, password.length);
 		setText(placeholderString);
 	}
 
@@ -255,4 +249,33 @@ public class SecPasswordField extends PasswordField {
 		Arrays.fill(buffer, SWIPE_CHAR);
 	}
 
+	/* Observable Properties */
+
+	public ReadOnlyBooleanProperty capsLockedProperty() {
+		return capsLocked;
+	}
+
+	public boolean isCapsLocked() {
+		return capsLocked.get();
+	}
+
+	public ReadOnlyBooleanProperty containingNonPrintableCharsProperty() {
+		return containingNonPrintableChars;
+	}
+
+	public boolean isContainingNonPrintableChars() {
+		return containingNonPrintableChars.get();
+	}
+
+	public BooleanProperty revealPasswordProperty() {
+		return revealPassword;
+	}
+
+	public boolean isRevealPassword() {
+		return revealPassword.get();
+	}
+
+	public void setRevealPassword(boolean revealPassword) {
+		this.revealPassword.set(revealPassword);
+	}
 }

+ 3 - 2
main/ui/src/main/java/org/cryptomator/ui/unlock/UnlockController.java

@@ -29,7 +29,8 @@ import org.cryptomator.ui.common.FxController;
 import org.cryptomator.ui.common.FxmlFile;
 import org.cryptomator.ui.common.FxmlScene;
 import org.cryptomator.ui.common.Tasks;
-import org.cryptomator.ui.controls.SecPasswordField;
+import org.cryptomator.ui.controls.NiceSecurePasswordField;
+import org.cryptomator.ui.controls.SecurePasswordField;
 import org.cryptomator.ui.util.DialogBuilderUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -55,7 +56,7 @@ public class UnlockController implements FxController {
 	private final ResourceBundle resourceBundle;
 	private final Lazy<Scene> successScene;
 	private final BooleanProperty unlockButtonDisabled;
-	public SecPasswordField passwordField;
+	public NiceSecurePasswordField passwordField;
 	public CheckBox savePassword;
 
 	@Inject

+ 9 - 0
main/ui/src/main/resources/css/dark_theme.css

@@ -455,6 +455,15 @@
 	-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
 }
 
+.nice-secure-password-field .secure-password-field {
+	-fx-padding: 0.3em 48px 0.3em 0.5em;
+}
+
+.nice-secure-password-field .icons {
+	-fx-width: 42px;
+	-fx-padding: 4px 6px 4px 0;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Buttons                                                                     *

+ 9 - 0
main/ui/src/main/resources/css/light_theme.css

@@ -455,6 +455,15 @@
 	-fx-background-color: CONTROL_BORDER_DISABLED, CONTROL_BG_DISABLED;
 }
 
+.nice-secure-password-field .secure-password-field {
+	-fx-padding: 0.3em 48px 0.3em 0.5em;
+}
+
+.nice-secure-password-field .icons {
+	-fx-width: 42px;
+	-fx-padding: 4px 6px 4px 0;
+}
+
 /*******************************************************************************
  *                                                                             *
  * Buttons                                                                     *

+ 4 - 3
main/ui/src/main/resources/fxml/addvault_new_password.fxml

@@ -11,7 +11,8 @@
 <?import javafx.scene.layout.VBox?>
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
 <?import org.cryptomator.ui.controls.PasswordStrengthIndicator?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
+<?import org.cryptomator.ui.controls.SecurePasswordField?>
+<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
 <VBox xmlns="http://javafx.com/javafx"
 	  xmlns:fx="http://javafx.com/fxml"
 	  fx:controller="org.cryptomator.ui.addvaultwizard.CreateNewVaultPasswordController"
@@ -27,13 +28,13 @@
 
 		<VBox spacing="6">
 			<Label text="%addvaultwizard.new.enterPassword" labelFor="$passwordField"/>
-			<SecPasswordField fx:id="passwordField"/>
+			<NiceSecurePasswordField fx:id="passwordField"/>
 			<PasswordStrengthIndicator spacing="6" prefHeight="6" strength="${controller.passwordStrength}"/>
 			<Label fx:id="passwordStrengthLabel" styleClass="label-secondary" labelFor="$passwordField" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
 		</VBox>
 		<VBox spacing="6">
 			<Label text="%addvaultwizard.new.reenterPassword" labelFor="$reenterField"/>
-			<SecPasswordField fx:id="reenterField"/>
+			<NiceSecurePasswordField fx:id="reenterField"/>
 			<HBox fx:id="passwordMatchBox" spacing="6" alignment="CENTER_RIGHT">
 				<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
 				<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>

+ 4 - 4
main/ui/src/main/resources/fxml/changepassword.fxml

@@ -10,8 +10,8 @@
 <?import javafx.scene.layout.VBox?>
 <?import org.cryptomator.ui.controls.FontAwesome5IconView?>
 <?import org.cryptomator.ui.controls.FormattedLabel?>
+<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
 <?import org.cryptomator.ui.controls.PasswordStrengthIndicator?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
 <VBox xmlns="http://javafx.com/javafx"
 	  xmlns:fx="http://javafx.com/fxml"
 	  fx:controller="org.cryptomator.ui.changepassword.ChangePasswordController"
@@ -24,20 +24,20 @@
 	<children>
 		<VBox spacing="6">
 			<FormattedLabel format="%changepassword.enterOldPassword" arg1="${controller.vault.displayableName}" wrapText="true"/>
-			<SecPasswordField fx:id="oldPasswordField"/>
+			<NiceSecurePasswordField fx:id="oldPasswordField"/>
 		</VBox>
 
 		<Region prefHeight="12" VBox.vgrow="NEVER"/>
 
 		<VBox spacing="6">
 			<Label labelFor="$newPasswordField" text="%changepassword.enterNewPassword"/>
-			<SecPasswordField fx:id="newPasswordField"/>
+			<NiceSecurePasswordField fx:id="newPasswordField"/>
 			<PasswordStrengthIndicator prefHeight="6" spacing="6" strength="${controller.passwordStrength}"/>
 			<Label fx:id="passwordStrengthLabel" styleClass="label-secondary" alignment="CENTER_RIGHT" maxWidth="Infinity"/>
 		</VBox>
 		<VBox spacing="6">
 			<Label labelFor="$reenterPasswordField" text="%changepassword.reenterNewPassword"/>
-			<SecPasswordField fx:id="reenterPasswordField"/>
+			<NiceSecurePasswordField fx:id="reenterPasswordField"/>
 			<HBox fx:id="passwordMatchBox" spacing="6" alignment="CENTER_RIGHT">
 				<FontAwesome5IconView fx:id="checkmark" styleClass="glyph-icon-primary" glyph="CHECK"/>
 				<FontAwesome5IconView fx:id="cross" styleClass="glyph-icon-red" glyph="TIMES"/>

+ 3 - 3
main/ui/src/main/resources/fxml/initialize.fxml

@@ -19,7 +19,7 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.ProgressBar?>
 <?import javafx.scene.control.ProgressIndicator?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
+<?import org.cryptomator.ui.controls.SecurePasswordField?>
 <?import javafx.scene.layout.Region?>
 <?import javafx.scene.layout.VBox?>
 
@@ -36,11 +36,11 @@
 	<children>
 		<!-- Row 0 -->
 		<Label GridPane.rowIndex="0" GridPane.columnIndex="0" text="%initialize.label.password" cacheShape="true" cache="true" />
-		<SecPasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="0" GridPane.columnIndex="1" cacheShape="true" cache="true" />
+		<SecurePasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="0" GridPane.columnIndex="1" cacheShape="true" cache="true" />
 
 		<!-- Row 1 -->
 		<Label GridPane.rowIndex="1" GridPane.columnIndex="0" text="%initialize.label.retypePassword" cacheShape="true" cache="true" />
-		<SecPasswordField fx:id="retypePasswordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="1" GridPane.columnIndex="1" cacheShape="true" cache="true" />
+		<SecurePasswordField fx:id="retypePasswordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" GridPane.rowIndex="1" GridPane.columnIndex="1" cacheShape="true" cache="true" />
 		
 		<!-- Row 2 -->
 		<VBox GridPane.columnIndex="1" GridPane.rowIndex="2" spacing="6.0">

+ 2 - 2
main/ui/src/main/resources/fxml/unlock.fxml

@@ -21,7 +21,7 @@
 <?import javafx.scene.layout.VBox?>
 <?import javafx.scene.text.Text?>
 <?import javafx.scene.text.TextFlow?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
+<?import org.cryptomator.ui.controls.SecurePasswordField?>
 <VBox fx:controller="org.cryptomator.ui.controllers.UnlockController" fx:id="root" spacing="12" alignment="BOTTOM_CENTER" xmlns:fx="http://javafx.com/fxml" prefWidth="400">
 
 	<padding>
@@ -31,7 +31,7 @@
 	<!-- Password Field -->
 	<HBox spacing="12" alignment="BASELINE_LEFT">
 		<Label text="%unlock.label.password" HBox.hgrow="NEVER"/>
-		<SecPasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" maxWidth="Infinity" HBox.hgrow="ALWAYS"/>
+		<SecurePasswordField fx:id="passwordField" capslockWarning="%ctrl.secPasswordField.capsLocked" nonPrintableCharsWarning="%ctrl.secPasswordField.nonPrintableChars" maxWidth="Infinity" HBox.hgrow="ALWAYS"/>
 	</HBox>
 
 	<!-- Unlock Button / Advanced Options Button -->

+ 2 - 2
main/ui/src/main/resources/fxml/unlock2.fxml

@@ -7,7 +7,7 @@
 <?import javafx.scene.control.ProgressIndicator?>
 <?import javafx.scene.layout.VBox?>
 <?import org.cryptomator.ui.controls.FormattedLabel?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
+<?import org.cryptomator.ui.controls.NiceSecurePasswordField?>
 <VBox xmlns="http://javafx.com/javafx"
 	  xmlns:fx="http://javafx.com/fxml"
 	  fx:controller="org.cryptomator.ui.unlock.UnlockController"
@@ -21,7 +21,7 @@
 	<children>
 		<VBox spacing="6">
 			<FormattedLabel format="%unlock.passwordPrompt" arg1="${controller.vault.displayableName}" wrapText="true"/>
-			<SecPasswordField fx:id="passwordField"/>
+			<NiceSecurePasswordField fx:id="passwordField"/>
 			<CheckBox fx:id="savePassword" text="%unlock.savePassword" onAction="#didClickSavePasswordCheckbox"/>
 		</VBox>
 

+ 2 - 2
main/ui/src/main/resources/fxml/upgrade.fxml

@@ -11,7 +11,7 @@
 <?import javafx.scene.control.Label?>
 <?import javafx.scene.control.ProgressIndicator?>
 <?import javafx.scene.control.Button?>
-<?import org.cryptomator.ui.controls.SecPasswordField?>
+<?import org.cryptomator.ui.controls.SecurePasswordField?>
 <?import javafx.geometry.Insets?>
 <?import javafx.scene.layout.GridPane?>
 <?import javafx.scene.layout.ColumnConstraints?>
@@ -39,7 +39,7 @@
 		</Pane>
 
 		<Label text="%unlock.label.password" GridPane.rowIndex="3" GridPane.columnIndex="0" cacheShape="true" cache="true" />
-		<SecPasswordField fx:id="passwordField" GridPane.rowIndex="3" GridPane.columnIndex="1" cacheShape="true" cache="true" />
+		<SecurePasswordField fx:id="passwordField" GridPane.rowIndex="3" GridPane.columnIndex="1" cacheShape="true" cache="true" />
 		
 		<CheckBox fx:id="confirmationCheckbox" text="%upgrade.confirmation.label" wrapText="true" GridPane.rowIndex="4" GridPane.columnIndex="0" GridPane.columnSpan="2" cacheShape="true" cache="true" />
 

+ 2 - 2
main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java

@@ -12,9 +12,9 @@ import java.awt.GraphicsEnvironment;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
-class SecPasswordFieldTest {
+class SecurePasswordFieldTest {
 
-	private SecPasswordField pwField = new SecPasswordField();
+	private SecurePasswordField pwField = new SecurePasswordField();
 
 	@BeforeAll
 	static void initJavaFx() throws InterruptedException {