Rewrites encryption
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good

Adds an optional real encryption mode, which encrypts pages using AES, without saving plaintext.
Re-implements the old magic encryption in non-real encryption mode.
Fixes incorrect key generation for use in the substitution cipher and the gene cipher.
Removes the option for saving books as txt.
Adds tests for all encryption methods.
Saves all necessary decryption data when storing encrypted books.
Removes the old book updating code.
This commit is contained in:
2025-08-10 14:23:18 +02:00
parent 0ac051e24e
commit 32f0f9f7a1
34 changed files with 938 additions and 426 deletions

View File

@@ -20,7 +20,7 @@
<description>A continuation of the original Books Without Borders</description>
<properties>
<java.version>16</java.version>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

View File

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

View File

@@ -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 <p>True if the book was deleted successfully</p>
*/
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<String> 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 <p>Whether the book to delete is public or not</p>
*/
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);
}
}

View File

@@ -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<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String[] arguments) {
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(sender, arguments, true);
}

View File

@@ -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("<password>");
} else if (argumentsCount == 2) {
if (BooksWithoutBorders.getConfiguration().useRealEncryption()) {
return List.of();
}
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[1]);
}
}

View File

@@ -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;

View File

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

View File

@@ -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 <p>Whether to use YML for saving books</p>
*/
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 <p>True if real encryption should be used</p>
*/
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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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 <p>The initialization vector to use for CBC</p>
* @param passwordSalt <p>The password salt to use</p>
* @param aesConfiguration <p>The AES configuration to use</p>
*/
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 <p>An initialization vector</p>
*/
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 <p>The input to encrypt or decrypt</p>
* @param password <p>The password to use for key generation</p>
* @param encrypt <p>Whether to encrypt or decrypt the input</p>
* <p>Note: The same IV and salt must be used during instantiation in order to decrypt an encrypted message.</p>
*
* @param input <p>The input to encrypt or decrypt</p>
* @param encrypt <p>Whether to encrypt or decrypt the input</p>
* @return <p>The encrypted/decrypted input, or null if anything went wrong</p>
*/
@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 <p>An initialization vector</p>
*/
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;

View File

@@ -0,0 +1,24 @@
package net.knarcraft.bookswithoutborders.encryption;
import org.jetbrains.annotations.NotNull;
/**
* A configuration for AES encryption
*
* @param iv <p>The initialization vector</p>
* @param salt <p>The encryption salt</p>
* @param key <p>The encryption key</p>
*/
public record AESConfiguration(byte @NotNull [] iv, byte @NotNull [] salt, @NotNull String key) {
/**
* Generates a new AES configuration with randomized IV and salt
*
* @param key <p>The encryption key to use</p>
* @return <p>The new AES configuration</p>
*/
public static AESConfiguration getNewConfiguration(@NotNull String key) {
return new AESConfiguration(AES.generateIV(), AES.generateIV(), key);
}
}

View File

@@ -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;

View File

@@ -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 <p>The input to encrypt</p>
* @return <p>The resulting cypher text, or null if unsuccessful</p>
*/
@Nullable
String encryptText(@NotNull String input);
/**
* Decrypts the given cypher text
*
* @param input <p>The cypher text to decrypt</p>
* @return <p>The resulting plaintext, or null if unsuccessful</p>
*/
@Nullable
String decryptText(@NotNull String input);
}

View File

@@ -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;
* <p>Not sure where this was gotten from, but it does exist at
* <a href="https://crypto.stackexchange.com/questions/11614/how-do-i-test-my-encryption-absolute-amateur">Stack Exchange</a>.</p>
*/
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<String, String[]> codonTable;
private final HashMap<String, String> 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 <p>The encrypted input</p>
*/
@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 <p>The decrypted input</p>
*/
@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);

View File

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

View File

@@ -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 <p>The key to use</p>
*/
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
*
* <p>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.</p>
*
* @param input <p>The input to encrypt/decrypt</p>
* @return <p>The encrypted/decrypted output</p>
*/
@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();
}
}

View File

@@ -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 <p>The key to use</p>
*/
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
*
* <p>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.</p>
*
* @param input <p>The input to encrypt/decrypt</p>
* @param encrypt <p>Whether to encrypt or decrypt the input</p>
* @return <p>The encryption output</p>
*/
@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 <p>The key to make an offset array for</p>
* @return <p>The offset array</p>
*/
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;
}
}

View File

@@ -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<String> 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<String> lore = bookshelfSection.getStringList(loreKey);
if (title != null) {
registerBookshelf(new Bookshelf(bookshelfLocation, title, loreStrings));
registerBookshelf(new Bookshelf(bookshelfLocation, title, lore));
}
}
}

View File

@@ -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 <p>The player to update</p>
* @param itemMetadata <p>Information about the held book</p>
* @param mainHand <p>Whether to update the book in the player's main hand</p>
*/
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 <p>The player holding the book</p>
* @param oldBook <p>Metadata about the held book</p>
* @return <p>An updated book</p>
*/
@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;
}
}

View File

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

View File

@@ -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 <p>The path to get the extension from</p>
* @return <p>The extension of the input</p>
* @param folder <p>The folder the book is in</p>
* @param bookMeta <p>The book meta of the book to find</p>
* @return <p>The book's file, or null if not found</p>
*/
@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 <p>The folder the book is in</p>
* @param fileName <p>The name of the book to find</p>
* @return <p>The book's file, or null if not found</p>
*/
@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 <p>The filename to replace the author of</p>
* @return <p>The filename, or the filename with the author replaced with UUID</p>
*/
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 "";
}
}

View File

@@ -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 <p>The command sender trying to load the book</p>
* @param fileName <p>The index or file name of the book to load</p>
* @param isSigned <p>Whether to load the book as signed, and not unsigned</p>
* @param bookDirectory <p>The type of directory to save in</p>
* @param directory <p>The directory to save the book in</p>
* @param numCopies <p>The number of copies to load</p>
* @return <p>The loaded book</p>
*/
@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<String> availableFiles = BooksWithoutBorders.getAvailableBooks(sender, bookDirectory == BookDirectory.PUBLIC);
List<String> 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;

View File

@@ -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 <p>If unable to save the book</p>
*/
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 <p>The path of the folder to save to. Must end with a slash</p>
* @param fileName <p>The name of the file to load to</p>
* @param bookMetadata <p>Metadata about the book to save</p>
* @throws IOException <p>If unable to save the book</p>
*/
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<String> 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 <p>Metadata about the book to save</p>
*/
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 <p>The folder path to save to. Must end with a slash</p>
* @param fileName <p>The name of the file to save to</p>
* @param bookMetadata <p>Metadata about the book to save</p>
* @throws IOException <p>If unable to save the book</p>
* @param file <p>The path of the file to load</p>
* @param bookMetadata <p>Metadata which will be altered with the book's contents</p>
* @param userKey <p>The user-supplied decryption key</p>
* @param forceDecrypt <p>Whether to use the saved key for decryption, ignoring the supplied key</p>
* @return <p>Metadata for the loaded book</p>
*/
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<String> 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<String> 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<String> decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle,
aesConfiguration, userKey, false);
if (decryptedPages != null && !decryptedPages.isEmpty()) {
meta.setPages(decryptedPages);
} else {
return null;
}
return meta;
}
/**

View File

@@ -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 <p>The key to transform</p>
* @return <p>The numbers representing the key's characters</p>
* @return <p>A comma-separated string of the numbers representing the key's characters</p>
*/
@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 <p>The input to hash</p>
* @return <p>The hashed input</p>
*/
@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 <p>The book to encrypt</p>
* @param style <p>The encryption style to use</p>
* @param integerKey <p>The encryption key to use</p>
* @param player <p>The player trying to encrypt a book</p>
* @param book <p>The book to encrypt</p>
* @param style <p>The encryption style to use</p>
* @param aesConfiguration <p>The AES configuration to use, if encrypting using AES</p>
* @param key <p>The encryption key to use</p>
* @param encrypt <p>Whether to perform an encryption or a decryption</p>
* @return <p>The pages of the book in encrypted form</p>
*/
@Nullable
public static List<String> encryptBookPages(@NotNull BookMeta book, @NotNull EncryptionStyle style,
@NotNull String integerKey, @NotNull Player player) {
public static List<String> 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<String> 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<String> encryptedPages = EncryptionHelper.encryptBookPages(book, style, integerKey, player);
List<String> 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 <p>The group who's allowed to decrypt the book, or ""</p>
* @param player <p>The player trying to encrypt the book</p>
* @param book <p>The book to encrypt</p>
* @param integerKey <p>The key used to encrypt the book</p>
* @param groupName <p>The group who's allowed to decrypt the book, or ""</p>
* @param player <p>The player trying to encrypt the book</p>
* @param book <p>The book to encrypt</p>
* @param encryptionStyle <p>The encryption style used for the book</p>
* @param key <p>The key used to encrypt the book</p>
* @param aesConfiguration <p>The AES configuration to use, if encrypting using AES</p>
* @return <p>The new metadata for the book, or null if it could not be saved</p>
*/
@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 <p>The player trying to load the book</p>
* @param key <p>The encryption key/password for decryption</p>
* @param deleteEncryptedFile <p>Whether to delete the plaintext file after decryption is finished</p>
* @param forceDecrypt <p>Whether to force decryption using the stored key</p>
* @return <p>The loaded book, or null if no book could be loaded</p>
*/
@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 <p>The player trying to load the book</p>
* @param key <p>The encryption key/password for decryption</p>
* @param deleteEncryptedFile <p>Whether to delete the plaintext file after decryption is finished</p>
* @return <p>The loaded book, or null if no book could be loaded</p>
*/
@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 <p>The bytes to convert</p>
* @return <p>The resulting hexadecimal string</p>
*/
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 <p>The hexadecimal input to parse</p>
* @return <p>The resulting byte array</p>
*/
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 <p>The player encrypting the book</p>
* @param bookMetaData <p>Metadata for the book to encrypt</p>
* @param key <p>The key to use for encryption</p>
* @param player <p>The player encrypting the book</p>
* @param bookMetaData <p>Metadata for the book to encrypt</p>
* @param encryptionStyle <p>The style of encryption used</p>
* @param key <p>The key to use for encryption</p>
* @param aesConfiguration <p>The AES configuration to use if encrypting with AES</p>
* @return <p>The new encrypted metadata for the book, or null if encryption failed</p>
*/
@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;

View File

@@ -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
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

View File

@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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