|
@@ -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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 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);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * {@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.
|
|
|
- * <br/>
|
|
|
- * Imagine the following example with <code>pass</code> being the password, <code>x</code> being the swipe char and <code>'</code> being the offset of the char array:
|
|
|
- * <ol>
|
|
|
- * <li>Append filling chars to the end of the password: <code>passxxxx'</code></li>
|
|
|
- * <li>Delete first 4 chars. Internal implementation will then copy subsequent chars to the position, where the deletion occured: <code>xxxx'xxxx</code></li>
|
|
|
- * <li>Delete first 4 chars again, as we appended 4 chars in step 1: <code>'xxxxxx</code></li>
|
|
|
- * </ol>
|
|
|
+ * 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);
|
|
|
}
|
|
|
|
|
|
}
|