diff --git a/Cargo.lock b/Cargo.lock index d3d4ab8..0aedc66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -62,12 +62,18 @@ version = "0.2.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" +[[package]] +name = "lz-string" +version = "0.0.1" +source = "git+https://github.com/adumbidiot/lz-string-rs#f968dd54b0b7d0a560235f50f204557d84d1f0c0" + [[package]] name = "rpgmv_decryptor" version = "0.1.0" dependencies = [ "anyhow", "clap", + "lz-string", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 93766d9..cda3130 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,4 +8,5 @@ edition = "2018" [dependencies] anyhow = "1.0.31" -clap = "2.33.1" \ No newline at end of file +clap = "2.33.1" +lz-string = { git = "https://github.com/adumbidiot/lz-string-rs" } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 655f855..531be7c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ use anyhow::{anyhow, Context, Result}; use clap::{App, Arg}; use std::fs::File; -use std::io::{Read, Write}; +use std::io::Write; use std::path::Path; /// # RPGMV Decryptor -/// fn main() -> Result<()> { let matches = App::new("RPGMV Decoder") .version("0.1.0") @@ -25,122 +24,195 @@ fn main() -> Result<()> { .long("key") .value_name("KEY") .help("The Encryption Key (look in www/data/System.json or www/js/rpg_core.js") - .takes_value(true) - .required(true), + .takes_value(true), ) .get_matches(); let path_string = matches.value_of("path").context("No file path provided")?; - let key_string = matches.value_of("key").context("No key provided")?; let path = Path::new(path_string); - let key = get_key_bytes(key_string)?; - - // let default_rpgmv_header: [u8; 16] = [ - // 0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x3, 0x1, 0x00, 0x00, 0x00, 0x00, - // 0x00, - // ]; - - let file_buf = get_file_data(path)?; - let decrypted = decrypt(file_buf, key); - - let mut file; - let filename = path .file_stem() .context("Unable to determine file stem")? .to_str() - .context("Unable to convert file stem to UTF-8 string")?; + .context("file stem is not valid UTF-8")?; + let path_str = path.to_string_lossy(); - match detect_original_extension(path)? { - RPGFile::Photo => file = File::create(format!("{}.png", filename))?, // .rpgmvps are .pngs - RPGFile::Video => file = File::create(format!("{}.m4a", filename))?, // .rpgmvms are .m4as - RPGFile::Audio => file = File::create(format!("{}.ogg", filename))?, // .rpgmvos are .oggs - } + match path.extension() { + Some(ext) => match ext.to_str() { + Some("rpgsave") => { + use rpgmv_save::*; - file.write_all(&decrypted)?; + let decompressed = decompress_save(path)?; + write_decompressed_save(format!("{}.json", filename), &decompressed)?; - Ok(()) -} - -/// Determines the original file type for the encrypted .rpgmv* file -fn detect_original_extension>(path: P) -> Result { - match path.as_ref().extension() { - Some(ext) => { - let ext = ext - .to_str() - .context("File extension was not UTF-8 Compatible")?; - - match ext { - "rpgmvp" => Ok(RPGFile::Photo), - "rpgmvm" => Ok(RPGFile::Video), - "rpgmvo" => Ok(RPGFile::Audio), - _ => Err(anyhow!("File was lacking a supported file extension")), + Ok(()) } + Some("rpgmvp") | Some("rpgmvm") | Some("rpgmvo") => { + use rpgmv_media::*; + + let key_string = matches.value_of("key").context("No key provided")?; + let key = get_key_bytes(key_string)?; + + // let default_rpgmv_header: [u8; 16] = [ + // 0x52, 0x50, 0x47, 0x4d, 0x56, 0x00, 0x00, 0x00, 0x00, 0x3, 0x1, 0x00, 0x00, 0x00, 0x00, + // 0x00, + // ]; + + let file_buf = get_file_data(path)?; + let decrypted = decrypt(file_buf, key); + + let mut file; + + match detect_original_extension(path)? { + RPGFile::Photo => file = File::create(format!("{}.png", filename))?, // .rpgmvps are .pngs + RPGFile::Video => file = File::create(format!("{}.m4a", filename))?, // .rpgmvms are .m4as + RPGFile::Audio => file = File::create(format!("{}.ogg", filename))?, // .rpgmvos are .oggs + } + + Ok(file.write_all(&decrypted)?) + } + Some(ext_str) => Err(anyhow!("{} is not a supported .ext", ext_str)), + None => Err(anyhow!("{}'s .ext is not valid UTF-8", path_str)), + }, + None => Err(anyhow!("Unable to determine the extension of {}", path_str)), + } +} + +mod rpgmv_save { + use anyhow::{anyhow, Result}; + use std::char::decode_utf16; + use std::fs::File; + use std::io::{Read, Write}; + use std::path::Path; + + pub fn decompress_save>(path: P) -> Result { + let mut save_file = File::open(path.as_ref())?; + let mut base64_string = String::new(); + save_file.read_to_string(&mut base64_string)?; + + let maybe_compressed = convert_base64_string(&base64_string); + + match maybe_compressed { + Some(compressed) => Ok(lz_string::decompress(&compressed, 32) + .ok_or_else(|| anyhow!("Failed to decompress .rpgsave"))?), + None => Err(anyhow!("Failed to convert the .rpgsave from base64")), } - None => Err(anyhow!("File is lacking a file extension")), + } + + pub fn write_decompressed_save>(path: P, decompressed: &str) -> Result<()> { + let mut out = File::create(path.as_ref())?; + out.write_all(decompressed.as_bytes())?; + Ok(()) + } + + pub fn convert_base64_string(base64: &str) -> Option { + let codes = convert_to_utf16_char_codes(base64)?; + + let res = decode_utf16(codes.iter().cloned()) + .map(|res| res.expect("UTF-16 Decoding failed")) + .collect(); + + Some(res) + } + + pub fn convert_to_utf16_char_codes(base64: &str) -> Option> { + let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + let mut codes: Vec = Vec::with_capacity(base64.chars().count()); + + for c in base64.chars() { + codes.push(alphabet.find(c)? as u16); + } + + Some(codes) } } -/// get_key_bytes takes a 32-character hexadecimal string and interperets -/// every two characters as an unsined 8-bit integer. -/// -/// # Examples -/// `71e7bdd9b28010980c2a4e8a1386e100` will be split up as: -/// -/// `0x71`, `0xe7`, `0xbd`, `0xd9`, `0xb2`, `0x80`, `0x10`, `0x98`, `0x0c`, `0x2a`, `0x4e`, `0x8a`, `0x13`, `0x86`, `0xe1`, and `0x00` -fn get_key_bytes(key: &str) -> Result> { - let sub_len = 2; +mod rpgmv_media { + use anyhow::{anyhow, Context, Result}; + use std::fs::File; + use std::io::Read; + use std::path::Path; - let mut key_buf = Vec::with_capacity(key.len() / 2); + /// get_key_bytes takes a 32-character hexadecimal string and interperets + /// every two characters as an unsined 8-bit integer. + /// + /// # Examples + /// `71e7bdd9b28010980c2a4e8a1386e100` will be split up as: + /// + /// `0x71`, `0xe7`, `0xbd`, `0xd9`, `0xb2`, `0x80`, `0x10`, `0x98`, `0x0c`, `0x2a`, `0x4e`, `0x8a`, `0x13`, `0x86`, `0xe1`, and `0x00` + pub fn get_key_bytes(key: &str) -> Result> { + let sub_len = 2; - let mut chars = key.chars(); - let sub_strings = (0..) - .map(|_| chars.by_ref().take(sub_len).collect::()) - .take_while(|s| !s.is_empty()) - .collect::>(); + let mut key_buf = Vec::with_capacity(key.len() / 2); - for string in sub_strings { - key_buf.push(u8::from_str_radix(&string, 16)?); + let mut chars = key.chars(); + let sub_strings = (0..) + .map(|_| chars.by_ref().take(sub_len).collect::()) + .take_while(|s| !s.is_empty()) + .collect::>(); + + for string in sub_strings { + key_buf.push(u8::from_str_radix(&string, 16)?); + } + + Ok(key_buf) } - Ok(key_buf) -} + /// get_file_data returns a buffer containing a data of a file which is on disk. + pub fn get_file_data>(path: P) -> Result> { + let mut buf = Vec::new(); -/// get_file_data returns a buffer containing a data of a file which is on disk. -fn get_file_data>(path: P) -> Result> { - let mut buf = Vec::new(); + let mut file = File::open(path.as_ref())?; + file.read_to_end(&mut buf)?; - let mut file = File::open(path.as_ref())?; - file.read_to_end(&mut buf)?; - - Ok(buf) -} - -fn _encrypt(_bytes: Vec, _key: Vec) -> Vec { - unimplemented!(); -} - -/// Decrypts a RPGMV media file -/// -/// The only part of the data that is actually encrypted (read: has a cipher applied to it) -/// is the header of the original file, which is bytes 17 -> 33 (16 bytes). Our key provides us with -/// 16 values which we can use to perform a bitwise XOR. The bitwise XOR will undo the cipher, returning the header to -/// it's original position. At this point, byte #17 -> end of file will now be recognized as their respected unencrypted -// file formats. -fn decrypt(bytes: Vec, key: Vec) -> Vec { - let mut decrypted = bytes[16..].to_owned(); // Throw out the first 16 bytes (the RPGMV Header) - - for i in 0..16 as usize { - decrypted[i as usize] = decrypted[i] ^ key[i]; + Ok(buf) } - decrypted -} + fn _encrypt(_bytes: Vec, _key: Vec) -> Vec { + unimplemented!(); + } -/// An enum that represents the types of supported RPGMV media files. -enum RPGFile { - Photo, - Video, - Audio, + /// Decrypts a RPGMV media file + /// + /// The only part of the data that is actually encrypted (read: has a cipher applied to it) + /// is the header of the original file, which is bytes 17 -> 33 (16 bytes). Our key provides us with + /// 16 values which we can use to perform a bitwise XOR. The bitwise XOR will undo the cipher, returning the header to + /// it's original position. At this point, byte #17 -> end of file will now be recognized as their respected unencrypted + // file formats. + pub fn decrypt(bytes: Vec, key: Vec) -> Vec { + let mut decrypted = bytes[16..].to_owned(); // Throw out the first 16 bytes (the RPGMV Header) + + for i in 0..16 as usize { + decrypted[i as usize] = decrypted[i] ^ key[i]; + } + + decrypted + } + + /// Determines the original file type for the encrypted .rpgmv* file + pub fn detect_original_extension>(path: P) -> Result { + match path.as_ref().extension() { + Some(ext) => { + let ext = ext + .to_str() + .context("File extension was not UTF-8 Compatible")?; + + match ext { + "rpgmvp" => Ok(RPGFile::Photo), + "rpgmvm" => Ok(RPGFile::Video), + "rpgmvo" => Ok(RPGFile::Audio), + _ => Err(anyhow!("File was lacking a supported file extension")), + } + } + None => Err(anyhow!("File is lacking a file extension")), + } + } + + /// An enum that represents the types of supported RPGMV media files. + pub enum RPGFile { + Photo, + Video, + Audio, + } }