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); } }