Added ClusterIOTest and improved some stuff.
This commit is contained in:
parent
4e401d98a5
commit
0c5cd95905
7
pom.xml
7
pom.xml
|
@ -36,6 +36,11 @@
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.8.1</version>
|
<version>3.8.1</version>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>2.22.2</version>
|
||||||
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
@ -60,7 +65,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
<artifactId>junit-jupiter-params</artifactId>
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
<version>5.7.0</version>
|
<version>5.7.1</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
|
@ -2,21 +2,19 @@ package nl.andrewlalis.crystalkeep.control;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.control.TreeItem;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.TreeView;
|
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.FileChooser;
|
import javafx.stage.FileChooser;
|
||||||
|
import nl.andrewlalis.crystalkeep.io.ClusterIO;
|
||||||
import nl.andrewlalis.crystalkeep.model.*;
|
import nl.andrewlalis.crystalkeep.model.*;
|
||||||
import nl.andrewlalis.crystalkeep.model.serialization.ClusterLoader;
|
|
||||||
import nl.andrewlalis.crystalkeep.view.ClusterTreeItem;
|
import nl.andrewlalis.crystalkeep.view.ClusterTreeItem;
|
||||||
import nl.andrewlalis.crystalkeep.view.CrystalItemTreeCell;
|
import nl.andrewlalis.crystalkeep.view.CrystalItemTreeCell;
|
||||||
import nl.andrewlalis.crystalkeep.view.ShardTreeItem;
|
import nl.andrewlalis.crystalkeep.view.ShardTreeItem;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.security.GeneralSecurityException;
|
import java.util.Optional;
|
||||||
|
|
||||||
public class MainViewController implements ModelListener {
|
public class MainViewController implements ModelListener {
|
||||||
private Model model;
|
private Model model;
|
||||||
|
@ -65,42 +63,50 @@ public class MainViewController implements ModelListener {
|
||||||
public void load() {
|
public void load() {
|
||||||
FileChooser chooser = new FileChooser();
|
FileChooser chooser = new FileChooser();
|
||||||
chooser.setTitle("Load a Cluster");
|
chooser.setTitle("Load a Cluster");
|
||||||
chooser.setInitialDirectory(ClusterLoader.CLUSTER_PATH.toFile());
|
chooser.setInitialDirectory(ClusterIO.CLUSTER_PATH.toFile());
|
||||||
chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts"));
|
chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts"));
|
||||||
File file = chooser.showOpenDialog(this.clusterTreeView.getScene().getWindow());
|
File file = chooser.showOpenDialog(this.clusterTreeView.getScene().getWindow());
|
||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
ClusterLoader loader = new ClusterLoader();
|
ClusterIO loader = new ClusterIO();
|
||||||
var password = loader.promptPassword();
|
var password = this.promptPassword();
|
||||||
if (password.isEmpty() || password.get().isEmpty()) return;
|
Cluster cluster;
|
||||||
try {
|
try {
|
||||||
var cluster = loader.load(file.toPath(), password.get());
|
if (password.isEmpty() || password.get().isEmpty()) {
|
||||||
model.setActiveCluster(cluster);
|
cluster = loader.loadUnencrypted(file.toPath());
|
||||||
model.setActiveClusterPath(file.toPath());
|
} else {
|
||||||
|
cluster = loader.load(file.toPath(), password.get());
|
||||||
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
new Alert(Alert.AlertType.WARNING, "Could not load cluster.").showAndWait();
|
new Alert(Alert.AlertType.WARNING, "Could not load cluster.").showAndWait();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
model.setActiveCluster(cluster);
|
||||||
|
model.setActiveClusterPath(file.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void save() {
|
public void save() {
|
||||||
if (model.getActiveCluster() == null) return;
|
if (model.getActiveCluster() == null) return;
|
||||||
ClusterLoader loader = new ClusterLoader();
|
ClusterIO loader = new ClusterIO();
|
||||||
Path path = model.getActiveClusterPath();
|
Path path = model.getActiveClusterPath();
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
FileChooser chooser = new FileChooser();
|
FileChooser chooser = new FileChooser();
|
||||||
chooser.setTitle("Save Cluster");
|
chooser.setTitle("Save Cluster");
|
||||||
chooser.setInitialDirectory(ClusterLoader.CLUSTER_PATH.toFile());
|
chooser.setInitialDirectory(ClusterIO.CLUSTER_PATH.toFile());
|
||||||
chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts"));
|
chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts"));
|
||||||
File file = chooser.showSaveDialog(this.clusterTreeView.getScene().getWindow());
|
File file = chooser.showSaveDialog(this.clusterTreeView.getScene().getWindow());
|
||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
path = file.toPath();
|
path = file.toPath();
|
||||||
}
|
}
|
||||||
var password = loader.promptPassword();
|
var password = this.promptPassword();
|
||||||
if (password.isEmpty() || password.get().isEmpty()) return;
|
|
||||||
try {
|
try {
|
||||||
new ClusterLoader().save(model.getActiveCluster(), path, password.get());
|
if (password.isEmpty() || password.get().isEmpty()) {
|
||||||
} catch (IOException | GeneralSecurityException e) {
|
loader.saveUnencrypted(model.getActiveCluster(), path);
|
||||||
|
} else {
|
||||||
|
loader.save(model.getActiveCluster(), path, password.get());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster.");
|
var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster.");
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
|
@ -113,4 +119,23 @@ public class MainViewController implements ModelListener {
|
||||||
model.setActiveCluster(c);
|
model.setActiveCluster(c);
|
||||||
model.setActiveClusterPath(null);
|
model.setActiveClusterPath(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Optional<String> promptPassword() {
|
||||||
|
Dialog<String> d = new Dialog<>();
|
||||||
|
d.setTitle("Enter Password");
|
||||||
|
d.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
||||||
|
|
||||||
|
PasswordField pwField = new PasswordField();
|
||||||
|
VBox content = new VBox(10);
|
||||||
|
content.setAlignment(Pos.CENTER);
|
||||||
|
content.getChildren().addAll(new Label("Enter password"), pwField);
|
||||||
|
d.getDialogPane().setContent(content);
|
||||||
|
d.setResultConverter(param -> {
|
||||||
|
if (param == ButtonType.OK) {
|
||||||
|
return pwField.getText();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
return d.showAndWait();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
package nl.andrewlalis.crystalkeep.io;
|
||||||
|
|
||||||
|
import nl.andrewlalis.crystalkeep.io.serialization.ClusterSerializer;
|
||||||
|
import nl.andrewlalis.crystalkeep.model.Cluster;
|
||||||
|
|
||||||
|
import javax.crypto.*;
|
||||||
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.PBEKeySpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.spec.InvalidKeySpecException;
|
||||||
|
import java.security.spec.KeySpec;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is responsible for reading and writing clusters to files, which
|
||||||
|
* may be encrypted with a password-based AES-256 CBC.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* All saved files start with a single byte with value 1 to indicate that
|
||||||
|
* the file is encrypted, or 0 if it is not encrypted. If encrypted, the
|
||||||
|
* next 8 bytes will contain the salt that was used to encrypt the file.
|
||||||
|
* Encrypted files will then contain a 16 byte initialization vector.
|
||||||
|
* The remaining part of the file is simply the contents.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see nl.andrewlalis.crystalkeep.io.serialization.ClusterSerializer
|
||||||
|
*/
|
||||||
|
public class ClusterIO {
|
||||||
|
public static final Path CLUSTER_PATH = Path.of("clusters");
|
||||||
|
|
||||||
|
private final SecureRandom random;
|
||||||
|
|
||||||
|
public ClusterIO() {
|
||||||
|
try {
|
||||||
|
this.random = SecureRandom.getInstanceStrong();
|
||||||
|
} catch (NoSuchAlgorithmException e) {
|
||||||
|
throw new RuntimeException("Could not initialize secure random.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an encrypted cluster from the given path, using the given password
|
||||||
|
* as the secret key value.
|
||||||
|
* @param path The path of the file to load from.
|
||||||
|
* @param password The password to use to decrypt the contents.
|
||||||
|
* @return The cluster that was loaded.
|
||||||
|
* @throws Exception If the file could not be found, or could not be
|
||||||
|
* decrypted and read properly.
|
||||||
|
*/
|
||||||
|
public Cluster load(Path path, String password) throws Exception {
|
||||||
|
try (var is = Files.newInputStream(path)) {
|
||||||
|
int encryptionFlag = is.read();
|
||||||
|
if (encryptionFlag == 0) throw new IOException("File is not encrypted.");
|
||||||
|
byte[] salt = is.readNBytes(8);
|
||||||
|
byte[] iv = is.readNBytes(16);
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
cipher.init(Cipher.DECRYPT_MODE, this.getSecretKey(password.toCharArray(), salt), new IvParameterSpec(iv));
|
||||||
|
try (CipherInputStream cis = new CipherInputStream(is, cipher)) {
|
||||||
|
return ClusterSerializer.readCluster(cis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a cluster to an encrypted file, using the given password as the
|
||||||
|
* secret key.
|
||||||
|
* @param cluster The cluster to save.
|
||||||
|
* @param path The path to the file to save the cluster at.
|
||||||
|
* @param password The password to use when saving.
|
||||||
|
* @throws IOException If an error occurs when writing the file.
|
||||||
|
* @throws GeneralSecurityException If we could not obtain a secret key.
|
||||||
|
*/
|
||||||
|
public void save(Cluster cluster, Path path, String password) throws IOException, GeneralSecurityException {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||||
|
byte[] iv = new byte[16];
|
||||||
|
this.random.nextBytes(iv);
|
||||||
|
byte[] salt = new byte[8];
|
||||||
|
this.random.nextBytes(salt);
|
||||||
|
cipher.init(Cipher.ENCRYPT_MODE, this.getSecretKey(password.toCharArray(), salt), new IvParameterSpec(iv));
|
||||||
|
try (OutputStream fos = Files.newOutputStream(path)) {
|
||||||
|
fos.write(1);
|
||||||
|
fos.write(salt);
|
||||||
|
fos.write(iv);
|
||||||
|
try (CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
|
||||||
|
ClusterSerializer.writeCluster(cluster, cos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves an unencrypted cluster file.
|
||||||
|
* @param cluster The cluster to save.
|
||||||
|
* @param path The file to save the cluster to.
|
||||||
|
* @throws IOException If the file could not be saved.
|
||||||
|
*/
|
||||||
|
public void saveUnencrypted(Cluster cluster, Path path) throws IOException {
|
||||||
|
Files.createDirectories(path.getParent());
|
||||||
|
try (var os = Files.newOutputStream(path)) {
|
||||||
|
os.write(0);
|
||||||
|
ClusterSerializer.writeCluster(cluster, os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads an unencrypted cluster from a file.
|
||||||
|
* @param path The path to load the cluster from.
|
||||||
|
* @return The cluster that was loaded.
|
||||||
|
* @throws IOException If the file could not be loaded.
|
||||||
|
*/
|
||||||
|
public Cluster loadUnencrypted(Path path) throws IOException {
|
||||||
|
try (var is = Files.newInputStream(path)) {
|
||||||
|
int encryptionFlag = is.read();
|
||||||
|
if (encryptionFlag == 1) throw new IOException("File is encrypted.");
|
||||||
|
return ClusterSerializer.readCluster(is);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecretKey getSecretKey(char[] password, byte[] salt) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
|
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
||||||
|
KeySpec spec = new PBEKeySpec(password, salt, 65536, 256);
|
||||||
|
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package nl.andrewlalis.crystalkeep.model.serialization;
|
package nl.andrewlalis.crystalkeep.io.serialization;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
|
@ -1,4 +1,4 @@
|
||||||
package nl.andrewlalis.crystalkeep.model.serialization;
|
package nl.andrewlalis.crystalkeep.io.serialization;
|
||||||
|
|
||||||
import nl.andrewlalis.crystalkeep.model.Cluster;
|
import nl.andrewlalis.crystalkeep.model.Cluster;
|
||||||
import nl.andrewlalis.crystalkeep.model.Shard;
|
import nl.andrewlalis.crystalkeep.model.Shard;
|
||||||
|
@ -16,7 +16,7 @@ import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static nl.andrewlalis.crystalkeep.model.serialization.ByteUtils.toInt;
|
import static nl.andrewlalis.crystalkeep.io.serialization.ByteUtils.toInt;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This serializer class offers some methods for reading and writing clusters
|
* This serializer class offers some methods for reading and writing clusters
|
|
@ -1,4 +1,4 @@
|
||||||
package nl.andrewlalis.crystalkeep.model.serialization;
|
package nl.andrewlalis.crystalkeep.io.serialization;
|
||||||
|
|
||||||
import nl.andrewlalis.crystalkeep.model.Shard;
|
import nl.andrewlalis.crystalkeep.model.Shard;
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
package nl.andrewlalis.crystalkeep.model.serialization;
|
|
||||||
|
|
||||||
import javafx.geometry.Pos;
|
|
||||||
import javafx.scene.control.*;
|
|
||||||
import javafx.scene.layout.HBox;
|
|
||||||
import javafx.scene.layout.VBox;
|
|
||||||
import nl.andrewlalis.crystalkeep.model.Cluster;
|
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
|
||||||
import javax.crypto.SecretKey;
|
|
||||||
import javax.crypto.SecretKeyFactory;
|
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
|
||||||
import javax.crypto.spec.PBEKeySpec;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.KeySpec;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public class ClusterLoader {
|
|
||||||
public static final Path CLUSTER_PATH = Path.of("clusters");
|
|
||||||
public static final Path DEFAULT_CLUSTER = CLUSTER_PATH.resolve("default.cts");
|
|
||||||
private static final byte[] SALT = "zf9i78vy".getBytes(StandardCharsets.UTF_8);
|
|
||||||
private static final byte[] IV = "Fafioje;a324fsde".getBytes(StandardCharsets.UTF_8);
|
|
||||||
|
|
||||||
public Optional<String> promptPassword() {
|
|
||||||
Dialog<String> d = new Dialog<>();
|
|
||||||
d.setTitle("Enter Password");
|
|
||||||
d.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
|
|
||||||
|
|
||||||
PasswordField pwField = new PasswordField();
|
|
||||||
VBox content = new VBox(10);
|
|
||||||
content.setAlignment(Pos.CENTER);
|
|
||||||
content.getChildren().addAll(new Label("Enter password"), pwField);
|
|
||||||
d.getDialogPane().setContent(content);
|
|
||||||
d.setResultConverter(param -> {
|
|
||||||
if (param == ButtonType.OK) {
|
|
||||||
return pwField.getText();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
return d.showAndWait();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Cluster load(Path path, String password) throws Exception {
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
||||||
cipher.init(Cipher.DECRYPT_MODE, this.getSecretKey(password), new IvParameterSpec(IV));
|
|
||||||
byte[] raw = Files.readAllBytes(path);
|
|
||||||
return ClusterSerializer.readCluster(new ByteArrayInputStream(cipher.doFinal(raw)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save(Cluster cluster, Path path, String password) throws IOException, GeneralSecurityException {
|
|
||||||
Files.createDirectories(CLUSTER_PATH);
|
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
||||||
ClusterSerializer.writeCluster(cluster, bos);
|
|
||||||
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, this.getSecretKey(password), new IvParameterSpec(IV));
|
|
||||||
Files.write(path, cipher.doFinal(bos.toByteArray()));
|
|
||||||
bos.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private SecretKey getSecretKey(String password) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
|
|
||||||
KeySpec spec = new PBEKeySpec(password.toCharArray(), SALT, 65536, 256);
|
|
||||||
return new SecretKeySpec(factory.generateSecret(spec).getEncoded(), "AES");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,8 +2,8 @@ package nl.andrewlalis.crystalkeep.model.shards;
|
||||||
|
|
||||||
import nl.andrewlalis.crystalkeep.model.Shard;
|
import nl.andrewlalis.crystalkeep.model.Shard;
|
||||||
import nl.andrewlalis.crystalkeep.model.ShardType;
|
import nl.andrewlalis.crystalkeep.model.ShardType;
|
||||||
import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils;
|
import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils;
|
||||||
import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer;
|
import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
|
@ -2,8 +2,8 @@ package nl.andrewlalis.crystalkeep.model.shards;
|
||||||
|
|
||||||
import nl.andrewlalis.crystalkeep.model.Shard;
|
import nl.andrewlalis.crystalkeep.model.Shard;
|
||||||
import nl.andrewlalis.crystalkeep.model.ShardType;
|
import nl.andrewlalis.crystalkeep.model.ShardType;
|
||||||
import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils;
|
import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils;
|
||||||
import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer;
|
import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
|
@ -2,11 +2,11 @@ package nl.andrewlalis.crystalkeep.view.shard_details;
|
||||||
|
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.input.Clipboard;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.input.ClipboardContent;
|
||||||
import javafx.scene.control.TextField;
|
|
||||||
import javafx.scene.layout.GridPane;
|
import javafx.scene.layout.GridPane;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.Pane;
|
import javafx.scene.layout.Pane;
|
||||||
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
|
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
|
||||||
|
|
||||||
|
@ -47,13 +47,28 @@ public class LoginCredentialsViewModel extends ShardViewModel<LoginCredentialsSh
|
||||||
passwordsContainer.getChildren().add(passwordField);
|
passwordsContainer.getChildren().add(passwordField);
|
||||||
passwordsContainer.getChildren().add(rawPasswordField);
|
passwordsContainer.getChildren().add(rawPasswordField);
|
||||||
gp.add(passwordsContainer, 1, 1);
|
gp.add(passwordsContainer, 1, 1);
|
||||||
|
|
||||||
|
var passwordActionsPane = new HBox(5);
|
||||||
var showPasswordCheckbox = new CheckBox("Show password");
|
var showPasswordCheckbox = new CheckBox("Show password");
|
||||||
showPasswordCheckbox.setSelected(false);
|
showPasswordCheckbox.setSelected(false);
|
||||||
showPasswordCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
showPasswordCheckbox.selectedProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
passwordField.setVisible(!newValue);
|
passwordField.setVisible(!newValue);
|
||||||
rawPasswordField.setVisible(newValue);
|
rawPasswordField.setVisible(newValue);
|
||||||
});
|
});
|
||||||
gp.add(showPasswordCheckbox, 1, 2);
|
var copyPasswordButton = new Button("Copy to Clipboard");
|
||||||
|
copyPasswordButton.setOnAction(event -> {
|
||||||
|
ClipboardContent content = new ClipboardContent();
|
||||||
|
content.putString(shard.getPassword());
|
||||||
|
Clipboard.getSystemClipboard().setContent(content);
|
||||||
|
});
|
||||||
|
var typePasswordButton = new Button("Auto Type");
|
||||||
|
typePasswordButton.setOnAction(event -> {
|
||||||
|
System.out.println("Not yet implemented.");
|
||||||
|
});
|
||||||
|
typePasswordButton.setDisable(true);
|
||||||
|
passwordActionsPane.getChildren().addAll(showPasswordCheckbox, copyPasswordButton, typePasswordButton);
|
||||||
|
|
||||||
|
gp.add(passwordActionsPane, 1, 2);
|
||||||
return gp;
|
return gp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package nl.andrewlalis.crystalkeep.io;
|
||||||
|
|
||||||
|
import nl.andrewlalis.crystalkeep.model.Cluster;
|
||||||
|
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
|
||||||
|
import nl.andrewlalis.crystalkeep.model.shards.TextShard;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.MethodSource;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.security.GeneralSecurityException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests the functionality of the {@link ClusterIO} class in loading and saving
|
||||||
|
* clusters.
|
||||||
|
*/
|
||||||
|
public class ClusterIOTest {
|
||||||
|
public static List<Cluster> testClusters() {
|
||||||
|
List<Cluster> clusters = new ArrayList<>();
|
||||||
|
clusters.add(new Cluster("Root"));
|
||||||
|
Cluster c1 = new Cluster("C1");
|
||||||
|
c1.addShard(new TextShard("text", LocalDateTime.now(), "hello world!"));
|
||||||
|
clusters.add(c1);
|
||||||
|
Cluster c2 = new Cluster("C2");
|
||||||
|
Cluster c2Nested = new Cluster("Nested");
|
||||||
|
c2Nested.addShard(new LoginCredentialsShard("login", LocalDateTime.now(), "andrew", "pass"));
|
||||||
|
c2Nested.addShard(new TextShard("data", LocalDateTime.now(), "test, 1, 2, 3"));
|
||||||
|
c2.addCluster(c2Nested);
|
||||||
|
c2.addShard(new TextShard("more_data", LocalDateTime.now(), "Antidisestablishmentarianism"));
|
||||||
|
clusters.add(c2);
|
||||||
|
return clusters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("testClusters")
|
||||||
|
public void testUnencryptedIO(Cluster cluster) throws IOException, GeneralSecurityException {
|
||||||
|
Path file = Files.createTempFile(UUID.randomUUID().toString(), "cts");
|
||||||
|
var io = new ClusterIO();
|
||||||
|
// Test normal save and load.
|
||||||
|
io.saveUnencrypted(cluster, file);
|
||||||
|
Cluster loaded = io.loadUnencrypted(file);
|
||||||
|
assertEquals(cluster, loaded);
|
||||||
|
// Test attempting to load an encrypted file, which throws an IOException.
|
||||||
|
io.save(cluster, file, "testpass");
|
||||||
|
assertThrows(IOException.class, () -> io.loadUnencrypted(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@MethodSource("testClusters")
|
||||||
|
public void testEncryptedIO(Cluster cluster) throws Exception {
|
||||||
|
Path file = Files.createTempFile(UUID.randomUUID().toString(), "cts");
|
||||||
|
var io = new ClusterIO();
|
||||||
|
// Test normal save and load.
|
||||||
|
io.save(cluster, file, "test");
|
||||||
|
Cluster loaded = io.load(file, "test");
|
||||||
|
assertEquals(cluster, loaded);
|
||||||
|
// Test attempting to load an unencrypted file, which throws an IOException.
|
||||||
|
io.saveUnencrypted(cluster, file);
|
||||||
|
assertThrows(IOException.class, () -> io.load(file, "test"));
|
||||||
|
// Test attempting to load an encrypted file with the wrong password. An exception is thrown when reading it.
|
||||||
|
io.save(cluster, file, "other");
|
||||||
|
assertThrows(Exception.class, () -> io.load(file, "not_password"));
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package nl.andrewlalis.crystalkeep.model.serialization;
|
package nl.andrewlalis.crystalkeep.io.serialization;
|
||||||
|
|
||||||
import nl.andrewlalis.crystalkeep.model.Cluster;
|
import nl.andrewlalis.crystalkeep.model.Cluster;
|
||||||
import nl.andrewlalis.crystalkeep.model.Shard;
|
import nl.andrewlalis.crystalkeep.model.Shard;
|
||||||
|
@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.*;
|
||||||
* transforming key model components to and from byte arrays for storage.
|
* transforming key model components to and from byte arrays for storage.
|
||||||
*/
|
*/
|
||||||
public class ClusterSerializerTest {
|
public class ClusterSerializerTest {
|
||||||
private static List<Shard> testShardIOData() {
|
public static List<Shard> testShardIOData() {
|
||||||
return List.of(
|
return List.of(
|
||||||
new TextShard("a", LocalDateTime.now(), "Hello world!"),
|
new TextShard("a", LocalDateTime.now(), "Hello world!"),
|
||||||
new TextShard("Another", LocalDateTime.now(), "Testing"),
|
new TextShard("Another", LocalDateTime.now(), "Testing"),
|
||||||
|
@ -58,7 +58,7 @@ public class ClusterSerializerTest {
|
||||||
assertArrayEquals(data, data2, "Serialized data from a shard should not change.");
|
assertArrayEquals(data, data2, "Serialized data from a shard should not change.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Cluster> testClusterIOData() {
|
public static List<Cluster> testClusterIOData() {
|
||||||
List<Cluster> clusters = new ArrayList<>();
|
List<Cluster> clusters = new ArrayList<>();
|
||||||
clusters.add(new Cluster("test"));
|
clusters.add(new Cluster("test"));
|
||||||
|
|
Loading…
Reference in New Issue