Skip to content

Instantly share code, notes, and snippets.

@Vizmute
Last active September 12, 2021 23:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Vizmute/eb008e836ea42c5645e346407e05250a to your computer and use it in GitHub Desktop.
Save Vizmute/eb008e836ea42c5645e346407e05250a to your computer and use it in GitHub Desktop.
RPGMaker MV File Decrypter/Encrypter

RPGMaker MV File Decrypter/Encrypter (v1.0)

A little Node.js script for decrypting/encrypting RPGMaker MV assets.

Usage

  1. Install Node.js (>=14) if you don't have it already.
  2. Drop rpgmvcrypt.js inside a game folder. The script will look for the www folder inside this folder.
  3. Run the file with node node rpgmvcrypt on the command line, or if you have .js files associated with Node, just doubleclick
  4. The script will run and you will hopefully have a decrypted game that still functions fine when executed afterwards.
  5. You can rerun the script to encrypt the files again.

Notes

  • This script should hopefully not cause any issues with the game files, but it is still recommended that you back up the www directory first!
  • If the script decides to decrypt on an initial run and finds unencrypted files inside the game files, it will generate a rpgmvcryptignore.txt so that it knows not to encrypt those files on future execution.
  • The script will also generate a rpgmvcrypt.log file on every run with details about what it did.

Other

You can contact me on Discord (Vizmute#4098) or Twitter (@vizmute) if you run into issues with this. No warranty is provided for accidentally breaking your game files.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = require("path");
const promises_1 = require("fs/promises");
const isSystem = /System(\.json)?$/i;
const isMvFile = /\.(?:(rpgmv[pmo])|(?:(png|m4a|ogg)_?)|)$/i;
const ops = {
hasArgs: false,
decrypt: false,
encrypt: false,
verbose: true,
};
const argv = process.argv.slice(1);
if (argv.includes("--decrypt")) {
ops.hasArgs = true;
ops.decrypt = true;
}
if (argv.includes("--encrypt")) {
ops.hasArgs = true;
ops.encrypt = true;
}
if (argv.includes("--quiet")) {
ops.verbose = false;
}
// prettier-ignore
const mvHeader = Uint8Array.from([
0x52, 0x50, 0x47, 0x4d,
0x56, 0x00, 0x00, 0x00,
0x00, 0x03, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00,
]);
// prettier-ignore
const pngHeader = Uint8Array.from([
0x89, 0x50, 0x4e, 0x47,
0x0d, 0x0a, 0x1a, 0x0a,
0x00, 0x00, 0x00, 0x0d,
0x49, 0x48, 0x44, 0x52
]);
const HEADER_LENGTH = 16;
function getKey(key) {
if (!key)
return null;
return Uint8Array.from(key
.split(/([0-9a-f]{2})/i)
.filter(Boolean)
.map((x) => parseInt(x, 16)));
}
function verifyMvHeader(buf) {
for (let i = 0; i < mvHeader.length; ++i) {
if (buf[i] !== mvHeader[i])
return false;
}
return true;
}
async function pngDecrypt(path) {
const buf = await (0, promises_1.readFile)(path);
if (!verifyMvHeader(buf))
throw new Error("Not an RPG Maker MV file.");
const res = new Uint8Array(buf.length - mvHeader.length);
res.set(buf.slice(HEADER_LENGTH));
res.set(pngHeader, 0);
return res;
}
async function decrypt(key, path) {
const buf = await (0, promises_1.readFile)(path);
if (!verifyMvHeader(buf))
throw new Error("Not an RPG Maker MV file.");
const res = new Uint8Array(buf.length - mvHeader.length);
res.set(buf.slice(HEADER_LENGTH));
for (let i = 0; i < HEADER_LENGTH; ++i) {
res[i] = res[i] ^ key[i];
}
return res;
}
async function encrypt(key, path) {
const buf = await (0, promises_1.readFile)(path);
const res = new Uint8Array(buf.length + mvHeader.length);
res.set(buf, mvHeader.length);
res.set(mvHeader, 0);
for (let i = 0; i < HEADER_LENGTH; ++i) {
res[i + HEADER_LENGTH] = res[i + HEADER_LENGTH] ^ key[i];
}
return res;
}
async function getPaths(input, basePath, ret = []) {
for (const p of input) {
const path = (0, path_1.join)(basePath, p);
const stats = await (0, promises_1.stat)(path);
if (stats.isDirectory())
await getPaths(await (0, promises_1.readdir)(path), path, ret);
else
ret.push(path);
}
return ret;
}
function pad(input, len = 5, char = " ", right = false) {
input = "" + input;
while (input.length < len) {
input = right ? input + char : char + input;
}
return input;
}
let logFile = "";
function log(...input) {
for (let i = 0; i < input.length; ++i) {
logFile += input[i].toString() + (i < input.length - 1 ? " " : "\n");
}
console.log(...input);
}
function strip(path, base) {
return path.replace(base, "");
}
async function main() {
let sysFile = "";
let sysData = {
encryptionKey: "",
hasEncryptedImages: true,
hasEncryptedAudio: true,
};
const cwd = (0, path_1.join)(process.cwd());
const basePath = (0, path_1.join)(cwd, "www");
const dirs = await (0, promises_1.readdir)(basePath);
if (dirs.includes("data")) {
const sys = await (0, promises_1.readdir)((0, path_1.join)(basePath, "data"));
for (const p of sys) {
if (isSystem.test(p)) {
sysFile = p;
break;
}
}
}
const sysPath = (0, path_1.join)(basePath, "data", sysFile);
if (sysFile) {
sysData = JSON.parse(await (0, promises_1.readFile)(sysPath, "utf8"));
}
const key = getKey(sysData.encryptionKey);
if (!ops.hasArgs) {
if (sysData.hasEncryptedAudio || sysData.hasEncryptedImages) {
ops.decrypt = true;
}
if (!sysData.hasEncryptedAudio && !sysData.hasEncryptedImages) {
ops.encrypt = true;
}
}
// log("encryptionKey:", key, "\nBase path:", basePath);
let ignore = [];
const ignorePath = (0, path_1.join)(cwd, "rpgmvcryptignore.txt");
try {
const ignoreList = await (0, promises_1.readFile)(ignorePath, "utf8");
ignore = ignore.concat(ignoreList.split(/\r?\n|\r/).map(p => (0, path_1.join)(cwd, strip((0, path_1.normalize)(p), cwd))));
}
catch (e) {
// ignorefile doesn't exist
}
const buildIgnore = !ignore.length;
const files = await getPaths(dirs, basePath);
const stats = {
files: 0,
encrypt: {
png: 0,
m4a: 0,
ogg: 0,
},
decrypt: {
png: 0,
m4a: 0,
ogg: 0,
},
ignore: {
png: 0,
m4a: 0,
ogg: 0,
},
};
log("+--------------------------------------+");
log("| RPGMaker MV File Decrypter/Encrypter |");
log("| v1.0 - Made by Vizmute (@vizmute) |");
log("+--------------------------------------+");
const op = [];
if (ops.decrypt)
op.push("decrypt");
if (ops.encrypt)
op.push("encrypt");
log(`| ${pad(`Running ${op.join(", ")}...`, 36, " ", true)} |`);
log("+------------+------------+------------+");
for (const path of files) {
const m = path.match(isMvFile);
if (!m)
continue;
let ext = ".";
let type = "png";
if (m[1] === "rpgmvo" || m[2] === "ogg_") {
ext = ".ogg";
type = "ogg";
}
if (m[1] === "rpgmvm" || m[2] === "m4a_") {
ext = ".m4a";
type = "m4a";
}
if (m[1] === "rpgmvp" || m[2] === "png_") {
ext = ".png";
type = "png";
}
if (m[2] === "ogg") {
ext = ".rpgmvo";
type = "ogg";
}
if (m[2] === "m4a") {
ext = ".rpgmvm";
type = "m4a";
}
if (m[2] === "png") {
ext = ".rpgmvp";
type = "png";
}
try {
let file = null;
switch (ext) {
case ".png": {
if (!ops.decrypt || ignore.includes(path)) {
++stats.ignore.png;
break;
}
if (ops.verbose)
log(`+ Decrypting ${type}: ${strip(path, cwd)}`);
++stats.decrypt.png;
file = await pngDecrypt(path);
break;
}
case ".rpgmvo":
case ".rpgmvm":
case ".rpgmvp": {
if (!key)
break;
if (ops.decrypt && !ops.encrypt && buildIgnore) {
ignore.push(strip(path, cwd));
}
if (!ops.encrypt || ignore.includes(path)) {
++stats.ignore[type];
break;
}
if (ops.verbose)
log(`+ Encrypting ${type}: ${strip(path, cwd)}`);
++stats.encrypt[type];
file = await encrypt(key, path);
break;
}
default: {
if (!key)
break;
if (!ops.decrypt || ignore.includes(path)) {
++stats.ignore[type];
break;
}
if (ops.verbose)
log(`+ Decrypting ${type}: ${strip(path, cwd)}`);
++stats.decrypt[type];
file = await decrypt(key, path);
break;
}
}
if (!file)
continue;
++stats.files;
await (0, promises_1.writeFile)(path.replace(m[0], ext), file);
await (0, promises_1.unlink)(path);
}
catch (e) {
log(e);
}
}
if (sysFile) {
let write = "";
if (ops.decrypt && !ops.encrypt) {
sysData.hasEncryptedAudio = false;
sysData.hasEncryptedImages = false;
write = "decrypt";
}
if (ops.encrypt && !ops.decrypt) {
sysData.hasEncryptedAudio = true;
sysData.hasEncryptedImages = true;
write = "encrypt";
}
if (write) {
// log(`Running file ${write}...`);
// log("----------");
await (0, promises_1.writeFile)(sysPath, JSON.stringify(sysData), "utf8");
}
}
if (buildIgnore && ignore.length) {
await (0, promises_1.writeFile)(ignorePath, ignore.join("\n"));
}
if (ops.verbose)
log("+------------+------------+------------+");
log("| Decrypted | Encrypted | Ignored |");
log("+------------+------------+------------+");
log(`| PNG: ${pad(stats.decrypt.png)} | PNG: ${pad(stats.encrypt.png)} | PNG: ${pad(stats.ignore.png)} |`);
log(`| OGG: ${pad(stats.decrypt.ogg)} | OGG: ${pad(stats.encrypt.ogg)} | OGG: ${pad(stats.ignore.ogg)} |`);
log(`| M4A: ${pad(stats.decrypt.m4a)} | M4A: ${pad(stats.encrypt.m4a)} | M4A: ${pad(stats.ignore.m4a)} |`);
log(`| ALL: ${pad(stats.decrypt.png + stats.decrypt.ogg + stats.decrypt.m4a)} | ALL: ${pad(stats.encrypt.png + stats.encrypt.ogg + stats.encrypt.m4a)} | ALL: ${pad(stats.ignore.png + stats.ignore.ogg + stats.ignore.m4a)} |`);
log("+------------+------------+------------+");
log(`| TOTAL: ${pad(stats.files, 29)} |`);
log("+--------------------------------------+");
await (0, promises_1.writeFile)((0, path_1.join)(process.cwd(), "rpgmvcrypt.log"), logFile, "utf8");
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment