Stops migration from altering encrypted books
All checks were successful
EpicKnarvik97/Books-Without-Borders/pipeline/head This commit looks good

This commit is contained in:
2025-08-21 16:36:28 +02:00
parent 8c61d801e2
commit 93ce915a30
10 changed files with 125 additions and 69 deletions

View File

@@ -46,8 +46,6 @@ Books without Borders has got your back!
- The `/migrateBooks` command allows for easy fixing of old book naming, changing the title author separator (the
default changed from `,` to `¤`, as a comma is a natural character to use in a title), or updating books saved as txt
to yml.
- Note that if real encryption is enabled, migrating the books will store them unencrypted (like they used to before
real encryption was implemented) afterward.
### Book formatting

View File

@@ -307,6 +307,11 @@ public enum Translatable implements TranslatableMessage {
* The error displayed when attempting to change a book's title with a title that's too long
*/
ERROR_TITLE_LENGTH,
/**
* The error displayed when attempting to encrypt a book that has an existing encrypted file
*/
ERROR_ENCRYPT_ALREADY_SAVED,
;
@Override

View File

@@ -0,0 +1,23 @@
package net.knarcraft.bookswithoutborders.container;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import org.bukkit.inventory.meta.BookMeta;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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>
*/
public record EncryptedBook(@NotNull BookMeta bookMeta, @NotNull EncryptionStyle encryptionStyle,
@NotNull String encryptionKey, @NotNull List<String> data,
@Nullable AESConfiguration aesConfiguration) {
}

View File

@@ -1,6 +1,7 @@
package net.knarcraft.bookswithoutborders.thread;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.EncryptedBook;
import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper;
@@ -89,12 +90,17 @@ public class MigrationQueueThread implements Runnable {
if (bookMeta == null) {
return false;
}
String slash = BooksWithoutBorders.getConfiguration().getSlash();
BookMeta loadedBook;
EncryptedBook encryptedBook = null;
BookMeta loadedBook = null;
String extension = BookFileHelper.getExtensionFromPath(file.getName());
if (extension.equalsIgnoreCase("yml")) {
loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true);
} else if (extension.equalsIgnoreCase("txt")) {
if (file.getAbsolutePath().contains("Books" + slash + "Encrypted")) {
encryptedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true);
if (encryptedBook != null) {
loadedBook = encryptedBook.bookMeta();
}
} else if (extension.equalsIgnoreCase("txt") || extension.equalsIgnoreCase("yml")) {
loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta);
} else {
BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!");
@@ -102,7 +108,7 @@ public class MigrationQueueThread implements Runnable {
}
if (loadedBook == null) {
BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getName());
BooksWithoutBorders.log(Level.SEVERE, "Unable to load book: " + file.getAbsolutePath());
return false;
}
@@ -125,8 +131,13 @@ public class MigrationQueueThread implements Runnable {
newName = "[" + key + "]" + newName;
}
return saveBook(file.getParentFile(), newName, loadedBook, file);
} catch (IllegalArgumentException exception) {
if (encryptedBook != null) {
BookToFromTextHelper.encryptedBookToYml(file.getParentFile().getAbsolutePath(), newName, encryptedBook);
} else {
BookToFromTextHelper.bookToYml(file.getParentFile().getAbsolutePath(), newName, bookMeta);
}
return deleteBook(file.getParentFile(), newName, file);
} catch (IllegalArgumentException | IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Failed to migrate book: " + file.getName() + " Cause:");
BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return false;
@@ -134,25 +145,19 @@ public class MigrationQueueThread implements Runnable {
}
/**
* Saves a migrated book
* Deletes a migrated book, if the file path changed
*
* @param parent <p>The parent folder the file belongs to</p>
* @param newName <p>The new name of the file</p>
* @param bookMeta <p>The metadata of the book to migrate</p>
* @param oldFile <p>The old file path, in case it should be deleted</p>
* @param parent <p>The parent folder the file belongs to</p>
* @param newName <p>The new name of the file</p>
* @param oldFile <p>The old file path, in case it should be deleted</p>
* @return <p>True if successfully saved</p>
*/
private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta,
@NotNull File oldFile) {
try {
BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta);
if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) {
return oldFile.delete();
}
private boolean deleteBook(@NotNull File parent, @NotNull String newName,
@NotNull File oldFile) {
if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) {
return oldFile.delete();
} else {
return true;
} catch (IOException exception) {
BooksWithoutBorders.log(Level.SEVERE, "Failed to save migrated book: " + newName);
return false;
}
}

View File

@@ -198,6 +198,8 @@ public final class BookFileHelper {
String stripped = stripExtensionFromPath(path);
if (stripped.contains(separator)) {
return stripped.split(separator)[0];
} else if (stripped.contains(",")) {
return stripped.split(",")[0];
} else {
return stripped;
}
@@ -215,6 +217,8 @@ public final class BookFileHelper {
String stripped = stripExtensionFromPath(path);
if (stripped.contains(separator)) {
return stripped.split(separator)[1];
} else if (stripped.contains(",")) {
return stripped.split(",")[1];
} else {
return BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Formatting.NEUTRAL_UNKNOWN_AUTHOR);
}

View File

@@ -140,7 +140,6 @@ public final class BookLoader {
BookHelper.increaseGeneration(book);
book.setAmount(numCopies);
if (!isSigned && book.getItemMeta() != null) {
return BookHelper.unsignBook((BookMeta) book.getItemMeta(), book.getAmount());
}

View File

@@ -3,6 +3,7 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.translation.Formatting;
import net.knarcraft.bookswithoutborders.container.EncryptedBook;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.knarlib.formatting.StringFormatter;
@@ -65,39 +66,43 @@ public final class BookToFromTextHelper {
/**
* 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>
* @param path <p>The path of the folder to save to</p>
* @param fileName <p>The name of the file to load to</p>
* @param encryptedBook <p>The encrypted 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);
public static void encryptedBookToYml(@NotNull String path, @NotNull String fileName,
@NotNull EncryptedBook encryptedBook) throws IOException {
FileConfiguration bookYml = getBookConfiguration(encryptedBook.bookMeta());
bookYml.set("Encryption.Style", encryptionStyle.toString());
bookYml.set("Encryption.Key", encryptionKey);
if (encryptionStyle == EncryptionStyle.AES) {
if (aesConfiguration == null) {
bookYml.set("Encryption.Style", encryptedBook.encryptionStyle().toString());
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!");
}
bookYml.set("Encryption.AES.IV", EncryptionHelper.bytesToHex(aesConfiguration.iv()));
bookYml.set("Encryption.AES.Salt", EncryptionHelper.bytesToHex(aesConfiguration.salt()));
bookYml.set("Encryption.AES.IV", EncryptionHelper.bytesToHex(encryptedBook.aesConfiguration().iv()));
bookYml.set("Encryption.AES.Salt", EncryptionHelper.bytesToHex(encryptedBook.aesConfiguration().salt()));
}
List<String> encryptedPages = EncryptionHelper.encryptDecryptBookPages(bookMetadata, encryptionStyle,
aesConfiguration, encryptionKey, true);
if (encryptedPages == null || encryptedPages.isEmpty()) {
throw new IOException("Book encryption failed!");
// If data is non-empty, that means the book is already encrypted
if (!encryptedBook.data().isEmpty()) {
bookYml.set("Encryption.Data", encryptedBook.data());
} else {
List<String> encryptedPages = EncryptionHelper.encryptDecryptBookPages(encryptedBook.bookMeta(),
encryptedBook.encryptionStyle(), encryptedBook.aesConfiguration(), encryptedBook.encryptionKey(), true);
if (encryptedPages == null || encryptedPages.isEmpty()) {
throw new IOException("Book encryption failed!");
}
bookYml.set("Encryption.Data", encryptedPages);
}
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");
bookYml.save(new File(path, fileName + ".yml"));
}
/**
@@ -110,8 +115,8 @@ public final class BookToFromTextHelper {
* @return <p>Metadata for the loaded book</p>
*/
@Nullable
public static BookMeta encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata,
@NotNull String userKey, boolean forceDecrypt) {
public static EncryptedBook encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata,
@NotNull String userKey, boolean forceDecrypt) {
BookMeta meta;
try {
@@ -123,25 +128,21 @@ public final class BookToFromTextHelper {
return null;
}
// If the plaintext is stored in the file, don't bother with real decryption
if (!meta.getPages().isEmpty()) {
return meta;
}
FileConfiguration bookYml = YamlConfiguration.loadConfiguration(file);
// If key is blank, it's either not real encrypted, or admin decryption is disabled for the book
userKey = EncryptionHelper.sha256(userKey);
String realKey = bookYml.getString("Encryption.Key", "");
if (forceDecrypt) {
userKey = realKey;
}
if (!userKey.equals(realKey)) {
return null;
if (!realKey.isBlank()) {
if (forceDecrypt) {
userKey = realKey;
}
if (!userKey.equals(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()));
@@ -153,15 +154,28 @@ public final class BookToFromTextHelper {
aesConfiguration = new AESConfiguration(iv, salt, userKey);
}
// If the plaintext is stored in the file, don't bother with real decryption
EncryptedBook encryptedBook = new EncryptedBook(meta, encryptionStyle, userKey, data, aesConfiguration);
if (!meta.getPages().isEmpty()) {
return encryptedBook;
}
// Neither plaintext nor cipher text is stored
if (data.isEmpty()) {
return null;
}
// Perform the actual AES decryption
meta.setPages(data);
List<String> decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle,
aesConfiguration, userKey, false);
List<String> decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle, aesConfiguration,
userKey, false);
if (decryptedPages != null && !decryptedPages.isEmpty()) {
meta.setPages(decryptedPages);
} else {
return null;
}
return meta;
return encryptedBook;
}
/**
@@ -260,7 +274,7 @@ public final class BookToFromTextHelper {
//Update the metadata of the book with its new values
bookMetadata.setAuthor(authorFromUUID(author));
bookMetadata.setTitle(title.substring(0, 32));
bookMetadata.setTitle(title.substring(0, Math.min(title.length(), 32)));
bookMetadata.setPages(pages);
return bookMetadata;

View File

@@ -3,6 +3,8 @@ package net.knarcraft.bookswithoutborders.utility;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.BwBConfig;
import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.translation.Translatable;
import net.knarcraft.bookswithoutborders.container.EncryptedBook;
import net.knarcraft.bookswithoutborders.encryption.AES;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
@@ -273,9 +275,11 @@ public final class EncryptionHelper {
return null;
} else {
try {
bookMetadata = BookToFromTextHelper.encryptedBookFromYml(file, bookMetadata, key, forceDecrypt);
if (bookMetadata == null) {
EncryptedBook book = BookToFromTextHelper.encryptedBookFromYml(file, bookMetadata, key, forceDecrypt);
if (book == null) {
throw new IllegalArgumentException();
} else {
bookMetadata = book.bookMeta();
}
} catch (Exception e) {
BooksWithoutBorders.sendErrorMessage(player, "Decryption failed!");
@@ -489,13 +493,15 @@ public final class EncryptionHelper {
fileName = cleanString(fileName);
//cancels saving if file is already encrypted
File file = new File(path + fileName + ".yml");
File file = new File(path, fileName + ".yml");
if (file.isFile()) {
return true;
BooksWithoutBorders.getStringFormatter().displayErrorMessage(player, Translatable.ERROR_ENCRYPT_ALREADY_SAVED);
return false;
}
try {
BookToFromTextHelper.encryptedBookToYml(path, fileName, bookMetaData, encryptionStyle, key, aesConfiguration);
BookToFromTextHelper.encryptedBookToYml(path, fileName,
new EncryptedBook(bookMetaData, encryptionStyle, key, new ArrayList<>(), aesConfiguration));
} catch (IOException exception) {
BooksWithoutBorders.sendErrorMessage(player, "Encryption failed!");
return false;

View File

@@ -232,7 +232,6 @@ commands:
All txt files will be converted to yml.
All books will be re-saved, fixing any incorrect file-names, a changed title-author separator and files using
underscores "_" instead of spaces.
Books encrypted using real encryption will have their decrypted contents show up in the file system.
aliases:
- bwbmigrate
usage: /<command>

View File

@@ -90,6 +90,9 @@ en:
ERROR_NO_ITEM: "You must be holding an item to use this command!"
ERROR_TITLE_EMPTY: "You must specify the new title/display name to set!"
ERROR_TITLE_LENGTH: "Book titles are capped at 32 characters!"
ERROR_ENCRYPT_ALREADY_SAVED: |
An encrypted version of this book is already saved.
Please decrypt your previously encrypted copy first.
# ---------------------------------- #
# Custom formatting for some output #
# ---------------------------------- #