From 55f2784ba00cd64cc5ceb960b072e1fb3156a57e Mon Sep 17 00:00:00 2001
From: ByteDream <63594396+ByteDream@users.noreply.github.com>
Date: Thu, 3 Sep 2020 22:25:23 +0200
Subject: [PATCH] Add files via upload
---
src/org/bytedream/cryptogx/Controller.java | 1647 +++++++++++++++++
src/org/bytedream/cryptogx/EnDecrypt.java | 302 +++
src/org/bytedream/cryptogx/Main.java | 326 ++++
src/org/bytedream/cryptogx/SecureDelete.java | 90 +
src/org/bytedream/cryptogx/Settings.java | 739 ++++++++
src/org/bytedream/cryptogx/Utils.java | 51 +
.../cryptogx/resources/addSettingsGUI.fxml | 82 +
.../bytedream/cryptogx/resources/close.png | Bin 0 -> 14702 bytes
.../bytedream/cryptogx/resources/cryptoGX.png | Bin 0 -> 51400 bytes
.../cryptogx/resources/exportSettingsGUI.fxml | 24 +
.../cryptogx/resources/loadSettingsGUI.fxml | 25 +
.../bytedream/cryptogx/resources/loading.gif | Bin 0 -> 9270 bytes
.../bytedream/cryptogx/resources/mainGUI.fxml | 98 +
.../bytedream/cryptogx/resources/minimize.png | Bin 0 -> 14698 bytes
14 files changed, 3384 insertions(+)
create mode 100644 src/org/bytedream/cryptogx/Controller.java
create mode 100644 src/org/bytedream/cryptogx/EnDecrypt.java
create mode 100644 src/org/bytedream/cryptogx/Main.java
create mode 100644 src/org/bytedream/cryptogx/SecureDelete.java
create mode 100644 src/org/bytedream/cryptogx/Settings.java
create mode 100644 src/org/bytedream/cryptogx/Utils.java
create mode 100644 src/org/bytedream/cryptogx/resources/addSettingsGUI.fxml
create mode 100644 src/org/bytedream/cryptogx/resources/close.png
create mode 100644 src/org/bytedream/cryptogx/resources/cryptoGX.png
create mode 100644 src/org/bytedream/cryptogx/resources/exportSettingsGUI.fxml
create mode 100644 src/org/bytedream/cryptogx/resources/loadSettingsGUI.fxml
create mode 100644 src/org/bytedream/cryptogx/resources/loading.gif
create mode 100644 src/org/bytedream/cryptogx/resources/mainGUI.fxml
create mode 100644 src/org/bytedream/cryptogx/resources/minimize.png
diff --git a/src/org/bytedream/cryptogx/Controller.java b/src/org/bytedream/cryptogx/Controller.java
new file mode 100644
index 0000000..bb9071c
--- /dev/null
+++ b/src/org/bytedream/cryptogx/Controller.java
@@ -0,0 +1,1647 @@
+package org.bytedream.cryptogx;
+
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.event.Event;
+import javafx.fxml.Initializable;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.Menu;
+import javafx.scene.control.MenuBar;
+import javafx.scene.control.MenuItem;
+import javafx.scene.control.ScrollPane;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TextField;
+import javafx.scene.image.Image;
+import javafx.scene.image.ImageView;
+import javafx.scene.input.*;
+import javafx.scene.layout.AnchorPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.*;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.DataFlavor;
+import java.awt.datatransfer.Transferable;
+import java.awt.datatransfer.UnsupportedFlavorException;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.net.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.CopyOption;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.*;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.bytedream.cryptogx.Settings.*;
+import static org.bytedream.cryptogx.Main.*;
+
+public class Controller implements Initializable {
+
+ private Event fileEnDecryptLabelEvent;
+
+ private double menubarX, menubarY;
+ private boolean textLoading = false;
+ private boolean fileEnDecryptLoading = false;
+ private boolean fileDeleteLoading = false;
+ private final AtomicInteger textThreads = new AtomicInteger(0);
+ private final AtomicInteger totalThreads = new AtomicInteger(0);
+ private final int tooltipShow = 15;
+ private final int DATAFILEURL = 2;
+ private final int FILEFILEURL = 1;
+ private final int NONSPECIFICFILEURL = 0;
+ private final byte[] buffer = new byte[64];
+ private final KeyCombination paste = new KeyCodeCombination(KeyCode.V, KeyCombination.CONTROL_DOWN);
+ private final Image loadingImage = new Image(getClass().getResource("resources/loading.gif").toExternalForm());
+
+ private HashMap currentConfigSettings = new HashMap<>();
+
+ private final HashMap> enDecryptInputOutputFiles = new HashMap<>();
+ private final HashMap> enDecryptInputOutputInternetFiles = new HashMap<>();
+ private final HashMap enDecryptInputOutputClipboardImages = new HashMap<>();
+ private final HashMap deleteInputFiles = new HashMap<>();
+ private final List fileEnDecryptThreads = Collections.synchronizedList(new ArrayList<>());
+ private final List fileDeleteThreads = Collections.synchronizedList(new ArrayList<>());
+
+ private final ContextMenu fileEnDecryptInputContextMenu = new ContextMenu();
+ private final ContextMenu fileDeleteInputContextMenu = new ContextMenu();
+ private Label choosedLabel = null;
+ private String choosedLabelType = null;
+ private final MenuItem fileOutputFileChangeDest = new MenuItem("Change output file");
+ private final MenuItem getChoosedLabelInputFileFolder = new MenuItem("Open source directory");
+ private final MenuItem getChoosedLabelOutputFileFolder = new MenuItem("Open source directory");
+ private final Tooltip tooltip = new Tooltip();
+
+ public AnchorPane rootWindow;
+
+ public Button fileEnDecryptFilesButton;
+ public Button fileDecrypt;
+ public Button fileEncrypt;
+ public Button fileEnDecryptStop;
+
+ public ComboBox textAlgorithmBox;
+ public ComboBox fileEnDecryptAlgorithmBox;
+
+ public ImageView minimizeWindow;
+ public ImageView closeWindow;
+ public ImageView textLoadingImage;
+ public ImageView fileEnDecryptLoadingImage;
+ public ImageView fileDeleteLoadingImage;
+
+ public Menu settingsMenu;
+ public Menu helpMenu;
+
+ public MenuBar menubar;
+
+ public MenuItem setDefaultOutputPath;
+ public MenuItem saveSettings;
+ public MenuItem loadSettings;
+ public MenuItem exportSettings;
+ public MenuItem importSettings;
+
+ public RadioMenuItem removeFileFromFileBox;
+ public RadioMenuItem limitNumberOfThreads;
+
+ public ScrollPane fileEnDecryptInputScroll;
+
+ public TextArea textDecryptedEntry;
+ public TextArea textEncryptedEntry;
+
+ public TextField textKeyEntry;
+ public TextField textSaltEntry;
+ public TextField fileEnDecryptKeyEntry;
+ public TextField fileDecryptOutputFile;
+ public TextField fileEncryptOutputFile;
+ public TextField fileEnDecryptSaltEntry;
+ public TextField fileDeleteIterationsEntry;
+
+ public VBox fileEnDecryptInputFiles;
+ public VBox fileDeleteInputFiles;
+
+ //-----general-----//
+
+ /**
+ * Shows a tooltip when the user type in some text in a text field, text area, etc. and the mouse is over this entry
+ *
+ * @param event from which this method is called
+ */
+ public void keyTypedTooltip(KeyEvent event) {
+ String id = null;
+ String text = "";
+ try {
+ id = ((TextField) event.getSource()).getId();
+ text = ((TextField) event.getSource()).getText() + event.getCharacter();
+ tooltip.setText(text);
+ } catch (ClassCastException e) {
+ tooltip.setText(((TextArea) event.getSource()).getText() + event.getCharacter());
+ }
+ if (id != null) {
+ switch (id) {
+ case ("textKeyEntry"):
+ currentConfigSettings.replace("textKey", text);
+ break;
+ case ("textSaltEntry"):
+ currentConfigSettings.replace("textSalt", text);
+ break;
+ case ("fileEnDecryptKeyEntry"):
+ currentConfigSettings.replace("fileEnDecryptKey", text);
+ break;
+ case ("fileEnDecryptSaltEntry"):
+ currentConfigSettings.replace("fileEnDecryptSalt", text);
+ break;
+ case ("fileDeleteIterationsEntry"):
+ currentConfigSettings.replace("fileDeleteIterations", String.valueOf(Integer.parseInt(text)));
+ break;
+ }
+ }
+ }
+
+ /**
+ * Shows a tooltip when to mouse is over a text field, text area, etc.
+ *
+ * @param event from which this method is called
+ */
+ public void mouseOverEntryTooltip(MouseEvent event) {
+ try {
+ tooltip.setText(((TextField) event.getSource()).getText());
+ } catch (ClassCastException e) {
+ try {
+ tooltip.setText(((TextArea) event.getSource()).getText());
+ } catch (ClassCastException ex) {
+ tooltip.setText(((Label) event.getSource()).getText());
+ }
+ }
+ if (!tooltip.getText().trim().isEmpty()) {
+ tooltip.show(rootWindow.getScene().getWindow(), event.getScreenX(), event.getScreenY() + tooltipShow);
+ }
+ }
+
+ /**
+ * Hides the tooltip if the mouse exit a text field, text area, etc.
+ */
+ public void mouseExitEntryTooltip() {
+ tooltip.hide();
+ }
+
+ //-----menu / close bar-----//
+
+ /**
+ * Closed the application.
+ * Get called if red close button is pressed
+ *
+ * @since 1.0.0
+ */
+ public void closeApplication() {
+ Stage rootStage = (Stage) rootWindow.getScene().getWindow();
+ rootStage.close();
+ System.exit(0);
+ }
+
+ /**
+ * Hides the application.
+ * Get called if the green minimize button is pressed
+ *
+ * @since 1.0.0
+ */
+ public void minimizeApplication() {
+ Stage rootStage = (Stage) rootWindow.getScene().getWindow();
+ rootStage.setIconified(true);
+ }
+
+ //-----text-----//
+
+ /**
+ * Encrypt text in {@link Controller#textDecryptedEntry}.
+ * Get called if the text 'Encrypt' button is pressed
+ *
+ * @since 1.0.0
+ */
+ public void textEncryptButton() {
+ final byte[] salt;
+ if (!textSaltEntry.getText().isEmpty()) {
+ salt = textSaltEntry.getText().getBytes(StandardCharsets.UTF_8);
+ } else {
+ salt = new byte[16];
+ }
+ if (!textLoading) {
+ textLoadingImage.setImage(loadingImage);
+ }
+ Thread textEncrypt = new Thread(() -> {
+ textThreads.getAndIncrement();
+ if (limitNumberOfThreads.isSelected()) {
+ while (totalThreads.get() >= Runtime.getRuntime().availableProcessors()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().stop();
+ }
+ }
+ }
+ totalThreads.getAndIncrement();
+ String textAlgorithm = textAlgorithmBox.getSelectionModel().getSelectedItem();
+ EnDecrypt.AES encrypt = new EnDecrypt.AES(textKeyEntry.getText(), salt, Integer.parseInt(textAlgorithm.substring(textAlgorithm.indexOf('-') + 1)));
+ try {
+ String encryptedText = encrypt.encrypt(textDecryptedEntry.getText());
+ Platform.runLater(() -> textEncryptedEntry.setText(encryptedText));
+ } catch (NoSuchPaddingException | InvalidKeySpecException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException e) {
+ e.printStackTrace();
+ } catch (IllegalArgumentException | IllegalBlockSizeException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("Wrong text for encryption is given", e.getMessage()));
+ }
+ if ((textThreads.get() - 1) <= 0) {
+ textLoadingImage.setImage(null);
+ textLoading = false;
+ }
+ textThreads.getAndDecrement();
+ totalThreads.getAndDecrement();
+ });
+ textEncrypt.setDaemon(false);
+ textEncrypt.start();
+ textLoading = true;
+ }
+
+ /**
+ * Decrypt text in {@link Controller#textEncryptedEntry}.
+ * Get called if the text 'Decrypt' button is pressed
+ *
+ * @since 1.0.0
+ */
+ public void textDecryptButton() {
+ final byte[] salt;
+ if (!textSaltEntry.getText().isEmpty()) {
+ salt = textSaltEntry.getText().getBytes(StandardCharsets.UTF_8);
+ } else {
+ salt = new byte[16];
+ }
+ if (!textLoading) {
+ textLoadingImage.setImage(loadingImage);
+ }
+ Thread textDecrypt = new Thread(() -> {
+ textThreads.getAndIncrement();
+ if (limitNumberOfThreads.isSelected()) {
+ while (totalThreads.get() >= Runtime.getRuntime().availableProcessors()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().stop();
+ }
+ }
+ }
+ totalThreads.getAndIncrement();
+ String textAlgorithm = textAlgorithmBox.getSelectionModel().getSelectedItem();
+ EnDecrypt.AES decrypt = new EnDecrypt.AES(textKeyEntry.getText(), salt, Integer.parseInt(textAlgorithm.substring(textAlgorithm.indexOf('-') + 1)));
+ try {
+ String DecryptedText = decrypt.decrypt(textEncryptedEntry.getText());
+ Platform.runLater(() -> textDecryptedEntry.setText(DecryptedText));
+ } catch (NoSuchPaddingException | InvalidKeySpecException | InvalidKeyException | NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (BadPaddingException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("Wrong key and / or salt is given", e.getMessage()));
+ } catch (IllegalArgumentException | IllegalBlockSizeException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("Wrong text for decryption is given", e.getMessage()));
+ }
+ if ((textThreads.get() - 1) <= 0) {
+ textLoading = false;
+ Platform.runLater(() -> textLoadingImage.setImage(null));
+ }
+ textThreads.getAndDecrement();
+ totalThreads.getAndDecrement();
+ });
+ textDecrypt.setDaemon(false);
+ textDecrypt.start();
+ textLoading = true;
+ }
+
+ //-----fileEnDecrypt-----//
+
+ /**
+ * Synchronized method to get the list of threads which en- / decrypt files
+ *
+ * @return list of en- / decryption threads
+ */
+ private synchronized List getFileEnDecryptThreads() {
+ return fileEnDecryptThreads;
+ }
+
+ /**
+ * Synchronized method to get the number of threads which en- / decrypt files
+ *
+ * @return number of en- / decryption threads
+ *
+ * @since 1.2.0
+ */
+ private synchronized int getFileEnDecryptThreadsSize() {
+ return fileEnDecryptThreads.size();
+ }
+
+ /**
+ * Synchronized method to add a thread to the file en- / decryption list of current running file en- / decryption threads
+ *
+ * @param thread that should be added
+ *
+ * @since 1.2.0
+ */
+ private synchronized void addFileEnDecryptThread(Thread thread) {
+ fileEnDecryptThreads.add(thread);
+ }
+
+ /**
+ * Synchronized method to remove a thread from the file en- / decryption list of current running file en- / decryption threads
+ *
+ * @param thread that should be removed
+ *
+ * @since 1.2.0
+ */
+ private synchronized void removeFileEnDecryptThread(Thread thread) {
+ fileEnDecryptThreads.remove(thread);
+ }
+
+ /**
+ * Adds a file for en- / decryption
+ *
+ * @param file that should be added
+ *
+ * @since 1.0.0
+ */
+ private void fileEnDecryptAddFile(File file) {
+ for (Label l: enDecryptInputOutputFiles.keySet()) {
+ if (l.getText().equals(file.getAbsolutePath())) {
+ return;
+ }
+ }
+ Label newLabel = new Label(file.getAbsolutePath());
+ newLabel.setOnKeyTyped(this::keyTypedTooltip);
+ newLabel.setOnMouseMoved(this::mouseOverEntryTooltip);
+ newLabel.setOnMouseExited(event -> mouseExitEntryTooltip());
+ newLabel.setOnMouseClicked(event -> {
+ fileEnDecryptSelected(newLabel);
+ fileOutputFilesChangeText(newLabel, null, null);
+ fileEnDecryptLabelEvent = event;
+ });
+ newLabel.setContextMenu(fileEnDecryptInputContextMenu);
+ String fileAbsolutePath = file.getAbsolutePath();
+ String fileName = file.getName();
+
+ File encryptFile;
+ File decryptFile;
+ String fileOutputPath = file.getParent() + "/";;
+ String fileEnding;
+ ArrayList inputOutputList = new ArrayList<>();
+ if (!currentConfigSettings.get("fileOutputPath").trim().isEmpty()) {
+ fileOutputPath = currentConfigSettings.get("fileOutputPath").trim() + "/";
+ }
+ if (file.isFile()) {
+ fileEnding = ".cryptoGX";
+ } else {
+ fileEnding = "_cryptoGX";
+ }
+ encryptFile = new File(fileOutputPath + fileName + fileEnding);
+ while (encryptFile.exists()) {
+ encryptFile = new File(encryptFile.getAbsolutePath() + fileEnding);
+ }
+ if (fileAbsolutePath.endsWith(".cryptoGX") || fileAbsolutePath.endsWith("_cryptoGX")) {
+ decryptFile = new File(fileOutputPath + fileName.substring(0, fileName.length() - 9));
+ } else {
+ decryptFile = new File(fileOutputPath + fileName + fileEnding);
+ }
+ while (decryptFile.exists()) {
+ decryptFile = new File(decryptFile.getAbsolutePath() + fileEnding);
+ }
+ inputOutputList.add(0, encryptFile);
+ inputOutputList.add(1, decryptFile);
+ fileEnDecryptInputFiles.getChildren().add(newLabel);
+ enDecryptInputOutputFiles.put(newLabel, inputOutputList);
+ }
+
+ /**
+ * Adds an file from the internet for en- / decryption
+ *
+ * @param url of the file
+ * @param fileType of the file
+ * @throws URISyntaxException
+ *
+ * @since 1.5.0
+ */
+ private void fileEnDecryptAddInternetFile(String url, int fileType) throws URISyntaxException {
+ String filename;
+ switch (fileType) {
+ case FILEFILEURL:
+ filename = url.substring(url.lastIndexOf("/") + 1);
+ break;
+ case DATAFILEURL:
+ filename = url.substring(5, url.indexOf("/")) + "." + url.substring(url.indexOf("/") + 1, url.indexOf(";"));
+ break;
+ case NONSPECIFICFILEURL:
+ filename = "unknown" + System.nanoTime();
+ break;
+ default:
+ warningAlert("Cannot read given url '" + url + "'");
+ return;
+ }
+ for (Label l: enDecryptInputOutputInternetFiles.keySet()) {
+ if (l.getText().equals(filename)) {
+ return;
+ }
+ }
+ Label newLabel = new Label(filename);
+ newLabel.setOnKeyTyped(this::keyTypedTooltip);
+ newLabel.setOnMouseMoved(this::mouseOverEntryTooltip);
+ newLabel.setOnMouseExited(event -> mouseExitEntryTooltip());
+ newLabel.setOnMouseClicked(event -> {
+ fileEnDecryptSelected(newLabel);
+ fileOutputFilesChangeText(newLabel, null, null);
+ fileEnDecryptLabelEvent = event;
+ });
+ newLabel.setContextMenu(fileEnDecryptInputContextMenu);
+
+ File encryptFile;
+ File decryptFile;
+ ArrayList fileSpecs = new ArrayList<>();
+ ArrayList inputOutputFiles = new ArrayList<>();
+ fileSpecs.add(0, fileType);
+ fileSpecs.add(1, url);
+ String currentDir = this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
+
+ if (currentConfigSettings.get("fileOutputPath").trim().isEmpty()) {
+ encryptFile = new File(currentDir + "/" + filename + ".cryptoGX");
+ while (encryptFile.isFile()) {
+ encryptFile = new File(encryptFile.getAbsolutePath() + ".cryptoGX");
+ }
+ if (url.endsWith(".cryptoGX") && filename.endsWith(".cryptoGX")) {
+ decryptFile = new File(currentDir + "/" + filename.substring(0, filename.length() - 9));
+ } else {
+ decryptFile = new File(currentDir + "/" + filename);
+ }
+ } else {
+ encryptFile = new File(currentConfigSettings.get("fileOutputPath") + "/" + filename + ".cryptoGX");
+ while (encryptFile.isFile()) {
+ encryptFile = new File(encryptFile.getAbsolutePath() + ".cryptoGX");
+ }
+ if (url.endsWith(".cryptoGX") && filename.endsWith(".cryptoGX")) {
+ decryptFile = new File(currentConfigSettings.get("fileOutputPath") + "/" + filename.substring(0, filename.length() - 9));
+ } else {
+ decryptFile = new File(currentConfigSettings.get("fileOutputPath") + "/" + filename);
+ }
+ }
+ while (decryptFile.isFile()) {
+ decryptFile = new File(decryptFile.getAbsolutePath() + ".cryptoGX");
+ }
+ inputOutputFiles.add(0, encryptFile);
+ inputOutputFiles.add(1, decryptFile);
+
+ fileEnDecryptInputFiles.getChildren().add(newLabel);
+ enDecryptInputOutputInternetFiles.put(newLabel, fileSpecs);
+ enDecryptInputOutputFiles.put(newLabel, inputOutputFiles);
+ }
+
+ /**
+ * Adds an clipboard image for en- / decryption.
+ * This can be a normal image and an image stream
+ *
+ * @param image that should be added
+ * @throws URISyntaxException
+ *
+ * @since 1.7.0
+ */
+ private void fileEnDecryptAddClipboardImage(BufferedImage image) throws URISyntaxException {
+ String filename = "clipboardImage" + System.nanoTime() + ".png";
+ for (Label l: enDecryptInputOutputClipboardImages.keySet()) {
+ if (l.getText().equals(filename)) {
+ return;
+ }
+ }
+ Label newLabel = new Label(filename);
+ newLabel.setOnKeyTyped(this::keyTypedTooltip);
+ newLabel.setOnMouseMoved(this::mouseOverEntryTooltip);
+ newLabel.setOnMouseExited(event -> mouseExitEntryTooltip());
+ newLabel.setOnMouseClicked(event -> {
+ fileEnDecryptSelected(newLabel);
+ fileOutputFilesChangeText(newLabel, null, null);
+ fileEnDecryptLabelEvent = event;
+ });
+ newLabel.setContextMenu(fileEnDecryptInputContextMenu);
+
+ File encryptFile;
+ File decryptFile;
+ String currentDir = this.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
+ ArrayList inputOutputFiles = new ArrayList<>();
+
+ if (currentConfigSettings.get("fileOutputPath").trim().isEmpty()) {
+ encryptFile = new File(currentDir + "/" + filename + ".cryptoGX");
+ decryptFile = new File(currentDir + "/" + filename);
+ } else {
+ encryptFile = new File(currentConfigSettings.get("fileOutputPath").trim() + "/" + filename + ".cryptoGX");
+ decryptFile = new File(currentConfigSettings.get("fileOutputPath").trim() + "/" + filename);
+ }
+ while (encryptFile.isFile()) {
+ encryptFile = new File(encryptFile.getAbsolutePath() + ".cryptoGX");
+ }
+ while (decryptFile.isFile()) {
+ decryptFile = new File(decryptFile.getAbsolutePath() + ".cryptoGX");
+ }
+ inputOutputFiles.add(0, encryptFile);
+ inputOutputFiles.add(1, decryptFile);
+
+ fileEnDecryptInputFiles.getChildren().add(newLabel);
+ enDecryptInputOutputClipboardImages.put(newLabel, image);
+ enDecryptInputOutputFiles.put(newLabel, inputOutputFiles);
+ }
+
+ /**
+ * Changes the text in the file en- / decryption output file text fields
+ *
+ * @param label
+ * @param encryptOutputFile is the filename of the file it gets encrypted
+ * @param decryptOutputFile is the filename of the file it gets decrypted
+ *
+ * @since 1.2.0
+ */
+ private void fileOutputFilesChangeText(Label label, String encryptOutputFile, String decryptOutputFile) {
+ File encryptFile;
+ File decryptFile;
+ ArrayList change = new ArrayList<>();
+ if (encryptOutputFile == null) {
+ encryptFile = enDecryptInputOutputFiles.get(label).get(0);
+ } else {
+ encryptFile = new File(encryptOutputFile);
+ }
+ if (decryptOutputFile == null) {
+ decryptFile = enDecryptInputOutputFiles.get(label).get(1);
+ } else {
+ decryptFile = new File(decryptOutputFile);
+ }
+ change.add(0, encryptFile);
+ change.add(1, decryptFile);
+ if (encryptFile.toString().trim().isEmpty()) {
+ fileEncryptOutputFile.setText("");
+ } else {
+ fileEncryptOutputFile.setText(encryptFile.getAbsolutePath());
+ }
+ if (decryptFile.toString().trim().isEmpty()) {
+ fileDecryptOutputFile.setText("");
+ } else {
+ fileDecryptOutputFile.setText(decryptFile.getAbsolutePath());
+ }
+ enDecryptInputOutputFiles.replace(label, change);
+ }
+
+ /**
+ * Deletes an entry for en- / decryption.
+ * Get called if the user presses 'del' or delete the entry in the en- / decryption box via the right click tooltip
+ *
+ * @param label that should be deleted
+ *
+ * @since 1.2.0
+ */
+ private void fileEnDecryptDeleteEntry(Label label) {
+ enDecryptInputOutputFiles.remove(label);
+ if (fileEnDecryptInputFiles.getChildren().size() - 1 >= 1) {
+ for (int i = 0; i < fileEnDecryptInputFiles.getChildren().size(); i++) {
+ if (fileEnDecryptInputFiles.getChildren().get(i) == label) {
+ fileEnDecryptInputFiles.getChildren().remove(label);
+ if (label == choosedLabel) {
+ try {
+ choosedLabel = (Label) fileEnDecryptInputFiles.getChildren().get(i - 1);
+ choosedLabelType = "ENDECRYPT";
+ fileOutputFilesChangeText(choosedLabel, null, null);
+ fileEnDecryptSelected(choosedLabel);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ e.printStackTrace();
+ fileOutputFileChangeDest.setDisable(true);
+ getChoosedLabelOutputFileFolder.setDisable(true);
+ fileEncryptOutputFile.setEditable(false);
+ fileDecryptOutputFile.setEditable(false);
+ fileOutputFilesChangeText(choosedLabel, "", "");
+ choosedLabel = null;
+ choosedLabelType = null;
+ }
+ break;
+ }
+ }
+ }
+ } else {
+ fileEnDecryptInputFiles.getChildren().remove(label);
+ fileOutputFileChangeDest.setDisable(true);
+ getChoosedLabelOutputFileFolder.setDisable(true);
+ fileEncryptOutputFile.setEditable(false);
+ fileDecryptOutputFile.setEditable(false);
+ if (label == choosedLabel) {
+ fileOutputFilesChangeText(choosedLabel, "", "");
+ choosedLabel = null;
+ choosedLabelType = null;
+ }
+ }
+ }
+
+ /**
+ * Changes the highlight of the clicked item in the en- / decryption box.
+ * Get called if the user click an non-highlighted item in the en- / decryption box
+ *
+ * @param changeLabel is the label that the user has clicked
+ *
+ * @since 1.0.0
+ */
+ private void fileEnDecryptSelected(Label changeLabel) {
+ if (changeLabel != null) {
+ fileDeleteSelected(null);
+ enDecryptInputOutputFiles.keySet().forEach(label -> label.setStyle(null));
+ changeLabel.setStyle("-fx-background-color: lightblue; -fx-border-color: #292929");
+ fileDecryptOutputFile.setEditable(true);
+ fileEncryptOutputFile.setEditable(true);
+ fileOutputFileChangeDest.setDisable(false);
+ getChoosedLabelOutputFileFolder.setDisable(false);
+ choosedLabel = changeLabel;
+ choosedLabelType = "ENDECRYPT";
+ } else {
+ enDecryptInputOutputFiles.keySet().forEach(label -> label.setStyle(null));
+ fileDecryptOutputFile.setEditable(false);
+ fileEncryptOutputFile.setEditable(false);
+ fileOutputFileChangeDest.setDisable(true);
+ getChoosedLabelOutputFileFolder.setDisable(true);
+ }
+ }
+
+ /**
+ * Opens a file chooser GUI where the user can select the files that should be en- / decrypted.
+ * Get called if the 'Choose files...' in the file en- / decrypt section button is pressed
+ *
+ * @since 1.12.0
+ */
+ public void fileEnDecryptChooseFiles() {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Choose files");
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*"));
+ List files = fileChooser.showOpenMultipleDialog(rootWindow.getScene().getWindow());
+ try {
+ if (files.size() > 0) {
+ files.forEach(this::fileEnDecryptAddFile);
+ }
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Opens a directory chooser GUI where the user can select the directories that should be en- / decrypted.
+ * Get called if the 'directories...' in the file en- / decrypt section button is pressed
+ *
+ * @since 1.12.0
+ */
+ public void fileEnDecryptChooseDirectories() {
+ DirectoryChooser directoryChooser = new DirectoryChooser();
+ directoryChooser.setTitle("Choose directories");
+ File file = directoryChooser.showDialog(rootWindow.getScene().getWindow());
+ try {
+ fileEnDecryptAddFile(file);
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Get called if user drags a (normal or internet) file over the en- / decrypt file box
+ *
+ * @param event source
+ *
+ * @since 1.2.0
+ */
+ public void onFileEnDecryptDragOver(DragEvent event) {
+ Dragboard dragboard = event.getDragboard();
+ if (event.getGestureSource() != fileEnDecryptInputFiles) {
+ if (dragboard.hasFiles()) {
+ event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
+ } else if (dragboard.hasUrl()) {
+ String url = dragboard.getUrl();
+ String urlFilename = dragboard.getUrl().split("/")[dragboard.getUrl().split("/").length - 1];
+ if (url.startsWith("data:")) {
+ try {
+ final int dataStartIndex = url.indexOf(",") + 1;
+ final String data = url.substring(dataStartIndex);
+ java.util.Base64.getDecoder().decode(data);
+ event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ } else if (urlFilename.contains(".") && Utils.hasAnyCharacter("\\/:*?|<>\"", urlFilename)) {
+ try {
+ new URL(url);
+ event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
+ } catch (Exception e) {
+ e.printStackTrace();
+ return;
+ }
+ } else {
+ event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
+ }
+ }
+ }
+ }
+
+ /**
+ * Get called if the user drops the dragged (normal or internet) file over the en- / decrypt file box
+ *
+ * @param event source
+ * @throws URISyntaxException
+ *
+ * @since 1.2.0
+ */
+ public void onFileEnDecryptDragNDrop(DragEvent event) throws URISyntaxException {
+ Dragboard dragboard = event.getDragboard();
+ if (dragboard.hasFiles()) {
+ dragboard.getFiles().forEach(this::fileEnDecryptAddFile);
+ } else if (dragboard.hasUrl()) {
+ String url = dragboard.getUrl();
+ String urlFilename = dragboard.getUrl().split("/")[dragboard.getUrl().split("/").length - 1];
+ if (url.startsWith("data:")) {
+ fileEnDecryptAddInternetFile(url, DATAFILEURL);
+ } else if (urlFilename.contains(".") && Utils.hasAnyCharacter("\\/:*?|<>\"", urlFilename)) {
+ fileEnDecryptAddInternetFile(url, FILEFILEURL);
+ } else {
+ fileEnDecryptAddInternetFile(url, NONSPECIFICFILEURL);
+ }
+ }
+ }
+
+ /**
+ * If the user presses Ctrl + V: Adds the last object in clipboard (if file) for en- / decryption.
+ * Get called if the user presses a key while selected file en- / decryption box
+ *
+ * @param event source
+ * @throws URISyntaxException
+ *
+ * @since 1.7.0
+ */
+ public void onFileEnDecryptPaste(KeyEvent event) throws URISyntaxException {
+ if (paste.match(event)) {
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ Transferable transferable = clipboard.getContents(null);
+ try {
+ if (transferable != null) {
+ if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
+ Object objectFileList = transferable.getTransferData(DataFlavor.javaFileListFlavor);
+ List files = (List) objectFileList;
+ files.forEach(o -> fileEnDecryptAddFile((File) o));
+ } else if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) {
+ Object objectImage = transferable.getTransferData(DataFlavor.imageFlavor);
+ fileEnDecryptAddClipboardImage((BufferedImage) objectImage);
+ }
+ }
+ } catch (UnsupportedFlavorException | IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ /**
+ * Encrypt all files given files.
+ * Get called if file 'Encrypt' button is pressed
+ *
+ * @since 1.0.0
+ */
+ public void fileEncryptButton() {
+ final byte[] salt;
+ if (!fileEnDecryptSaltEntry.getText().isEmpty()) {
+ salt = fileEnDecryptSaltEntry.getText().getBytes(StandardCharsets.UTF_8);
+ } else {
+ salt = new byte[16];
+ }
+ if (!enDecryptInputOutputFiles.isEmpty()) {
+ removeFileFromFileBox.setDisable(true);
+ limitNumberOfThreads.setDisable(true);
+ for(Map.Entry> entry: enDecryptInputOutputFiles.entrySet()) {
+ Thread thread = new Thread(() -> {
+ addFileEnDecryptThread(Thread.currentThread());
+ if (limitNumberOfThreads.isSelected()) {
+ while (totalThreads.get() >= Runtime.getRuntime().availableProcessors()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().stop();
+ }
+ }
+ }
+ totalThreads.getAndIncrement();
+ Label inputFileLabel = entry.getKey();
+ ArrayList outputFileList = entry.getValue();
+ String fileEnDecryptAlgorithm = fileEnDecryptAlgorithmBox.getSelectionModel().getSelectedItem();
+ EnDecrypt.AES fileEncrypt = new EnDecrypt.AES(fileEnDecryptKeyEntry.getText(), salt, Integer.parseInt(fileEnDecryptAlgorithm.substring(fileEnDecryptAlgorithm.indexOf('-') + 1)));
+ if (enDecryptInputOutputInternetFiles.containsKey(inputFileLabel)) {
+ ArrayList fileSpecs = enDecryptInputOutputInternetFiles.get(inputFileLabel);
+ int urlType = (int) fileSpecs.get(0);
+ String url = (String) fileSpecs.get(1);
+ try {
+ if (urlType == FILEFILEURL || urlType == NONSPECIFICFILEURL) {
+ URLConnection openURL = new URL(url).openConnection();
+ openURL.addRequestProperty("User-Agent", "Mozilla/5.0");
+ fileEncrypt.encryptFile(openURL.getInputStream(), new FileOutputStream((File) fileSpecs.get(2)), buffer);
+ } else if (urlType == DATAFILEURL) {
+ final int dataStartIndex = url.indexOf(",") + 1;
+ final String data = url.substring(dataStartIndex);
+ byte[] decoded = java.util.Base64.getDecoder().decode(data);
+ fileEncrypt.encryptFile(new ByteArrayInputStream(decoded), new FileOutputStream((File) fileSpecs.get(2)), buffer);
+ }
+ } catch (FileNotFoundException | InvalidKeySpecException | NoSuchAlgorithmException | MalformedURLException | InvalidKeyException | NoSuchPaddingException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("IO Exception occurred", e.getMessage()));
+ }
+ } else if (enDecryptInputOutputClipboardImages.containsKey(inputFileLabel)) {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ BufferedImage bufferedImage = enDecryptInputOutputClipboardImages.get(inputFileLabel);
+ try {
+ ImageIO.write(bufferedImage, "png", byteArrayOutputStream);
+ fileEncrypt.encryptFile(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()), new FileOutputStream(outputFileList.get(0).getAbsoluteFile()), buffer);
+ } catch (IOException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("IO Exception occurred", e.getMessage()));
+ } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+ } else {
+ try {
+ File inputFile = new File(inputFileLabel.getText());
+ if (inputFile.isFile()) {
+ fileEncrypt.encryptFile(new FileInputStream(inputFile), new FileOutputStream(outputFileList.get(0)), buffer);
+ } else {
+ fileEncrypt.encryptDirectory(inputFileLabel.getText(), outputFileList.get(0).getAbsolutePath(), ".cryptoGX", buffer);
+ if (!outputFileList.get(0).isDirectory()) {
+ Platform.runLater(() -> warningAlert("Couldn't create directory\n '" + outputFileList.get(0).getAbsolutePath() + "'.\nTry again or restart cryptoGX with admin privileges"));
+ }
+ }
+ } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("IO Exception occurred", e.getMessage()));
+ }
+ }
+ if (removeFileFromFileBox.isSelected()) {
+ Platform.runLater(() -> fileEnDecryptDeleteEntry(entry.getKey()));
+ }
+ if ((getFileEnDecryptThreadsSize() - 1) <= 0) {
+ fileEnDecryptLoading = false;
+ Platform.runLater(() -> {
+ fileEnDecryptLoadingImage.setImage(null);
+ removeFileFromFileBox.setDisable(false);
+ limitNumberOfThreads.setDisable(false);
+ });
+ }
+ removeFileEnDecryptThread(Thread.currentThread());
+ totalThreads.getAndDecrement();
+ });
+ thread.setDaemon(false);
+ thread.start();
+ if (!fileEnDecryptLoading) {
+ fileEnDecryptLoadingImage.setImage(loadingImage);
+ }
+ fileEnDecryptLoading = true;
+ }
+ }
+ }
+
+ /**
+ * Decrypt all files given files.
+ * Get called if file 'Decrypt' button is pressed
+ *
+ * @since 1.0.0
+ */
+ public void fileDecryptButton() {
+ final byte[] salt;
+ if (!fileEnDecryptSaltEntry.getText().isEmpty()) {
+ salt = fileEnDecryptSaltEntry.getText().getBytes(StandardCharsets.UTF_8);
+ } else {
+ salt = new byte[16];
+ }
+ if (!enDecryptInputOutputFiles.isEmpty()) {
+ removeFileFromFileBox.setDisable(true);
+ limitNumberOfThreads.setDisable(true);
+ for(Map.Entry> entry: enDecryptInputOutputFiles.entrySet()) {
+ Thread thread = new Thread(() -> {
+ addFileEnDecryptThread(Thread.currentThread());
+ if (limitNumberOfThreads.isSelected()) {
+ while (totalThreads.get() >= Runtime.getRuntime().availableProcessors()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().stop();
+ }
+ }
+ }
+ totalThreads.getAndIncrement();
+ Label inputFileLabel = entry.getKey();
+ ArrayList outputFileList = entry.getValue();
+ String fileEnDecryptAlgorithm = fileEnDecryptAlgorithmBox.getSelectionModel().getSelectedItem();
+ EnDecrypt.AES fileDecrypt = new EnDecrypt.AES(fileEnDecryptKeyEntry.getText(), salt, Integer.parseInt(fileEnDecryptAlgorithm.substring(fileEnDecryptAlgorithm.indexOf('-') + 1)));
+ if (enDecryptInputOutputInternetFiles.containsKey(entry.getKey())) {
+ ArrayList imageSpecs = enDecryptInputOutputInternetFiles.get(entry.getKey());
+ int urlType = (int) imageSpecs.get(0);
+ String url = (String) imageSpecs.get(1);
+ try {
+ if (urlType == FILEFILEURL) {
+ URLConnection openURL = new URL(url).openConnection();
+ openURL.addRequestProperty("User-Agent", "Mozilla/5.0");
+ fileDecrypt.decryptFile(openURL.getInputStream(), new FileOutputStream((File) imageSpecs.get(2)), buffer);
+ fileDecrypt.decryptFile(openURL.getInputStream(), new FileOutputStream((File) imageSpecs.get(2)), buffer);
+ } else if (urlType == DATAFILEURL) {
+ final int dataStartIndex = url.indexOf(",") + 1;
+ final String data = url.substring(dataStartIndex);
+ byte[] decoded = java.util.Base64.getDecoder().decode(data);
+ fileDecrypt.decryptFile(new ByteArrayInputStream(decoded), new FileOutputStream((File) imageSpecs.get(2)), buffer);
+ } else if (urlType == NONSPECIFICFILEURL) {
+ URLConnection openURL = new URL(url).openConnection();
+ openURL.addRequestProperty("User-Agent", "Mozilla/5.0");
+ fileDecrypt.decryptFile(openURL.getInputStream(), new FileOutputStream((File) imageSpecs.get(2)), buffer);
+ }
+ } catch (FileNotFoundException | InvalidKeySpecException | NoSuchAlgorithmException | MalformedURLException | InvalidKeyException | NoSuchPaddingException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("IO Exception occurred", e.getMessage()));
+ }
+ } else if (enDecryptInputOutputClipboardImages.containsKey(inputFileLabel)) {
+ ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+ BufferedImage bufferedImage = enDecryptInputOutputClipboardImages.get(inputFileLabel);
+ try {
+ ImageIO.write(bufferedImage, "png", byteArrayOutputStream);
+ fileDecrypt.decryptFile(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()), new FileOutputStream(outputFileList.get(1).getAbsolutePath()), buffer);
+ } catch (IOException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("IO Exception occurred", e.getMessage()));
+ } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+ } else {
+ try {
+ File inputFile = new File(inputFileLabel.getText());
+ if (inputFile.isFile()) {
+ fileDecrypt.decryptFile(new FileInputStream(inputFile), new FileOutputStream(outputFileList.get(1)), buffer);
+ } else {
+ fileDecrypt.decryptDirectory(inputFileLabel.getText(), outputFileList.get(1).getAbsolutePath(), "@.cryptoGX@", buffer);
+ if (!outputFileList.get(1).isDirectory()) {
+ Platform.runLater(() -> warningAlert("Couldn't create directory\n '" + outputFileList.get(1).getAbsolutePath() + "'.\nTry again or restart cryptoGX with admin privileges"));
+ }
+ }
+ } catch (NoSuchPaddingException | InvalidKeySpecException | InvalidKeyException | NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("IO Exception occurred", e.getMessage()));
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ Platform.runLater(() -> errorAlert("Wrong text for encryption is given", e.getMessage()));
+ }
+ }
+ if (removeFileFromFileBox.isSelected()) {
+ Platform.runLater(() -> fileEnDecryptDeleteEntry(entry.getKey()));
+ }
+ if ((getFileEnDecryptThreadsSize() - 1) <= 0) {
+ fileEnDecryptLoading = false;
+ Platform.runLater(() -> {
+ fileEnDecryptLoadingImage.setImage(null);
+ removeFileFromFileBox.setDisable(false);
+ limitNumberOfThreads.setDisable(false);
+ });
+ }
+ removeFileEnDecryptThread(Thread.currentThread());
+ totalThreads.getAndDecrement();
+ });
+ thread.setDaemon(false);
+ thread.start();
+ if (!fileEnDecryptLoading) {
+ fileEnDecryptLoadingImage.setImage(loadingImage);
+ }
+ fileEnDecryptLoading = true;
+ }
+ }
+ }
+
+ /**
+ * Cancels the file en- / decryption.
+ * Get called if the file en- / decrypt 'Cancel' button is pressed
+ *
+ * @since 1.12.0
+ */
+ public void fileEnDecryptCancelButton() {
+ for (Iterator iterator = getFileEnDecryptThreads().iterator(); iterator.hasNext();) {
+ Thread thread = iterator.next();
+ while (thread.isAlive() && !thread.isInterrupted()) {
+ thread.stop();
+ thread.interrupt();
+ }
+ iterator.remove();
+ }
+ fileEnDecryptLoading = false;
+ fileEnDecryptLoadingImage.setImage(null);
+ removeFileFromFileBox.setDisable(false);
+ limitNumberOfThreads.setDisable(false);
+ }
+
+ //-----fileDelete-----//
+
+ /**
+ * Synchronized method to get the list of threads which delete files
+ *
+ * @return list of threads which delete files
+ *
+ * @since 1.2.0
+ */
+ private synchronized List getFileDeleteThreads() {
+ return fileDeleteThreads;
+ }
+
+ /**
+ * Synchronized method to get the number of threads which delete files
+ *
+ * @return number of threads which delete files
+ *
+ * @since 1.2.0
+ */
+ private synchronized int getFileDeleteThreadsSize() {
+ return fileDeleteThreads.size();
+ }
+
+ /**
+ * Synchronized method to add a thread to the file delete list of current running file delete threads
+ *
+ * @param thread that should be added
+ *
+ * @since 1.2.0
+ */
+ private synchronized void addFileDeleteThread(Thread thread) {
+ fileDeleteThreads.add(thread);
+ }
+
+ /**
+ * Synchronized method to remove a thread from the file delete list of current file delete threads
+ *
+ * @param thread that should be removed
+ *
+ * @since 1.2.0
+ */
+ private synchronized void removeFileDeleteThread(Thread thread) {
+ fileDeleteThreads.remove(thread);
+ }
+
+ /**
+ * Adds a file that should be deleted
+ *
+ * @param file that should be added
+ *
+ * @since 1.2.0
+ */
+ private void fileDeleteAddFile(File file) {
+ for (File f: deleteInputFiles.values()) {
+ if (f.getAbsolutePath().equals(file.getAbsolutePath())) {
+ return;
+ }
+ }
+ Label newLabel = new Label(file.getAbsolutePath());
+ newLabel.setOnKeyTyped(this::keyTypedTooltip);
+ newLabel.setOnMouseMoved(this::mouseOverEntryTooltip);
+ newLabel.setOnMouseExited(event -> mouseExitEntryTooltip());
+ newLabel.setOnMouseClicked(event -> fileDeleteSelected(newLabel));
+ newLabel.setContextMenu(fileDeleteInputContextMenu);
+ fileDeleteInputFiles.getChildren().add(newLabel);
+ deleteInputFiles.put(newLabel, file.getAbsoluteFile());
+ }
+
+ /**
+ * Changes the highlight of the clicked item in the file delete box.
+ * Get called if the user click an non-highlighted item in the file delete box
+ *
+ * @param changeLabel is the label that the user has clicked
+ *
+ * @since 1.2.0
+ */
+ private void fileDeleteSelected(Label changeLabel) {
+ if (changeLabel != null) {
+ fileEnDecryptSelected(null);
+ deleteInputFiles.keySet().forEach(label -> label.setStyle(null));
+ changeLabel.setStyle("-fx-background-color: lightblue; -fx-border-color: #292929");
+ choosedLabel = changeLabel;
+ choosedLabelType = "DELETE";
+ } else {
+ deleteInputFiles.keySet().forEach(label -> label.setStyle(null));
+ }
+ }
+
+ /**
+ * Deletes an entry for file delete.
+ * Get called if the user presses 'del' or delete the entry in the file delete box via the right click tooltip
+ *
+ * @param label that should be deleted
+ *
+ * @since 1.12.0
+ */
+ private void fileDeleteDeleteEntry(Label label) {
+ deleteInputFiles.remove(choosedLabel);
+ if (fileDeleteInputFiles.getChildren().size() - 1 >= 1) {
+ for (int i=0; iOpens a file chooser GUI where the user can select the files that should be en- / decrypted.
+ * Get called if the 'Choose files...' in the delete section button is pressed
+ *
+ * @since 1.12.0
+ */
+ public void fileDeleteChooseFiles() {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Choose files");
+ fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All Files", "*.*"));
+ List files = fileChooser.showOpenMultipleDialog(rootWindow.getScene().getWindow());
+ try {
+ if (files.size() > 0) {
+ files.forEach(this::fileDeleteAddFile);
+ }
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Opens a directory chooser GUI where the user can select the directories that should be en- / decrypted.
+ * Get called if the 'Choose directories...' in the delete section button is pressed
+ *
+ * @since 1.12.0
+ */
+ public void fileDeleteChooseDirectories() {
+ DirectoryChooser directoryChooser = new DirectoryChooser();
+ directoryChooser.setTitle("Choose directories");
+ File file = directoryChooser.showDialog(rootWindow.getScene().getWindow());
+ try {
+ fileDeleteAddFile(file);
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Get called if user drags a file over the delete file box
+ *
+ * @param event source
+ *
+ * @since 1.2.0
+ */
+ public void onFileDeleteDragOver(DragEvent event) {
+ Dragboard dragboard = event.getDragboard();
+ if (event.getGestureSource() != fileDeleteInputFiles && dragboard.hasFiles()) {
+ event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
+ }
+ }
+
+ /**
+ * Get called if the user drops the dragged file over the delete file box
+ *
+ * @param event source
+ *
+ * @since 1.2.0
+ */
+ public void onFileDeleteDragNDrop(DragEvent event) {
+ Dragboard dragboard = event.getDragboard();
+ if (dragboard.hasFiles()) {
+ dragboard.getFiles().forEach(file -> {
+ if (file.isFile() || file.isDirectory()) {
+ fileDeleteAddFile(file);
+ }
+ });
+ }
+ }
+
+ /**
+ * Delete all given files.
+ * Get called if 'Delete' button is pressed
+ *
+ * @since 1.2.0
+ */
+ public void fileDelete() {
+ if (!fileDeleteLoading && !deleteInputFiles.isEmpty()) {
+ fileDeleteLoadingImage.setImage(loadingImage);
+ }
+ int deleteIterations = Integer.parseInt(fileDeleteIterationsEntry.getText());
+ for (Map.Entry map : deleteInputFiles.entrySet()) {
+ Label label = map.getKey();
+ File file = map.getValue();
+ Thread thread = new Thread(() -> {
+ addFileDeleteThread(Thread.currentThread());
+ if (limitNumberOfThreads.isSelected()) {
+ while (totalThreads.get() >= Runtime.getRuntime().availableProcessors()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ Thread.currentThread().stop();
+ }
+ }
+ }
+ totalThreads.getAndIncrement();
+ try {
+ if (file.isFile()) {
+ SecureDelete.deleteFile(file, deleteIterations, buffer);
+ } else if (file.isDirectory()) {
+ SecureDelete.deleteDirectory(file.getAbsolutePath(), deleteIterations, buffer);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ if ((getFileDeleteThreadsSize() - 1) <= 0) {
+ fileDeleteLoading = false;
+ Platform.runLater(() -> fileDeleteLoadingImage.setImage(null));
+ }
+ if (label == choosedLabel) {
+ choosedLabel = null;
+ choosedLabelType = null;
+ }
+ Platform.runLater(() -> fileDeleteInputFiles.getChildren().remove(label));
+ removeFileDeleteThread(Thread.currentThread());
+ totalThreads.getAndDecrement();
+ });
+ thread.setDaemon(false);
+ thread.start();
+ fileDeleteLoading = true;
+ }
+ }
+
+ /**
+ * Cancels the file en- / decryption.
+ * Get called if the file delete 'Cancel' button is pressed
+ *
+ * @since 1.12.0
+ */
+ public void fileDeleteCancelButton() {
+ for (Iterator iterator = getFileDeleteThreads().iterator(); iterator.hasNext();) {
+ Thread thread = iterator.next();
+ while (thread.isAlive() & !thread.isInterrupted()) {
+ thread.stop();
+ thread.interrupt();
+ }
+ iterator.remove();
+ }
+ fileDeleteLoading = false;
+ fileDeleteLoadingImage.setImage(null);
+ }
+
+ /**
+ * Called to initialize a controller after its root element has been
+ * completely processed.
+ *
+ * @param location
+ * The location used to resolve relative paths for the root object, or
+ * null if the location is not known.
+ *
+ * @param resources
+ * The resources used to localize the root object, or null if
+ * the root object was not localized.
+ *
+ * @since 1.0.0
+ */
+ @Override
+ public void initialize(URL location, ResourceBundle resources) {
+
+ //-----general-----//
+
+ currentConfigSettings.put("textKey", configDefaultTextKey);
+ currentConfigSettings.put("textSalt", configDefaultTextSalt);
+ currentConfigSettings.put("textAlgorithm", configDefaultTextAlgorithm);
+
+ currentConfigSettings.put("fileEnDecryptKey", configDefaultFileEnDecryptKey);
+ currentConfigSettings.put("fileEnDecryptSalt", configDefaultFileEnDecryptSalt);
+ currentConfigSettings.put("fileEnDecryptAlgorithm", configDefaultFileEnDecryptAlgorithm);
+
+ currentConfigSettings.put("fileDeleteIterations", String.valueOf(configDefaultFileDeleteIterations));
+
+ currentConfigSettings.put("fileOutputPath", configDefaultFileOutputPath);
+ currentConfigSettings.put("removeFromFileBox", String.valueOf(configDefaultRemoveFileFromFileBox));
+ currentConfigSettings.put("limitNumberOfThreads", String.valueOf(configDefaultLimitNumberOfThreads));
+
+ menubar.setOnMouseDragged(event -> {
+ Stage stage = (Stage) ((Node) event.getSource()).getScene().getWindow();
+ stage.setX(event.getScreenX() + menubarX);
+ stage.setY(event.getScreenY() + menubarY);
+ });
+ menubar.setOnMousePressed(event -> {
+ Scene scene = ((Node) event.getSource()).getScene();
+ menubarX = scene.getX() - event.getSceneX();
+ menubarY = scene.getY() - event.getSceneY();
+ });
+
+ rootWindow.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.DELETE && choosedLabelType != null) {
+ if (choosedLabelType.equals("ENDECRYPT")) {
+ fileEnDecryptDeleteEntry(choosedLabel);
+ } else if (choosedLabelType.equals("DELETE")) {
+ fileDeleteDeleteEntry(choosedLabel);
+ }
+ }
+ });
+
+ getChoosedLabelInputFileFolder.setOnAction(event -> {
+ Desktop desktop = Desktop.getDesktop();
+ String filePath = choosedLabel.getText();
+ try {
+ desktop.open(new File(filePath.substring(0, filePath.lastIndexOf(System.getProperty("file.separator")))));
+ } catch (IOException e) {
+ errorAlert("An unexpected IO Exception occurred", e.getMessage());
+ }
+ });
+
+ getChoosedLabelOutputFileFolder.setOnAction(event -> {
+ Desktop desktop;
+ String filePath;
+ if (enDecryptInputOutputFiles.containsKey(choosedLabel)) {
+ desktop = Desktop.getDesktop();
+ filePath = enDecryptInputOutputFiles.get(choosedLabel).get(0).getAbsolutePath();
+ } else {
+ return;
+ }
+ try {
+ desktop.open(new File(filePath.substring(0, filePath.lastIndexOf(System.getProperty("file.separator")))));
+ } catch (IOException e) {
+ errorAlert("An unexpected IO Exception occurred", e.getMessage());
+ }
+ });
+
+ setDefaultOutputPath.setOnAction(event -> {
+ DirectoryChooser directoryChooser = new DirectoryChooser();
+ File directory = directoryChooser.showDialog(rootWindow.getScene().getWindow());
+ try {
+ currentConfigSettings.replace("fileOutputPath", directory.getAbsolutePath());
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ });
+
+ settingsMenu.setOnShowing(event -> {
+ loadSettings.setDisable(!isConfig);
+ exportSettings.setDisable(!isConfig);
+ });
+
+ removeFileFromFileBox.setOnAction(event -> currentConfigSettings.replace("removeFromFileBox", String.valueOf(removeFileFromFileBox.isSelected())));
+ limitNumberOfThreads.setOnAction(event -> currentConfigSettings.replace("limitNumberOfThreads", String.valueOf(limitNumberOfThreads.isSelected())));
+ saveSettings.setOnAction(event -> {
+ try {
+ addSettingGUI(rootWindow.getScene().getWindow(), currentConfigSettings);
+ if (config.isFile()) {
+ isConfig = true;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ });
+ loadSettings.setOnAction(event -> {
+ try {
+ currentConfigSettings = (HashMap) loadSettingsGUI(rootWindow.getScene().getWindow()).values().toArray()[0];
+ textKeyEntry.setText(currentConfigSettings.get("textKey"));
+ textSaltEntry.setText(currentConfigSettings.get("textSalt"));
+ textAlgorithmBox.setValue(currentConfigSettings.get("textAlgorithm"));
+
+ fileEnDecryptKeyEntry.setText(currentConfigSettings.get("fileEnDecryptKey"));
+ fileEnDecryptSaltEntry.setText(currentConfigSettings.get("fileEnDecryptSalt"));
+ fileEnDecryptAlgorithmBox.setValue(currentConfigSettings.get("fileEnDecryptAlgorithm"));
+
+ fileDeleteIterationsEntry.setText(currentConfigSettings.get("fileDeleteIterations"));
+
+ removeFileFromFileBox.setSelected(Boolean.parseBoolean(currentConfigSettings.get("removeFromFileBox")));
+ limitNumberOfThreads.setSelected(Boolean.parseBoolean(currentConfigSettings.get("limitNumberOfThreads")));
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (ArrayIndexOutOfBoundsException ex) {
+ try {
+ SecureDelete.deleteFile(config, 5, buffer);
+ isConfig = false;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ exportSettings.setOnAction(event -> {
+ try {
+ exportSettingsGUI(rootWindow.getScene().getWindow());
+ } catch (IOException e) {
+ e.printStackTrace();
+ errorAlert("IO Exception occurred", e.getMessage());
+ }
+ });
+ importSettings.setOnAction(event -> {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Import settings");
+ fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Config files", "*.config*", "*.xml"), new FileChooser.ExtensionFilter("All files", "*.*"));
+ File file = fileChooser.showOpenDialog(rootWindow.getScene().getWindow());
+ if (file != null) {
+ if (isConfig) {
+ writeSettings(config, readSettings(file));
+ } else {
+ writeSettings(config, readSettings(file));
+ isConfig = true;
+ }
+ }
+ });
+
+ //-----text------//
+
+ textAlgorithmBox.setItems(FXCollections.observableArrayList(Utils.algorithms.keySet()));
+ textAlgorithmBox.setValue(Utils.algorithms.keySet().toArray(new String[Utils.algorithms.size()])[0]);
+
+ //-----fileEnDecrypt-----//
+
+ fileEnDecryptAlgorithmBox.setItems(FXCollections.observableArrayList(Utils.algorithms.keySet()));
+ fileEnDecryptAlgorithmBox.setValue(Utils.algorithms.keySet().toArray(new String[Utils.algorithms.size()])[0]);
+
+ MenuItem enDecryptRemove = new MenuItem();
+ enDecryptRemove.setText("Remove");
+ enDecryptRemove.setOnAction(removeEvent -> fileEnDecryptDeleteEntry(choosedLabel));
+ MenuItem enDecryptChangeDest = new MenuItem();
+ enDecryptChangeDest.setText("Change output file / directory");
+ enDecryptChangeDest.setOnAction(outputFileChangeEvent -> {
+ File file;
+ if (new File(choosedLabel.getText()).isFile()) {
+ FileChooser destChooser = new FileChooser();
+ destChooser.setTitle("Choose or create new file");
+ destChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*"));
+ file = destChooser.showSaveDialog(rootWindow.getScene().getWindow());
+ } else {
+ DirectoryChooser destChooser = new DirectoryChooser();
+ destChooser.setTitle("Choose or create new directory");
+ file = destChooser.showDialog(rootWindow.getScene().getWindow());
+ }
+ if (file != null) {
+ for (Map.Entry> entry : enDecryptInputOutputFiles.entrySet()) {
+ if (entry.getKey().getText().equals(choosedLabel.getText())) {
+ ArrayList changedFile = new ArrayList<>();
+ changedFile.add(0, file);
+ changedFile.add(1, file);
+ enDecryptInputOutputFiles.replace(entry.getKey(), entry.getValue(), changedFile);
+ fileOutputFilesChangeText((Label) fileEnDecryptLabelEvent.getSource(), file.getAbsolutePath(), file.getAbsolutePath());
+ break;
+ }
+ }
+ }
+ });
+ fileEnDecryptInputContextMenu.getItems().addAll(enDecryptRemove, enDecryptChangeDest);
+
+ ContextMenu fileEnDecryptInputFilesMenu = new ContextMenu();
+ MenuItem enDecryptPaste = new MenuItem();
+ enDecryptPaste.setText("Paste");
+ enDecryptPaste.setOnAction(pasteEvent -> {
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ Transferable transferable = clipboard.getContents(null);
+ try {
+ if (transferable != null) {
+ if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
+ Object objectFileList = transferable.getTransferData(DataFlavor.javaFileListFlavor);
+ List files = (List) objectFileList;
+ files.forEach(o -> fileEnDecryptAddFile((File) o));
+ } else if (transferable.isDataFlavorSupported(DataFlavor.imageFlavor)) {
+ Object objectImage = transferable.getTransferData(DataFlavor.imageFlavor);
+ fileEnDecryptAddClipboardImage((BufferedImage) objectImage);
+ }
+ }
+ } catch (UnsupportedFlavorException | IOException | URISyntaxException e) {
+ e.printStackTrace();
+ }
+ });
+ fileEnDecryptInputFilesMenu.getItems().add(enDecryptPaste);
+
+ fileEnDecryptInputFiles.setOnContextMenuRequested(event -> {
+ if (!fileEnDecryptInputContextMenu.isShowing()) {
+ fileEnDecryptInputFilesMenu.show(((VBox) event.getSource()).getParent().getScene().getWindow(), event.getScreenX(), event.getScreenY());
+ }
+ });
+
+ fileOutputFileChangeDest.setOnAction(event -> {
+ FileChooser fileDestChooser = new FileChooser();
+ fileDestChooser.setTitle("Choose or create new file");
+ fileDestChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("All files", "*.*"));
+ File file = fileDestChooser.showSaveDialog(rootWindow.getScene().getWindow());
+ if (file != null) {
+ for (Map.Entry> entry : enDecryptInputOutputFiles.entrySet()) {
+ if (entry.getKey().getText().equals(choosedLabel.getText())) {
+ ArrayList changedFile = new ArrayList<>();
+ changedFile.add(0, file);
+ changedFile.add(1, file);
+ enDecryptInputOutputFiles.replace(entry.getKey(), entry.getValue(), changedFile);
+ fileOutputFilesChangeText((Label) fileEnDecryptLabelEvent.getSource(), file.getAbsolutePath(), file.getAbsolutePath());
+ break;
+ }
+ }
+ }
+ });
+
+ fileOutputFileChangeDest.setDisable(true);
+ getChoosedLabelOutputFileFolder.setDisable(true);
+
+ fileEncryptOutputFile.textProperty().addListener((observable, oldValue, newValue) -> fileOutputFilesChangeText(choosedLabel, newValue, fileDecryptOutputFile.getText()));
+ fileDecryptOutputFile.textProperty().addListener((observable, oldValue, newValue) -> fileOutputFilesChangeText(choosedLabel, fileEncryptOutputFile.getText(), newValue));
+
+ //-----fileDelete-----//
+
+ MenuItem deleteRemove = new MenuItem();
+ deleteRemove.setText("Remove");
+ deleteRemove.setOnAction(removeEvent -> fileDeleteDeleteEntry(choosedLabel));
+ fileDeleteInputContextMenu.getItems().add(deleteRemove);
+
+ ContextMenu fileDeleteInputFilesMenu = new ContextMenu();
+ MenuItem deletePaste = new MenuItem();
+ deletePaste.setText("Paste");
+ deletePaste.setOnAction(pasteEvent -> {
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ Transferable transferable = clipboard.getContents(null);
+ try {
+ if (transferable != null && transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
+ Object objectFileList = transferable.getTransferData(DataFlavor.javaFileListFlavor);
+ List files = (List) objectFileList;
+ files.forEach(o -> fileDeleteAddFile((File) o));
+ }
+ } catch (UnsupportedFlavorException | IOException e) {
+ e.printStackTrace();
+ }
+ });
+ fileDeleteInputFilesMenu.getItems().add(deletePaste);
+
+ fileDeleteInputFiles.setOnContextMenuRequested(event -> {
+ if (!fileDeleteInputContextMenu.isShowing()) {
+ fileDeleteInputFilesMenu.show(((VBox) event.getSource()).getParent().getScene().getWindow(), event.getScreenX(), event.getScreenY());
+ }
+ });
+
+ fileDeleteIterationsEntry.textProperty().addListener((observable, oldValue, newValue) -> {
+ if (!newValue.matches("[0-9]*")) {
+ fileDeleteIterationsEntry.setText(oldValue);
+ }
+ });
+
+ Thread t = new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ if (isConfig) {
+ Platform.runLater(() -> {
+ try {
+ currentConfigSettings = (HashMap) loadSettingsGUI(rootWindow.getScene().getWindow()).values().toArray()[0];
+ textKeyEntry.setText(currentConfigSettings.get("textKey"));
+ textSaltEntry.setText(currentConfigSettings.get("textSalt"));
+ textAlgorithmBox.setValue(currentConfigSettings.get("textAlgorithm"));
+
+ fileEnDecryptKeyEntry.setText(currentConfigSettings.get("fileEnDecryptKey"));
+ fileEnDecryptSaltEntry.setText(currentConfigSettings.get("fileEnDecryptSalt"));
+ fileEnDecryptAlgorithmBox.setValue(currentConfigSettings.get("fileEnDecryptAlgorithm"));
+
+ fileDeleteIterationsEntry.setText(currentConfigSettings.get("fileDeleteIterations"));
+
+ removeFileFromFileBox.setSelected(Boolean.parseBoolean(currentConfigSettings.get("removeFromFileBox")));
+ limitNumberOfThreads.setSelected(Boolean.parseBoolean(currentConfigSettings.get("limitNumberOfThreads")));
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (ArrayIndexOutOfBoundsException ex) {
+ try {
+ SecureDelete.deleteFile(config, 5, buffer);
+ isConfig = false;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ });
+ t.start();
+ }
+}
diff --git a/src/org/bytedream/cryptogx/EnDecrypt.java b/src/org/bytedream/cryptogx/EnDecrypt.java
new file mode 100644
index 0000000..bf7fbd9
--- /dev/null
+++ b/src/org/bytedream/cryptogx/EnDecrypt.java
@@ -0,0 +1,302 @@
+package org.bytedream.cryptogx;
+
+import javax.crypto.*;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.*;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.KeySpec;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Class for en- / decrypt text and files
+ *
+ * @since 1.0.0
+ */
+public class EnDecrypt {
+
+ public static class AES extends Thread {
+
+ public int iterations = 65536;
+
+ private final String secretKeyFactoryAlgorithm = "PBKDF2WithHmacSHA1";
+ private int keySize = 256;
+
+ private final String key;
+ private final byte[] salt;
+
+ public AES(String key, byte[] salt) {
+ this.key = key;
+ this.salt = salt;
+ }
+
+ public AES(String key, byte[] salt, int keySize) {
+ this.key = key;
+ this.salt = salt;
+ this.keySize = keySize;
+ }
+
+ public AES(String key, byte[] salt, int iterations, int keySize) {
+ this.key = key;
+ this.salt = salt;
+ this.iterations = iterations;
+ this.keySize = keySize;
+ }
+
+ /**
+ * Creates a secret key from given (plain text) key and salt
+ *
+ * @return the secret key
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ *
+ * @since 1.0.0
+ */
+ private byte[] createSecretKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ SecretKeyFactory factory = SecretKeyFactory.getInstance(secretKeyFactoryAlgorithm);
+ PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray(), salt, iterations, keySize);
+
+ return factory.generateSecret(keySpec).getEncoded();
+ }
+
+ /**
+ * Writes {@param inputStream} to {@param outputStream}
+ *
+ * @param inputStream from which is written
+ * @param outputStream to which is written
+ * @param buffer
+ * @throws IOException
+ *
+ * @since 1.12.0
+ */
+ private void write(InputStream inputStream, OutputStream outputStream, byte[] buffer) throws IOException {
+ int numOfBytesRead;
+ while ((numOfBytesRead = inputStream.read(buffer)) != -1) {
+ outputStream.write(buffer, 0, numOfBytesRead);
+ }
+ outputStream.close();
+ inputStream.close();
+ }
+
+ /**
+ * Encrypts the {@param inputStream} to {@param outputStream}
+ *
+ * @param inputStream that should be encrypted
+ * @param outputStream to which the encrypted {@param inputFile} should be written to
+ * @param buffer
+ * @throws InvalidKeySpecException
+ * @throws NoSuchAlgorithmException
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeyException
+ * @throws IOException
+ *
+ * @since 1.12.0
+ */
+ public void encryptFile(InputStream inputStream, OutputStream outputStream, byte[] buffer) throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IOException {
+ Key secretKey = new SecretKeySpec(createSecretKey(), "AES");
+
+ Cipher cipher = Cipher.getInstance("AES");
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey);
+
+ CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
+ write(cipherInputStream, outputStream, buffer);
+ }
+
+ /**
+ * Encrypts all files in the {@param inputDirectory} to the {@param outputDirectory}
+ *
+ * @param inputDirectory that should be encrypted
+ * @param outputDirectory to which the encrypted {@param inputDirectory} files should be written to
+ * @param fileEnding get added to every file that gets encrypted (if the {@param fileEnding} starts and ends with
+ * a '@', the {@param fileEnding} will get removed from the file if it exists)
+ * @param buffer
+ * @throws InvalidKeySpecException
+ * @throws NoSuchAlgorithmException
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeyException
+ * @throws IOException
+ *
+ * @since 1.12.0
+ */
+ public void encryptDirectory(String inputDirectory, String outputDirectory, String fileEnding, byte[] buffer) throws IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, NoSuchPaddingException {
+ AtomicBoolean remove = new AtomicBoolean(false);
+
+ if (fileEnding == null) {
+ fileEnding = "";
+ } else if (fileEnding.startsWith("@") && fileEnding.endsWith("@")) {
+ fileEnding = fileEnding.substring(1, fileEnding.length() - 1);
+ remove.set(true);
+ }
+
+ HashMap files = new HashMap<>();
+ final String finalFileEnding = fileEnding;
+ Files.walk(Paths.get(inputDirectory)).map(Path::toFile).forEach(oldFile -> {
+ String oldFilePath = oldFile.getAbsolutePath();
+ if (oldFile.isDirectory()) {
+ new File(oldFilePath.replace(inputDirectory, outputDirectory + "/")).mkdir();
+ }else if (remove.get() && oldFilePath.endsWith(finalFileEnding)) {
+ files.put(oldFile, new File(oldFilePath.substring(0, oldFilePath.lastIndexOf(finalFileEnding))
+ .replace(inputDirectory, outputDirectory + "/") + finalFileEnding));
+ } else {
+ files.put(oldFile, new File(oldFilePath.replace(inputDirectory, outputDirectory + "/") + finalFileEnding));
+ }
+ });
+
+ File newFile;
+ for (Map.Entry entry: files.entrySet()) {
+ newFile = entry.getValue();
+ encryptFile(new FileInputStream(entry.getKey()), new FileOutputStream(newFile), buffer);
+ }
+ }
+
+ /**
+ * Decrypts the {@param inputStream} to {@param outputStream}
+ *
+ * @param inputStream that should be decrypted
+ * @param outputStream to which the decrypted {@param inputFile} should be written to
+ * @param buffer
+ * @throws InvalidKeySpecException
+ * @throws NoSuchAlgorithmException
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeyException
+ * @throws IOException
+ * @throws InvalidAlgorithmParameterException
+ *
+ * @since 1.12.0
+ */
+ public void decryptFile(InputStream inputStream, OutputStream outputStream, byte[] buffer) throws InvalidKeySpecException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IOException{
+ Key secretKey = new SecretKeySpec(createSecretKey(), "AES");
+
+ Cipher cipher = Cipher.getInstance("AES");
+ cipher.init(Cipher.DECRYPT_MODE, secretKey);
+
+ CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher);
+ write(inputStream, cipherOutputStream, buffer);
+ }
+
+ /**
+ * Decrypts all files in the {@param inputDirectory} to the {@param outputDirectory}
+ *
+ * @param inputDirectory that should be decrypted
+ * @param outputDirectory to which the decrypted {@param inputDirectory} files should be written to
+ * @param fileEnding get added to every file that gets decrypted (if the {@param fileEnding} starts and ends with
+ * a '@', the {@param fileEnding} will get removed from the file if it exists)
+ * @param buffer
+ * @throws InvalidKeySpecException
+ * @throws NoSuchAlgorithmException
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeyException
+ * @throws IOException
+ *
+ * @since 1.12.0
+ */
+ public void decryptDirectory(String inputDirectory, String outputDirectory, String fileEnding, byte[] buffer) throws IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidKeySpecException, NoSuchPaddingException {
+ AtomicBoolean remove = new AtomicBoolean(false);
+
+ if (fileEnding == null) {
+ fileEnding = "";
+ } else if (fileEnding.startsWith("@") && fileEnding.endsWith("@")) {
+ fileEnding = fileEnding.substring(1, fileEnding.length() - 1);
+ remove.set(true);
+ }
+
+ HashMap files = new HashMap<>();
+ final String finalFileEnding = fileEnding;
+ Files.walk(Paths.get(inputDirectory)).map(Path::toFile).forEach(oldFile -> {
+ String oldFilePath = oldFile.getAbsolutePath();
+ if (oldFile.isDirectory()) {
+ new File(oldFilePath.replace(inputDirectory, outputDirectory + "/")).mkdir();
+ }
+ else if (remove.get() && oldFilePath.endsWith(finalFileEnding)) {
+ files.put(oldFile, new File(oldFilePath.substring(0, oldFilePath.lastIndexOf(finalFileEnding))
+ .replace(inputDirectory, outputDirectory + "/") + finalFileEnding));
+ } else {
+ files.put(oldFile, new File(oldFilePath.replace(inputDirectory, outputDirectory + "/") + finalFileEnding));
+ }
+ });
+
+ File newFile;
+ for (Map.Entry entry: files.entrySet()) {
+ newFile = entry.getValue();
+ decryptFile(new FileInputStream(entry.getKey()), new FileOutputStream(newFile), buffer);
+ }
+ }
+
+ /**
+ * Encrypt {@param bytes}
+ *
+ * @param bytes that should be encrypted
+ * @return encrypted bytes
+ * @throws BadPaddingException
+ * @throws IllegalBlockSizeException
+ * @throws NoSuchPaddingException
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ * @throws InvalidKeyException
+ *
+ * @since 1.0.0
+ */
+ public byte[] encrypt(byte[] bytes) throws BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
+ Key secretKey = new SecretKeySpec(createSecretKey(), "AES");
+
+ Cipher encryptCipher = Cipher.getInstance("AES");
+ encryptCipher.init(Cipher.ENCRYPT_MODE, secretKey);
+ return encryptCipher.doFinal(bytes);
+ }
+
+ /**
+ * Encrypt {@param bytes}
+ *
+ * @param string that should be encrypted
+ *
+ * @see EnDecrypt.AES#encrypt(byte[])
+ *
+ * @since 1.0.0
+ */
+ public String encrypt(String string) throws BadPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException, InvalidKeyException, InvalidKeySpecException {
+ return Base64.getEncoder().encodeToString(encrypt(string.getBytes(StandardCharsets.UTF_8)));
+ }
+
+ /**
+ * Decrypt encrypted {@param bytes}
+ *
+ * @param bytes that should be decrypted
+ * @return decrypted bytes
+ * @throws BadPaddingException
+ * @throws IllegalBlockSizeException
+ * @throws NoSuchPaddingException
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ * @throws InvalidKeyException
+ *
+ * @since 1.12.0
+ */
+ public byte[] decrypt(byte[] bytes) throws BadPaddingException, IllegalBlockSizeException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
+ Key secretKey = new SecretKeySpec(createSecretKey(), "AES");
+
+ Cipher decryptCipher = Cipher.getInstance("AES");
+ decryptCipher.init(Cipher.DECRYPT_MODE, secretKey);
+ return decryptCipher.doFinal(Base64.getDecoder().decode(bytes));
+ }
+
+ /**
+ * Decrypt encrypted {@param string}
+ *
+ * @param string that should be decrypted
+ *
+ * @see EnDecrypt.AES#decrypt(byte[])
+ *
+ * @since 1.0.0
+ */
+ public String decrypt(String string) throws BadPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException, InvalidKeyException, InvalidKeySpecException {
+ return new String(decrypt(string.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
+ }
+
+ }
+}
diff --git a/src/org/bytedream/cryptogx/Main.java b/src/org/bytedream/cryptogx/Main.java
new file mode 100644
index 0000000..437a4a3
--- /dev/null
+++ b/src/org/bytedream/cryptogx/Main.java
@@ -0,0 +1,326 @@
+/*
+ * @author bytedream
+ * @version 1.12.0
+ *
+ * Some @since
versions may be not correct, because the @since
tag got added in
+ * version 1.12.0 and I don't have all versions (1.0.0 - 1.11.0), so I cannot see when some methods were added
+
+ */
+
+package org.bytedream.cryptogx;
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.control.*;
+import javafx.scene.image.Image;
+import javafx.stage.Screen;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import javax.swing.*;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.spec.InvalidKeySpecException;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * Main class
+ *
+ * @since 1.0.0
+ */
+public class Main extends Application {
+
+ protected static final int NON_PORTABLE = 1;
+ protected static final int PORTABLE = 0;
+
+ protected static final int TYPE = NON_PORTABLE;
+
+ protected final static String configDefaultTextKey = "";
+ protected final static String configDefaultTextSalt = "";
+ protected final static String configDefaultTextAlgorithm = "AES-128";
+ protected final static String configDefaultFileEnDecryptKey = "";
+ protected final static String configDefaultFileEnDecryptSalt = "";
+ protected final static String configDefaultFileEnDecryptAlgorithm = "AES-128";
+ protected final static int configDefaultFileDeleteIterations = 5;
+ protected final static String configDefaultFileOutputPath = "";
+ protected final static boolean configDefaultRemoveFileFromFileBox = false;
+ protected final static boolean configDefaultLimitNumberOfThreads = true;
+
+ private final static byte[] buffer = new byte[64];
+
+ private static Stage mainStage;
+ private double rootWindowX, rootWindowY;
+ protected static File config;
+ protected static boolean isConfig;
+
+ /**
+ * Start the GUI
+ *
+ * @param primaryStage of the GUI
+ * @throws IOException if issues with loading 'mainGUI.fxml'
+ *
+ * @since 1.0.0
+ */
+ @Override
+ public void start(Stage primaryStage) throws IOException {
+ Thread.setDefaultUncaughtExceptionHandler(Main::exceptionAlert);
+
+ mainStage = primaryStage;
+
+ Parent root = FXMLLoader.load(getClass().getResource("resources/mainGUI.fxml"));
+ primaryStage.initStyle(StageStyle.UNDECORATED);
+ primaryStage.setResizable(false);
+ primaryStage.setTitle("cryptoGX");
+ primaryStage.getIcons().add(new Image(getClass().getResource("resources/cryptoGX.png").toExternalForm()));
+ Scene scene = new Scene(root);
+ //Scene scene = new Scene(root, 900, 470);
+
+ scene.setOnMouseDragged(event -> {
+ primaryStage.setX(event.getScreenX() + rootWindowX);
+ primaryStage.setY(event.getScreenY() + rootWindowY);
+ });
+ scene.setOnMousePressed(event -> {
+ rootWindowX = scene.getX() - event.getSceneX();
+ rootWindowY = scene.getY() - event.getSceneY();
+ });
+
+ primaryStage.setScene(scene);
+ primaryStage.show();
+ }
+
+ /**
+ * Enter method for the application.
+ * Can also be used to en- / decrypt text and files or secure delete files without starting GUI
+ *
+ * @param args from the command line
+ * @return
+ * @throws BadPaddingException
+ * @throws NoSuchAlgorithmException if wrong algorithm is given (command line)
+ * @throws IllegalBlockSizeException if wrong size for key is given (command line)
+ * @throws NoSuchPaddingException
+ * @throws InvalidKeyException if invalid key is given (command line)
+ * @throws InvalidKeySpecException
+ * @throws IOException if files cannot be en- / decrypted or deleted correctly (command line)
+ * @throws InvalidAlgorithmParameterException if wrong algorithm parameters are given (command line)
+ *
+ * @since 1.0.0
+ */
+ public static void main(String[] args) throws BadPaddingException, NoSuchAlgorithmException, IllegalBlockSizeException, NoSuchPaddingException, InvalidKeyException, InvalidKeySpecException, IOException, InvalidAlgorithmParameterException {
+ if (Main.TYPE == Main.PORTABLE) {
+ String system = System.getProperty("os.name").toLowerCase();
+ if (system.startsWith("windows")) {
+ config = new File("C:\\Users\\" + System.getProperty("user.name") + "\\AppData\\Roaming\\cryptoGX\\cryptoGX.config");
+ File directory = new File("C:\\Users\\" + System.getProperty("user.name") + "\\AppData\\Roaming\\cryptoGX");
+ if (!directory.isDirectory()) {
+ directory.mkdir();
+ }
+ } else if (system.startsWith("linux")) {
+ config = new File(System.getProperty("user.home") + "/.cryptoGX/cryptoGX.config");
+ File directory = new File(System.getProperty("user.home") + "/.cryptoGX/");
+ if (!directory.isDirectory()) {
+ directory.mkdir();
+ }
+ } else {
+ config = new File("cryptoGX.config");
+ }
+ } else {
+ config = new File("cryptoGX.config");
+ }
+ isConfig = config.isFile();
+ if (args.length == 0) {
+ launch(args);
+ } else {
+ args[0] = args[0].replace("-", "");
+ if (args[0].toLowerCase().equals("help") || args[0].toUpperCase().equals("H")) {
+ System.out.println("Usage AES: \n\n" +
+ " Text en- / decryption\n" +
+ " encrypt: AES encrypt \n" +
+ " decrypt: AES decrypt \n\n" +
+ " File en- / decryption\n" +
+ " encrypt: AES encrypt \n" +
+ " decrypt: AES decrypt \n\n" +
+ "File secure delete: delete "); //for the argument 'default' can be used, which is 5
+ } else if (args[0].toLowerCase().equals("delete")) {
+ if (args.length > 3) {
+ System.err.println("To many arguments were given, expected 3");
+ } else if (args.length < 3) {
+ System.err.println("To few arguments were given, expected 3");
+ }
+ try {
+ if (args[1].equals("default")) {
+ args[1] = "5";
+ }
+ File deleteFile = new File(args[2]);
+ if (deleteFile.isFile()) {
+ SecureDelete.deleteFile(deleteFile, Integer.parseInt(args[1]), buffer);
+ } else if (deleteFile.isDirectory()) {
+ SecureDelete.deleteDirectory(args[2], Integer.parseInt(args[1]), buffer);
+ } else {
+ System.err.println("Couldn't find file " + args[4]);
+ System.exit(1);
+ }
+ } catch (NumberFormatException e) {
+ System.err.println(args[1] + " must be a number\n Error: " + e.getMessage());
+ }
+ } else if (args[0].toLowerCase().equals("aes")) {
+ if (args.length < 5) {
+ System.err.println("To few arguments were given");
+ System.exit(1);
+ } else if (args.length > 6) {
+ System.err.println("To many arguments were given");
+ System.exit(1);
+ }
+ EnDecrypt.AES aes;
+ if (args[2].isEmpty()) {
+ aes = new EnDecrypt.AES(args[1], new byte[16]);
+ } else {
+ aes = new EnDecrypt.AES(args[1], args[2].getBytes(StandardCharsets.UTF_8));
+ }
+ String type = args[3].toLowerCase();
+ if (args.length == 5) {
+ if (type.equals("encrypt")) {
+ System.out.println(Base64.getEncoder().encodeToString(aes.encrypt(args[4].getBytes(StandardCharsets.UTF_8))));
+ } else if (type.equals("decrypt")) {
+ System.out.println(aes.decrypt(args[4]));
+ } else {
+ System.err.println("Couldn't resolve argument " + args[3] + ", expected 'encrypt' or 'decrypt'");
+ System.exit(1);
+ }
+ } else {
+ if (type.equals("encrypt")) {
+ File inputFile = new File(args[4]);
+ if (inputFile.isFile()) {
+ aes.encryptFile(new FileInputStream(inputFile), new FileOutputStream(args[5]), new byte[64]);
+ } else if (inputFile.isDirectory()) {
+ aes.encryptDirectory(args[4], args[5], ".cryptoGX", new byte[64]);
+ } else {
+ System.err.println("Couldn't find file " + args[4]);
+ System.exit(1);
+ }
+ } else if (type.equals("decrypt")) {
+ File inputFile = new File(args[4]);
+ if (inputFile.isFile()) {
+ aes.decryptFile(new FileInputStream(inputFile), new FileOutputStream(args[5]), new byte[64]);
+ } else if (inputFile.isDirectory()) {
+ aes.decryptDirectory(args[4], args[5], "@.cryptoGX@", new byte[64]);
+ } else {
+ System.err.println("Couldn't find file " + args[4]);
+ System.exit(1);
+ }
+ } else {
+ System.err.println("Couldn't resolve argument " + args[3] + ", expected 'encrypt' or 'decrypt'");
+ System.exit(1);
+ }
+ }
+ System.exit(0);
+ }
+ }
+ }
+
+ /**
+ * "Catch" all uncatched exceptions and opens an alert window
+ *
+ * @param thread which called this method
+ * @param throwable of the thread which called the method
+ *
+ * @since 1.3.0
+ */
+ private static void exceptionAlert(Thread thread, Throwable throwable) {
+ throwable.printStackTrace();
+
+ AtomicReference exceptionAlertX = new AtomicReference<>(Screen.getPrimary().getBounds().getMaxX() / 2);
+ AtomicReference exceptionAlertY = new AtomicReference<>(Screen.getPrimary().getBounds().getMaxY() / 2);
+
+ Alert enDecryptError = new Alert(Alert.AlertType.ERROR, "Error: " + throwable, ButtonType.OK);
+ enDecryptError.initStyle(StageStyle.UNDECORATED);
+ enDecryptError.setTitle("Error");
+ enDecryptError.setResizable(true);
+ ((Stage) enDecryptError.getDialogPane().getScene().getWindow()).getIcons().add(new Image(Main.class.getResource("resources/cryptoGX.png").toExternalForm()));
+ enDecryptError.getDialogPane().setContent(new Label("Error: " + throwable));
+
+ Scene window = enDecryptError.getDialogPane().getScene();
+
+ window.setOnMouseDragged(dragEvent -> {
+ enDecryptError.setX(dragEvent.getScreenX() + exceptionAlertX.get());
+ enDecryptError.setY(dragEvent.getScreenY() + exceptionAlertY.get());
+ });
+ window.setOnMousePressed(pressEvent -> {
+ exceptionAlertX.set(window.getX() - pressEvent.getSceneX());
+ exceptionAlertY.set(window.getY() - pressEvent.getSceneY());
+ });
+
+ enDecryptError.show();
+ }
+
+ /**
+ * Shows an error alert window
+ *
+ * @param message which will the alert show
+ * @param error which will show after the message
+ */
+ protected static void errorAlert(String message, String error) {
+ AtomicReference alertX = new AtomicReference<>(Screen.getPrimary().getBounds().getMaxX() / 2);
+ AtomicReference alertY = new AtomicReference<>(Screen.getPrimary().getBounds().getMaxY() / 2);
+
+ Alert enDecryptError = new Alert(Alert.AlertType.ERROR, message +
+ "\nError: " + error, ButtonType.OK);
+ enDecryptError.initStyle(StageStyle.UNDECORATED);
+ enDecryptError.setTitle("Error");
+ enDecryptError.setResizable(true);
+ ((Stage) enDecryptError.getDialogPane().getScene().getWindow()).getIcons().add(new Image(Main.class.getResource("resources/cryptoGX.png").toExternalForm()));
+ enDecryptError.getDialogPane().setContent(new Label(message));
+
+ Scene window = enDecryptError.getDialogPane().getScene();
+
+ window.setOnMouseDragged(dragEvent -> {
+ enDecryptError.setX(dragEvent.getScreenX() + alertX.get());
+ enDecryptError.setY(dragEvent.getScreenY() + alertY.get());
+ });
+ window.setOnMousePressed(pressEvent -> {
+ alertX.set(window.getX() - pressEvent.getSceneX());
+ alertY.set(window.getY() - pressEvent.getSceneY());
+ });
+
+ enDecryptError.show();
+ }
+
+ /**
+ * Shows an warning alert window
+ *
+ * @param message that the alert window will show
+ *
+ * @since 1.4.0
+ */
+ protected static void warningAlert(String message) {
+ AtomicReference alertX = new AtomicReference<>(Screen.getPrimary().getBounds().getMaxX() / 2);
+ AtomicReference alertY = new AtomicReference<>(Screen.getPrimary().getBounds().getMaxY() / 2);
+
+ Alert enDecryptError = new Alert(Alert.AlertType.WARNING, message, ButtonType.OK);
+ enDecryptError.initStyle(StageStyle.UNDECORATED);
+ enDecryptError.setTitle("Error");
+ ((Stage) enDecryptError.getDialogPane().getScene().getWindow()).getIcons().add(new Image(Main.class.getResource("resources/cryptoGX.png").toExternalForm()));
+ enDecryptError.getDialogPane().setContent(new Label(message));
+
+ Scene window = enDecryptError.getDialogPane().getScene();
+
+ window.setOnMouseDragged(dragEvent -> {
+ enDecryptError.setX(dragEvent.getScreenX() + alertX.get());
+ enDecryptError.setY(dragEvent.getScreenY() + alertY.get());
+ });
+ window.setOnMousePressed(pressEvent -> {
+ alertX.set(window.getX() - pressEvent.getSceneX());
+ alertY.set(window.getY() - pressEvent.getSceneY());
+ });
+
+ enDecryptError.show();
+ }
+}
diff --git a/src/org/bytedream/cryptogx/SecureDelete.java b/src/org/bytedream/cryptogx/SecureDelete.java
new file mode 100644
index 0000000..0d1ee05
--- /dev/null
+++ b/src/org/bytedream/cryptogx/SecureDelete.java
@@ -0,0 +1,90 @@
+package org.bytedream.cryptogx;
+
+import java.io.*;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Random;
+import java.util.TreeSet;
+
+/**
+ * Class for secure delete files
+ *
+ * @since 1.2.0
+ */
+public class SecureDelete {
+
+ public static void deleteDirectory(String directory, int iterations, byte[] buffer) throws IOException {
+ TreeSet directories = new TreeSet<>();
+ Files.walk(Paths.get(directory)).map(Path::toFile).forEach(directoryFile -> {
+ if (directoryFile.isDirectory()) {
+ directories.add(directoryFile);
+ } else {
+ try {
+ SecureDelete.deleteFile(directoryFile, iterations, buffer);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ while (directoryFile.exists()) {
+ if (directoryFile.delete()) {
+ break;
+ }
+ }
+ }
+ });
+
+ File deleteDirectory = directories.last();
+ while (deleteDirectory != null) {
+ deleteDirectory.delete();
+
+ while (deleteDirectory.delete()) {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ deleteDirectory = directories.lower(deleteDirectory);
+ }
+ }
+
+ /**
+ * Overwrites the file {@param iterations} times line by line with random bytes (minimal size {@param minFileSize}; maximal size {@param maxFileSize}) and delete it
+ *
+ * @param file that should be deleted
+ * @param iterations how many times the file should be overwritten before it gets deleted
+ * @return if the file could be deleted
+ * @throws IOException
+ *
+ * @since 1.12.0
+ */
+ public static void deleteFile(File file, int iterations, byte[] buffer) throws IOException {
+ SecureRandom secureRandom = new SecureRandom();
+ RandomAccessFile raf = new RandomAccessFile(file, "rws");
+ for (int i=0; iClass for the user configuration / settings
+ *
+ * @since 1.12.0
+ */
+public class Settings {
+
+ private static double addSettingsGUIX, addSettingsGUIY;
+
+ private static final HashSet protectedSettingsNames = new HashSet<>(Arrays.asList("cryptoGX", "settings"));
+
+ /**
+ * Shows a GUI where the user can save settings, which can load later
+ *
+ * @param rootWindow from which this GUI will get called
+ * @param userSetting
+ * @throws IOException
+ *
+ * @since 1.11.0
+ */
+ public static void addSettingGUI(Window rootWindow, Map userSetting) throws IOException {
+ Map newSettingItems = new HashMap<>();
+
+ Stage rootStage = new Stage();
+ rootStage.initOwner(rootWindow);
+ Parent addSettingsRoot = FXMLLoader.load(Settings.class.getResource("resources/addSettingsGUI.fxml"));
+ rootStage.initStyle(StageStyle.UNDECORATED);
+ rootStage.initModality(Modality.WINDOW_MODAL);
+ rootStage.setResizable(false);
+ rootStage.setTitle("cryptoGX");
+ Scene scene = new Scene(addSettingsRoot, 320, 605);
+
+ rootStage.setScene(scene);
+
+ scene.setOnMouseDragged(event -> {
+ rootStage.setX(event.getScreenX() + addSettingsGUIX);
+ rootStage.setY(event.getScreenY() + addSettingsGUIY);
+ });
+ scene.setOnMousePressed(event -> {
+ addSettingsGUIX = scene.getX() - event.getSceneX();
+ addSettingsGUIY = scene.getY() - event.getSceneY();
+ });
+
+ Thread thread = new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+
+ Platform.runLater(() -> {
+ MenuBar menuBar = (MenuBar) addSettingsRoot.lookup("#menuBar");
+ menuBar.setOnMouseDragged(event -> {
+ rootStage.setX(event.getScreenX() + addSettingsGUIX);
+ rootStage.setY(event.getScreenY() + addSettingsGUIY);
+ });
+ menuBar.setOnMousePressed(event -> {
+ addSettingsGUIX = menuBar.getLayoutX() - event.getSceneX();
+ addSettingsGUIY = menuBar.getLayoutY() - event.getSceneY();
+ });
+
+ ImageView closeButton = (ImageView) addSettingsRoot.lookup("#closeButton");
+ closeButton.setOnMouseClicked(event -> rootStage.close());
+
+ TextField settingsNameEntry = (TextField) addSettingsRoot.lookup("#settingsNameEntry");
+
+ TextField textKeyEntry = (TextField) addSettingsRoot.lookup("#textKeyEntry");
+ textKeyEntry.setText(userSetting.get("textKey"));
+ TextField textSaltEntry = (TextField) addSettingsRoot.lookup("#textSaltEntry");
+ textSaltEntry.setText(userSetting.get("textSalt"));
+ ComboBox textAlgorithmBox = (ComboBox) addSettingsRoot.lookup("#textAlgorithmComboBox");
+ textAlgorithmBox.setItems(FXCollections.observableArrayList(Utils.algorithms.keySet()));
+ textAlgorithmBox.setValue(userSetting.get("textAlgorithm"));
+
+ TextField fileEnDecryptKeyEntry = (TextField) addSettingsRoot.lookup("#fileEnDecryptKeyEntry");
+ fileEnDecryptKeyEntry.setText(userSetting.get("fileEnDecryptKey"));
+ TextField fileEnDecryptSaltEntry = (TextField) addSettingsRoot.lookup("#fileEnDecryptSaltEntry");
+ fileEnDecryptSaltEntry.setText(userSetting.get("fileEnDecryptSalt"));
+ ComboBox fileEnDecryptAlgorithmBox = (ComboBox) addSettingsRoot.lookup("#fileEnDecryptAlgorithmComboBox");
+ fileEnDecryptAlgorithmBox.setItems(FXCollections.observableArrayList(Utils.algorithms.keySet()));
+ fileEnDecryptAlgorithmBox.setValue(userSetting.get("fileEnDecryptAlgorithm"));
+
+ TextField fileDeleteIterationEntry = (TextField) addSettingsRoot.lookup("#fileDeleteIterationsEntry");
+ fileDeleteIterationEntry.setText(userSetting.get("fileDeleteIterations"));
+ fileDeleteIterationEntry.textProperty().addListener((observable, oldValue, newValue) -> {
+ if (!newValue.matches("[0-9]*")) {
+ fileDeleteIterationEntry.setText(oldValue);
+ }
+ });
+
+ TextField fileOutputPathEntry = (TextField) addSettingsRoot.lookup("#fileOutputPathEntry");
+ fileOutputPathEntry.setText(userSetting.get("fileOutputPath"));
+ Button fileOutputPathButton = (Button) addSettingsRoot.lookup("#fileOutputPathButton");
+ fileOutputPathButton.setOnAction(event -> {
+ DirectoryChooser directoryChooser = new DirectoryChooser();
+ File directory = directoryChooser.showDialog(rootWindow.getScene().getWindow());
+ try {
+ fileOutputPathEntry.setText(directory.getAbsolutePath());
+ } catch (NullPointerException e) {
+ e.printStackTrace();
+ }
+ });
+ CheckBox removeFromFileBoxCheckBox = (CheckBox) addSettingsRoot.lookup("#removeFromFileBoxCheckBox");
+ removeFromFileBoxCheckBox.setSelected(Boolean.parseBoolean(userSetting.get("removeFromFileBox")));
+ CheckBox limitNumberOfThreadsCheckBox = (CheckBox) addSettingsRoot.lookup("#limitNumberOfThreadsCheckBox");
+ limitNumberOfThreadsCheckBox.setSelected(Boolean.parseBoolean(userSetting.get("limitNumberOfThreads")));
+
+ PasswordField hiddenPasswordEntry = (PasswordField) addSettingsRoot.lookup("#hiddenPasswordEntry");
+ TextField visiblePasswordEntry = (TextField) addSettingsRoot.lookup("#visiblePasswordEntry");
+ CheckBox showPassword = (CheckBox) addSettingsRoot.lookup("#showPassword");
+
+ showPassword.setOnAction(event -> {
+ if (showPassword.isSelected()) {
+ visiblePasswordEntry.setText(hiddenPasswordEntry.getText());
+ visiblePasswordEntry.setVisible(true);
+ hiddenPasswordEntry.setVisible(false);
+ } else {
+ hiddenPasswordEntry.setText(visiblePasswordEntry.getText());
+ hiddenPasswordEntry.setVisible(true);
+ visiblePasswordEntry.setVisible(false);
+ }
+ });
+ CheckBox encryptSettings = (CheckBox) addSettingsRoot.lookup("#encryptSettings");
+ encryptSettings.setOnAction(event -> {
+ if (encryptSettings.isSelected()) {
+ hiddenPasswordEntry.setDisable(false);
+ visiblePasswordEntry.setDisable(false);
+ showPassword.setDisable(false);
+ } else {
+ hiddenPasswordEntry.setDisable(true);
+ visiblePasswordEntry.setDisable(true);
+ showPassword.setDisable(true);
+ }
+ });
+ Button saveButton = (Button) addSettingsRoot.lookup("#saveButton");
+ saveButton.setOnAction(event -> {
+ if (settingsNameEntry.getText().trim().isEmpty()) {
+ warningAlert("Add a name for the setting");
+ } else if (protectedSettingsNames.contains(settingsNameEntry.getText())) {
+ warningAlert("Please choose another name for this setting");
+ } else if (settingsNameEntry.getText().trim().contains(" ")) {
+ warningAlert("Setting name must not contain free space");
+ } else if (encryptSettings.isSelected()) {
+ try {
+ EnDecrypt.AES encrypt;
+ if (!hiddenPasswordEntry.isDisabled() && !hiddenPasswordEntry.getText().trim().isEmpty()) {
+ encrypt = new EnDecrypt.AES(hiddenPasswordEntry.getText(), new byte[16]);
+ } else if (!visiblePasswordEntry.getText().trim().isEmpty()) {
+ encrypt = new EnDecrypt.AES(visiblePasswordEntry.getText(), new byte[16]);
+ } else {
+ throw new InvalidKeyException("The key must not be empty");
+ }
+
+ newSettingItems.put("encrypted", "true");
+
+ newSettingItems.put("textKey", encrypt.encrypt(textKeyEntry.getText()));
+ newSettingItems.put("textSalt", encrypt.encrypt(textSaltEntry.getText()));
+ newSettingItems.put("textAlgorithm", encrypt.encrypt(textAlgorithmBox.getSelectionModel().getSelectedItem().toString()));
+
+ newSettingItems.put("fileEnDecryptKey", encrypt.encrypt(fileEnDecryptKeyEntry.getText()));
+ newSettingItems.put("fileEnDecryptSalt", encrypt.encrypt(fileEnDecryptSaltEntry.getText()));
+ newSettingItems.put("fileEnDecryptAlgorithm", encrypt.encrypt(fileEnDecryptAlgorithmBox.getSelectionModel().getSelectedItem().toString()));
+
+ newSettingItems.put("fileDeleteIterations", encrypt.encrypt(fileDeleteIterationEntry.getText()));
+
+ newSettingItems.put("fileOutputPath", encrypt.encrypt(fileOutputPathEntry.getText()));
+ newSettingItems.put("removeFromFileBox", encrypt.encrypt(String.valueOf(removeFromFileBoxCheckBox.isSelected())));
+ newSettingItems.put("limitNumberOfThreads", encrypt.encrypt(String.valueOf(limitNumberOfThreadsCheckBox.isSelected())));
+
+ if (!config.isFile()) {
+ try {
+ if (!config.createNewFile()) {
+ warningAlert("Couldn't create config file");
+ } else {
+ addSetting(config, settingsNameEntry.getText().trim(), newSettingItems);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ errorAlert("Couldn't create config file", e.getMessage());
+ }
+ } else {
+ addSetting(config, settingsNameEntry.getText().trim(), newSettingItems);
+ }
+
+ rootStage.close();
+ } catch (InvalidKeyException e) {
+ warningAlert("The key must not be empty");
+ } catch (NoSuchPaddingException | NoSuchAlgorithmException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+ } else {
+ newSettingItems.put("textKey", textKeyEntry.getText());
+ newSettingItems.put("textSalt", textSaltEntry.getText());
+ newSettingItems.put("textAlgorithm", textAlgorithmBox.getSelectionModel().getSelectedItem().toString());
+
+ newSettingItems.put("fileEnDecryptKey", fileEnDecryptKeyEntry.getText());
+ newSettingItems.put("fileEnDecryptSalt", fileEnDecryptSaltEntry.getText());
+ newSettingItems.put("fileEnDecryptAlgorithm", fileEnDecryptAlgorithmBox.getSelectionModel().getSelectedItem().toString());
+
+ newSettingItems.put("fileDeleteIterations", fileDeleteIterationEntry.getText());
+
+ newSettingItems.put("fileOutputPath", fileOutputPathEntry.getText());
+ newSettingItems.put("removeFromFileBox", String.valueOf(removeFromFileBoxCheckBox.isSelected()));
+ newSettingItems.put("limitNumberOfThreads", String.valueOf(limitNumberOfThreadsCheckBox.isSelected()));
+
+ if (!config.isFile()) {
+ try {
+ if (!config.createNewFile()) {
+ warningAlert("Couldn't create config file");
+ } else {
+ addSetting(config, settingsNameEntry.getText().trim(), newSettingItems);
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ errorAlert("Couldn't create config file", e.getMessage());
+ }
+ } else {
+ addSetting(config, settingsNameEntry.getText().trim(), newSettingItems);
+ }
+
+ rootStage.close();
+ }
+ });
+ });
+ });
+
+ thread.start();
+
+ rootStage.showAndWait();
+ }
+
+ /**
+ * Shows a GUI where the user can export settings to a extra file
+ *
+ * @param rootWindow from which this GUI will get called
+ * @throws IOException
+ *
+ * @since 1.11.0
+ */
+ public static void exportSettingsGUI(Window rootWindow) throws IOException {
+ Stage rootStage = new Stage();
+ rootStage.initOwner(rootWindow);
+ Parent exportSettingsRoot = FXMLLoader.load(Settings.class.getResource("resources/exportSettingsGUI.fxml"));
+ rootStage.initStyle(StageStyle.UNDECORATED);
+ rootStage.initModality(Modality.WINDOW_MODAL);
+ rootStage.setResizable(false);
+ rootStage.setTitle("cryptoGX");
+ Scene scene = new Scene(exportSettingsRoot, 254, 253);
+
+ rootStage.setScene(scene);
+
+ scene.setOnMouseDragged(event -> {
+ rootStage.setX(event.getScreenX() + addSettingsGUIX);
+ rootStage.setY(event.getScreenY() + addSettingsGUIY);
+ });
+ scene.setOnMousePressed(event -> {
+ addSettingsGUIX = scene.getX() - event.getSceneX();
+ addSettingsGUIY = scene.getY() - event.getSceneY();
+ });
+
+ Thread thread = new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ MenuBar menuBar = (MenuBar) exportSettingsRoot.lookup("#menuBar");
+ menuBar.setOnMouseDragged(event -> {
+ rootStage.setX(event.getScreenX() + addSettingsGUIX);
+ rootStage.setY(event.getScreenY() + addSettingsGUIY);
+ });
+ menuBar.setOnMousePressed(event -> {
+ addSettingsGUIX = menuBar.getLayoutX() - event.getSceneX();
+ addSettingsGUIY = menuBar.getLayoutY() - event.getSceneY();
+ });
+ ImageView closeButton = (ImageView) exportSettingsRoot.lookup("#closeButton");
+ closeButton.setOnMouseClicked(event -> rootStage.close());
+
+ VBox settingsBox = (VBox) exportSettingsRoot.lookup("#settingsBox");
+ Platform.runLater(() -> readSettings(config).keySet().forEach(s -> {
+ CheckBox newCheckBox = new CheckBox();
+ newCheckBox.setText(s);
+ settingsBox.getChildren().add(newCheckBox);
+ }));
+
+ Button exportButton = (Button) exportSettingsRoot.lookup("#exportButton");
+ exportButton.setOnAction(event -> {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Export settings");
+ fileChooser.setInitialFileName("settings.config");
+ fileChooser.getExtensionFilters().addAll(new FileChooser.ExtensionFilter("Config files", "*.config"),
+ new FileChooser.ExtensionFilter("All files", "*.*"));
+ File file = fileChooser.showSaveDialog(exportSettingsRoot.getScene().getWindow());
+ if (file != null) {
+ TreeMap> writeInfos = new TreeMap<>();
+ TreeMap> settings = readSettings(config);
+ for (int i=0; iShows a GUI where the user can load saved settings
+ *
+ * @param rootWindow from which this GUI will get called
+ * @return the settings that the user has chosen
+ * @throws IOException
+ *
+ * @since 1.11.0
+ */
+ public static TreeMap> loadSettingsGUI(Window rootWindow) throws IOException {
+ Button[] outerLoadButton = new Button[1];
+ HashMap setting = new HashMap<>();
+ TreeMap> settingItems = readSettings(config);
+ TreeMap> returnItems = new TreeMap<>();
+
+ Stage rootStage = new Stage();
+ rootStage.initOwner(rootWindow);
+ AnchorPane loadSettingsRoot = FXMLLoader.load(Settings.class.getResource("resources/loadSettingsGUI.fxml"));
+ rootStage.initStyle(StageStyle.UNDECORATED);
+ rootStage.initModality(Modality.WINDOW_MODAL);
+ rootStage.setResizable(false);
+ rootStage.setTitle("cryptoGX");
+ rootStage.getIcons().add(new Image(Settings.class.getResource("resources/cryptoGX.png").toExternalForm()));
+ Scene scene = new Scene(loadSettingsRoot, 242, 235);
+
+ scene.setOnMouseDragged(event -> {
+ rootStage.setX(event.getScreenX() + addSettingsGUIX);
+ rootStage.setY(event.getScreenY() + addSettingsGUIY);
+ });
+ scene.setOnMousePressed(event -> {
+ addSettingsGUIX = scene.getX() - event.getSceneX();
+ addSettingsGUIY = scene.getY() - event.getSceneY();
+ });
+
+ scene.setOnKeyReleased(event -> {
+ if (event.getCode() == KeyCode.ENTER) {
+ outerLoadButton[0].fire();
+ }
+ });
+
+ Thread thread = new Thread(() -> {
+ try {
+ Thread.sleep(100);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ Platform.runLater(() -> {
+ MenuBar menuBar = (MenuBar) loadSettingsRoot.lookup("#menuBar");
+ menuBar.setOnMouseDragged(event -> {
+ rootStage.setX(event.getScreenX() + addSettingsGUIX);
+ rootStage.setY(event.getScreenY() + addSettingsGUIY);
+ });
+ menuBar.setOnMousePressed(event -> {
+ addSettingsGUIX = menuBar.getLayoutX() - event.getSceneX();
+ addSettingsGUIY = menuBar.getLayoutY() - event.getSceneY();
+ });
+
+ ImageView closeButton = (ImageView) loadSettingsRoot.lookup("#closeButton");
+ if (settingItems.isEmpty()) {
+ rootStage.close();
+ }
+
+ closeButton.setOnMouseClicked(event -> {
+ setting.put("textKey", configDefaultTextKey);
+ setting.put("textSalt", configDefaultTextSalt);
+ setting.put("textAlgorithm", configDefaultTextAlgorithm);
+
+ setting.put("fileEnDecryptKey", configDefaultFileEnDecryptKey);
+ setting.put("fileEnDecryptSalt", configDefaultFileEnDecryptSalt);
+ setting.put("fileEnDecryptAlgorithm", configDefaultFileEnDecryptAlgorithm);
+
+ setting.put("fileDeleteIterations", String.valueOf(configDefaultFileDeleteIterations));
+
+ setting.put("fileOutputPath", configDefaultFileOutputPath);
+ setting.put("removeFromFileBox", String.valueOf(configDefaultRemoveFileFromFileBox));
+ setting.put("limitNumberOfThreads", String.valueOf(configDefaultLimitNumberOfThreads));
+
+ returnItems.put("default", setting);
+
+ rootStage.close();
+ });
+
+ PasswordField keyHideEntry = (PasswordField) loadSettingsRoot.lookup("#passwordEntryHide");
+ TextField keyShowEntry = (TextField) loadSettingsRoot.lookup("#passwordEntryShow");
+
+ CheckBox showPassword = (CheckBox) loadSettingsRoot.lookup("#showPassword");
+ showPassword.setOnAction(event -> {
+ if (showPassword.isSelected()) {
+ keyShowEntry.setText(keyHideEntry.getText());
+ keyShowEntry.setVisible(true);
+ keyHideEntry.setVisible(false);
+ } else {
+ keyHideEntry.setText(keyShowEntry.getText());
+ keyHideEntry.setVisible(true);
+ keyShowEntry.setVisible(false);
+ }
+ });
+
+ ComboBox settingsBox = (ComboBox) loadSettingsRoot.lookup("#settingsBox");
+ settingsBox.setItems(FXCollections.observableArrayList(settingItems.keySet()));
+ settingsBox.setValue(settingItems.firstKey());
+ if (!Boolean.parseBoolean(settingItems.firstEntry().getValue().get("encrypted").trim())) {
+ keyHideEntry.clear();
+ keyHideEntry.setDisable(true);
+ keyShowEntry.setDisable(true);
+ showPassword.setDisable(true);
+ }
+ settingsBox.setOnAction(event -> {
+ try {
+ if (!Boolean.parseBoolean(settingItems.get(settingsBox.getSelectionModel().getSelectedItem().toString()).get("encrypted").trim())) {
+ keyHideEntry.clear();
+ keyHideEntry.setDisable(true);
+ keyShowEntry.clear();
+ keyShowEntry.setDisable(true);
+ showPassword.setDisable(true);
+ } else {
+ keyHideEntry.clear();
+ keyHideEntry.setDisable(false);
+ keyShowEntry.clear();
+ keyShowEntry.setDisable(false);
+ showPassword.setDisable(false);
+ }
+ } catch (NullPointerException e) {
+ //get called when delete button is pressed
+ }
+ });
+
+ Button loadButton = (Button) loadSettingsRoot.lookup("#loadButton");
+ loadButton.setOnAction(event -> {
+ String settingName = settingsBox.getSelectionModel().getSelectedItem().toString();
+ Map selectedSetting = settingItems.get(settingName);
+ if (keyHideEntry.isDisabled() && showPassword.isDisabled() && showPassword.isDisabled()) {
+ setting.put("textKey", selectedSetting.get("textKey"));
+ setting.put("textSalt", selectedSetting.get("textSalt"));
+ setting.put("textAlgorithm", selectedSetting.get("textAlgorithm"));
+
+ setting.put("fileEnDecryptKey", selectedSetting.get("fileEnDecryptKey"));
+ setting.put("fileEnDecryptSalt", selectedSetting.get("fileEnDecryptSalt"));
+ setting.put("fileEnDecryptAlgorithm", selectedSetting.get("fileEnDecryptAlgorithm"));
+
+ setting.put("fileDeleteIterations", selectedSetting.get("fileDeleteIterations"));
+
+ setting.put("fileOutputPath", selectedSetting.get("fileOutputPath"));
+ setting.put("removeFromFileBox", selectedSetting.get("removeFromFileBox"));
+ setting.put("limitNumberOfThreads", selectedSetting.get("limitNumberOfThreads"));
+
+ returnItems.put(settingsBox.getSelectionModel().getSelectedItem().toString(), setting);
+
+ rootStage.close();
+ } else {
+ EnDecrypt.AES decryptSetting;
+ if (keyHideEntry.isVisible()) {
+ decryptSetting = new EnDecrypt.AES(keyHideEntry.getText(), new byte[16]);
+ } else {
+ decryptSetting = new EnDecrypt.AES(keyShowEntry.getText(), new byte[16]);
+ }
+ try {
+ Map selectedEncryptedSetting = settingItems.get(settingName);
+ setting.put("textKey", decryptSetting.decrypt(selectedEncryptedSetting.get("textKey")));
+ setting.put("textSalt", decryptSetting.decrypt(selectedEncryptedSetting.get("textSalt")));
+ setting.put("textAlgorithm", decryptSetting.decrypt(selectedEncryptedSetting.get("textAlgorithm")));
+
+ setting.put("fileEnDecryptKey", decryptSetting.decrypt(selectedEncryptedSetting.get("fileEnDecryptKey")));
+ setting.put("fileEnDecryptSalt", decryptSetting.decrypt(selectedEncryptedSetting.get("fileEnDecryptSalt")));
+ setting.put("fileEnDecryptAlgorithm", decryptSetting.decrypt(selectedEncryptedSetting.get("fileEnDecryptAlgorithm")));
+
+ setting.put("fileDeleteIterations", String.valueOf(Integer.parseInt(decryptSetting.decrypt(selectedEncryptedSetting.get("fileDeleteIterations")))));
+
+ setting.put("fileOutputPath", decryptSetting.decrypt(selectedEncryptedSetting.get("fileOutputPath")));
+ setting.put("removeFromFileBox", decryptSetting.decrypt(selectedEncryptedSetting.get("removeFromFileBox")));
+ setting.put("limitNumberOfThreads", decryptSetting.decrypt(selectedEncryptedSetting.get("limitNumberOfThreads")));
+
+ returnItems.put(settingsBox.getSelectionModel().getSelectedItem().toString(), setting);
+
+ rootStage.close();
+ } catch (InvalidKeyException e) {
+ warningAlert("Wrong key is given");
+ } catch (NoSuchPaddingException | NoSuchAlgorithmException | IllegalBlockSizeException | BadPaddingException | InvalidKeySpecException e) {
+ e.printStackTrace();
+ warningAlert("Wrong key is given or the config wasn't\nsaved correctly");
+ }
+ }
+ });
+ outerLoadButton[0] = loadButton;
+
+ Button deleteButton = (Button) loadSettingsRoot.lookup("#deleteButton");
+ deleteButton.setOnAction(event -> {
+ AtomicReference deleteQuestionX = new AtomicReference<>((double) 0);
+ AtomicReference deleteQuestionY = new AtomicReference<>((double) 0);
+ Alert deleteQuestion = new Alert(Alert.AlertType.CONFIRMATION, "Delete " + settingsBox.getSelectionModel().getSelectedItem().toString() + "?", ButtonType.OK, ButtonType.CANCEL);
+ deleteQuestion.initStyle(StageStyle.UNDECORATED);
+ deleteQuestion.setTitle("Confirmation");
+ ((Stage) deleteQuestion.getDialogPane().getScene().getWindow()).getIcons().add(new Image(Settings.class.getResource("resources/cryptoGX.png").toExternalForm()));
+
+ Scene window = deleteQuestion.getDialogPane().getScene();
+
+ window.setOnMouseDragged(dragEvent -> {
+ deleteQuestion.setX(dragEvent.getScreenX() + deleteQuestionX.get());
+ deleteQuestion.setY(dragEvent.getScreenY() + deleteQuestionY.get());
+ });
+ window.setOnMousePressed(pressEvent -> {
+ deleteQuestionX.set(window.getX() - pressEvent.getSceneX());
+ deleteQuestionY.set(window.getY() - pressEvent.getSceneY());
+ });
+
+ Optional result = deleteQuestion.showAndWait();
+ if (result.get() == ButtonType.OK) {
+ if (settingItems.size() - 1 <= 0) {
+ for (int i = 0; i < 100; i++) {
+ if (config.isFile()) {
+ try {
+ SecureDelete.deleteFile(config, 5, new byte[64]);
+ isConfig = false;
+ rootStage.close();
+ break;
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ rootStage.close();
+ } else if (deleteSetting(config, settingsBox.getSelectionModel().getSelectedItem().toString())) {
+ settingItems.remove(settingsBox.getSelectionModel().getSelectedItem().toString());
+
+ settingsBox.setItems(FXCollections.observableArrayList(settingItems.keySet()));
+ settingsBox.setValue(settingItems.firstKey());
+ } else {
+ warningAlert("Couldn't delete setting '" + settingsBox.getSelectionModel().getSelectedItem().toString() + "'");
+ }
+ }
+ });
+ });
+ });
+
+ thread.start();
+
+ rootStage.setScene(scene);
+ rootStage.showAndWait();
+
+ return returnItems;
+ }
+
+ /**
+ * Shows a GUI where the user can save the current settings
+ *
+ * @param settingName name of the new setting
+ * @param newSetting is the new setting key value pair
+ *
+ * @since 1.12.0
+ */
+ public static void addSetting(File file, String settingName, Map newSetting) {
+ TreeMap> settings = readSettings(file);
+ settings.put(settingName, newSetting);
+ writeSettings(file, settings);
+ }
+
+ /**
+ * Deletes a saved setting
+ *
+ * @param settingName of the setting
+ * @return if the setting could be found
+ *
+ * @since 1.12.0
+ */
+ public static boolean deleteSetting(File file, String settingName) {
+ StringBuilder newConfig = new StringBuilder();
+ boolean delete = false;
+ boolean found = false;
+
+ try {
+ BufferedReader configReader = new BufferedReader(new FileReader(file));
+
+ String line;
+
+ while ((line = configReader.readLine()) != null) {
+ line = line.trim();
+
+ if (line.startsWith("[") && line.endsWith("]")) {
+ if (line.replace("[", "").replace("]", "").split(" ")[0].equals(settingName)) {
+ delete = true;
+ found = true;
+ } else if (delete) {
+ delete = false;
+ newConfig.append(line).append("\n");
+ } else {
+ newConfig.append(line).append("\n");
+ }
+ } else if (!delete) {
+ newConfig.append(line).append("\n");
+ }
+ }
+
+ configReader.close();
+
+ BufferedWriter configFile = new BufferedWriter(new FileWriter(file));
+ configFile.write(newConfig.toString());
+ configFile.newLine();
+
+ configFile.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ return found;
+ }
+
+ /**
+ * Reads all settings saved in a file>
+ *
+ * @param file from which the settings should be read from
+ * @return the settings
+ *
+ * @since 1.12.0
+ */
+ public static TreeMap> readSettings(File file) {
+ TreeMap> returnMap = new TreeMap<>();
+ String settingName = null;
+ Map settingValues = new HashMap<>();
+
+ try {
+ BufferedReader configReader = new BufferedReader(new FileReader(file));
+
+ String line;
+
+ while ((line = configReader.readLine()) != null) {
+
+ if (line.isEmpty()) {
+ continue;
+ } else if (line.startsWith("[") && line.endsWith("]")) {
+ if (settingName != null) {
+ returnMap.put(settingName, settingValues);
+ settingValues = new HashMap<>();
+ }
+ String[] newSetting = line.replace("[", "").replace("]", "").split(" ");
+ settingName = newSetting[0].trim();
+ String[] encoded = newSetting[1].split("=");
+ settingValues.put("encrypted", encoded[1]);
+ } else {
+ String[] keyValue = line.split("=");
+ try {
+ settingValues.put(keyValue[0], keyValue[1]);
+ } catch (IndexOutOfBoundsException e) {
+ settingValues.put(keyValue[0], "");
+ }
+ }
+ }
+
+ if (settingName != null) {
+ returnMap.put(settingName, settingValues);
+ }
+
+ configReader.close();
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ warningAlert("Couldn't find file '" + file.getAbsolutePath() + "'"); // this should never raise
+ } catch (IOException e) {
+ e.printStackTrace();
+ errorAlert("An IO Exception occurred", e.getMessage());
+ }
+
+ return returnMap;
+ }
+
+ /**
+ * Writes settings (could be more than one) to a file
+ *
+ * @param file where the settings should be written in
+ * @param settings of the user
+ *
+ * @since 1.12.0
+ */
+ public static void writeSettings(File file, TreeMap> settings) {
+ try {
+ BufferedWriter configWriter = new BufferedWriter(new FileWriter(file));
+
+ for (Map.Entry> settingElement: settings.entrySet()) {
+ configWriter.write("[" + settingElement.getKey() + " encrypted=" + Boolean.parseBoolean(settingElement.getValue().get("encrypted")) + "]");
+ configWriter.newLine();
+ for (Map.Entry entry : settingElement.getValue().entrySet()) {
+ String key = entry.getKey();
+ if (!key.equals("encrypted")) {
+ configWriter.write(entry.getKey() + "=" + entry.getValue());
+ configWriter.newLine();
+ }
+ }
+ }
+ configWriter.newLine();
+
+ configWriter.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ errorAlert("An error occurred while saving the settings", e.getMessage());
+ }
+ }
+
+}
diff --git a/src/org/bytedream/cryptogx/Utils.java b/src/org/bytedream/cryptogx/Utils.java
new file mode 100644
index 0000000..8b4f8e0
--- /dev/null
+++ b/src/org/bytedream/cryptogx/Utils.java
@@ -0,0 +1,51 @@
+package org.bytedream.cryptogx;
+
+import java.util.TreeMap;
+
+/**
+ * Support class
+ *
+ * @since 1.3.0
+ */
+public class Utils {
+
+ public static TreeMap algorithms = allAlgorithms();
+
+ /**
+ * Get all available algorithms
+ *
+ * @return all available algorithms
+ *
+ * @since 1.12.0
+ */
+ private static TreeMap allAlgorithms() {
+ TreeMap return_map = new TreeMap<>();
+
+ int[] aesKeySizes = {128, 192, 256};
+
+ for (int i: aesKeySizes) {
+ return_map.put("AES-" + i, "AES");
+ }
+
+ return return_map;
+ }
+
+ /**
+ * Checks if any character in {@param characters} appears in {@param string}
+ *
+ * @param characters that should be searched in {@param string}
+ * @param string that should be searched for the characters
+ * @return if any character in {@param characters} appears in {@param string}
+ *
+ * @since 1.3.0
+ */
+ public static boolean hasAnyCharacter(CharSequence characters, String string) {
+ for (char c: characters.toString().toCharArray()) {
+ if (string.indexOf(c) != -1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/src/org/bytedream/cryptogx/resources/addSettingsGUI.fxml b/src/org/bytedream/cryptogx/resources/addSettingsGUI.fxml
new file mode 100644
index 0000000..235a089
--- /dev/null
+++ b/src/org/bytedream/cryptogx/resources/addSettingsGUI.fxml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/org/bytedream/cryptogx/resources/close.png b/src/org/bytedream/cryptogx/resources/close.png
new file mode 100644
index 0000000000000000000000000000000000000000..1113eafd348d9b058f08e335d86fcea4d48e3946
GIT binary patch
literal 14702
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%0AN=LaQB=3_H?
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