Added actual AES 256 encryption, and file operations to UI.

This commit is contained in:
Andrew Lalis 2021-05-30 10:48:45 +02:00
parent 5ab1abbfff
commit c09923906c
14 changed files with 356 additions and 84 deletions

View File

@ -2,6 +2,7 @@ module crystalkeep {
requires javafx.fxml;
requires javafx.controls;
opens nl.andrewlalis.crystalkeep.control;
opens nl.andrewlalis.crystalkeep;
exports nl.andrewlalis.crystalkeep.control to javafx.fxml;

View File

@ -3,6 +3,7 @@ package nl.andrewlalis.crystalkeep;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.stage.Stage;
import nl.andrewlalis.crystalkeep.control.MainViewController;
import nl.andrewlalis.crystalkeep.model.Cluster;
@ -13,10 +14,12 @@ import nl.andrewlalis.crystalkeep.model.shards.TextShard;
import java.io.IOException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.time.LocalDateTime;
import java.util.Optional;
public class CrystalKeep extends Application {
public static void main(String[] args) {
public static void main(String[] args) throws GeneralSecurityException {
launch(args);
}
@ -29,7 +32,7 @@ public class CrystalKeep extends Application {
stage.setScene(scene);
stage.setTitle("CrystalKeep");
stage.sizeToScene();
model.setActiveCluster(this.loadRootCluster());
model.setActiveCluster(null);
stage.show();
@ -37,22 +40,33 @@ public class CrystalKeep extends Application {
controller.init(model);
}
private Cluster loadRootCluster() throws IOException {
ClusterLoader clusterLoader = new ClusterLoader();
Cluster rootCluster;
try {
rootCluster = clusterLoader.loadDefault();
System.out.println("Loaded existing root cluster.");
} catch (IOException e) {
rootCluster = new Cluster("Root");
rootCluster.addShard(new TextShard("Example Shard", LocalDateTime.now(), "Hello world!"));
rootCluster.addShard(new LoginCredentialsShard("Netflix", LocalDateTime.now(), "user", "secret password"));
for (int i = 0; i < 100; i++) {
rootCluster.addShard(new TextShard("test " + i, LocalDateTime.now(), "value: " + i));
}
clusterLoader.saveDefault(rootCluster);
System.out.println("Saved root cluster on first load.");
}
return rootCluster;
}
// private Cluster loadRootCluster() throws IOException, GeneralSecurityException {
// ClusterLoader clusterLoader = new ClusterLoader();
// Cluster rootCluster;
//
//// Optional<Cluster> oc = clusterLoader.load(ClusterLoader.DEFAULT_CLUSTER);
//// if (oc.isEmpty()) {
//// new Alert(Alert.AlertType.ERROR, "Could not load cluster.").show();
//// System.exit(1);
//// }
//// return oc.get();
//
//// try {
//// long start = System.currentTimeMillis();
//// rootCluster = clusterLoader.load(ClusterLoader.DEFAULT_CLUSTER);
//// long dur = System.currentTimeMillis() - start;
//// System.out.println("Loaded existing root cluster in " + dur + " ms.");
//// } catch (Exception e) {
//// e.printStackTrace();
//// rootCluster = new Cluster("Root");
//// rootCluster.addShard(new TextShard("Example Shard", LocalDateTime.now(), "Hello world!"));
//// rootCluster.addShard(new LoginCredentialsShard("Netflix", LocalDateTime.now(), "user", "secret password"));
//// for (int i = 0; i < 100; i++) {
//// rootCluster.addShard(new TextShard("test " + i, LocalDateTime.now(), "value: " + i));
//// }
//// clusterLoader.save(rootCluster, ClusterLoader.DEFAULT_CLUSTER, "test");
//// System.out.println("Saved root cluster on first load.");
//// }
//// return rootCluster;
// }
}

View File

@ -0,0 +1,31 @@
package nl.andrewlalis.crystalkeep.control;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextInputDialog;
import nl.andrewlalis.crystalkeep.model.Cluster;
import nl.andrewlalis.crystalkeep.model.Model;
public class AddClusterHandler implements EventHandler<ActionEvent> {
private final Cluster cluster;
private final Model model;
public AddClusterHandler(Cluster cluster, Model model) {
this.cluster = cluster;
this.model = model;
}
@Override
public void handle(ActionEvent event) {
Dialog<String> d = new TextInputDialog();
d.setContentText("Enter the name of the new cluster.");
d.setHeaderText(null);
d.setTitle("Add Cluster");
d.setGraphic(null);
d.showAndWait().ifPresent(s -> {
cluster.addCluster(new Cluster(s.trim()));
model.notifyListeners();
});
}
}

View File

@ -0,0 +1,56 @@
package nl.andrewlalis.crystalkeep.control;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ChoiceDialog;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextInputDialog;
import nl.andrewlalis.crystalkeep.model.Cluster;
import nl.andrewlalis.crystalkeep.model.Model;
import nl.andrewlalis.crystalkeep.model.Shard;
import nl.andrewlalis.crystalkeep.model.ShardType;
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
import nl.andrewlalis.crystalkeep.model.shards.TextShard;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class AddShardHandler implements EventHandler<ActionEvent> {
private final Cluster cluster;
private final Model model;
public AddShardHandler(Cluster cluster, Model model) {
this.cluster = cluster;
this.model = model;
}
@Override
public void handle(ActionEvent event) {
Dialog<String> d = new TextInputDialog();
d.setContentText("Enter the name of the new shard.");
d.showAndWait().ifPresent(s -> {
List<String> choices = Arrays.stream(ShardType.values())
.map(Enum::name)
.collect(Collectors.toList());
Dialog<String> d1 = new ChoiceDialog<>("TEXT", choices);
d1.setContentText("Choose the type of shard to create.");
d1.showAndWait().ifPresent(typeName -> {
ShardType type = ShardType.valueOf(typeName.toUpperCase());
Shard shard;
switch (type) {
case TEXT:
shard = new TextShard(s);
break;
case LOGIN_CREDENTIALS:
shard = new LoginCredentialsShard(s);
break;
default:
throw new IllegalStateException("Invalid shard type selected.");
}
cluster.addShard(shard);
model.notifyListeners();
});
});
}
}

View File

@ -0,0 +1,46 @@
package nl.andrewlalis.crystalkeep.control;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.VBox;
import nl.andrewlalis.crystalkeep.model.CrystalItem;
import nl.andrewlalis.crystalkeep.model.Shard;
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
import nl.andrewlalis.crystalkeep.model.shards.TextShard;
import nl.andrewlalis.crystalkeep.view.ShardTreeItem;
import nl.andrewlalis.crystalkeep.view.shard_details.LoginCredentialsPane;
import nl.andrewlalis.crystalkeep.view.shard_details.ShardPane;
import nl.andrewlalis.crystalkeep.view.shard_details.TextShardPane;
import java.util.HashMap;
import java.util.Map;
public class ClusterTreeViewItemSelectionListener implements ChangeListener<TreeItem<CrystalItem>> {
private static final Map<Class<? extends Shard>, Class<? extends ShardPane<? extends Shard>>> shardPanesMap = new HashMap<>();
static {
shardPanesMap.put(TextShard.class, TextShardPane.class);
shardPanesMap.put(LoginCredentialsShard.class, LoginCredentialsPane.class);
}
private final VBox shardDetailContainer;
public ClusterTreeViewItemSelectionListener(VBox shardDetailContainer) {
this.shardDetailContainer = shardDetailContainer;
}
@Override
public void changed(ObservableValue<? extends TreeItem<CrystalItem>> observable, TreeItem<CrystalItem> oldValue, TreeItem<CrystalItem> newValue) {
shardDetailContainer.getChildren().clear();
if (newValue instanceof ShardTreeItem) {
var node = (ShardTreeItem) newValue;
var paneClass = shardPanesMap.get(node.getShard().getClass());
try {
var pane = paneClass.getDeclaredConstructor(node.getShard().getClass()).newInstance(node.getShard());
shardDetailContainer.getChildren().add(pane);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

View File

@ -0,0 +1,41 @@
package nl.andrewlalis.crystalkeep.control;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Alert;
import javafx.scene.control.ButtonType;
import javafx.scene.control.TreeItem;
import nl.andrewlalis.crystalkeep.model.Cluster;
import nl.andrewlalis.crystalkeep.model.CrystalItem;
import nl.andrewlalis.crystalkeep.model.Model;
import nl.andrewlalis.crystalkeep.model.Shard;
import nl.andrewlalis.crystalkeep.view.ClusterTreeItem;
import java.util.Optional;
public class DeleteItemHandler implements EventHandler<ActionEvent> {
private final TreeItem<CrystalItem> treeItem;
private final Model model;
public DeleteItemHandler(TreeItem<CrystalItem> treeItem, Model model) {
this.treeItem = treeItem;
this.model = model;
}
@Override
public void handle(ActionEvent event) {
if (this.treeItem.getParent() != null && this.treeItem.getParent() instanceof ClusterTreeItem) {
Optional<ButtonType> result = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete this item?").showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
var cluster = ((ClusterTreeItem) this.treeItem.getParent()).getCluster();
var item = this.treeItem.getValue();
if (item instanceof Shard) {
cluster.removeShard((Shard) item);
} else if (item instanceof Cluster) {
cluster.removeCluster((Cluster) item);
}
this.model.notifyListeners();
}
}
}
}

View File

@ -1,32 +1,24 @@
package nl.andrewlalis.crystalkeep.control;
import javafx.event.ActionEvent;
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.scene.layout.VBox;
import javafx.stage.FileChooser;
import nl.andrewlalis.crystalkeep.model.*;
import nl.andrewlalis.crystalkeep.model.serialization.ClusterLoader;
import nl.andrewlalis.crystalkeep.model.shards.LoginCredentialsShard;
import nl.andrewlalis.crystalkeep.model.shards.TextShard;
import nl.andrewlalis.crystalkeep.view.ClusterTreeItem;
import nl.andrewlalis.crystalkeep.view.CrystalItemTreeCell;
import nl.andrewlalis.crystalkeep.view.ShardTreeItem;
import nl.andrewlalis.crystalkeep.view.shard_details.LoginCredentialsPane;
import nl.andrewlalis.crystalkeep.view.shard_details.ShardPane;
import nl.andrewlalis.crystalkeep.view.shard_details.TextShardPane;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
public class MainViewController implements ModelListener {
private static final Map<Class<? extends Shard>, Class<? extends ShardPane<? extends Shard>>> shardPanesMap = new HashMap<>();
static {
shardPanesMap.put(TextShard.class, TextShardPane.class);
shardPanesMap.put(LoginCredentialsShard.class, LoginCredentialsPane.class);
}
private Model model;
@FXML
@ -40,29 +32,8 @@ public class MainViewController implements ModelListener {
this.activeClusterUpdated();
assert(this.clusterTreeView != null);
this.clusterTreeView.setCellFactory(param -> new CrystalItemTreeCell(this.model));
this.clusterTreeView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
shardDetailContainer.getChildren().clear();
if (newValue instanceof ShardTreeItem) {
var node = (ShardTreeItem) newValue;
var paneClass = shardPanesMap.get(node.getShard().getClass());
try {
var pane = paneClass.getDeclaredConstructor(node.getShard().getClass()).newInstance(node.getShard());
shardDetailContainer.getChildren().add(pane);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
@FXML
public void exit(ActionEvent event) {
System.out.println("Exiting...");
try {
new ClusterLoader().saveDefault(model.getActiveCluster());
} catch (IOException e) {
e.printStackTrace();
}
this.clusterTreeView.getSelectionModel().selectedItemProperty()
.addListener(new ClusterTreeViewItemSelectionListener(this.shardDetailContainer));
}
@Override
@ -84,4 +55,62 @@ public class MainViewController implements ModelListener {
}
return node;
}
@FXML
public void exit() {
Platform.exit();
}
@FXML
public void load() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Load a Cluster");
chooser.setInitialDirectory(ClusterLoader.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;
try {
var cluster = loader.load(file.toPath(), password.get());
model.setActiveCluster(cluster);
model.setActiveClusterPath(file.toPath());
} catch (Exception e) {
e.printStackTrace();
new Alert(Alert.AlertType.WARNING, "Could not load cluster.").showAndWait();
}
}
@FXML
public void save() {
if (model.getActiveCluster() == null) return;
ClusterLoader loader = new ClusterLoader();
Path path = model.getActiveClusterPath();
if (path == null) {
FileChooser chooser = new FileChooser();
chooser.setTitle("Save Cluster");
chooser.setInitialDirectory(ClusterLoader.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;
try {
new ClusterLoader().save(model.getActiveCluster(), path, password.get());
} catch (IOException | GeneralSecurityException e) {
e.printStackTrace();
var alert = new Alert(Alert.AlertType.ERROR, "Could not save cluster.");
alert.showAndWait();
}
}
@FXML
public void newCluster() {
Cluster c = new Cluster("Root");
model.setActiveCluster(c);
model.setActiveClusterPath(null);
}
}

View File

@ -1,11 +1,13 @@
package nl.andrewlalis.crystalkeep.model;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.Set;
public class Model {
private Cluster activeCluster;
private Path activeClusterPath;
private final Set<ModelListener> listeners = new HashSet<>();
@ -13,6 +15,10 @@ public class Model {
return activeCluster;
}
public Path getActiveClusterPath() {
return activeClusterPath;
}
public void addListener(ModelListener listener) {
this.listeners.add(listener);
}
@ -22,6 +28,10 @@ public class Model {
this.notifyListeners();
}
public void setActiveClusterPath(Path activeClusterPath) {
this.activeClusterPath = activeClusterPath;
}
public void notifyListeners() {
this.listeners.forEach(ModelListener::activeClusterUpdated);
}

View File

@ -1,27 +1,62 @@
package nl.andrewlalis.crystalkeep.model.serialization;
import javafx.scene.control.Dialog;
import javafx.scene.control.TextInputDialog;
import nl.andrewlalis.crystalkeep.model.Cluster;
import java.io.FileInputStream;
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.io.InputStream;
import java.io.OutputStream;
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 {
private static final Path CLUSTER_PATH = Path.of("clusters");
private static final Path DEFAULT_CLUSTER = CLUSTER_PATH.resolve("default.cts");
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 Cluster loadDefault() throws IOException {
InputStream is = new FileInputStream(DEFAULT_CLUSTER.toFile());
return ClusterSerializer.readCluster(is);
public Optional<String> promptPassword() {
Dialog<String> d = new TextInputDialog();
d.setContentText("Enter the password");
d.setGraphic(null);
d.setHeaderText(null);
d.setTitle("Enter Password");
return d.showAndWait();
}
public void saveDefault(Cluster cluster) throws IOException {
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);
OutputStream os = Files.newOutputStream(DEFAULT_CLUSTER);
ClusterSerializer.writeCluster(cluster, os);
os.close();
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");
}
}

View File

@ -19,6 +19,10 @@ public class LoginCredentialsShard extends Shard {
this.password = password;
}
public LoginCredentialsShard(String name) {
this(name, LocalDateTime.now(), "", "");
}
public String getUsername() {
return username;
}

View File

@ -17,6 +17,10 @@ public class TextShard extends Shard {
this.text = text;
}
public TextShard(String name) {
this(name, LocalDateTime.now(), "");
}
public String getText() {
return text;
}

View File

@ -5,10 +5,12 @@ import javafx.scene.control.MenuItem;
import javafx.scene.control.TreeCell;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import nl.andrewlalis.crystalkeep.control.AddClusterHandler;
import nl.andrewlalis.crystalkeep.control.AddShardHandler;
import nl.andrewlalis.crystalkeep.control.DeleteItemHandler;
import nl.andrewlalis.crystalkeep.model.Cluster;
import nl.andrewlalis.crystalkeep.model.CrystalItem;
import nl.andrewlalis.crystalkeep.model.Model;
import nl.andrewlalis.crystalkeep.model.Shard;
import java.io.InputStream;
@ -27,28 +29,23 @@ public class CrystalItemTreeCell extends TreeCell<CrystalItem> {
if (!empty) {
ContextMenu menu = new ContextMenu();
if (item instanceof Cluster) {
var cluster = (Cluster) item;
var addShardItem = new MenuItem("Add Shard");
addShardItem.setOnAction(new AddShardHandler(cluster, model));
var addClusterItem = new MenuItem("Add Cluster");
addClusterItem.setOnAction(new AddClusterHandler(cluster, model));
menu.getItems().addAll(addShardItem, addClusterItem);
}
var deleteItem = new MenuItem("Delete");
deleteItem.setOnAction(event -> {
if (this.getTreeItem().getParent() != null && this.getTreeItem().getParent() instanceof ClusterTreeItem) {
var cluster = ((ClusterTreeItem) this.getTreeItem().getParent()).getCluster();
if (item instanceof Shard) {
cluster.removeShard((Shard) item);
} else if (item instanceof Cluster) {
cluster.removeCluster((Cluster) item);
}
this.model.notifyListeners();
}
});
menu.getItems().add(deleteItem);
if (this.getTreeItem().getParent() != null && this.getTreeItem().getParent() instanceof ClusterTreeItem) {
var deleteItem = new MenuItem("Delete");
deleteItem.setOnAction(new DeleteItemHandler(this.getTreeItem(), this.model));
menu.getItems().add(deleteItem);
}
this.setText(item.getName());
InputStream is = getClass().getResourceAsStream(item.getIconPath());
if (is != null) {
ImageView icon = new ImageView(new Image(is));
icon.setFitHeight(24);
icon.setFitHeight(16);
icon.setPreserveRatio(true);
this.setGraphic(icon);
}

View File

@ -12,6 +12,7 @@ public class TextShardPane extends ShardPane<TextShard> {
@Override
protected Node getContent(TextShard shard) {
var textArea = new TextArea(shard.getText());
textArea.setWrapText(true);
textArea.textProperty().addListener((observable, oldValue, newValue) -> {
shard.setText(newValue);
});

View File

@ -13,6 +13,9 @@
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="nl.andrewlalis.crystalkeep.control.MainViewController">
<MenuBar>
<Menu text="File">
<MenuItem text="New" onAction="#newCluster" />
<MenuItem text="Load" onAction="#load" />
<MenuItem text="Save" onAction="#save" />
<MenuItem text="Exit" onAction="#exit" />
</Menu>
</MenuBar>