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 - 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 default changed from `,` to `¤`, as a comma is a natural character to use in a title), or updating books saved as txt
to yml. 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 ### 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 * The error displayed when attempting to change a book's title with a title that's too long
*/ */
ERROR_TITLE_LENGTH, ERROR_TITLE_LENGTH,
/**
* The error displayed when attempting to encrypt a book that has an existing encrypted file
*/
ERROR_ENCRYPT_ALREADY_SAVED,
; ;
@Override @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; package net.knarcraft.bookswithoutborders.thread;
import net.knarcraft.bookswithoutborders.BooksWithoutBorders; import net.knarcraft.bookswithoutborders.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.container.EncryptedBook;
import net.knarcraft.bookswithoutborders.container.MigrationRequest; import net.knarcraft.bookswithoutborders.container.MigrationRequest;
import net.knarcraft.bookswithoutborders.utility.BookFileHelper; import net.knarcraft.bookswithoutborders.utility.BookFileHelper;
import net.knarcraft.bookswithoutborders.utility.BookHelper; import net.knarcraft.bookswithoutborders.utility.BookHelper;
@@ -89,12 +90,17 @@ public class MigrationQueueThread implements Runnable {
if (bookMeta == null) { if (bookMeta == null) {
return false; return false;
} }
String slash = BooksWithoutBorders.getConfiguration().getSlash();
BookMeta loadedBook; EncryptedBook encryptedBook = null;
BookMeta loadedBook = null;
String extension = BookFileHelper.getExtensionFromPath(file.getName()); String extension = BookFileHelper.getExtensionFromPath(file.getName());
if (extension.equalsIgnoreCase("yml")) { if (file.getAbsolutePath().contains("Books" + slash + "Encrypted")) {
loadedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true); encryptedBook = BookToFromTextHelper.encryptedBookFromYml(file, bookMeta, "", true);
} else if (extension.equalsIgnoreCase("txt")) { if (encryptedBook != null) {
loadedBook = encryptedBook.bookMeta();
}
} else if (extension.equalsIgnoreCase("txt") || extension.equalsIgnoreCase("yml")) {
loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta); loadedBook = BookToFromTextHelper.bookFromFile(file, bookMeta);
} else { } else {
BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!"); BooksWithoutBorders.log(Level.WARNING, "File with unexpected extension " + extension + " encountered!");
@@ -102,7 +108,7 @@ public class MigrationQueueThread implements Runnable {
} }
if (loadedBook == null) { 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; return false;
} }
@@ -125,8 +131,13 @@ public class MigrationQueueThread implements Runnable {
newName = "[" + key + "]" + newName; newName = "[" + key + "]" + newName;
} }
return saveBook(file.getParentFile(), newName, loadedBook, file); if (encryptedBook != null) {
} catch (IllegalArgumentException exception) { 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, "Failed to migrate book: " + file.getName() + " Cause:");
BooksWithoutBorders.sendErrorMessage(player, exception.getMessage()); BooksWithoutBorders.sendErrorMessage(player, exception.getMessage());
return false; 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 parent <p>The parent folder the file belongs to</p>
* @param newName <p>The new name of the file</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 oldFile <p>The old file path, in case it should be deleted</p>
* @return <p>True if successfully saved</p> * @return <p>True if successfully saved</p>
*/ */
private boolean saveBook(@NotNull File parent, @NotNull String newName, @NotNull BookMeta bookMeta, private boolean deleteBook(@NotNull File parent, @NotNull String newName,
@NotNull File oldFile) { @NotNull File oldFile) {
try { if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) {
BookToFromTextHelper.bookToYml(parent.getAbsolutePath(), newName, bookMeta); return oldFile.delete();
if (!oldFile.getAbsolutePath().equalsIgnoreCase(new File(parent, newName + ".yml").getAbsolutePath())) { } else {
return oldFile.delete();
}
return true; 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); String stripped = stripExtensionFromPath(path);
if (stripped.contains(separator)) { if (stripped.contains(separator)) {
return stripped.split(separator)[0]; return stripped.split(separator)[0];
} else if (stripped.contains(",")) {
return stripped.split(",")[0];
} else { } else {
return stripped; return stripped;
} }
@@ -215,6 +217,8 @@ public final class BookFileHelper {
String stripped = stripExtensionFromPath(path); String stripped = stripExtensionFromPath(path);
if (stripped.contains(separator)) { if (stripped.contains(separator)) {
return stripped.split(separator)[1]; return stripped.split(separator)[1];
} else if (stripped.contains(",")) {
return stripped.split(",")[1];
} else { } else {
return BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Formatting.NEUTRAL_UNKNOWN_AUTHOR); return BooksWithoutBorders.getStringFormatter().getUnFormattedColoredMessage(Formatting.NEUTRAL_UNKNOWN_AUTHOR);
} }

View File

@@ -140,7 +140,6 @@ public final class BookLoader {
BookHelper.increaseGeneration(book); BookHelper.increaseGeneration(book);
book.setAmount(numCopies); book.setAmount(numCopies);
if (!isSigned && book.getItemMeta() != null) { if (!isSigned && book.getItemMeta() != null) {
return BookHelper.unsignBook((BookMeta) book.getItemMeta(), book.getAmount()); 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.BooksWithoutBorders;
import net.knarcraft.bookswithoutborders.config.StaticMessage; import net.knarcraft.bookswithoutborders.config.StaticMessage;
import net.knarcraft.bookswithoutborders.config.translation.Formatting; import net.knarcraft.bookswithoutborders.config.translation.Formatting;
import net.knarcraft.bookswithoutborders.container.EncryptedBook;
import net.knarcraft.bookswithoutborders.encryption.AESConfiguration; import net.knarcraft.bookswithoutborders.encryption.AESConfiguration;
import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle; import net.knarcraft.bookswithoutborders.encryption.EncryptionStyle;
import net.knarcraft.knarlib.formatting.StringFormatter; import net.knarcraft.knarlib.formatting.StringFormatter;
@@ -65,39 +66,43 @@ public final class BookToFromTextHelper {
/** /**
* Saves an encrypted book's contents to a .yml file * 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 path <p>The path of the folder to save to</p>
* @param fileName <p>The name of the file to load to</p> * @param fileName <p>The name of the file to load to</p>
* @param bookMetadata <p>Metadata about the book to save</p> * @param encryptedBook <p>The encrypted book to save</p>
* @throws IOException <p>If unable to save the book</p> * @throws IOException <p>If unable to save the book</p>
*/ */
public static void encryptedBookToYml(@NotNull String path, @NotNull String fileName, @NotNull BookMeta bookMetadata, public static void encryptedBookToYml(@NotNull String path, @NotNull String fileName,
@NotNull EncryptionStyle encryptionStyle, @NotNull String encryptionKey, @NotNull EncryptedBook encryptedBook) throws IOException {
@Nullable AESConfiguration aesConfiguration) throws IOException { FileConfiguration bookYml = getBookConfiguration(encryptedBook.bookMeta());
FileConfiguration bookYml = getBookConfiguration(bookMetadata);
bookYml.set("Encryption.Style", encryptionStyle.toString()); bookYml.set("Encryption.Style", encryptedBook.encryptionStyle().toString());
bookYml.set("Encryption.Key", encryptionKey); bookYml.set("Encryption.Key", encryptedBook.encryptionKey());
if (encryptionStyle == EncryptionStyle.AES) { if (encryptedBook.encryptionStyle() == EncryptionStyle.AES) {
if (aesConfiguration == null) { if (encryptedBook.aesConfiguration() == null) {
throw new IOException("Attempted to save AES encrypted book without supplying a configuration!"); 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.IV", EncryptionHelper.bytesToHex(encryptedBook.aesConfiguration().iv()));
bookYml.set("Encryption.AES.Salt", EncryptionHelper.bytesToHex(aesConfiguration.salt())); bookYml.set("Encryption.AES.Salt", EncryptionHelper.bytesToHex(encryptedBook.aesConfiguration().salt()));
} }
List<String> encryptedPages = EncryptionHelper.encryptDecryptBookPages(bookMetadata, encryptionStyle, // If data is non-empty, that means the book is already encrypted
aesConfiguration, encryptionKey, true); if (!encryptedBook.data().isEmpty()) {
if (encryptedPages == null || encryptedPages.isEmpty()) { bookYml.set("Encryption.Data", encryptedBook.data());
throw new IOException("Book encryption failed!"); } 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 // Make sure the plaintext cannot simply be seen in the file
if (BooksWithoutBorders.getConfiguration().useRealEncryption()) { if (BooksWithoutBorders.getConfiguration().useRealEncryption()) {
bookYml.set("Pages", null); 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> * @return <p>Metadata for the loaded book</p>
*/ */
@Nullable @Nullable
public static BookMeta encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata, public static EncryptedBook encryptedBookFromYml(@NotNull File file, @NotNull BookMeta bookMetadata,
@NotNull String userKey, boolean forceDecrypt) { @NotNull String userKey, boolean forceDecrypt) {
BookMeta meta; BookMeta meta;
try { try {
@@ -123,25 +128,21 @@ public final class BookToFromTextHelper {
return null; 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); 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); userKey = EncryptionHelper.sha256(userKey);
String realKey = bookYml.getString("Encryption.Key", ""); String realKey = bookYml.getString("Encryption.Key", "");
if (forceDecrypt) { if (!realKey.isBlank()) {
userKey = realKey; if (forceDecrypt) {
} userKey = realKey;
if (!userKey.equals(realKey)) { }
return null; if (!userKey.equals(realKey)) {
return null;
}
} }
List<String> data = bookYml.getStringList("Encryption.Data"); List<String> data = bookYml.getStringList("Encryption.Data");
if (data.isEmpty()) {
return null;
}
EncryptionStyle encryptionStyle = EncryptionStyle.getFromString(bookYml.getString("Encryption.Style", EncryptionStyle encryptionStyle = EncryptionStyle.getFromString(bookYml.getString("Encryption.Style",
EncryptionStyle.SUBSTITUTION.toString())); EncryptionStyle.SUBSTITUTION.toString()));
@@ -153,15 +154,28 @@ public final class BookToFromTextHelper {
aesConfiguration = new AESConfiguration(iv, salt, userKey); 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); meta.setPages(data);
List<String> decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle, List<String> decryptedPages = EncryptionHelper.encryptDecryptBookPages(meta, encryptionStyle, aesConfiguration,
aesConfiguration, userKey, false); userKey, false);
if (decryptedPages != null && !decryptedPages.isEmpty()) { if (decryptedPages != null && !decryptedPages.isEmpty()) {
meta.setPages(decryptedPages); meta.setPages(decryptedPages);
} else { } else {
return null; return null;
} }
return meta;
return encryptedBook;
} }
/** /**
@@ -260,7 +274,7 @@ public final class BookToFromTextHelper {
//Update the metadata of the book with its new values //Update the metadata of the book with its new values
bookMetadata.setAuthor(authorFromUUID(author)); bookMetadata.setAuthor(authorFromUUID(author));
bookMetadata.setTitle(title.substring(0, 32)); bookMetadata.setTitle(title.substring(0, Math.min(title.length(), 32)));
bookMetadata.setPages(pages); bookMetadata.setPages(pages);
return bookMetadata; return bookMetadata;

View File

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

View File

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

View File

@@ -90,6 +90,9 @@ en:
ERROR_NO_ITEM: "You must be holding an item to use this command!" 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_EMPTY: "You must specify the new title/display name to set!"
ERROR_TITLE_LENGTH: "Book titles are capped at 32 characters!" 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 # # Custom formatting for some output #
# ---------------------------------- # # ---------------------------------- #