From 0c5cd959057453f79fa5ca96b447878d859e03df Mon Sep 17 00:00:00 2001 From: Andrew Lalis Date: Mon, 31 May 2021 00:17:31 +0200 Subject: [PATCH] Added ClusterIOTest and improved some stuff. --- pom.xml | 7 +- .../control/MainViewController.java | 63 ++++++--- .../andrewlalis/crystalkeep/io/ClusterIO.java | 130 ++++++++++++++++++ .../serialization/ByteUtils.java | 2 +- .../serialization/ClusterSerializer.java | 4 +- .../serialization/ShardSerializer.java | 2 +- .../model/serialization/ClusterLoader.java | 74 ---------- .../model/shards/LoginCredentialsShard.java | 4 +- .../crystalkeep/model/shards/TextShard.java | 4 +- .../LoginCredentialsViewModel.java | 25 +++- .../crystalkeep/io/ClusterIOTest.java | 72 ++++++++++ .../serialization/ClusterSerializerTest.java | 6 +- 12 files changed, 283 insertions(+), 110 deletions(-) create mode 100644 src/main/java/nl/andrewlalis/crystalkeep/io/ClusterIO.java rename src/main/java/nl/andrewlalis/crystalkeep/{model => io}/serialization/ByteUtils.java (95%) rename src/main/java/nl/andrewlalis/crystalkeep/{model => io}/serialization/ClusterSerializer.java (96%) rename src/main/java/nl/andrewlalis/crystalkeep/{model => io}/serialization/ShardSerializer.java (85%) delete mode 100644 src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java create mode 100644 src/test/java/nl/andrewlalis/crystalkeep/io/ClusterIOTest.java rename src/test/java/nl/andrewlalis/crystalkeep/{model => io}/serialization/ClusterSerializerTest.java (95%) diff --git a/pom.xml b/pom.xml index 5fa2bc0..a28633b 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,11 @@ maven-compiler-plugin 3.8.1 + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + @@ -60,7 +65,7 @@ org.junit.jupiter junit-jupiter-params - 5.7.0 + 5.7.1 test diff --git a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java index c2ac4f0..8395b13 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/control/MainViewController.java @@ -2,21 +2,19 @@ package nl.andrewlalis.crystalkeep.control; import javafx.application.Platform; import javafx.fxml.FXML; -import javafx.scene.control.Alert; -import javafx.scene.control.TreeItem; -import javafx.scene.control.TreeView; +import javafx.geometry.Pos; +import javafx.scene.control.*; import javafx.scene.layout.VBox; import javafx.stage.FileChooser; +import nl.andrewlalis.crystalkeep.io.ClusterIO; import nl.andrewlalis.crystalkeep.model.*; -import nl.andrewlalis.crystalkeep.model.serialization.ClusterLoader; import nl.andrewlalis.crystalkeep.view.ClusterTreeItem; import nl.andrewlalis.crystalkeep.view.CrystalItemTreeCell; import nl.andrewlalis.crystalkeep.view.ShardTreeItem; import java.io.File; -import java.io.IOException; import java.nio.file.Path; -import java.security.GeneralSecurityException; +import java.util.Optional; public class MainViewController implements ModelListener { private Model model; @@ -65,42 +63,50 @@ public class MainViewController implements ModelListener { public void load() { FileChooser chooser = new FileChooser(); 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")); File file = chooser.showOpenDialog(this.clusterTreeView.getScene().getWindow()); if (file == null) return; - ClusterLoader loader = new ClusterLoader(); - var password = loader.promptPassword(); - if (password.isEmpty() || password.get().isEmpty()) return; + ClusterIO loader = new ClusterIO(); + var password = this.promptPassword(); + Cluster cluster; try { - var cluster = loader.load(file.toPath(), password.get()); - model.setActiveCluster(cluster); - model.setActiveClusterPath(file.toPath()); + if (password.isEmpty() || password.get().isEmpty()) { + cluster = loader.loadUnencrypted(file.toPath()); + } else { + cluster = loader.load(file.toPath(), password.get()); + } } catch (Exception e) { e.printStackTrace(); new Alert(Alert.AlertType.WARNING, "Could not load cluster.").showAndWait(); + return; } + model.setActiveCluster(cluster); + model.setActiveClusterPath(file.toPath()); } @FXML public void save() { if (model.getActiveCluster() == null) return; - ClusterLoader loader = new ClusterLoader(); + ClusterIO loader = new ClusterIO(); Path path = model.getActiveClusterPath(); if (path == null) { FileChooser chooser = new FileChooser(); chooser.setTitle("Save Cluster"); - chooser.setInitialDirectory(ClusterLoader.CLUSTER_PATH.toFile()); + chooser.setInitialDirectory(ClusterIO.CLUSTER_PATH.toFile()); chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Cluster Files", "cts")); File file = chooser.showSaveDialog(this.clusterTreeView.getScene().getWindow()); if (file == null) return; path = file.toPath(); } - var password = loader.promptPassword(); - if (password.isEmpty() || password.get().isEmpty()) return; + var password = this.promptPassword(); try { - new ClusterLoader().save(model.getActiveCluster(), path, password.get()); - } catch (IOException | GeneralSecurityException e) { + if (password.isEmpty() || password.get().isEmpty()) { + loader.saveUnencrypted(model.getActiveCluster(), path); + } else { + loader.save(model.getActiveCluster(), path, password.get()); + } + } catch (Exception e) { e.printStackTrace(); var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster."); alert.showAndWait(); @@ -113,4 +119,23 @@ public class MainViewController implements ModelListener { model.setActiveCluster(c); model.setActiveClusterPath(null); } + + private Optional promptPassword() { + Dialog 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(); + } } diff --git a/src/main/java/nl/andrewlalis/crystalkeep/io/ClusterIO.java b/src/main/java/nl/andrewlalis/crystalkeep/io/ClusterIO.java new file mode 100644 index 0000000..26f24c5 --- /dev/null +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/ClusterIO.java @@ -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. + * + *

+ * 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. + *

+ * + * @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"); + } +} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ByteUtils.java b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java similarity index 95% rename from src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ByteUtils.java rename to src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java index 397a1a2..4eec879 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ByteUtils.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ByteUtils.java @@ -1,4 +1,4 @@ -package nl.andrewlalis.crystalkeep.model.serialization; +package nl.andrewlalis.crystalkeep.io.serialization; import java.io.ByteArrayOutputStream; import java.io.IOException; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java similarity index 96% rename from src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java rename to src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java index 1e5fe2e..8603034 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializer.java @@ -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.Shard; @@ -16,7 +16,7 @@ import java.util.HashMap; import java.util.HashSet; 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 diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java similarity index 85% rename from src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java rename to src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java index b789351..1106aec 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ShardSerializer.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/io/serialization/ShardSerializer.java @@ -1,4 +1,4 @@ -package nl.andrewlalis.crystalkeep.model.serialization; +package nl.andrewlalis.crystalkeep.io.serialization; import nl.andrewlalis.crystalkeep.model.Shard; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java b/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java deleted file mode 100644 index 8abfc54..0000000 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterLoader.java +++ /dev/null @@ -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 promptPassword() { - Dialog 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"); - } -} diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java index 5189dba..cc4ce07 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/LoginCredentialsShard.java @@ -2,8 +2,8 @@ package nl.andrewlalis.crystalkeep.model.shards; import nl.andrewlalis.crystalkeep.model.Shard; import nl.andrewlalis.crystalkeep.model.ShardType; -import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; -import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; +import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils; +import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java index 0d42427..729d94c 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/model/shards/TextShard.java @@ -2,8 +2,8 @@ package nl.andrewlalis.crystalkeep.model.shards; import nl.andrewlalis.crystalkeep.model.Shard; import nl.andrewlalis.crystalkeep.model.ShardType; -import nl.andrewlalis.crystalkeep.model.serialization.ByteUtils; -import nl.andrewlalis.crystalkeep.model.serialization.ShardSerializer; +import nl.andrewlalis.crystalkeep.io.serialization.ByteUtils; +import nl.andrewlalis.crystalkeep.io.serialization.ShardSerializer; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java index 989f361..887c0d4 100644 --- a/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java +++ b/src/main/java/nl/andrewlalis/crystalkeep/view/shard_details/LoginCredentialsViewModel.java @@ -2,11 +2,11 @@ package nl.andrewlalis.crystalkeep.view.shard_details; import javafx.geometry.Insets; import javafx.scene.Node; -import javafx.scene.control.CheckBox; -import javafx.scene.control.Label; -import javafx.scene.control.PasswordField; -import javafx.scene.control.TextField; +import javafx.scene.control.*; +import javafx.scene.input.Clipboard; +import javafx.scene.input.ClipboardContent; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard; @@ -47,13 +47,28 @@ public class LoginCredentialsViewModel extends ShardViewModel { passwordField.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; } } diff --git a/src/test/java/nl/andrewlalis/crystalkeep/io/ClusterIOTest.java b/src/test/java/nl/andrewlalis/crystalkeep/io/ClusterIOTest.java new file mode 100644 index 0000000..b854761 --- /dev/null +++ b/src/test/java/nl/andrewlalis/crystalkeep/io/ClusterIOTest.java @@ -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 testClusters() { + List 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")); + } +} diff --git a/src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java b/src/test/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializerTest.java similarity index 95% rename from src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java rename to src/test/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializerTest.java index 7af4f6a..19f38e2 100644 --- a/src/test/java/nl/andrewlalis/crystalkeep/model/serialization/ClusterSerializerTest.java +++ b/src/test/java/nl/andrewlalis/crystalkeep/io/serialization/ClusterSerializerTest.java @@ -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.Shard; @@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; * transforming key model components to and from byte arrays for storage. */ public class ClusterSerializerTest { - private static List testShardIOData() { + public static List testShardIOData() { return List.of( new TextShard("a", LocalDateTime.now(), "Hello world!"), new TextShard("Another", LocalDateTime.now(), "Testing"), @@ -58,7 +58,7 @@ public class ClusterSerializerTest { assertArrayEquals(data, data2, "Serialized data from a shard should not change."); } - private static List testClusterIOData() { + public static List testClusterIOData() { List clusters = new ArrayList<>(); clusters.add(new Cluster("test"));