Improved SecPasswordField and added unit tests

SecPasswordFields will now normalize any input to NFC on the fly. Any input typed into the password field will now get converted to NFC on-the-fly. This allows subsequent code to keep working on the CharSequence returned by getCharacters() without the need of additional Normalization. Affects #521
This commit is contained in:
Sebastian Stenzel
2019-02-22 16:38:39 +01:00
parent deded33da8
commit 4bfd1e6433
4 changed files with 234 additions and 7 deletions

View File

@@ -221,6 +221,12 @@
<artifactId>hamcrest-all</artifactId>
<version>${hamcrest.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -109,5 +109,10 @@
<version>1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -15,6 +15,8 @@ import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import java.nio.CharBuffer;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
/**
@@ -53,15 +55,38 @@ public class SecPasswordField extends PasswordField {
event.consume();
}
/**
* Replaces a range of characters with the given text.
* The text will be normalized to <a href="https://www.unicode.org/glossary/#normalization_form_c">NFC</a>.
*
* @param start The starting index in the range, inclusive. This must be &gt;= 0 and &lt; the end.
* @param end The ending index in the range, exclusive. This is one-past the last character to
* delete (consistent with the String manipulation methods). This must be &gt; the start,
* and &lt;= the length of the text.
* @param text The text that is to replace the range. This must not be null.
* @implNote Internally calls {@link PasswordField#replaceText(int, int, String)} with a dummy String for visual purposes.
*/
@Override
public void replaceText(int start, int end, String text) {
String normalizedText = Normalizer.normalize(text, Form.NFC);
int removed = end - start;
int added = text.length();
this.length += added - removed;
growContentIfNeeded();
text.getChars(0, text.length(), content, start);
int added = normalizedText.length();
int delta = added - removed;
String placeholderString = Strings.repeat(PLACEHOLDER, text.length());
// ensure sufficient content buffer size
int oldLength = length;
this.length += delta;
growContentIfNeeded();
// shift existing content
if (delta != 0 && start < oldLength) {
System.arraycopy(content, end, content, end + delta, oldLength - end);
}
// copy new text to content buffer
normalizedText.getChars(0, normalizedText.length(), content, start);
String placeholderString = Strings.repeat(PLACEHOLDER, normalizedText.length());
super.replaceText(start, end, placeholderString);
}
@@ -69,7 +94,7 @@ public class SecPasswordField extends PasswordField {
if (length > content.length) {
char[] newContent = new char[length + GROW_BUFFER_SIZE];
System.arraycopy(content, 0, newContent, 0, content.length);
swipe();
swipe(content);
this.content = newContent;
}
}
@@ -80,6 +105,7 @@ public class SecPasswordField extends PasswordField {
* @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.
* @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()
*/
@Override
@@ -87,6 +113,28 @@ public class SecPasswordField extends PasswordField {
return CharBuffer.wrap(content, 0, length);
}
/**
* Convenience method wrapper for {@link #setPassword(char[])}.
*
* @param password
* @see #setPassword(char[])
*/
public void setPassword(CharSequence password) {
char[] buf = new char[password.length()];
for (int i = 0; i < password.length(); i++) {
buf[i] = password.charAt(i);
}
setPassword(buf);
Arrays.fill(buf, SWIPE_CHAR);
}
/**
* Directly sets the content of this password field to a copy of the given password.
* No conversion whatsoever happens. If you want to normalize the unicode representation of the password,
* do it before calling this method.
*
* @param password
*/
public void setPassword(char[] password) {
swipe();
content = Arrays.copyOf(password, password.length);
@@ -100,7 +148,12 @@ public class SecPasswordField extends PasswordField {
* Destroys the stored password by overriding each character with a different character.
*/
public void swipe() {
Arrays.fill(content, SWIPE_CHAR);
swipe(content);
length = 0;
}
private void swipe(char[] buffer) {
Arrays.fill(buffer, SWIPE_CHAR);
}
}

View File

@@ -0,0 +1,163 @@
package org.cryptomator.ui.controls;
import javafx.embed.swing.JFXPanel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import javax.swing.SwingUtilities;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
class SecPasswordFieldTest {
private SecPasswordField pwField = new SecPasswordField();
@BeforeAll
static void initJavaFx() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
SwingUtilities.invokeLater(() -> {
new JFXPanel(); // initializes JavaFX environment
latch.countDown();
});
if (!latch.await(5L, TimeUnit.SECONDS)) {
throw new ExceptionInInitializerError();
}
}
@Nested
@DisplayName("Content Update Events")
class TextChange {
@Test
@DisplayName("\"ant\".append(\"eater\")")
public void append() {
pwField.setPassword("ant");
pwField.appendText("eater");
Assertions.assertEquals("anteater", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"eater\".insert(0, \"ant\")")
public void insert1() {
pwField.setPassword("eater");
pwField.insertText(0, "ant");
Assertions.assertEquals("anteater", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"anteater\".insert(3, \"b\")")
public void insert2() {
pwField.setPassword("anteater");
pwField.insertText(3, "b");
Assertions.assertEquals("antbeater", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"anteater\".delete(0, 3)")
public void delete1() {
pwField.setPassword("anteater");
pwField.deleteText(0, 3);
Assertions.assertEquals("eater", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"anteater\".delete(3, 8)")
public void delete2() {
pwField.setPassword("anteater");
pwField.deleteText(3, 8);
Assertions.assertEquals("ant", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"anteater\".replace(0, 3, \"hand\")")
public void replace1() {
pwField.setPassword("anteater");
pwField.replaceText(0, 3, "hand");
Assertions.assertEquals("handeater", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"anteater\".replace(3, 6, \"keep\")")
public void replace2() {
pwField.setPassword("anteater");
pwField.replaceText(3, 6, "keep");
Assertions.assertEquals("antkeeper", pwField.getCharacters().toString());
}
@Test
@DisplayName("\"anteater\".replace(0, 3, \"bee\")")
public void replace3() {
pwField.setPassword("anteater");
pwField.replaceText(0, 3, "bee");
Assertions.assertEquals("beeeater", pwField.getCharacters().toString());
}
}
@Test
@DisplayName("entering NFC string leads to NFC char[]")
public void enterNfcString() {
pwField.appendText("str\u00F6m"); // ström
pwField.insertText(0, "\u212Bng"); // Ång
pwField.appendText("\uD83D\uDCA9"); // 💩
CharSequence result = pwField.getCharacters();
Assertions.assertEquals('\u00C5', result.charAt(0));
Assertions.assertEquals('n', result.charAt(1));
Assertions.assertEquals('g', result.charAt(2));
Assertions.assertEquals('s', result.charAt(3));
Assertions.assertEquals('t', result.charAt(4));
Assertions.assertEquals('r', result.charAt(5));
Assertions.assertEquals('ö', result.charAt(6));
Assertions.assertEquals('m', result.charAt(7));
Assertions.assertEquals('\uD83D', result.charAt(8));
Assertions.assertEquals('\uDCA9', result.charAt(9));
}
@Test
@DisplayName("entering NFD string leads to NFC char[]")
public void enterNfdString() {
pwField.appendText("str\u006F\u0308m"); // ström
pwField.insertText(0, "\u0041\u030Ang"); // Ång
pwField.appendText("\uD83D\uDCA9"); // 💩
CharSequence result = pwField.getCharacters();
Assertions.assertEquals('\u00C5', result.charAt(0));
Assertions.assertEquals('n', result.charAt(1));
Assertions.assertEquals('g', result.charAt(2));
Assertions.assertEquals('s', result.charAt(3));
Assertions.assertEquals('t', result.charAt(4));
Assertions.assertEquals('r', result.charAt(5));
Assertions.assertEquals('ö', result.charAt(6));
Assertions.assertEquals('m', result.charAt(7));
Assertions.assertEquals('\uD83D', result.charAt(8));
Assertions.assertEquals('\uDCA9', result.charAt(9));
}
@Test
@DisplayName("test swipe char[]")
public void swipe() {
pwField.appendText("topSecret");
CharSequence result1 = pwField.getCharacters();
Assertions.assertEquals("topSecret", result1.toString());
pwField.swipe();
CharSequence result2 = pwField.getCharacters();
Assertions.assertEquals(" ", result1.toString());
Assertions.assertEquals("", result2.toString());
}
}