Adds a per-book choice for preventing admin decryption when using real encryption
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good

This commit is contained in:
2025-08-21 22:52:22 +02:00
parent 93ce915a30
commit cbe3a977ac
12 changed files with 152 additions and 104 deletions

View File

@@ -1,6 +1,8 @@
package net.knarcraft.bookswithoutborders.command;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BwBConfig;
import net.knarcraft.bookswithoutborders.config.Permission;
import net.knarcraft.bookswithoutborders.config.translation.Translatable;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.bookswithoutborders.state.ItemSlot;
@@ -29,7 +31,7 @@ public class CommandEncrypt implements TabExecutor {
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] arguments) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (performPreChecks(sender, arguments, 1,
if (performPreChecks(sender, arguments, 1, 2,
stringFormatter.getUnFormattedColoredMessage(Translatable.ERROR_ENCRYPT_NO_KEY)) == null) {
return false;
}
@@ -37,11 +39,16 @@ public class CommandEncrypt implements TabExecutor {
EncryptionStyle encryptionStyle = arguments.length == 2 ? EncryptionStyle.getFromString(arguments[1]) : EncryptionStyle.AES;
// AES is the only reliable method for retaining the plaintext
if (BooksWithoutBorders.getConfiguration().useRealEncryption() && !encryptionStyle.isRealEncryptionSupported()) {
BwBConfig config = BooksWithoutBorders.getConfiguration();
boolean realEncryption = config.useRealEncryption();
if (realEncryption && !encryptionStyle.isRealEncryptionSupported()) {
encryptionStyle = EncryptionStyle.AES;
}
return encryptBook(encryptionStyle, (Player) sender, arguments[0], "");
boolean preventAdminDecryption = realEncryption && config.allowPreventAdminDecryption() &&
sender.hasPermission(Permission.PREVENT_ADMIN_DECRYPTION.toString());
return encryptBook(encryptionStyle, (Player) sender, arguments[0], "", preventAdminDecryption);
}
/**
@@ -55,7 +62,7 @@ public class CommandEncrypt implements TabExecutor {
*/
@Nullable
protected BookMeta performPreChecks(@NotNull CommandSender sender, @NotNull String[] arguments,
int necessaryArguments, @NotNull String missingArgumentsError) {
int necessaryArguments, int optionalArguments, @NotNull String missingArgumentsError) {
StringFormatter stringFormatter = BooksWithoutBorders.getStringFormatter();
if (!(sender instanceof Player player)) {
stringFormatter.displayErrorMessage(sender, Translatable.ERROR_PLAYER_ONLY);
@@ -75,7 +82,7 @@ public class CommandEncrypt implements TabExecutor {
stringFormatter.displayErrorMessage(player, missingArgumentsError);
return null;
}
if (argumentCount > necessaryArguments + 1) {
if (argumentCount > necessaryArguments + optionalArguments) {
stringFormatter.displayErrorMessage(player, Translatable.ERROR_TOO_MANY_ARGUMENTS_COMMAND);
return null;
}
@@ -96,16 +103,18 @@ public class CommandEncrypt implements TabExecutor {
/**
* Encrypts the given book
*
* @param encryptionStyle <p>The encryption style to use</p>
* @param player <p>The player encrypting the book</p>
* @param key <p>The encryption key to use</p>
* @param group <p>The group to encrypt for</p>
* @param encryptionStyle <p>The encryption style to use</p>
* @param player <p>The player encrypting the book</p>
* @param key <p>The encryption key to use</p>
* @param group <p>The group to encrypt for</p>
* @param preventAdminDecryption <p>Whether to prevent storage of a key that can be used for admin decryption</p>
* @return <p>True if the book was encrypted successfully</p>
*/
protected boolean encryptBook(@NotNull EncryptionStyle encryptionStyle, @NotNull Player player, @NotNull String key,
@NotNull String group) {
@NotNull String group, boolean preventAdminDecryption) {
ItemSlot heldSlot = InventoryHelper.getHeldSlotBook(player, false, false, true, true);
ItemStack encryptedBook = EncryptionHelper.encryptBook(player, heldSlot == ItemSlot.MAIN_HAND, key, encryptionStyle, group);
ItemStack encryptedBook = EncryptionHelper.encryptBook(player, heldSlot == ItemSlot.MAIN_HAND, key,
encryptionStyle, group, preventAdminDecryption);
if (encryptedBook != null) {
InventoryHelper.setHeldWrittenBook(player, encryptedBook);
@@ -119,19 +128,20 @@ public class CommandEncrypt implements TabExecutor {
@NotNull
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias,
@NotNull String[] arguments) {
return doTabCompletion(arguments, false);
return doTabCompletion(sender, arguments, false);
}
/**
* Gets a list of string for tab completions
*
* @param args <p>The arguments given</p>
* @param sender <p>The command sender executing this command</p>
* @param arguments <p>The arguments given</p>
* @param groupEncrypt <p>Whether to auto-complete for group encryption</p>
* @return <p>The strings to auto-complete</p>
*/
@NotNull
protected List<String> doTabCompletion(@NotNull String[] args, boolean groupEncrypt) {
int argumentsCount = args.length;
protected List<String> doTabCompletion(@NotNull CommandSender sender, @NotNull String[] arguments, boolean groupEncrypt) {
int argumentsCount = arguments.length;
boolean useRealEncryption = BooksWithoutBorders.getConfiguration().useRealEncryption();
List<String> encryptionStyles = new ArrayList<>();
@@ -147,13 +157,17 @@ public class CommandEncrypt implements TabExecutor {
} else if (argumentsCount == 2) {
return List.of("<password>");
} else if (argumentsCount == 3) {
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[2]);
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, arguments[2]);
}
} else {
BwBConfig config = BooksWithoutBorders.getConfiguration();
if (argumentsCount == 1) {
return List.of("<password>");
} else if (argumentsCount == 2) {
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, args[1]);
return TabCompletionHelper.filterMatchingStartsWith(encryptionStyles, arguments[1]);
} else if (argumentsCount == 3 && (config.useRealEncryption() && config.allowPreventAdminDecryption()) &&
sender.hasPermission(Permission.PREVENT_ADMIN_DECRYPTION.toString())) {
return TabCompletionHelper.filterMatchingStartsWith(List.of("true", "false"), arguments[2]);
}
}
return List.of();

View File

@@ -27,7 +27,7 @@ public class CommandGroupEncrypt extends CommandEncrypt implements TabExecutor {
return false;
}
BookMeta bookMetadata = performPreChecks(sender, arguments, 2,
BookMeta bookMetadata = performPreChecks(sender, arguments, 2, 1,
stringFormatter.getUnFormattedColoredMessage(Translatable.ERROR_GROUP_ENCRYPT_ARGUMENTS_MISSING));
if (bookMetadata == null) {
@@ -42,13 +42,13 @@ public class CommandGroupEncrypt extends CommandEncrypt implements TabExecutor {
}
EncryptionStyle encryptionStyle = arguments.length == 3 ? EncryptionStyle.getFromString(arguments[2]) : EncryptionStyle.SUBSTITUTION;
return encryptBook(encryptionStyle, player, arguments[1], arguments[0]);
return encryptBook(encryptionStyle, player, arguments[1], arguments[0], false);
}
@Override
public @NotNull List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command,
@NotNull String alias, @NotNull String[] arguments) {
return doTabCompletion(arguments, true);
return doTabCompletion(sender, arguments, true);
}
}

View File

@@ -42,6 +42,7 @@ public class BwBConfig {
private boolean changeGenerationOnCopy;
private boolean enableBookshelfPeeking;
private boolean useRealEncryption;
private boolean allowPreventAdminDecryption;
private final Translator translator;
private EconomyManager economyManager;
@@ -275,6 +276,15 @@ public class BwBConfig {
return this.useRealEncryption;
}
/**
* Checks whether to allow removing the admin decrypt ability
*
* @return <p>True if admin decrypt removal is allowed</p>
*/
public boolean allowPreventAdminDecryption() {
return this.allowPreventAdminDecryption;
}
/**
* Gets the path used to store encrypted books
*
@@ -316,6 +326,7 @@ public class BwBConfig {
config.set(ConfigOption.AUTHOR_ONLY_UNSIGN.getConfigNode(), this.authorOnlyUnsign);
config.set(ConfigOption.AUTHOR_ONLY_SAVE.getConfigNode(), this.authorOnlySave);
config.set(ConfigOption.CHANGE_GENERATION_ON_COPY.getConfigNode(), this.changeGenerationOnCopy);
config.set(ConfigOption.ALLOW_PREVENT_ADMIN_DECRYPTION.getConfigNode(), this.allowPreventAdminDecryption);
BooksWithoutBorders.getInstance().saveConfig();
}
@@ -343,6 +354,7 @@ public class BwBConfig {
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);
this.allowPreventAdminDecryption = getBoolean(config, ConfigOption.ALLOW_PREVENT_ADMIN_DECRYPTION);
String language = config.getString("language", "en");
this.translator.loadLanguages(BooksWithoutBorders.getInstance().getDataFolder(), "en", language);

View File

@@ -81,6 +81,11 @@ public enum ConfigOption {
* 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),
/**
* Whether to allow disabling admin encryption for a real encrypted book
*/
ALLOW_PREVENT_ADMIN_DECRYPTION("Options.Allow_Prevent_Admin_Decryption", false),
;
private final String configNode;

View File

@@ -50,7 +50,13 @@ public enum Permission {
/**
* The permission for using special signs
*/
SIGNS("signs");
SIGNS("signs"),
/**
* The permission for preventing a real encrypted book to be decrypted by an admin
*/
PREVENT_ADMIN_DECRYPTION("preventAdminDecryption"),
;
private final @NotNull String node;

View File

@@ -11,13 +11,14 @@ import java.util.List;
/**
* A representation of an encrypted book
*
* @param bookMeta <p>The book's book meta</p>
* @param encryptionStyle <p>The book's encryption style</p>
* @param encryptionKey <p>The book's encryption key, or null if admin decrypt is forbidden</p>
* @param data <p>The encrypted pages of the book</p>
* @param aesConfiguration <p>The AES configuration for the book, or null if not AES encrypted</p>
* @param bookMeta <p>The book's book meta</p>
* @param encryptionStyle <p>The book's encryption style</p>
* @param encryptionKey <p>The book's encryption key, or null if admin decrypt is forbidden</p>
* @param data <p>The encrypted pages of the book</p>
* @param aesConfiguration <p>The AES configuration for the book, or null if not AES encrypted</p>
* @param preventAdminDecrypt <p>Whether this book should not support admin decryption</p>
*/
public record EncryptedBook(@NotNull BookMeta bookMeta, @NotNull EncryptionStyle encryptionStyle,
@NotNull String encryptionKey, @NotNull List<String> data,
@Nullable AESConfiguration aesConfiguration) {
@Nullable AESConfiguration aesConfiguration, boolean preventAdminDecrypt) {
}

View File

@@ -304,7 +304,7 @@ public class SignEventListener implements Listener {
if (heldItemType == Material.WRITTEN_BOOK) {
player.closeInventory();
eBook = EncryptionHelper.encryptBook(player, mainHand, BookFormatter.stripColor(lines[2]),
EncryptionStyle.getFromString(BookFormatter.stripColor(lines[3])));
EncryptionStyle.getFromString(BookFormatter.stripColor(lines[3])), false);
if (eBook != null) {
player.getInventory().setItem(hand, eBook);
}

View File

@@ -76,7 +76,9 @@ public final class BookToFromTextHelper {
FileConfiguration bookYml = getBookConfiguration(encryptedBook.bookMeta());
bookYml.set("Encryption.Style", encryptedBook.encryptionStyle().toString());
bookYml.set("Encryption.Key", encryptedBook.encryptionKey());
if (!encryptedBook.preventAdminDecrypt()) {
bookYml.set("Encryption.Key", encryptedBook.encryptionKey());
}
if (encryptedBook.encryptionStyle() == EncryptionStyle.AES) {
if (encryptedBook.aesConfiguration() == null) {
throw new IOException("Attempted to save AES encrypted book without supplying a configuration!");
@@ -155,7 +157,8 @@ public final class BookToFromTextHelper {
}
// If the plaintext is stored in the file, don't bother with real decryption
EncryptedBook encryptedBook = new EncryptedBook(meta, encryptionStyle, userKey, data, aesConfiguration);
EncryptedBook encryptedBook = new EncryptedBook(meta, encryptionStyle, userKey, data, aesConfiguration,
realKey.isBlank());
if (!meta.getPages().isEmpty()) {
return encryptedBook;
}

View File

@@ -127,31 +127,34 @@ public final class EncryptionHelper {
/**
* Encrypts a book
*
* @param player <p>The player encrypting the book</p>
* @param mainHand <p>Whether the player is holding the book in its main hand</p>
* @param key <p>The key/password to use for encryption</p>
* @param style <p>The encryption style to use</p>
* @param player <p>The player encrypting the book</p>
* @param mainHand <p>Whether the player is holding the book in its main hand</p>
* @param key <p>The key/password to use for encryption</p>
* @param style <p>The encryption style to use</p>
* @param preventAdminDecrypt <p>Whether to prevent storage of a key that can be used for admin decryption</p>
* @return <p>An encrypted version of the book</p>
*/
@Nullable
public static ItemStack encryptBook(@NotNull Player player, boolean mainHand, @NotNull String key,
@NotNull EncryptionStyle style) {
return encryptBook(player, mainHand, key, style, "");
@NotNull EncryptionStyle style, boolean preventAdminDecrypt) {
return encryptBook(player, mainHand, key, style, "", preventAdminDecrypt);
}
/**
* Encrypts a book
*
* @param player <p>The player encrypting the book</p>
* @param mainHand <p>Whether the player is holding the book in its main hand</p>
* @param key <p>The key/password to use for encryption</p>
* @param style <p>The encryption style to use</p>
* @param groupName <p>The name of the group to encrypt for, or "" otherwise</p>
* @param player <p>The player encrypting the book</p>
* @param mainHand <p>Whether the player is holding the book in its main hand</p>
* @param key <p>The key/password to use for encryption</p>
* @param style <p>The encryption style to use</p>
* @param groupName <p>The name of the group to encrypt for, or "" otherwise</p>
* @param preventAdminDecrypt <p>Whether to prevent storage of a key that can be used for admin decryption</p>
* @return <p>An encrypted version of the book</p>
*/
@Nullable
public static ItemStack encryptBook(Player player, boolean mainHand, @NotNull String key,
@NotNull EncryptionStyle style, @NotNull String groupName) {
@NotNull EncryptionStyle style, @NotNull String groupName,
boolean preventAdminDecrypt) {
BookMeta book = InventoryHelper.getHeldBookMetadata(player, mainHand);
if (book == null) {
BooksWithoutBorders.sendErrorMessage(player, "Unable to get metadata from the held book!");
@@ -167,7 +170,8 @@ public final class EncryptionHelper {
AESConfiguration configuration = AESConfiguration.getNewConfiguration(hashedKey);
//Save the book's un-encrypted contents to a file
BookMeta newMetadata = saveBookPlaintext(groupName, player, book, style, hashedKey, configuration);
BookMeta newMetadata = saveEncryptedBook(groupName, player,
new EncryptedBook(book, style, hashedKey, new ArrayList<>(), configuration, preventAdminDecrypt));
if (newMetadata == null) {
return null;
}
@@ -216,26 +220,22 @@ public final class EncryptionHelper {
}
/**
* Saves a book's plain text to a file
* Saves an encrypted book 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 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>
* @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 encryptedBook <p>The book to encrypt</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 EncryptionStyle encryptionStyle,
@NotNull String key, @NotNull AESConfiguration aesConfiguration) {
BookMeta newMetadata = book;
private static BookMeta saveEncryptedBook(@NotNull String groupName, @NotNull Player player,
@NotNull EncryptedBook encryptedBook) {
BookMeta newMetadata = encryptedBook.bookMeta();
boolean wasSaved;
if (groupName.trim().isEmpty()) {
wasSaved = saveEncryptedBook(player, book, encryptionStyle, key, aesConfiguration);
wasSaved = saveEncryptedBook(player, encryptedBook);
} else {
newMetadata = saveEncryptedBookForGroup(player, book, groupName);
newMetadata = saveEncryptedBookForGroup(player, encryptedBook.bookMeta(), groupName);
wasSaved = newMetadata != null;
}
if (wasSaved) {
@@ -327,7 +327,6 @@ public final class EncryptionHelper {
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;
@@ -476,20 +475,15 @@ 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 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>
* @param player <p>The player encrypting the book</p>
* @param encryptedBook <p>The book to save</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 EncryptionStyle encryptionStyle, @NotNull String key,
@Nullable AESConfiguration aesConfiguration) {
private static Boolean saveEncryptedBook(@NotNull Player player, @NotNull EncryptedBook encryptedBook) {
String path = BooksWithoutBorders.getConfiguration().getEncryptedBookPath();
String fileName = BookHelper.getBookFile(bookMetaData, player, true);
String fileName = BookHelper.getBookFile(encryptedBook.bookMeta(), player, true);
fileName = cleanString(fileName);
//cancels saving if file is already encrypted
@@ -500,8 +494,7 @@ public final class EncryptionHelper {
}
try {
BookToFromTextHelper.encryptedBookToYml(path, fileName,
new EncryptedBook(bookMetaData, encryptionStyle, key, new ArrayList<>(), aesConfiguration));
BookToFromTextHelper.encryptedBookToYml(path, fileName, encryptedBook);
} catch (IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!");
return false;

View File

@@ -38,4 +38,8 @@ Options:
# peek at books, if an admin gets a hold of a book with the same title and author, but only the encrypted AES cypher text is stored in the book.
# Note that real encryption might alter, corrupt or lose a book's contents, so don't use real encryption with books
# that have no backup in in-game book form or saved book form.
Use_Real_Encryption: false
Use_Real_Encryption: false
# Whether to allow players to specifically disable admin decryption for a real encrypted book. This is only available
# when real encryption is enabled. It allows a player to prevent the storage of the encryption key in the plugin
# folder, meaning that the only way to decrypt the book is to provide the correct key. THIS IS A DANGEROUS OPTION!
Allow_Prevent_Admin_Decryption: false

View File

@@ -102,9 +102,11 @@ commands:
Encrypts the book the player is holding. "key" is required and can be any phrase or number excluding spaces.
"style" is not required. Possible values are "dna", "substitution", "aes", "onetimepad" and "magic".
If real encryption is enabled, possible methods are restricted.
If real encryption and prevent admin decryption are enabled, the third argument prevents the key from being
stored in the server files, preventing admin decryption. If the password is lost, decryption is impossible.
aliases:
- bwbencrypt
usage: /<command> <key> [encryption style]
usage: /<command> <key> [encryption style] [prevent admin decrypt]
permission: bookswithoutborders.encrypt
setbookgeneration:
description: Sets the generation of your held book
@@ -265,6 +267,7 @@ permissions:
bookswithoutborders.reload: true
bookswithoutborders.setgeneration: true
bookswithoutborders.editbookshelf: true
bookswithoutborders.preventadmindecryption: true
bookswithoutborders.use:
description: Allows player to use commands to save/load/delete in their personal directory, and peeking at bookshelves if enabled
children:
@@ -341,4 +344,6 @@ permissions:
bookswithoutborders.addtitlepage:
description: Allows player to add a blank title page to a book
bookswithoutborders.deletepage:
description: Allows player to delete a page from a book
description: Allows player to delete a page from a book
bookswithoutborders.preventadmindecryption:
description: If use real encryption and prevent admin decryption options are enabled, allows player to disable admin decryption for a book