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 960a7220a..779b5aa97 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
@@ -9,11 +9,21 @@
package org.cryptomator.ui.controls;
import com.google.common.base.Strings;
+import javafx.beans.Observable;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+import javafx.geometry.Pos;
+import javafx.scene.control.Label;
+import javafx.scene.control.OverrunStyle;
import javafx.scene.control.PasswordField;
+import javafx.scene.control.Tooltip;
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 java.awt.Toolkit;
import java.nio.CharBuffer;
import java.text.Normalizer;
import java.text.Normalizer.Form;
@@ -31,12 +41,32 @@ public class SecPasswordField extends PasswordField {
private static final int GROW_BUFFER_SIZE = 50;
private static final String PLACEHOLDER = "*";
+ private final Tooltip tooltip = new Tooltip();
+ private final Label indicator = new Label();
+ private final StringProperty nonPrintableCharsWarning = new SimpleStringProperty();
+ private final StringProperty capslockWarning = new SimpleStringProperty();
+
private char[] content = new char[INITIAL_BUFFER_SIZE];
private int length = 0;
public SecPasswordField() {
- this.onDragOverProperty().set(this::handleDragOver);
- this.onDragDroppedProperty().set(this::handleDragDropped);
+ indicator.setAlignment(Pos.CENTER_RIGHT);
+ indicator.setMouseTransparent(true);
+ indicator.setTextOverrun(OverrunStyle.CLIP);
+ this.getChildren().add(indicator);
+ this.setTooltip(tooltip);
+ this.addEventHandler(DragEvent.DRAG_OVER, this::handleDragOver);
+ this.addEventHandler(DragEvent.DRAG_DROPPED, this::handleDragDropped);
+ this.addEventHandler(KeyEvent.ANY, this::handleKeyEvent);
+ this.focusedProperty().addListener(this::focusedChanged);
+ }
+
+ @Override
+ protected void layoutChildren() {
+ super.layoutChildren();
+ indicator.resize(50.0, getHeight());
+ indicator.relocate(getWidth() - indicator.getWidth(), 0);
+ indicator.layout();
}
private void handleDragOver(DragEvent event) {
@@ -55,6 +85,50 @@ public class SecPasswordField extends PasswordField {
event.consume();
}
+ private void handleKeyEvent(KeyEvent e) {
+ if (e.getCode() == KeyCode.CAPS) {
+ updateVisualHints(true);
+ }
+ }
+
+ private void focusedChanged(@SuppressWarnings("unused") Observable observable) {
+ updateVisualHints(isFocused());
+ }
+
+ private void updateVisualHints(boolean focused) {
+ StringBuilder tooltipSb = new StringBuilder();
+ StringBuilder indicatorSb = new StringBuilder();
+ if (containsNonPrintableCharacters()) {
+ indicatorSb.append('⚠');
+ tooltipSb.append(nonPrintableCharsWarning.get()).append('\n');
+ }
+ // 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(capslockWarning.get()).append('\n');
+ }
+ indicator.setText(indicatorSb.toString());
+ tooltip.setText(tooltipSb.toString());
+ if (tooltip.getText().isEmpty()) {
+ setTooltip(null);
+ } else {
+ setTooltip(tooltip);
+ }
+ }
+
+ /**
+ * @return true if any {@link Character#isISOControl(char) control character} is present in the current value of this password field.
+ * @implNote runs in O(n)
+ */
+ boolean containsNonPrintableCharacters() {
+ for (int i = 0; i < length; i++) {
+ if (Character.isISOControl(content[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Replaces a range of characters with the given text.
* The text will be normalized to NFC.
@@ -86,6 +160,8 @@ public class SecPasswordField extends PasswordField {
// copy new text to content buffer
normalizedText.getChars(0, normalizedText.length(), content, start);
+ // trigger visual hints
+ updateVisualHints(true);
String placeholderString = Strings.repeat(PLACEHOLDER, normalizedText.length());
super.replaceText(start, end, placeholderString);
}
@@ -156,4 +232,22 @@ public class SecPasswordField extends PasswordField {
Arrays.fill(buffer, SWIPE_CHAR);
}
+ /* Getter/Setter */
+
+ public void setNonPrintableCharsWarning(String value) {
+ nonPrintableCharsWarning.set(value);
+ }
+
+ public String getNonPrintableCharsWarning() {
+ return nonPrintableCharsWarning.get();
+ }
+
+ public void setCapslockWarning(String value) {
+ capslockWarning.set(value);
+ }
+
+ public String getCapslockWarning() {
+ return capslockWarning.get();
+ }
+
}
diff --git a/main/ui/src/main/resources/fxml/change_password.fxml b/main/ui/src/main/resources/fxml/change_password.fxml
index 9232e7f10..4e8947346 100644
--- a/main/ui/src/main/resources/fxml/change_password.fxml
+++ b/main/ui/src/main/resources/fxml/change_password.fxml
@@ -38,15 +38,15 @@
-
+
-
+
-
+
diff --git a/main/ui/src/main/resources/fxml/initialize.fxml b/main/ui/src/main/resources/fxml/initialize.fxml
index 649d36f43..3aef249ca 100644
--- a/main/ui/src/main/resources/fxml/initialize.fxml
+++ b/main/ui/src/main/resources/fxml/initialize.fxml
@@ -36,11 +36,11 @@
-
+
-
+
diff --git a/main/ui/src/main/resources/fxml/unlock.fxml b/main/ui/src/main/resources/fxml/unlock.fxml
index b64163b2a..585b60b50 100644
--- a/main/ui/src/main/resources/fxml/unlock.fxml
+++ b/main/ui/src/main/resources/fxml/unlock.fxml
@@ -34,7 +34,7 @@
-
+
diff --git a/main/ui/src/main/resources/localization/en.txt b/main/ui/src/main/resources/localization/en.txt
index 8548c8d4c..c4ee9eb2e 100644
--- a/main/ui/src/main/resources/localization/en.txt
+++ b/main/ui/src/main/resources/localization/en.txt
@@ -7,6 +7,9 @@
app.name=Cryptomator
+ctrl.secPasswordField.nonPrintableChars=Password contains control characters. Recommendation: Remove them to ensure compatibility with other clients.
+ctrl.secPasswordField.capsLocked=Caps Lock is activated.
+
# main.fxml
main.emptyListInstructions=Click here to add a vault
main.directoryList.contextMenu.remove=Remove from List
diff --git a/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java b/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java
index 34fac228f..db2ae9cea 100644
--- a/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java
+++ b/main/ui/src/test/java/org/cryptomator/ui/controls/SecPasswordFieldTest.java
@@ -152,7 +152,7 @@ class SecPasswordFieldTest {
@Test
@DisplayName("test swipe char[]")
- public void swipe() {
+ public void testSwipe() {
pwField.appendText("topSecret");
CharSequence result1 = pwField.getCharacters();
@@ -163,4 +163,17 @@ class SecPasswordFieldTest {
Assertions.assertEquals("", result2.toString());
}
+ @Test
+ @DisplayName("test control characters")
+ public void testControlCharacters() {
+ pwField.appendText("normal");
+ Assertions.assertFalse(pwField.containsNonPrintableCharacters());
+
+ pwField.appendText("\00\01\02");
+ Assertions.assertTrue(pwField.containsNonPrintableCharacters());
+
+ CharSequence result1 = pwField.getCharacters();
+ Assertions.assertEquals("normal\00\01\02", result1.toString());
+ }
+
}