diff --git a/pom.xml b/pom.xml
index 0d00958..3e5c06a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
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 ListWhether 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 ListWhether 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 + * + * @returnTrue 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 initializationVectorThe initialization vector to use for CBC
- * @param passwordSaltThe password salt to use
+ * @param aesConfigurationThe 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 + * + * @returnAn 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 inputThe input to encrypt or decrypt
- * @param passwordThe password to use for key generation
- * @param encryptWhether 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 inputThe input to encrypt or decrypt
+ * @param encryptWhether to encrypt or decrypt the input
* @returnThe 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 - * - * @returnAn 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 ivThe initialization vector
+ * @param saltThe encryption salt
+ * @param keyThe 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 keyThe encryption key to use
+ * @returnThe 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 inputThe input to encrypt
+ * @returnThe resulting cypher text, or null if unsuccessful
+ */ + @Nullable + String encryptText(@NotNull String input); + + /** + * Decrypts the given cypher text + * + * @param inputThe cypher text to decrypt
+ * @returnThe 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 HashMapThe 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 { * @returnThe 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 keyThe 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 inputThe input to encrypt/decrypt
+ * @returnThe 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 keyThe 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 inputThe input to encrypt/decrypt
+ * @param encryptWhether to encrypt or decrypt the input
+ * @returnThe 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 keyThe key to make an offset array for
+ * @returnThe 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); - ListThe player to update
- * @param itemMetadataInformation about the held book
- * @param mainHandWhether 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 playerThe player holding the book
- * @param oldBookMetadata about the held book
- * @returnAn 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 pathThe path to get the extension from
- * @returnThe extension of the input
+ * @param folderThe folder the book is in
+ * @param bookMetaThe book meta of the book to find
+ * @returnThe 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 folderThe folder the book is in
+ * @param fileNameThe name of the book to find
+ * @returnThe 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 fileNameThe filename to replace the author of
+ * @returnThe 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 senderThe command sender trying to load the book
+ * @param fileNameThe index or file name of the book to load
+ * @param isSignedWhether to load the book as signed, and not unsigned
+ * @param bookDirectoryThe type of directory to save in
+ * @param directoryThe directory to save the book in
+ * @param numCopiesThe number of copies to load
+ * @returnThe 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); - ListIf 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 pathThe path of the folder to save to. Must end with a slash
+ * @param fileNameThe name of the file to load to
+ * @param bookMetadataMetadata about the book to save
+ * @throws IOExceptionIf 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())); + } + + ListMetadata 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 folderPathThe folder path to save to. Must end with a slash
- * @param fileNameThe name of the file to save to
- * @param bookMetadataMetadata about the book to save
- * @throws IOExceptionIf unable to save the book
+ * @param fileThe path of the file to load
+ * @param bookMetadataMetadata which will be altered with the book's contents
+ * @param userKeyThe user-supplied decryption key
+ * @param forceDecryptWhether to use the saved key for decryption, ignoring the supplied key
+ * @returnMetadata 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); - ListThe key to transform
- * @returnThe numbers representing the key's characters
+ * @returnA 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 inputThe input to hash
+ * @returnThe 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 bookThe book to encrypt
- * @param styleThe encryption style to use
- * @param integerKeyThe encryption key to use
- * @param playerThe player trying to encrypt a book
+ * @param bookThe book to encrypt
+ * @param styleThe encryption style to use
+ * @param aesConfigurationThe AES configuration to use, if encrypting using AES
+ * @param keyThe encryption key to use
+ * @param encryptWhether to perform an encryption or a decryption
* @returnThe pages of the book in encrypted form
*/ @Nullable - public static ListThe group who's allowed to decrypt the book, or ""
- * @param playerThe player trying to encrypt the book
- * @param bookThe book to encrypt
- * @param integerKeyThe key used to encrypt the book
+ * @param groupNameThe group who's allowed to decrypt the book, or ""
+ * @param playerThe player trying to encrypt the book
+ * @param bookThe book to encrypt
+ * @param encryptionStyleThe encryption style used for the book
+ * @param keyThe key used to encrypt the book
+ * @param aesConfigurationThe AES configuration to use, if encrypting using AES
* @returnThe 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 playerThe player trying to load the book
* @param keyThe encryption key/password for decryption
* @param deleteEncryptedFileWhether to delete the plaintext file after decryption is finished
+ * @param forceDecryptWhether to force decryption using the stored key
* @returnThe 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 playerThe player trying to load the book
+ * @param keyThe encryption key/password for decryption
+ * @param deleteEncryptedFileWhether to delete the plaintext file after decryption is finished
+ * @returnThe 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 bytesThe bytes to convert
+ * @returnThe 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 inputThe hexadecimal input to parse
+ * @returnThe 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 playerThe player encrypting the book
- * @param bookMetaDataMetadata for the book to encrypt
- * @param keyThe key to use for encryption
+ * @param playerThe player encrypting the book
+ * @param bookMetaDataMetadata for the book to encrypt
+ * @param encryptionStyleThe style of encryption used
+ * @param keyThe key to use for encryption
+ * @param aesConfigurationThe AES configuration to use if encrypting with AES
* @returnThe 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); } }