From 32f0f9f7a1ab7c9ba62c4b269956eb1392dc2a1f Mon Sep 17 00:00:00 2001 From: EpicKnarvik97 Date: Sun, 10 Aug 2025 14:23:18 +0200 Subject: [PATCH] Rewrites encryption Adds an optional real encryption mode, which encrypts pages using AES, without saving plaintext. Re-implements the old magic encryption in non-real encryption mode. Fixes incorrect key generation for use in the substitution cipher and the gene cipher. Removes the option for saving books as txt. Adds tests for all encryption methods. Saves all necessary decryption data when storing encrypted books. Removes the old book updating code. --- pom.xml | 2 +- .../command/CommandDecrypt.java | 12 +- .../command/CommandDelete.java | 25 +- .../command/CommandDeletePublic.java | 6 +- .../command/CommandEncrypt.java | 17 +- .../command/CommandGroupEncrypt.java | 2 +- .../command/CommandSave.java | 6 +- .../config/BooksWithoutBordersConfig.java | 24 +- .../config/ConfigOption.java | 10 +- .../config/Permission.java | 5 + .../config/Translatable.java | 30 ++ .../bookswithoutborders/encryption/AES.java | 75 +++-- .../encryption/AESConfiguration.java | 24 ++ .../EncryptionStyle.java | 23 +- .../encryption/Encryptor.java | 29 ++ .../encryption/GenenCrypt.java | 27 +- .../bookswithoutborders/encryption/Magic.java | 22 ++ .../encryption/OneTimePad.java | 64 +++++ .../encryption/SubstitutionCipher.java | 140 +++++----- .../handler/BookshelfHandler.java | 14 +- .../listener/PlayerEventListener.java | 115 -------- .../listener/SignEventListener.java | 57 ++-- .../utility/BookFileHelper.java | 66 ++++- .../utility/BookLoader.java | 50 ++-- .../utility/BookToFromTextHelper.java | 130 +++++++-- .../utility/EncryptionHelper.java | 260 ++++++++++++++---- src/main/resources/config.yml | 8 +- src/main/resources/strings.yml | 6 + .../bookswithoutborders/GenenCryptTest.java | 8 +- .../encryption/AESTest.java | 25 +- .../encryption/GenenCryptTest.java | 28 ++ .../encryption/OneTimePadTest.java | 26 ++ .../encryption/SubstitutionCipherTest.java | 26 ++ .../util/EncryptionHelperTest.java | 2 +- 34 files changed, 938 insertions(+), 426 deletions(-) create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/encryption/AESConfiguration.java rename src/main/java/net/knarcraft/bookswithoutborders/{state => encryption}/EncryptionStyle.java (66%) create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/encryption/Encryptor.java create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/encryption/Magic.java create mode 100644 src/main/java/net/knarcraft/bookswithoutborders/encryption/OneTimePad.java create mode 100644 src/test/java/net/knarcraft/bookswithoutborders/encryption/GenenCryptTest.java create mode 100644 src/test/java/net/knarcraft/bookswithoutborders/encryption/OneTimePadTest.java create mode 100644 src/test/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipherTest.java diff --git a/pom.xml b/pom.xml index 0d00958..3e5c06a 100644 --- a/pom.xml +++ b/pom.xml @@ -20,7 +20,7 @@ A continuation of the original Books Without Borders - 16 + 17 UTF-8 diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDecrypt.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDecrypt.java index e6fbcd9..80c1bf0 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDecrypt.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDecrypt.java @@ -59,8 +59,11 @@ public class CommandDecrypt implements TabExecutor { } //Decrypt the book normally - String key = EncryptionHelper.getNumberKeyFromStringKey(arguments[0]); - ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, true); + ItemStack book = EncryptionHelper.loadEncryptedBook(player, arguments[0], true, false); + if (book == null) { + book = EncryptionHelper.loadEncryptedBookLegacy(player, arguments[0], true); + } + if (book != null) { InventoryHelper.setHeldWrittenBook(player, book); stringFormatter.displaySuccessMessage(player, Translatable.SUCCESS_DECRYPTED); @@ -98,7 +101,10 @@ public class CommandDecrypt implements TabExecutor { if (!key.equalsIgnoreCase("")) { //Decrypt the book - ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false); + ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false, true); + if (book == null) { + book = EncryptionHelper.loadEncryptedBookLegacy(player, key, false); + } if (book != null) { InventoryHelper.setHeldWrittenBook(player, book); stringFormatter.displaySuccessMessage(player, Translatable.SUCCESS_AUTO_DECRYPTED); diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDelete.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDelete.java index e72b8b5..834e134 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDelete.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDelete.java @@ -1,12 +1,15 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; +import net.knarcraft.bookswithoutborders.config.BwBCommand; +import net.knarcraft.bookswithoutborders.config.Translatable; import net.knarcraft.bookswithoutborders.gui.PagedBookIndex; import net.knarcraft.bookswithoutborders.state.BookDirectory; import net.knarcraft.bookswithoutborders.utility.BookFileHelper; import net.knarcraft.bookswithoutborders.utility.BookHelper; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; import net.knarcraft.bookswithoutborders.utility.TabCompletionTypeHelper; +import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.util.TabCompletionHelper; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -26,7 +29,7 @@ public class CommandDelete implements TabExecutor { public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] arguments) { if (!(sender instanceof Player)) { - BooksWithoutBorders.sendErrorMessage(sender, "This command can only be used by a player!"); + BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY); return false; } @@ -42,20 +45,16 @@ public class CommandDelete implements TabExecutor { * @return

True if the book was deleted successfully

*/ protected boolean deleteBook(@NotNull CommandSender sender, @NotNull String[] arguments, boolean deletePublic) { - String command = deletePublic ? "deletepublicbook" : "deletebook"; + String command = deletePublic ? BwBCommand.DELETE_PUBLIC_BOOK.toString().toLowerCase() : + BwBCommand.DELETE_BOOK.toString().toLowerCase(); if (PagedBookIndex.displayPage(arguments, sender, deletePublic, command)) { return true; } - if (arguments.length < 1) { - BooksWithoutBorders.sendErrorMessage(sender, "Incorrect number of arguments for this command!"); - return false; - } - //Delete the file List availableBooks = BooksWithoutBorders.getAvailableBooks(sender, deletePublic); if (availableBooks.isEmpty()) { - BooksWithoutBorders.sendErrorMessage(sender, "No files available to delete!"); + BooksWithoutBorders.getStringFormatter().displayErrorMessage(sender, Translatable.ERROR_DELETE_EMPTY); return false; } @@ -73,6 +72,7 @@ public class CommandDelete implements TabExecutor { * @param isPublic

Whether the book to delete is public or not

*/ public void performBookDeletion(@NotNull CommandSender sender, @NotNull String fileName, @NotNull Boolean isPublic) { + StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter(); //If the file name is an index of the load list, load the book try { int loadListIndex = Integer.parseInt(fileName); @@ -90,19 +90,20 @@ public class CommandDelete implements TabExecutor { //Send message if no such file could be found if (file == null) { - BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!"); + stringFormatter.displayErrorMessage(sender, Translatable.ERROR_INCORRECT_FILE_NAME); return; } //Try to delete the file try { if (file.delete()) { - BooksWithoutBorders.sendSuccessMessage(sender, "\"" + fileName + "\" deleted successfully"); + stringFormatter.displaySuccessMessage(sender, + stringFormatter.replacePlaceholder(Translatable.SUCCESS_DELETED, "{file}", fileName)); } else { - BooksWithoutBorders.sendErrorMessage(sender, "Deletion failed without an exception!"); + stringFormatter.displayErrorMessage(sender, Translatable.ERROR_DELETE_FAILED_SILENT); } } catch (Exception e) { - BooksWithoutBorders.sendErrorMessage(sender, "Deletion failed!"); + stringFormatter.displayErrorMessage(sender, Translatable.ERROR_DELETE_FAILED_EXCEPTION); } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDeletePublic.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDeletePublic.java index 08df240..3119cd1 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDeletePublic.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandDeletePublic.java @@ -13,12 +13,14 @@ import java.util.List; public class CommandDeletePublic extends CommandDelete implements TabExecutor { @Override - public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String[] arguments) { + public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, + @NotNull String[] arguments) { return deleteBook(sender, arguments, true); } @Override - public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] arguments) { + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, + @NotNull String[] arguments) { return doTabCompletion(sender, arguments, true); } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandEncrypt.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandEncrypt.java index 5939829..34e2cc1 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandEncrypt.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandEncrypt.java @@ -1,10 +1,12 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; -import net.knarcraft.bookswithoutborders.state.EncryptionStyle; +import net.knarcraft.bookswithoutborders.config.Translatable; +import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle; import net.knarcraft.bookswithoutborders.state.ItemSlot; import net.knarcraft.bookswithoutborders.utility.EncryptionHelper; import net.knarcraft.bookswithoutborders.utility.InventoryHelper; +import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.util.TabCompletionHelper; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; @@ -26,11 +28,19 @@ public class CommandEncrypt implements TabExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] arguments) { - if (performPreChecks(sender, arguments, 1, "You must specify a key to encrypt a book!") == null) { + StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter(); + if (performPreChecks(sender, arguments, 1, + stringFormatter.getUnFormattedColoredMessage(Translatable.ERROR_ENCRYPT_NO_KEY)) == null) { return false; } EncryptionStyle encryptionStyle = arguments.length == 2 ? EncryptionStyle.getFromString(arguments[1]) : EncryptionStyle.SUBSTITUTION; + + // AES is the only reliable method for retaining the plaintext + if (BooksWithoutBorders.getConfiguration().useRealEncryption()) { + encryptionStyle = EncryptionStyle.AES; + } + return encryptBook(encryptionStyle, (Player) sender, arguments[0], ""); } @@ -137,6 +147,9 @@ public class CommandEncrypt implements TabExecutor { if (argumentsCount == 1) { return List.of(""); } else if (argumentsCount == 2) { + if (BooksWithoutBorders.getConfiguration().useRealEncryption()) { + return List.of(); + } return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[1]); } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandGroupEncrypt.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandGroupEncrypt.java index 7ddfc23..9a31785 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandGroupEncrypt.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandGroupEncrypt.java @@ -1,7 +1,7 @@ package net.knarcraft.bookswithoutborders.command; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; -import net.knarcraft.bookswithoutborders.state.EncryptionStyle; +import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.command.TabExecutor; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java index 0f53d88..c9bf904 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/command/CommandSave.java @@ -145,11 +145,7 @@ public class CommandSave implements TabExecutor { } try { - if (config.getUseYml()) { - BookToFromTextHelper.bookToYml(savePath, fileName, book); - } else { - BookToFromTextHelper.bookToTXT(savePath, fileName, book); - } + BookToFromTextHelper.bookToYml(savePath, fileName, book); //Update the relevant book list BooksWithoutBorders.updateBooks(player, saveToPublicFolder); diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java b/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java index c0c09f4..a2abea6 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/BooksWithoutBordersConfig.java @@ -38,11 +38,11 @@ public class BooksWithoutBordersConfig { private boolean authorOnlyCopy; private boolean authorOnlyUnsign; private boolean authorOnlySave; - private boolean useYml; private boolean adminDecrypt; private boolean formatBooks; private boolean changeGenerationOnCopy; private boolean enableBookshelfPeeking; + private boolean useRealEncryption; private final Translator translator; private EconomyManager economyManager; @@ -153,15 +153,6 @@ public class BooksWithoutBordersConfig { return this.enableBookshelfPeeking; } - /** - * Gets whether to use YML, not TXT, for saving books - * - * @return

Whether to use YML for saving books

- */ - public boolean getUseYml() { - return this.useYml; - } - /** * Gets whether admins should be able to decrypt books without a password, and decrypt all group encrypted books * @@ -285,6 +276,15 @@ public class BooksWithoutBordersConfig { return (this.bookPriceType != null && this.bookPriceQuantity > 0); } + /** + * Checks whether to use real encryption for encrypted books + * + * @return

True if real encryption should be used

+ */ + public boolean useRealEncryption() { + return this.useRealEncryption; + } + /** * Gets the path used to store encrypted books * @@ -301,7 +301,6 @@ public class BooksWithoutBordersConfig { public void saveConfigValues() { Logger logger = BooksWithoutBorders.getInstance().getLogger(); Configuration config = BooksWithoutBorders.getInstance().getConfig(); - config.set(ConfigOption.USE_YAML.getConfigNode(), this.useYml); config.set(ConfigOption.MAX_DUPLICATES.getConfigNode(), this.bookDuplicateLimit); config.set(ConfigOption.TITLE_AUTHOR_SEPARATOR.getConfigNode(), this.titleAuthorSeparator); config.set(ConfigOption.LORE_LINE_SEPARATOR.getConfigNode(), this.loreSeparator); @@ -309,6 +308,7 @@ public class BooksWithoutBordersConfig { config.set(ConfigOption.MESSAGE_FOR_NEW_PLAYERS.getConfigNode(), this.welcomeMessage); config.set(ConfigOption.FORMAT_AFTER_SIGNING.getConfigNode(), this.formatBooks); config.set(ConfigOption.ENABLE_BOOKSHELF_PEEKING.getConfigNode(), this.enableBookshelfPeeking); + config.set(ConfigOption.USE_REAL_ENCRYPTION.getConfigNode(), this.useRealEncryption); String itemTypeNode = ConfigOption.PRICE_ITEM_TYPE.getConfigNode(); if (this.bookPriceType != null) { @@ -354,7 +354,6 @@ public class BooksWithoutBordersConfig { BooksWithoutBorders.getInstance().reloadConfig(); Configuration config = BooksWithoutBorders.getInstance().getConfig(); try { - this.useYml = getBoolean(config, ConfigOption.USE_YAML); this.bookDuplicateLimit = config.getInt(ConfigOption.MAX_DUPLICATES.getConfigNode(), (Integer) ConfigOption.MAX_DUPLICATES.getDefaultValue()); this.titleAuthorSeparator = getString(config, ConfigOption.TITLE_AUTHOR_SEPARATOR); @@ -368,6 +367,7 @@ public class BooksWithoutBordersConfig { this.formatBooks = getBoolean(config, ConfigOption.FORMAT_AFTER_SIGNING); this.changeGenerationOnCopy = getBoolean(config, ConfigOption.CHANGE_GENERATION_ON_COPY); this.enableBookshelfPeeking = getBoolean(config, ConfigOption.ENABLE_BOOKSHELF_PEEKING); + this.useRealEncryption = getBoolean(config, ConfigOption.USE_REAL_ENCRYPTION); String language = config.getString("language", "en"); this.translator.loadLanguages(BooksWithoutBorders.getInstance().getDataFolder(), "en", language); diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java b/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java index b984702..1f79a5b 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/ConfigOption.java @@ -7,11 +7,6 @@ import org.jetbrains.annotations.NotNull; */ public enum ConfigOption { - /** - * Whether YAML should be used to store books instead of simple text files - */ - USE_YAML("Options.Save_Books_in_Yaml_Format", true), - /** * The max duplicates of a book that can be saved */ @@ -81,6 +76,11 @@ public enum ConfigOption { * Whether hitting a bookshelf should display information about the contained books */ ENABLE_BOOKSHELF_PEEKING("Options.Enable_Book_Peeking", true), + + /** + * Whether to use real AES encryption instead of storing garbled book text, while the full plaintext is stored in a file + */ + USE_REAL_ENCRYPTION("Options.Use_Real_Encryption", false), ; private final String configNode; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java b/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java index 05a6a3b..220eec8 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/Permission.java @@ -22,6 +22,11 @@ public enum Permission { */ BYPASS_AUTHOR_ONLY_COPY("bypassAuthorOnlyCopy"), + /** + * Does nothing by itself, but its child nodes allow decrypting group encrypted books for the specified group + */ + DECRYPT("decrypt"), + ; private final @NotNull String node; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java b/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java index fda1f49..f012d6b 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/config/Translatable.java @@ -28,6 +28,11 @@ public enum Translatable implements TranslatableMessage { */ SUCCESS_AUTO_DECRYPTED, + /** + * The success message displayed when a book is successfully deleted + */ + SUCCESS_DELETED, + /** * The error to display when the console attempts to run a player-only command */ @@ -103,6 +108,31 @@ public enum Translatable implements TranslatableMessage { */ ERROR_ENCRYPTED_BOOK_UNKNOWN, + /** + * The error displayed when trying to delete a book while the book folder is empty or missing + */ + ERROR_DELETE_EMPTY, + + /** + * The error displayed when given the name of a book that does not exist + */ + ERROR_INCORRECT_FILE_NAME, + + /** + * The error displayed when a file failed to be deleted, without throwing an exception + */ + ERROR_DELETE_FAILED_SILENT, + + /** + * The error displayed when a file failed to be deleted, after throwing an exception + */ + ERROR_DELETE_FAILED_EXCEPTION, + + /** + * The error displayed when trying to encrypt a book without supplying a key + */ + ERROR_ENCRYPT_NO_KEY, + /** * The header displayed before printing all commands */ diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java index a38a78c..de8b29a 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/AES.java @@ -29,33 +29,58 @@ import java.util.logging.Level; * * @author EpicKnarvik97 */ -public class AES { +public class AES implements Encryptor { - //TODO: Generate salt for each installation, and figure out what to to with the iv parameter - private final IvParameterSpec ivParameterSpec; + private final @NotNull IvParameterSpec ivParameterSpec; private final byte[] passwordSalt; + private final @NotNull String password; /** * Instantiates a new AES encryptor * - * @param initializationVector

The initialization vector to use for CBC

- * @param passwordSalt

The password salt to use

+ * @param aesConfiguration

The AES configuration to use

*/ - public AES(byte[] initializationVector, byte[] passwordSalt) { - this.ivParameterSpec = new IvParameterSpec(initializationVector); - this.passwordSalt = passwordSalt; + public AES(@NotNull AESConfiguration aesConfiguration) { + this.ivParameterSpec = new IvParameterSpec(aesConfiguration.iv()); + this.passwordSalt = aesConfiguration.salt(); + this.password = aesConfiguration.key(); + } + + @Override + @Nullable + public String encryptText(@NotNull String input) { + return encryptDecryptText(input, true); + } + + @Override + @Nullable + public String decryptText(@NotNull String input) { + return encryptDecryptText(input, false); + } + + /** + * Generates a 16-byte initialization vector + * + * @return

An initialization vector

+ */ + public static byte[] generateIV() { + byte[] initializationVector = new byte[16]; + SecureRandom secureRandom = new SecureRandom(); + secureRandom.nextBytes(initializationVector); + return initializationVector; } /** * Encrypts or decrypts the given text * - * @param input

The input to encrypt or decrypt

- * @param password

The password to use for key generation

- * @param encrypt

Whether to encrypt or decrypt the input

+ *

Note: The same IV and salt must be used during instantiation in order to decrypt an encrypted message.

+ * + * @param input

The input to encrypt or decrypt

+ * @param encrypt

Whether to encrypt or decrypt the input

* @return

The encrypted/decrypted input, or null if anything went wrong

*/ @Nullable - public String encryptDecryptText(@NotNull String input, @NotNull String password, boolean encrypt) { + private String encryptDecryptText(@NotNull String input, boolean encrypt) { //Make a key from the password SecretKeySpec secretKeySpec = getKeyFromPassword(password); //Get cipher instance @@ -80,24 +105,15 @@ public class AES { try { byte[] output = aes.doFinal(getInputBytes(input, encrypt)); return createResult(output, encrypt); - } catch (IllegalBlockSizeException | BadPaddingException exception) { - BooksWithoutBorders.log(Level.SEVERE, "Invalid AES block size or padding"); + } catch (IllegalBlockSizeException exception) { + BooksWithoutBorders.log(Level.SEVERE, "Invalid AES block size during finalization"); + return null; + } catch (BadPaddingException exception) { + BooksWithoutBorders.log(Level.SEVERE, "Invalid AES padding during finalization"); return null; } } - /** - * Generates a 16-byte initialization vector - * - * @return

An initialization vector

- */ - public static byte[] generateIV() { - byte[] initializationVector = new byte[16]; - SecureRandom secureRandom = new SecureRandom(); - secureRandom.nextBytes(initializationVector); - return initializationVector; - } - /** * Transforms the input string into bytes * @@ -139,8 +155,11 @@ public class AES { Cipher aes; try { aes = Cipher.getInstance("AES/CBC/PKCS5Padding"); - } catch (NoSuchAlgorithmException | NoSuchPaddingException exception) { - BooksWithoutBorders.log(Level.SEVERE, "Invalid AES algorithm or padding"); + } catch (NoSuchAlgorithmException exception) { + BooksWithoutBorders.log(Level.SEVERE, "Invalid AES algorithm during Cipher generation"); + return null; + } catch (NoSuchPaddingException exception) { + BooksWithoutBorders.log(Level.SEVERE, "Invalid AES padding during Cipher generation"); return null; } return aes; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/AESConfiguration.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/AESConfiguration.java new file mode 100644 index 0000000..0038c96 --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/AESConfiguration.java @@ -0,0 +1,24 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import org.jetbrains.annotations.NotNull; + +/** + * A configuration for AES encryption + * + * @param iv

The initialization vector

+ * @param salt

The encryption salt

+ * @param key

The encryption key

+ */ +public record AESConfiguration(byte @NotNull [] iv, byte @NotNull [] salt, @NotNull String key) { + + /** + * Generates a new AES configuration with randomized IV and salt + * + * @param key

The encryption key to use

+ * @return

The new AES configuration

+ */ + public static AESConfiguration getNewConfiguration(@NotNull String key) { + return new AESConfiguration(AES.generateIV(), AES.generateIV(), key); + } + +} diff --git a/src/main/java/net/knarcraft/bookswithoutborders/state/EncryptionStyle.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/EncryptionStyle.java similarity index 66% rename from src/main/java/net/knarcraft/bookswithoutborders/state/EncryptionStyle.java rename to src/main/java/net/knarcraft/bookswithoutborders/encryption/EncryptionStyle.java index 9491051..5174826 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/state/EncryptionStyle.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/EncryptionStyle.java @@ -1,4 +1,4 @@ -package net.knarcraft.bookswithoutborders.state; +package net.knarcraft.bookswithoutborders.encryption; import org.jetbrains.annotations.NotNull; @@ -7,9 +7,30 @@ import org.jetbrains.annotations.NotNull; */ public enum EncryptionStyle { + /** + * Possibly lossy encryption using DNA codons + */ DNA("dna"), + + /** + * A simple cipher using the key to substitute one character for another + */ SUBSTITUTION("substitution"), + + /** + * A military-grade encryption cypher + */ AES("aes"), + + /** + * An unbreakable encryption method assuming the key is completely random and never used more than once, ever + */ + ONE_TIME_PAD("onetimepad"), + + /** + * Just a way of using magic text to make text illegible + */ + MAGIC("magic"), ; private final String name; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/Encryptor.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/Encryptor.java new file mode 100644 index 0000000..7ce1fe5 --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/Encryptor.java @@ -0,0 +1,29 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An interface describing a style of encryption + */ +public interface Encryptor { + + /** + * Encrypts the given plaintext + * + * @param input

The input to encrypt

+ * @return

The resulting cypher text, or null if unsuccessful

+ */ + @Nullable + String encryptText(@NotNull String input); + + /** + * Decrypts the given cypher text + * + * @param input

The cypher text to decrypt

+ * @return

The resulting plaintext, or null if unsuccessful

+ */ + @Nullable + String decryptText(@NotNull String input); + +} diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/GenenCrypt.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/GenenCrypt.java index 522e29a..f605cbc 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/encryption/GenenCrypt.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/GenenCrypt.java @@ -1,6 +1,7 @@ package net.knarcraft.bookswithoutborders.encryption; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.HashMap; @@ -12,11 +13,11 @@ import java.util.Random; *

Not sure where this was gotten from, but it does exist at * Stack Exchange.

*/ -public class GenenCrypt { +public class GenenCrypt implements Encryptor { private final Random ranGen; private final String[] bases; - private final String[] charList; + private final String[] availableCharacters; private final HashMap codonTable; private final HashMap decryptTable; private final String key; @@ -62,32 +63,40 @@ public class GenenCrypt { // 10 digits // space, newline, and tab // the symbols . , ? " ! @ # $ % ^ & * ( ) - + = / _ \ : ; < > - charList = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", + availableCharacters = new String[]{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", " ", "\t", "\n", ".", ",", "?", "\"", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "+", "=", "/", "_", "\\", ":", ";", "<", ">", "|"}; // define the codon table to encode text codonTable = new HashMap<>(); - for (int i = 0; i < charList.length; i++) { + for (int i = 0; i < availableCharacters.length; i++) { String[] tempArray = new String[]{shuffledCodonList.get(4 * i), shuffledCodonList.get(4 * i + 1), shuffledCodonList.get(4 * i + 2), shuffledCodonList.get(4 * i + 3)}; //System.out.println(i); - codonTable.put(charList[i], tempArray); + codonTable.put(availableCharacters[i], tempArray); } // define the decryption table decryptTable = new HashMap<>(); for (int i = 0; i < codonTable.size(); i++) { - String s = charList[i]; + String s = availableCharacters[i]; String[] sa = codonTable.get(s); decryptTable.put(sa[0], s); decryptTable.put(sa[1], s); decryptTable.put(sa[2], s); decryptTable.put(sa[3], s); } + } + @Override + public @Nullable String encryptText(@NotNull String input) { + return encrypt(input); + } + @Override + public @Nullable String decryptText(@NotNull String input) { + return decrypt(input); } /** @@ -96,7 +105,7 @@ public class GenenCrypt { public void printCodonTable() { // print the codon table for (int i = 0; i < codonTable.size(); i++) { - String s = charList[i]; + String s = availableCharacters[i]; String[] sa = codonTable.get(s); switch (s) { case "\t" -> @@ -117,7 +126,7 @@ public class GenenCrypt { * @return

The encrypted input

*/ @NotNull - public String encrypt(@NotNull String input) { + private String encrypt(@NotNull String input) { StringBuilder output = new StringBuilder(); for (int i = 0; i < input.length(); i++) { // insert junk bases @@ -151,7 +160,7 @@ public class GenenCrypt { * @return

The decrypted input

*/ @NotNull - public String decrypt(@NotNull String input) { + private String decrypt(@NotNull String input) { StringBuilder output = new StringBuilder(); int keyCount = 0; int junk = key.charAt(0); diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/Magic.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/Magic.java new file mode 100644 index 0000000..b865fc1 --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/Magic.java @@ -0,0 +1,22 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import net.knarcraft.bookswithoutborders.utility.BookFormatter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * So-called "Magic" encryption which simply makes the contents unreadable + */ +public class Magic implements Encryptor { + + @Override + public @Nullable String encryptText(@NotNull String input) { + return "§k" + BookFormatter.stripColor(input.replace("§", "")); + } + + @Override + public @Nullable String decryptText(@NotNull String input) { + return null; + } + +} diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/OneTimePad.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/OneTimePad.java new file mode 100644 index 0000000..ad7a362 --- /dev/null +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/OneTimePad.java @@ -0,0 +1,64 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; + +/** + * A one-time pad implementation + */ +public class OneTimePad implements Encryptor { + + private final @NotNull String key; + + /** + * Instantiates a new one-time pad + * + * @param key

The key to use

+ */ + public OneTimePad(@NotNull String key) { + this.key = key; + } + + @Override + public @Nullable String encryptText(@NotNull String input) { + return oneTimePad(input); + } + + @Override + public @Nullable String decryptText(@NotNull String input) { + return oneTimePad(input); + } + + /** + * Encrypts/decrypts the input using a one-time pad + * + *

The one time pad encryption is very secure, and encryption works just like decryption, but is vulnerable if + * the same key is used more than once.

+ * + * @param input

The input to encrypt/decrypt

+ * @return

The encrypted/decrypted output

+ */ + @NotNull + public String oneTimePad(@NotNull String input) { + String longKey; + try { + final MessageDigest digest = MessageDigest.getInstance("SHA3-256"); + final byte[] hashBytes = digest.digest(key.getBytes(StandardCharsets.UTF_8)); + longKey = Base64.getEncoder().encodeToString(hashBytes); + } catch (NoSuchAlgorithmException exception) { + longKey = key; + } + + StringBuilder output = new StringBuilder(); + for (int i = 0; i < input.length(); i++) { + output.append((char) (input.charAt(i) ^ longKey.charAt(i % longKey.length()))); + } + return output.toString(); + } + +} diff --git a/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java b/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java index c460161..24396a0 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipher.java @@ -1,6 +1,7 @@ package net.knarcraft.bookswithoutborders.encryption; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.math.BigInteger; import java.util.StringTokenizer; @@ -8,90 +9,89 @@ import java.util.StringTokenizer; /** * A simple substitution cipher */ -public class SubstitutionCipher { +public class SubstitutionCipher implements Encryptor { - public SubstitutionCipher() { + private final @NotNull String key; + /** + * Instantiates a new substitution cipher + * + * @param key

The key to use

+ */ + public SubstitutionCipher(@NotNull String key) { + this.key = key; } - // encrypts a string using a substitution cipher. - // the substitution is made harder to crack by - // using a string for the key, it is converted - // a series of offsets that each character in the - // original message is offset by - @NotNull - public String encrypt(@NotNull String in, @NotNull String key) { - StringBuilder output = new StringBuilder(); - if (!key.isEmpty()) { - StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key - // converts each number in the key to an integer and adds to an array - int[] offsetArray = new int[tokenizer.countTokens()]; - for (int i = 0; i < offsetArray.length; i++) { - String nt = tokenizer.nextToken(); + @Override + public @Nullable String encryptText(@NotNull String input) { + return encryptDecrypt(input, true); + } - try { - offsetArray[i] = Integer.parseInt(nt); - } catch (NumberFormatException e) { - BigInteger big = new BigInteger(nt); - offsetArray[i] = Math.abs(big.intValue()); - } + @Override + public @Nullable String decryptText(@NotNull String input) { + return encryptDecrypt(input, false); + } + + /** + * Encrypts or decrypts a string using a substitution cipher + * + *

The substitution is made harder to crack by using a string for the key, it is converted a series of offsets + * that each character in the original message is offset by.

+ * + * @param input

The input to encrypt/decrypt

+ * @param encrypt

Whether to encrypt or decrypt the input

+ * @return

The encryption output

+ */ + @NotNull + private String encryptDecrypt(@NotNull String input, boolean encrypt) { + StringBuilder output = new StringBuilder(); + if (this.key.isBlank()) { + return output.toString(); + } + + // converts each number in the key to an integer and adds to an array + int[] offsetArray = getOffsetArray(this.key); + + int offsetPosition = 0; + for (int i = 0; i < input.length(); i++) { + // encrypts the letter and adds to the output string + if (encrypt) { + output.append((char) (input.charAt(i) + offsetArray[offsetPosition])); + } else { + output.append((char) (input.charAt(i) - offsetArray[offsetPosition])); } - int offsetPosition = 0; - for (int i = 0; i < in.length(); i++) { - output.append((char) (in.charAt(i) + offsetArray[offsetPosition])); //encrypts the letter and adds to the output string - // uses the next offset in the key, goes back to first offset if at end of list - if (offsetPosition < offsetArray.length - 1) { - offsetPosition++; - } else { - offsetPosition = 0; - } + // uses the next offset in the key, goes back to first offset if at end of list + if (offsetPosition < offsetArray.length - 1) { + offsetPosition++; + } else { + offsetPosition = 0; } } return output.toString(); } - // decrypts a string using the same substitution method, - // but in reverse. Could probably be combined into one - // method with a flag for encryption / decryption, but - // I'm lazy. - @SuppressWarnings("unused") - @NotNull - public String decrypt(@NotNull String in, @NotNull String key) { - StringBuilder output = new StringBuilder(); - if (!key.isEmpty()) { - StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key - // converts each number in the key to an integer and adds to an array - int[] offsetArray = new int[tokenizer.countTokens()]; - for (int i = 0; i < offsetArray.length; i++) { - offsetArray[i] = Integer.parseInt(tokenizer.nextToken()); - } - int offsetPosition = 0; - for (int i = 0; i < in.length(); i++) { - output.append((char) (in.charAt(i) - offsetArray[offsetPosition])); //encrypts the letter and adds to the output string - // uses the next offset in the key, goes back to first offset if at end of list - if (offsetPosition < offsetArray.length - 1) { - offsetPosition++; - } else { - offsetPosition = 0; - } + /** + * Tokenizes a key and generates an offset array for substitution + * + * @param key

The key to make an offset array for

+ * @return

The offset array

+ */ + private int[] getOffsetArray(@NotNull String key) { + StringTokenizer tokenizer = new StringTokenizer(key, ", "); // tokenizes the key + // converts each number in the key to an integer and adds to an array + int[] offsetArray = new int[tokenizer.countTokens()]; + for (int i = 0; i < offsetArray.length; i++) { + String nextToken = tokenizer.nextToken(); + + try { + offsetArray[i] = Integer.parseInt(nextToken); + } catch (NumberFormatException e) { + BigInteger big = new BigInteger(nextToken); + offsetArray[i] = Math.abs(big.intValue()); } } - return output.toString(); - } - - - // the one time pad encryption is very secure, and - // encryption works just like decryption, but is - // vulnerable if the same key is used more than once. - @SuppressWarnings("unused") - @NotNull - public String oneTimePad(@NotNull String in, @NotNull String key) { - StringBuilder output = new StringBuilder(); - for (int i = 0; i < in.length(); i++) { - output.append((char) (in.charAt(i) ^ key.charAt(i % key.length()))); - } - return output.toString(); + return offsetArray; } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java b/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java index 7d70b2f..a8b8ee2 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/handler/BookshelfHandler.java @@ -12,7 +12,6 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; -import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -89,19 +88,10 @@ public class BookshelfHandler { String loreKey = key + ".lore"; String title = bookshelfSection.getString(titleKey, null); - List loreStrings = new ArrayList<>(); - List lore = bookshelfSection.getList(loreKey); - if (lore == null) { - throw new IllegalArgumentException("Lore is missing from bookshelf data!"); - } - lore.forEach((item) -> { - if (item instanceof String) { - loreStrings.add((String) item); - } - }); + List lore = bookshelfSection.getStringList(loreKey); if (title != null) { - registerBookshelf(new Bookshelf(bookshelfLocation, title, loreStrings)); + registerBookshelf(new Bookshelf(bookshelfLocation, title, lore)); } } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/listener/PlayerEventListener.java b/src/main/java/net/knarcraft/bookswithoutborders/listener/PlayerEventListener.java index 76b98d0..9d63f61 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/listener/PlayerEventListener.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/listener/PlayerEventListener.java @@ -2,23 +2,14 @@ package net.knarcraft.bookswithoutborders.listener; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; -import net.knarcraft.bookswithoutborders.state.BookDirectory; -import net.knarcraft.bookswithoutborders.utility.BookHelper; import net.knarcraft.bookswithoutborders.utility.BookLoader; import net.knarcraft.bookswithoutborders.utility.InputCleaningHelper; -import net.knarcraft.bookswithoutborders.utility.InventoryHelper; -import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerItemHeldEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.inventory.ItemStack; -import org.bukkit.inventory.PlayerInventory; -import org.bukkit.inventory.meta.BookMeta; -import org.bukkit.inventory.meta.ItemMeta; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.logging.Level; @@ -30,31 +21,6 @@ public class PlayerEventListener implements Listener { private final BooksWithoutBorders booksWithoutBorders = BooksWithoutBorders.getInstance(); - @EventHandler - public void onHold(@NotNull PlayerItemHeldEvent event) { - if (event.isCancelled()) { - return; - } - - Player player = event.getPlayer(); - int selectedSlot = event.getNewSlot(); - PlayerInventory playerInventory = player.getInventory(); - ItemStack selectedItem = playerInventory.getItem(selectedSlot); - - //Ignore irrelevant items - if (selectedItem == null || selectedItem.getType() != Material.WRITTEN_BOOK) { - return; - } - - ItemMeta itemMetadata = selectedItem.getItemMeta(); - if (itemMetadata == null) { - return; - } - - //Update the book the user is viewing - updateBookInHand(player, itemMetadata, true); - } - @EventHandler public void onPlayerJoin(@NotNull PlayerJoinEvent event) { Player player = event.getPlayer(); @@ -79,22 +45,6 @@ public class PlayerEventListener implements Listener { sendMessage = giveBookToNewPlayer(bookName, player, sendMessage); } } - - //Updates any books in either hand - ItemStack mainHandItem = InventoryHelper.getHeldItem(player, true); - ItemStack offHandItem = InventoryHelper.getHeldItem(player, false); - if (mainHandItem.getType() == Material.WRITTEN_BOOK) { - ItemMeta itemMetadata = mainHandItem.getItemMeta(); - if (itemMetadata != null) { - updateBookInHand(player, itemMetadata, true); - } - } - if (offHandItem.getType() == Material.WRITTEN_BOOK) { - ItemMeta itemMetadata = offHandItem.getItemMeta(); - if (itemMetadata != null) { - updateBookInHand(player, itemMetadata, false); - } - } } /** @@ -125,69 +75,4 @@ public class PlayerEventListener implements Listener { return sendMessage; } - /** - * Updates a book in one of the player's hands - * - * @param player

The player to update

- * @param itemMetadata

Information about the held book

- * @param mainHand

Whether to update the book in the player's main hand

- */ - private void updateBookInHand(@NotNull Player player, @NotNull ItemMeta itemMetadata, boolean mainHand) { - PlayerInventory playerInventory = player.getInventory(); - ItemStack updatedBook = updateBook(player, (BookMeta) itemMetadata); - if (updatedBook != null) { - if (mainHand) { - playerInventory.setItemInMainHand(updatedBook); - } else { - playerInventory.setItemInOffHand(updatedBook); - } - } - } - - /** - * Updates old books to a newer format - * - * @param player

The player holding the book

- * @param oldBook

Metadata about the held book

- * @return

An updated book

- */ - @Nullable - public ItemStack updateBook(@NotNull Player player, @NotNull BookMeta oldBook) { - //handles hacked title-less books - if (oldBook.getTitle() == null || oldBook.getTitle().length() < 3) { - return null; - } - - if (oldBook.getTitle().substring(oldBook.getTitle().length() - 3).equalsIgnoreCase("[U]")) { - String fileName; - - if (oldBook.getAuthor() != null && oldBook.getAuthor().equalsIgnoreCase("unknown")) { - //Unknown author is ignored - fileName = oldBook.getTitle(); - } else { - fileName = oldBook.getTitle() + BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator() + oldBook.getAuthor(); - } - - String playerFolderPath = BookHelper.getBookDirectoryPathString(BookDirectory.PLAYER, player); - String publicFolderPath = BookHelper.getBookDirectoryPathString(BookDirectory.PUBLIC, player); - - String[] possiblePaths = new String[]{ - publicFolderPath + fileName + ".yml", - publicFolderPath + fileName + ".txt", - playerFolderPath + fileName + ".yml", - playerFolderPath + fileName + ".txt" - }; - - for (String path : possiblePaths) { - File file = new File(path); - if (file.isFile()) { - return BookLoader.loadBook(player, fileName, "true", "player"); - } - } - return null; - } - - return null; - } - } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java b/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java index e27e81f..e26ef95 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/listener/SignEventListener.java @@ -2,7 +2,9 @@ package net.knarcraft.bookswithoutborders.listener; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; -import net.knarcraft.bookswithoutborders.state.EncryptionStyle; +import net.knarcraft.bookswithoutborders.config.Permission; +import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle; +import net.knarcraft.bookswithoutborders.state.BookDirectory; import net.knarcraft.bookswithoutborders.utility.BookFileHelper; import net.knarcraft.bookswithoutborders.utility.BookFormatter; import net.knarcraft.bookswithoutborders.utility.BookLoader; @@ -55,10 +57,11 @@ public class SignEventListener implements Listener { event.setLine(0, ChatColor.DARK_GREEN + "[BwB]"); //Check if the sign is of a valid type - if (!((lines[1].equalsIgnoreCase("[Encrypt]") || lines[1].equalsIgnoreCase("[Decrypt]") || - lines[1].equalsIgnoreCase("[Give]")) && !lines[2].trim().isEmpty())) { + if ((!lines[1].equalsIgnoreCase("[Encrypt]") && !lines[1].equalsIgnoreCase("[Decrypt]") && + !lines[1].equalsIgnoreCase("[Give]")) || lines[2].trim().isEmpty()) { //Mark the second line as invalid event.setLine(1, ChatColor.DARK_RED + lines[1]); + player.sendMessage("Invalid sign!"); return; } @@ -79,11 +82,6 @@ public class SignEventListener implements Listener { @EventHandler public void onClick(@NotNull PlayerInteractEvent event) { - if (event.getClickedBlock() == null) { - return; - } - - Material clickedBlockType = event.getClickedBlock().getType(); Player player = event.getPlayer(); PlayerInventory playerInventory = player.getInventory(); EquipmentSlot hand = event.getHand(); @@ -96,15 +94,18 @@ public class SignEventListener implements Listener { } Material heldItemType = heldItem.getType(); - if (event.getAction() == Action.RIGHT_CLICK_BLOCK && (Tag.SIGNS.isTagged(clickedBlockType) || - Tag.WALL_SIGNS.isTagged(clickedBlockType))) { - event.setUseItemInHand(Event.Result.DENY); + if (event.getClickedBlock() != null && (event.getAction() == Action.RIGHT_CLICK_BLOCK && + (Tag.SIGNS.isTagged(event.getClickedBlock().getType()) || + Tag.WALL_SIGNS.isTagged(event.getClickedBlock().getType())))) { //The player right-clicked a sign Sign sign = (Sign) event.getClickedBlock().getState(); if (!signLineEquals(sign, 0, "[BwB]", ChatColor.DARK_GREEN)) { return; } + event.setUseItemInHand(Event.Result.DENY); + event.setCancelled(true); + if (signLineEquals(sign, 1, "[Encrypt]", ChatColor.DARK_BLUE)) { encryptHeldBookUsingSign(sign, heldItemType, player, hand); } else if (signLineEquals(sign, 1, "[Decrypt]", ChatColor.DARK_BLUE)) { @@ -120,6 +121,7 @@ public class SignEventListener implements Listener { } } else if (heldItemType == Material.WRITTEN_BOOK && (event.getAction() == Action.LEFT_CLICK_AIR || event.getAction() == Action.LEFT_CLICK_BLOCK)) { + BookMeta oldBook = (BookMeta) heldItem.getItemMeta(); if (oldBook == null) { return; @@ -147,7 +149,11 @@ public class SignEventListener implements Listener { String lineText = BookFormatter.stripColor(sign.getSide(Side.FRONT).getLine(2)); String key = EncryptionHelper.getNumberKeyFromStringKey(lineText); - ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false); + ItemStack book = EncryptionHelper.loadEncryptedBook(player, key, false, false); + if (book == null) { + book = EncryptionHelper.loadEncryptedBookLegacy(player, key, false); + } + if (book != null) { player.getInventory().setItem(hand, book); player.sendMessage(ChatColor.GREEN + "Book decrypted!"); @@ -256,25 +262,32 @@ public class SignEventListener implements Listener { String groupName = oldBook.getLore().get(0).substring(3).split(" encrypted")[0]; //Permission check - if (!player.hasPermission("bookswithoutborders.decrypt." + groupName) && - !(config.getAdminDecrypt() && player.hasPermission("bookswithoutborders.admin"))) { + if (!player.hasPermission(Permission.DECRYPT + "." + groupName) && + !(config.getAdminDecrypt() && player.hasPermission(Permission.ADMIN.toString()))) { + BooksWithoutBorders.sendErrorMessage(player, "You are not allowed to decrypt that book"); return; } + String encryptedFolder = BooksWithoutBorders.getConfiguration().getEncryptedBookPath(); String fileName = oldBook.getTitle() + config.getTitleAuthorSeparator() + oldBook.getAuthor(); - String encryptionFile = InputCleaningHelper.cleanString(groupName) + config.getSlash() + fileName + ".yml"; - - File file = new File(BooksWithoutBorders.getConfiguration().getEncryptedBookPath() + encryptionFile); - if (!file.isFile()) { - file = new File(config.getBookFolder() + fileName + ".txt"); - if (!file.isFile()) { - return; + File file = BookFileHelper.findBookFile(encryptedFolder + InputCleaningHelper.cleanString(groupName) + + config.getSlash(), oldBook); + if (file == null) { + file = BookFileHelper.findBookFile(encryptedFolder, oldBook); + if (file == null) { + file = BookFileHelper.findBookFile(config.bookFolder, oldBook); + if (file == null) { + BooksWithoutBorders.sendErrorMessage(player, "Unable to find encrypted book"); + return; + } } } - newBook = BookLoader.loadBook(player, fileName, "true", groupName, heldItem.getAmount()); + + newBook = BookLoader.loadBook(player, fileName, "true", BookDirectory.ENCRYPTED, groupName, heldItem.getAmount()); if (newBook == null) { + BooksWithoutBorders.sendErrorMessage(player, "Unable to load the unencrypted book!"); return; } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java index 7320141..12c13ac 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookFileHelper.java @@ -3,7 +3,10 @@ package net.knarcraft.bookswithoutborders.utility; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.Translatable; import net.knarcraft.bookswithoutborders.state.BookDirectory; +import org.bukkit.Bukkit; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.inventory.meta.BookMeta; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -236,21 +239,60 @@ public final class BookFileHelper { } /** - * Gets the extension from the given path + * Attempts to find the correct book file * - * @param path

The path to get the extension from

- * @return

The extension of the input

+ * @param folder

The folder the book is in

+ * @param bookMeta

The book meta of the book to find

+ * @return

The book's file, or null if not found

*/ - @NotNull - public static String getExtensionFromPath(@NotNull String path) { - int dotIndex = path.lastIndexOf("."); - if (dotIndex > 0) { - String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator(); - if (path.lastIndexOf(separator) < dotIndex && (path.length() - dotIndex == 4)) { - return path.substring((path.length() - dotIndex) + 1); - } + @Nullable + public static File findBookFile(@NotNull String folder, @NotNull BookMeta bookMeta) { + String separator = BooksWithoutBorders.getConfiguration().getTitleAuthorSeparator(); + String fileName = bookMeta.getTitle() + separator + bookMeta.getAuthor(); + return findBookFile(folder, fileName); + } + + /** + * Attempts to find the correct book file + * + * @param folder

The folder the book is in

+ * @param fileName

The name of the book to find

+ * @return

The book's file, or null if not found

+ */ + @Nullable + public static File findBookFile(@NotNull String folder, @NotNull String fileName) { + fileName = InputCleaningHelper.cleanString(fileName); + File file = new File(folder, fileName + ".yml"); + if (file.exists()) { + return getBookFile(file.getAbsolutePath()); + } + file = new File(folder, fileName.replace(" ", "_") + ".yml"); + if (file.exists()) { + return getBookFile(file.getAbsolutePath()); + } + file = new File(folder, fileName.replace(" ", "_") + ".txt"); + if (file.exists()) { + return getBookFile(file.getAbsolutePath()); + } else { + return null; + } + } + + /** + * Replaces an author name with a player UUID if matched + * + * @param fileName

The filename to replace the author of

+ * @return

The filename, or the filename with the author replaced with UUID

+ */ + public static String replaceAuthorWithUUID(@NotNull String fileName) { + String userName = BookFormatter.stripColor(getBookAuthorFromPath(fileName)); + + Player player = Bukkit.getPlayer(userName); + if (player != null) { + return userName.replace(userName, player.getUniqueId().toString()); + } else { + return userName; } - return ""; } } diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookLoader.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookLoader.java index f53b55f..3e5988d 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookLoader.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookLoader.java @@ -3,7 +3,6 @@ package net.knarcraft.bookswithoutborders.utility; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.state.BookDirectory; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; @@ -15,6 +14,7 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.logging.Level; /** * A helper class for loading books from files @@ -57,11 +57,28 @@ public final class BookLoader { BooksWithoutBorders.sendErrorMessage(sender, "Unrecognized book directory!"); return null; } + return loadBook(sender, fileName, isSigned, bookDirectory, directory, numCopies); + } + /** + * Loads the given book + * + * @param sender

The command sender trying to load the book

+ * @param fileName

The index or file name of the book to load

+ * @param isSigned

Whether to load the book as signed, and not unsigned

+ * @param bookDirectory

The type of directory to save in

+ * @param directory

The directory to save the book in

+ * @param numCopies

The number of copies to load

+ * @return

The loaded book

+ */ + @Nullable + public static ItemStack loadBook(@NotNull CommandSender sender, @NotNull String fileName, @NotNull String isSigned, + @NotNull BookDirectory bookDirectory, @NotNull String directory, int numCopies) { //Find the filename if a book index is given try { int bookIndex = Integer.parseInt(fileName); - List availableFiles = BooksWithoutBorders.getAvailableBooks(sender, bookDirectory == BookDirectory.PUBLIC); + List availableFiles = BooksWithoutBorders.getAvailableBooks(sender, + bookDirectory == BookDirectory.PUBLIC); if (bookIndex <= availableFiles.size()) { fileName = availableFiles.get(Integer.parseInt(fileName) - 1); } @@ -74,20 +91,10 @@ public final class BookLoader { File file = getFullPath(sender, fileName, bookDirectory, directory); if (file == null) { //Try converting the username to UUID - String separator = config.getTitleAuthorSeparator(); - String userName = BookFileHelper.getBookAuthorFromPath(fileName); - String title = BookFileHelper.getBookTitleFromPath(fileName); - String extension = BookFileHelper.getExtensionFromPath(fileName); - - Player player = Bukkit.getPlayer(userName); - if (player != null) { - userName = userName.replace(BookFormatter.stripColor(userName), player.getUniqueId().toString()); - file = getFullPath(sender, title + separator + userName + extension, bookDirectory, directory); - if (file == null) { - BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!"); - return null; - } - } else { + String replaced = BookFileHelper.replaceAuthorWithUUID(fileName); + file = getFullPath(sender, replaced, bookDirectory, directory); + if (file == null) { + BooksWithoutBorders.sendErrorMessage(sender, "Incorrect file name!"); return null; } } @@ -152,12 +159,17 @@ public final class BookLoader { private static File getFullPath(@NotNull CommandSender sender, @NotNull String fileName, @NotNull BookDirectory bookDirectory, @NotNull String directory) { BooksWithoutBordersConfig config = BooksWithoutBorders.getConfiguration(); - File file; + File file = null; String slash = config.getSlash(); if (bookDirectory == BookDirectory.ENCRYPTED) { - file = BookFileHelper.getBookFile(config.getEncryptedBookPath() + directory + slash + fileName); + file = BookFileHelper.findBookFile(config.getEncryptedBookPath() + directory + slash, fileName); } else { - file = BookFileHelper.getBookFile(BookHelper.getBookDirectoryPathString(bookDirectory, sender) + fileName); + String folder = BookHelper.getBookDirectoryPathString(bookDirectory, sender); + if (folder != null) { + file = BookFileHelper.findBookFile(folder, fileName); + } else { + BooksWithoutBorders.log(Level.WARNING, "Unknown directory " + bookDirectory); + } } if (file == null || !file.isFile()) { return null; diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookToFromTextHelper.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookToFromTextHelper.java index 46b440b..df65342 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/BookToFromTextHelper.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/BookToFromTextHelper.java @@ -2,6 +2,8 @@ package net.knarcraft.bookswithoutborders.utility; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.Translatable; +import net.knarcraft.bookswithoutborders.encryption.AESConfiguration; +import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle; import net.knarcraft.knarlib.util.FileHelper; import org.bukkit.configuration.file.FileConfiguration; import org.bukkit.configuration.file.YamlConfiguration; @@ -12,10 +14,7 @@ import org.jetbrains.annotations.Nullable; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; -import java.io.FileWriter; import java.io.IOException; -import java.io.PrintWriter; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; @@ -39,7 +38,55 @@ public final class BookToFromTextHelper { * @throws IOException

If unable to save the book

*/ public static void bookToYml(@NotNull String path, @NotNull String fileName, @NotNull BookMeta bookMetadata) throws IOException { - FileConfiguration bookYml = YamlConfiguration.loadConfiguration(new File(path, "blank")); + FileConfiguration bookYml = getBookConfiguration(bookMetadata); + bookYml.save(path + fileName + ".yml"); + } + + /** + * Saves an encrypted book's contents to a .yml file + * + * @param path

The path of the folder to save to. Must end with a slash

+ * @param fileName

The name of the file to load to

+ * @param bookMetadata

Metadata about the book to save

+ * @throws IOException

If unable to save the book

+ */ + public static void encryptedBookToYml(@NotNull String path, @NotNull String fileName, @NotNull BookMeta bookMetadata, + @NotNull EncryptionStyle encryptionStyle, @NotNull String encryptionKey, + @Nullable AESConfiguration aesConfiguration) throws IOException { + FileConfiguration bookYml = getBookConfiguration(bookMetadata); + + bookYml.set("Encryption.Style", encryptionStyle.toString()); + bookYml.set("Encryption.Key", encryptionKey); + if (encryptionStyle == EncryptionStyle.AES) { + if (aesConfiguration == null) { + throw new IOException("Attempted to save AES encrypted book without supplying a configuration!"); + } + bookYml.set("Encryption.AES.IV", EncryptionHelper.bytesToHex(aesConfiguration.iv())); + bookYml.set("Encryption.AES.Salt", EncryptionHelper.bytesToHex(aesConfiguration.salt())); + } + + List encryptedPages = EncryptionHelper.encryptDecryptBookPages(bookMetadata, encryptionStyle, + aesConfiguration, encryptionKey, true); + if (encryptedPages == null || encryptedPages.isEmpty()) { + throw new IOException("Book encryption failed!"); + } + bookYml.set("Encryption.Data", encryptedPages); + + // Make sure the plaintext cannot simply be seen in the file + if (BooksWithoutBorders.getConfiguration().useRealEncryption()) { + bookYml.set("Pages", null); + } + + bookYml.save(path + fileName + ".yml"); + } + + /** + * Gets a file configuration containing a book's information + * + * @param bookMetadata

Metadata about the book to save

+ */ + private static FileConfiguration getBookConfiguration(@NotNull BookMeta bookMetadata) { + FileConfiguration bookYml = YamlConfiguration.loadConfiguration(new File("", "blank")); if (bookMetadata.hasTitle()) { bookYml.set("Title", bookMetadata.getTitle()); @@ -59,7 +106,7 @@ public final class BookToFromTextHelper { bookYml.set("Lore", bookMetadata.getLore()); } - bookYml.save(path + fileName + ".yml"); + return bookYml; } /** @@ -81,30 +128,67 @@ public final class BookToFromTextHelper { } /** - * Saves a book's contents to a text file + * Loads a book from a .yml file * - * @param folderPath

The folder path to save to. Must end with a slash

- * @param fileName

The name of the file to save to

- * @param bookMetadata

Metadata about the book to save

- * @throws IOException

If unable to save the book

+ * @param file

The path of the file to load

+ * @param bookMetadata

Metadata which will be altered with the book's contents

+ * @param userKey

The user-supplied decryption key

+ * @param forceDecrypt

Whether to use the saved key for decryption, ignoring the supplied key

+ * @return

Metadata for the loaded book

*/ - public static void bookToTXT(@NotNull String folderPath, @NotNull String fileName, @NotNull BookMeta bookMetadata) throws IOException { - FileWriter fileWriter = new FileWriter(folderPath + fileName + ".txt", StandardCharsets.UTF_8); - PrintWriter printWriter = new PrintWriter(fileWriter); - List pages = bookMetadata.getPages(); + @Nullable + public static BookMeta encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata, @NotNull String userKey, boolean forceDecrypt) { + BookMeta meta; - BookMeta.Generation generation = bookMetadata.getGeneration(); - if (generation == null) { - generation = BookMeta.Generation.ORIGINAL; + try { + meta = bookFromYml(file, bookMetadata); + if (meta == null) { + return null; + } + } catch (IllegalArgumentException exception) { + return null; } - String generationString = ":" + generation.name(); - //Save each page of the book as a text line - printWriter.println("[Book]" + generationString); - for (String page : pages) { - printWriter.println(page); + // If the plaintext is stored in the file, don't bother with real decryption + if (!meta.getPages().isEmpty()) { + return meta; } - printWriter.close(); + + FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file); + userKey = EncryptionHelper.sha256(userKey); + String realKey = bookYml.getString("Encryption.Key", ""); + if (forceDecrypt) { + userKey = realKey; + } + if (!userKey.equals(realKey)) { + BooksWithoutBorders.log(Level.INFO, "Supplied key: " + userKey + " does not match real key: " + realKey); + return null; + } + + List data = bookYml.getStringList("Encryption.Data"); + if (data.isEmpty()) { + return null; + } + + EncryptionStyle encryptionStyle = EncryptionStyle.getFromString(bookYml.getString("Encryption.Style", + EncryptionStyle.SUBSTITUTION.toString())); + + AESConfiguration aesConfiguration = null; + if (encryptionStyle == EncryptionStyle.AES) { + byte[] iv = EncryptionHelper.hexStringToByteArray(bookYml.getString("Encryption.AES.IV", "")); + byte[] salt = EncryptionHelper.hexStringToByteArray(bookYml.getString("Encryption.AES.Salt", "")); + aesConfiguration = new AESConfiguration(iv, salt, userKey); + } + + meta.setPages(data); + List decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle, + aesConfiguration, userKey, false); + if (decryptedPages != null && !decryptedPages.isEmpty()) { + meta.setPages(decryptedPages); + } else { + return null; + } + return meta; } /** diff --git a/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java b/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java index ca9b6e1..8c02728 100644 --- a/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java +++ b/src/main/java/net/knarcraft/bookswithoutborders/utility/EncryptionHelper.java @@ -3,9 +3,13 @@ package net.knarcraft.bookswithoutborders.utility; import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.config.BooksWithoutBordersConfig; import net.knarcraft.bookswithoutborders.encryption.AES; +import net.knarcraft.bookswithoutborders.encryption.AESConfiguration; +import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle; +import net.knarcraft.bookswithoutborders.encryption.Encryptor; import net.knarcraft.bookswithoutborders.encryption.GenenCrypt; +import net.knarcraft.bookswithoutborders.encryption.Magic; +import net.knarcraft.bookswithoutborders.encryption.OneTimePad; import net.knarcraft.bookswithoutborders.encryption.SubstitutionCipher; -import net.knarcraft.bookswithoutborders.state.EncryptionStyle; import net.md_5.bungee.api.ChatColor; import org.bukkit.Material; import org.bukkit.entity.Player; @@ -16,7 +20,11 @@ import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -28,6 +36,8 @@ import static net.knarcraft.bookswithoutborders.utility.InputCleaningHelper.clea */ public final class EncryptionHelper { + private static final byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + private EncryptionHelper() { } @@ -35,55 +45,79 @@ public final class EncryptionHelper { * Transforms a string key/password into its numerical values * * @param key

The key to transform

- * @return

The numbers representing the key's characters

+ * @return

A comma-separated string of the numbers representing the key's characters

*/ @NotNull public static String getNumberKeyFromStringKey(@NotNull String key) { - StringBuilder integerKey = new StringBuilder(); - for (int x = 0; x < key.length(); x++) { - integerKey.append(Character.getNumericValue(Character.codePointAt(key, x))); + StringBuilder integerKey = new StringBuilder(String.valueOf(Character.codePointAt(key, 0))); + for (int x = 1; x < key.length(); x++) { + integerKey.append(", ").append(Character.codePointAt(key, x)); } return integerKey.toString(); } + /** + * Performs sha256 hashing on the input string + * + * @param input

The input to hash

+ * @return

The hashed input

+ */ + @NotNull + public static String sha256(@NotNull String input) { + String hashed; + try { + final MessageDigest digest = MessageDigest.getInstance("SHA3-256"); + final byte[] hashBytes = digest.digest(input.getBytes(StandardCharsets.UTF_8)); + hashed = Base64.getEncoder().encodeToString(hashBytes); + } catch (NoSuchAlgorithmException exception) { + hashed = input; + } + return hashed; + } + /** * Encrypts the pages of a book * - * @param book

The book to encrypt

- * @param style

The encryption style to use

- * @param integerKey

The encryption key to use

- * @param player

The player trying to encrypt a book

+ * @param book

The book to encrypt

+ * @param style

The encryption style to use

+ * @param aesConfiguration

The AES configuration to use, if encrypting using AES

+ * @param key

The encryption key to use

+ * @param encrypt

Whether to perform an encryption or a decryption

* @return

The pages of the book in encrypted form

*/ @Nullable - public static List encryptBookPages(@NotNull BookMeta book, @NotNull EncryptionStyle style, - @NotNull String integerKey, @NotNull Player player) { + public static List encryptDecryptBookPages(@NotNull BookMeta book, @NotNull EncryptionStyle style, + @Nullable AESConfiguration aesConfiguration, @NotNull String key, + boolean encrypt) { + Encryptor encryptor = switch (style) { + case DNA -> new GenenCrypt(EncryptionHelper.getNumberKeyFromStringKey(key)); + case SUBSTITUTION -> new SubstitutionCipher(EncryptionHelper.getNumberKeyFromStringKey(key)); + case AES -> { + if (aesConfiguration == null) { + throw new IllegalArgumentException("Attempted to perform AES encryption without a valid AES configuration"); + } else { + yield new AES(aesConfiguration); + } + } + case ONE_TIME_PAD -> new OneTimePad(key); + case MAGIC -> new Magic(); + }; + List encryptedPages = new ArrayList<>(); - //Scramble the book's contents - if (style == EncryptionStyle.DNA) { - //Encrypt the pages using gene-based encryption - GenenCrypt gc = new GenenCrypt(integerKey); - for (int x = 0; x < book.getPages().size(); x++) { - encryptedPages.add(gc.encrypt(book.getPage(x + 1))); + for (int x = 0; x < book.getPages().size(); x++) { + String text = book.getPage(x + 1); + String output; + if (encrypt) { + output = encryptor.encryptText(text); + } else { + output = encryptor.decryptText(text); } - return encryptedPages; - } else if (style == EncryptionStyle.SUBSTITUTION) { - //Encrypt the pages using a substitution cipher - SubstitutionCipher sc = new SubstitutionCipher(); - for (int x = 0; x < book.getPages().size(); x++) { - encryptedPages.add(sc.encrypt(book.getPage(x + 1), integerKey)); + if (output == null || output.isEmpty()) { + return null; } - return encryptedPages; - } else if (style == EncryptionStyle.AES) { - AES aes = new AES(AES.generateIV(), AES.generateIV()); - for (int x = 0; x < book.getPages().size(); x++) { - encryptedPages.add(aes.encryptDecryptText(book.getPage(x + 1), integerKey, true)); - } - return encryptedPages; - } else { - BooksWithoutBorders.sendErrorMessage(player, "Invalid encryption style encountered!"); - return null; + encryptedPages.add(output); } + return encryptedPages; } /** @@ -114,9 +148,6 @@ public final class EncryptionHelper { @Nullable public static ItemStack encryptBook(Player player, boolean mainHand, @NotNull String key, @NotNull EncryptionStyle style, @NotNull String groupName) { - //converts user supplied key into integer form - String integerKey = EncryptionHelper.getNumberKeyFromStringKey(key); - BookMeta book = InventoryHelper.getHeldBookMetadata(player, mainHand); if (book == null) { BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata from the held book!"); @@ -128,14 +159,18 @@ public final class EncryptionHelper { return null; } + String hashedKey = sha256(key); + AESConfiguration configuration = AESConfiguration.getNewConfiguration(hashedKey); + //Save the book's un-encrypted contents to a file - BookMeta newMetadata = saveBookPlaintext(groupName, player, book, integerKey); + BookMeta newMetadata = saveBookPlaintext(groupName, player, book, style, hashedKey, configuration); if (newMetadata == null) { return null; } //Get the encrypted pages - List encryptedPages = EncryptionHelper.encryptBookPages(book, style, integerKey, player); + List encryptedPages = EncryptionHelper.encryptDecryptBookPages(book, style, configuration, hashedKey, + true); if (encryptedPages == null) { return null; } @@ -179,19 +214,22 @@ public final class EncryptionHelper { /** * Saves a book's plain text to a file * - * @param groupName

The group who's allowed to decrypt the book, or ""

- * @param player

The player trying to encrypt the book

- * @param book

The book to encrypt

- * @param integerKey

The key used to encrypt the book

+ * @param groupName

The group who's allowed to decrypt the book, or ""

+ * @param player

The player trying to encrypt the book

+ * @param book

The book to encrypt

+ * @param encryptionStyle

The encryption style used for the book

+ * @param key

The key used to encrypt the book

+ * @param aesConfiguration

The AES configuration to use, if encrypting using AES

* @return

The new metadata for the book, or null if it could not be saved

*/ @Nullable private static BookMeta saveBookPlaintext(@NotNull String groupName, @NotNull Player player, - @NotNull BookMeta book, @NotNull String integerKey) { + @NotNull BookMeta book, @NotNull EncryptionStyle encryptionStyle, + @NotNull String key, @NotNull AESConfiguration aesConfiguration) { BookMeta newMetadata = book; boolean wasSaved; if (groupName.trim().isEmpty()) { - wasSaved = saveEncryptedBook(player, book, integerKey); + wasSaved = saveEncryptedBook(player, book, encryptionStyle, key, aesConfiguration); } else { newMetadata = saveEncryptedBookForGroup(player, book, groupName); wasSaved = newMetadata != null; @@ -209,10 +247,12 @@ public final class EncryptionHelper { * @param player

The player trying to load the book

* @param key

The encryption key/password for decryption

* @param deleteEncryptedFile

Whether to delete the plaintext file after decryption is finished

+ * @param forceDecrypt

Whether to force decryption using the stored key

* @return

The loaded book, or null if no book could be loaded

*/ @Nullable - public static ItemStack loadEncryptedBook(@NotNull Player player, @NotNull String key, boolean deleteEncryptedFile) { + public static ItemStack loadEncryptedBook(@NotNull Player player, @NotNull String key, boolean deleteEncryptedFile, + boolean forceDecrypt) { ItemStack heldBook = InventoryHelper.getHeldBook(player, true); BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta(); String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath(); @@ -221,20 +261,24 @@ public final class EncryptionHelper { return null; } - String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetadata, player, true); + String fileName = BookHelper.getBookFile(bookMetadata, player, true); fileName = cleanString(fileName); + File file = new File(path + fileName + ".yml"); if (!file.isFile()) { file = new File(path + fileName + ".txt"); if (!file.isFile()) { - BooksWithoutBorders.sendErrorMessage(player, "Incorrect decryption key!"); + BooksWithoutBorders.sendErrorMessage(player, "Book not found!"); return null; } } else { try { - bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata); + bookMetadata = BookToFromTextHelper.encryptedBookFromYml(file, bookMetadata, key, forceDecrypt); + if (bookMetadata == null) { + throw new IllegalArgumentException(); + } } catch (Exception e) { BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!"); return null; @@ -259,6 +303,104 @@ public final class EncryptionHelper { return newBook; } + /** + * Loads an encrypted book + * + * @param player

The player trying to load the book

+ * @param key

The encryption key/password for decryption

+ * @param deleteEncryptedFile

Whether to delete the plaintext file after decryption is finished

+ * @return

The loaded book, or null if no book could be loaded

+ */ + @Nullable + public static ItemStack loadEncryptedBookLegacy(@NotNull Player player, @NotNull String key, boolean deleteEncryptedFile) { + BooksWithoutBorders.sendErrorMessage(player, "Attempting legacy decryption"); + ItemStack heldBook = InventoryHelper.getHeldBook(player, true); + BookMeta bookMetadata = (BookMeta) heldBook.getItemMeta(); + String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath(); + + if (bookMetadata == null) { + return null; + } + + StringBuilder integerKey = new StringBuilder(); + for (int x = 0; x < key.length(); x++) { + integerKey.append(Character.getNumericValue(Character.codePointAt(key, x))); + } + + String fileName = "[" + integerKey + "]" + BookHelper.getBookFile(bookMetadata, player, true); + fileName = cleanString(fileName).replace(" ", "_"); + + File file = new File(path + fileName + ".yml"); + if (!file.isFile()) { + file = new File(path + fileName + ".txt"); + + if (!file.isFile()) { + BooksWithoutBorders.sendErrorMessage(player, "Incorrect decryption key!"); + return null; + } + } else { + try { + bookMetadata = BookToFromTextHelper.bookFromFile(file, bookMetadata); + if (bookMetadata == null) { + BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!"); + return null; + } + } catch (Exception e) { + BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!"); + return null; + } + } + + if (deleteEncryptedFile) { + Logger logger = BooksWithoutBorders.getInstance().getLogger(); + try { + if (!file.delete()) { + logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\n" + + "File location:" + file.getPath()); + } + } catch (Exception e) { + logger.log(Level.SEVERE, "Book encryption data failed to delete upon decryption!\nFile location:" + file.getPath()); + } + } + + ItemStack newBook = new ItemStack(Material.WRITTEN_BOOK); + newBook.setItemMeta(bookMetadata); + newBook.setAmount(InventoryHelper.getHeldBook(player, true).getAmount()); + return newBook; + } + + /** + * Converts a byte array to a hexadecimal string + * + * @param bytes

The bytes to convert

+ * @return

The resulting hexadecimal string

+ */ + public static String bytesToHex(byte[] bytes) { + byte[] hexChars = new byte[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars, StandardCharsets.UTF_8); + } + + /** + * Converts a string of hexadecimals to a byte array + * + * @param input

The hexadecimal input to parse

+ * @return

The resulting byte array

+ */ + public static byte[] hexStringToByteArray(@NotNull String input) { + int len = input.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(input.charAt(i), 16) << 4) + + Character.digit(input.charAt(i + 1), 16)); + } + return data; + } + /** * Saves an encrypted book to be decryptable for the given group * @@ -299,8 +441,7 @@ public final class EncryptionHelper { bookMetadata.setLore(newLore); //Save file - File file = (BooksWithoutBorders.getConfiguration().getUseYml()) ? new File(path + fileName + ".yml") : - new File(path + fileName + ".txt"); + File file = new File(path + fileName + ".yml"); if (!file.isFile()) { try { BookToFromTextHelper.bookToYml(path, fileName, bookMetadata); @@ -316,27 +457,30 @@ public final class EncryptionHelper { /** * Saves an encrypted book to be decryptable for the given user * - * @param player

The player encrypting the book

- * @param bookMetaData

Metadata for the book to encrypt

- * @param key

The key to use for encryption

+ * @param player

The player encrypting the book

+ * @param bookMetaData

Metadata for the book to encrypt

+ * @param encryptionStyle

The style of encryption used

+ * @param key

The key to use for encryption

+ * @param aesConfiguration

The AES configuration to use if encrypting with AES

* @return

The new encrypted metadata for the book, or null if encryption failed

*/ @NotNull - private static Boolean saveEncryptedBook(@NotNull Player player, @NotNull BookMeta bookMetaData, @NotNull String key) { + private static Boolean saveEncryptedBook(@NotNull Player player, @NotNull BookMeta bookMetaData, + @NotNull EncryptionStyle encryptionStyle, @NotNull String key, + @Nullable AESConfiguration aesConfiguration) { String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath(); - String fileName = "[" + key + "]" + BookHelper.getBookFile(bookMetaData, player, true); + String fileName = BookHelper.getBookFile(bookMetaData, player, true); fileName = cleanString(fileName); //cancels saving if file is already encrypted - File file = (BooksWithoutBorders.getConfiguration().getUseYml()) ? new File(path + fileName + ".yml") : - new File(path + fileName + ".txt"); + File file = new File(path + fileName + ".yml"); if (file.isFile()) { return true; } try { - BookToFromTextHelper.bookToYml(path, fileName, bookMetaData); + BookToFromTextHelper.encryptedBookToYml(path, fileName, bookMetaData, encryptionStyle, key, aesConfiguration); } catch (IOException exception) { BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!"); return false; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index f36f47b..4e0ed4d 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,8 +1,6 @@ Options: # The language to use. Only "en" is built-in, but custom languages can be added Language: "en" - # Whether to use YAML for saved books instead of just storing them as text - Save_Books_in_Yaml_Format: true # The maximum number of duplicates of a saved book allowed Max_Number_of_Duplicates: 5 # The separator used to separate the book title and the book author. While this is a ',' by default for backwards @@ -34,4 +32,8 @@ Options: # vanilla behavior where a copy of a copy cannot be copied further. Change_Generation_On_Copy: false # Whether to enable hitting a chiseled bookshelf while sneaking to see the shelf's contents. - Enable_Book_Peeking: true \ No newline at end of file + Enable_Book_Peeking: true + # Whether to use true AES encryption when encrypting and decrypting books. While the hashed password used for + # encryption is still stored in the book file, the real contents of the book are not. Admin decrypt can be used to + # peek at books, if an admin gets a hold of one, but only the encrypted AES cypher text is stored in the book. + Use_Real_Encryption: false \ No newline at end of file diff --git a/src/main/resources/strings.yml b/src/main/resources/strings.yml index 2a65c2d..b651d91 100644 --- a/src/main/resources/strings.yml +++ b/src/main/resources/strings.yml @@ -4,6 +4,7 @@ en: SUCCESS_CLEARED: "Book cleared!" SUCCESS_DECRYPTED: "Book decrypted!" SUCCESS_AUTO_DECRYPTED: "Book auto-decrypted!" + SUCCESS_DELETED: "\"{file}\" deleted successfully" ERROR_PLAYER_ONLY: "This command can only be used by a player!" ERROR_NOT_HOLDING_WRITTEN_BOOK: "You must be holding a written book to {action} it!" ERROR_NOT_HOLDING_WRITABLE_BOOK: "You must be holding a writable book to {action} it!" @@ -21,6 +22,11 @@ en: ERROR_DECRYPT_FAILED: "Failed to decrypt book!" ERROR_ENCRYPTED_DIRECTORY_EMPTY_OR_MISSING: "Could not find any encrypted files!" ERROR_ENCRYPTED_BOOK_UNKNOWN: "No matching encrypted book found!" + ERROR_DELETE_EMPTY: "No files available to delete!" + ERROR_INCORRECT_FILE_NAME: "Incorrect file name!" + ERROR_DELETE_FAILED_SILENT: "Deletion failed without an exception!" + ERROR_DELETE_FAILED_EXCEPTION: "Deletion failed!" + ERROR_ENCRYPT_NO_KEY: "You must specify a key to encrypt a book!" NEUTRAL_COMMANDS_HEADER: | &e[] denote optional parameters <> denote required parameters diff --git a/src/test/java/net/knarcraft/bookswithoutborders/GenenCryptTest.java b/src/test/java/net/knarcraft/bookswithoutborders/GenenCryptTest.java index a43c575..46ad6c6 100644 --- a/src/test/java/net/knarcraft/bookswithoutborders/GenenCryptTest.java +++ b/src/test/java/net/knarcraft/bookswithoutborders/GenenCryptTest.java @@ -4,6 +4,7 @@ import net.knarcraft.bookswithoutborders.encryption.GenenCrypt; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; public class GenenCryptTest { @@ -11,8 +12,11 @@ public class GenenCryptTest { public void encryptDecryptTest() { GenenCrypt gc = new GenenCrypt("Another Key"); gc.printCodonTable(); - String encrypted = gc.encrypt("Hello World!"); - assertEquals("HELLO WORLD!", gc.decrypt(encrypted)); + String encrypted = gc.encryptText("Hello World!"); + + assertNotNull(encrypted); + + assertEquals("HELLO WORLD!", gc.decryptText(encrypted)); } } diff --git a/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java b/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java index 3ff608f..538d959 100644 --- a/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java +++ b/src/test/java/net/knarcraft/bookswithoutborders/encryption/AESTest.java @@ -3,22 +3,31 @@ package net.knarcraft.bookswithoutborders.encryption; import org.junit.Test; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNotSame; public class AESTest { @Test public void encryptDecryptTest() { - String plainText = "A lot of text"; - String password = "abc123"; + String plainText = "Flåklypa"; + String password = "TqOZdpY9RjjjVE9JjCWVecUYObv5MYidByrpI3cxjoY="; - AES aes = new AES(AES.generateIV(), AES.generateIV()); + System.out.println("Plaintext: " + plainText); + System.out.println("Encryption password: " + password); - String encrypted = aes.encryptDecryptText(plainText, password, true); - assertNotSame(encrypted, plainText); - assertNotNull(encrypted); - String decrypted = aes.encryptDecryptText(encrypted, password, false); + AESConfiguration configuration = new AESConfiguration(new byte[]{-85, 103, -82, 71, 119, 28, 73, -75, -81, 102, -127, -125, -8, -75, 81, -111}, + new byte[]{(byte) 104, -42, 63, 31, -120, -2, 14, -119, 35, 122, 109, -64, 122, 117, 33, -85}, password); + AES aes = new AES(configuration); + + String cypherText = aes.encryptText(plainText); + System.out.println("Cypher text: " + cypherText); + + assertNotNull(cypherText); + assertNotEquals(plainText, cypherText); + + String decrypted = aes.decryptText(cypherText); + System.out.println("Decrypted: " + decrypted); assertEquals(plainText, decrypted); } diff --git a/src/test/java/net/knarcraft/bookswithoutborders/encryption/GenenCryptTest.java b/src/test/java/net/knarcraft/bookswithoutborders/encryption/GenenCryptTest.java new file mode 100644 index 0000000..c40c34b --- /dev/null +++ b/src/test/java/net/knarcraft/bookswithoutborders/encryption/GenenCryptTest.java @@ -0,0 +1,28 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import net.knarcraft.bookswithoutborders.utility.EncryptionHelper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +public class GenenCryptTest { + + @Test + public void encryptDecryptTest() { + String encryptionKey = EncryptionHelper.getNumberKeyFromStringKey("My secret password!"); + String plaintext = "Very secret &4colored&r message."; + GenenCrypt genenCrypt = new GenenCrypt(encryptionKey); + + String cypherText = genenCrypt.encryptText(plaintext); + + assertNotNull(cypherText); + assertNotSame(cypherText, plaintext); + + String decrypted = genenCrypt.decryptText(cypherText); + + assertEquals(plaintext.toUpperCase(), decrypted); + } + +} diff --git a/src/test/java/net/knarcraft/bookswithoutborders/encryption/OneTimePadTest.java b/src/test/java/net/knarcraft/bookswithoutborders/encryption/OneTimePadTest.java new file mode 100644 index 0000000..75f3aae --- /dev/null +++ b/src/test/java/net/knarcraft/bookswithoutborders/encryption/OneTimePadTest.java @@ -0,0 +1,26 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +public class OneTimePadTest { + + @Test + public void oneTimePadTest() { + String plaintext = "Very secret text that should be kept secret"; + String key = "Very secret key!"; + + OneTimePad oneTimePad = new OneTimePad(key); + String cypherText = oneTimePad.encryptText(plaintext); + + assertNotNull(cypherText); + assertNotSame(plaintext, cypherText); + + String decrypted = oneTimePad.decryptText(cypherText); + assertEquals(plaintext, decrypted); + } + +} diff --git a/src/test/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipherTest.java b/src/test/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipherTest.java new file mode 100644 index 0000000..54bf4ce --- /dev/null +++ b/src/test/java/net/knarcraft/bookswithoutborders/encryption/SubstitutionCipherTest.java @@ -0,0 +1,26 @@ +package net.knarcraft.bookswithoutborders.encryption; + +import net.knarcraft.bookswithoutborders.utility.EncryptionHelper; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; + +public class SubstitutionCipherTest { + + @Test + public void encryptDecryptTest() { + String plaintext = "Very secret text that should be kept secret"; + String integerKey = EncryptionHelper.getNumberKeyFromStringKey("Very secret key!"); + SubstitutionCipher substitutionCipher = new SubstitutionCipher(integerKey); + String cypherText = substitutionCipher.encryptText(plaintext); + + assertNotNull(cypherText); + assertNotSame(plaintext, cypherText); + + String decrypted = substitutionCipher.decryptText(cypherText); + assertEquals(plaintext, decrypted); + } + +} diff --git a/src/test/java/net/knarcraft/bookswithoutborders/util/EncryptionHelperTest.java b/src/test/java/net/knarcraft/bookswithoutborders/util/EncryptionHelperTest.java index ef425ff..7ec3ac6 100644 --- a/src/test/java/net/knarcraft/bookswithoutborders/util/EncryptionHelperTest.java +++ b/src/test/java/net/knarcraft/bookswithoutborders/util/EncryptionHelperTest.java @@ -10,7 +10,7 @@ public class EncryptionHelperTest { @Test public void getNumberKeyFromStringKey() { String numberKey = EncryptionHelper.getNumberKeyFromStringKey("hello"); - assertEquals("1714212124", numberKey); + assertEquals("104, 101, 108, 108, 111", numberKey); } }