diff --git a/scripts/decrypt-save.js b/scripts/decrypt-save.js new file mode 100644 index 00000000000..a7239a40df6 --- /dev/null +++ b/scripts/decrypt-save.js @@ -0,0 +1,149 @@ +import pkg from "crypto-js"; +const { AES, enc } = pkg; +// biome-ignore lint: This is how you import fs from node +import * as fs from "node:fs"; + +const SAVE_KEY = "x0i2O7WRiANTqPmZ"; + +/** + * A map of condensed keynames to their associated full names + * NOTE: Update this if `src/system/game-data#systemShortKeys` ever changes! + */ +const systemShortKeys = { + seenAttr: "$sa", + caughtAttr: "$ca", + natureAttr: "$na", + seenCount: "$s", + caughtCount: "$c", + hatchedCount: "$hc", + ivs: "$i", + moveset: "$m", + eggMoves: "$em", + candyCount: "$x", + friendship: "$f", + abilityAttr: "$a", + passiveAttr: "$pa", + valueReduction: "$vr", + classicWinCount: "$wc", +}; + +/** + * Replace the shortened key names with their full names + * @param {string} dataStr - The string to convert + * @returns {string} The string with shortened keynames replaced with full names + */ +function convertSystemDataStr(dataStr) { + const fromKeys = Object.values(systemShortKeys); + const toKeys = Object.keys(systemShortKeys); + for (const k in fromKeys) { + dataStr = dataStr.replace(new RegExp(`${fromKeys[k].replace("$", "\\$")}`, "g"), toKeys[k]); + } + + return dataStr; +} + +/** + * Decrypt a save + * @param {string} path - The path to the encrypted save file + * @returns {string} The decrypted save data + */ +function decryptSave(path) { + // Check if the file exists + if (!fs.existsSync(path)) { + console.error(`File not found: ${path}`); + process.exit(1); + } + let fileData; + try { + fileData = fs.readFileSync(path, "utf8"); + } catch (e) { + switch (e.code) { + case "ENOENT": + console.error(`File not found: ${path}`); + break; + case "EACCES": + console.error(`Could not open ${path}: Permission denied`); + break; + case "EISDIR": + console.error(`Unable to read ${path} as it is a directory`); + break; + default: + console.error(`Error reading file: ${e.message}`); + } + process.exit(1); + } + return convertSystemDataStr(AES.decrypt(fileData, SAVE_KEY).toString(enc.Utf8)); +} + +/* Print the usage message and exits */ +function printUsage() { + console.log(` +Usage: node decrypt-save.js [save-file] + +Arguments: + file-path Path to the encrypted save file to decrypt. + save-file Path to where the decrypted data should be written. If not provided, the decrypted data will be printed to the console. + +Options: + -h, --help Show this help message and exit. + +Description: + This script decrypts an encrypted pokerogue save file +`); +} + +/** + * Write `data` to `filePath`, gracefully communicating errors that arise + * @param {string} filePath + * @param {string} data + */ +function writeToFile(filePath, data) { + try { + fs.writeFileSync(filePath, data); + } catch (e) { + switch (e.code) { + case "EACCES": + console.error(`Could not open ${filePath}: Permission denied`); + break; + case "EISDIR": + console.error(`Unable to write to ${filePath} as it is a directory`); + break; + default: + console.error(`Error writing file: ${e.message}`); + } + process.exit(1); + } +} + +function main() { + let args = process.argv.slice(2); + // Get options + const options = args.filter(arg => arg.startsWith("-")); + // get args + args = args.filter(arg => !arg.startsWith("-")); + + if (args.length === 0 || options.includes("-h") || options.includes("--help") || args.length > 2) { + printUsage(); + process.exit(0); + } + // If the user provided a second argument, check if the file exists already and refuse to write to it. + if (args.length === 2) { + const destPath = args[1]; + if (fs.existsSync(destPath)) { + console.error(`Refusing to overwrite ${destPath}`); + process.exit(1); + } + } + + // Otherwise, commence decryption. + const decrypt = decryptSave(args[0]); + + if (args.length === 1) { + process.stdout.write(decrypt); + process.exit(0); + } + + writeToFile(destPath, decrypt); +} + +main();